'  
'   Rebex Sample Code License
' 
'   Copyright 2025, REBEX CR s.r.o.
'   All rights reserved.
'   https://www.rebex.net/
' 
'   Permission to use, copy, modify, and/or distribute this software for any
'   purpose with or without fee is hereby granted.
' 
'   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
'   EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
'   OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
'   NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
'   HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
'   WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
'   FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
'   OTHER DEALINGS IN THE SOFTWARE.
' 

Imports System
Imports System.Text
Imports System.IO

Imports Rebex.IO
Imports Rebex.Net


'' <summary>
'' This sample is a simple command line FTP over TLS/SSL client.
'' In addition to basic operations, it demonstrates the following features:
'' - extending FtpItem and using a custom list parser to get Unix rights
'' - using a "Site" method for changing Unix rights using chmod command
'' - using FtpItemCollection class
'' </summary>

Class ConsoleClient

    Public Function ReadLine() As String
        Dim line As StringBuilder = New StringBuilder
        Dim complete As Boolean = False

        While (Not complete)
            Dim key As ConsoleKeyInfo = Console.ReadKey(True)
            Select Case key.Key
                Case ConsoleKey.Enter
                    complete = True
                Case ConsoleKey.Backspace
                    If (line.Length > 0) Then
                        line = line.Remove(line.Length - 1, 1)
                        Console.Write(key.KeyChar)
                        Console.Write(" ")
                        Console.Write(key.KeyChar)
                    End If
                Case Else
                    If ((key.KeyChar >= " "c) OrElse (key.Key = ConsoleKey.Tab)) Then
                        line = line.Append(key.KeyChar)
                        Console.Write("*")
                    End If
            End Select
        End While

        Console.WriteLine()
        Return line.ToString()
    End Function

    Private lastReport As DateTime = DateTime.MinValue


    Public Sub TransferProgressChanged(ByVal sender As Object, ByVal e As FtpTransferProgressChangedEventArgs)
        If e.TransferState <> TransferProgressState.DataBlockProcessed Then
            Return
        End If
        If (DateTime.Now.Subtract(lastReport).TotalSeconds) > 1 Then
            Console.WriteLine((e.BytesTransferred.ToString() + " bytes."))
            lastReport = DateTime.Now
        End If
    End Sub 'TransferProgressChanged

    Private ftp As Ftp

    Public Sub New()
        ' create Ftp object and set response handler
        ftp = New Ftp
        ftp.LogWriter = New ConsoleLogWriter(LogLevel.Info)
        AddHandler ftp.TransferProgressChanged, AddressOf TransferProgressChanged
    End Sub 'New


    Private Sub Help()
        Console.WriteLine("!         disconnect   rmdir       rmtree")
        Console.WriteLine("?         exit         user        auth")
        Console.WriteLine("ascii     help         chmod       ccc")
        Console.WriteLine("active    ls           get")
        Console.WriteLine("binary    mkdir        put")
        Console.WriteLine("bye       open         getr")
        Console.WriteLine("cd        passive      putr")
        Console.WriteLine("close     pwd          putdir")
        Console.WriteLine("dir       quit         getdir")
    End Sub 'Help



    Private Sub Close()
        Console.WriteLine("Disconnecting...")
        Ftp.Disconnect()
    End Sub 'Close


    Private Function Open(ByVal host As String) As Boolean
        Try

            If Ftp.State <> FtpState.Disconnected Then
                Console.WriteLine("Already connected. Disconnect first.")
                Return False
            End If

            If host Is Nothing OrElse host.Trim().Length = 0 Then
                Console.Write("Hostname: ")
                host = Console.ReadLine()
            End If

            Dim p As String() = host.Split(" "c)
            If p.Length < 0 OrElse p.Length > 2 Then
                Console.WriteLine("Usage: hostname [port]")
                Return False
            End If

            host = p(0)
            Dim port As Integer
            If p.Length = 1 Then
                port = Ftp.DefaultPort
            Else
                Try
                    port = Integer.Parse(p(1))
                Catch
                    port = -1
                End Try
                If port <= 0 OrElse port > 65535 Then
                    Console.WriteLine("Invalid port number.")
                    Return False
                End If
            End If

            Ftp.Connect(host, port)
            Return True
        Catch e As FtpException
            Console.WriteLine(e.Message)
            Return False
        End Try
    End Function 'Open

    Private Function Auth() As Boolean

        Try
            ftp.Settings.SslClientCertificateRequestHandler = New RequestHandler
            AddHandler ftp.ValidatingCertificate, AddressOf ConsoleVerifier.ValidatingCertificate
            ftp.Secure()
            Return True
        Catch e As TlsException
            Console.WriteLine(e.Message)
            Console.WriteLine("TLS negotiation failed, disconnecting.")
            ftp.Disconnect()
            Return False
        Catch e As FtpException
            Console.WriteLine(e.Message)
            If e.Response Is Nothing Then
                Console.WriteLine("TLS negotiation failed.")
                Console.WriteLine(e)
                Return False
            End If
            If e.Response.Code = 500 Then
                Console.WriteLine("Server does not support explicit SSL/TLS security.")
                Return True
            End If
            Return False
        End Try

    End Function

    Private Sub ClearCommandChannel()
        If Ftp.State = FtpState.Disconnected Then
            Console.WriteLine("Not connected.")
            Return
        End If

        If Not Ftp.IsSecured Then
            Console.WriteLine("The control connection is not secured.")
            Return
        End If

        Ftp.ClearCommandChannel()
    End Sub 'ClearCommandChannel

    Private Sub Login(ByVal user As String)
        If user Is Nothing OrElse user.Length = 0 Then
            Console.Write("User: ")
            user = Console.ReadLine()
        End If

        Console.Write("Password: ")
        Dim pass As String = ReadLine()

        Ftp.Login(user, pass)
    End Sub 'Login


    Private Sub Pwd()
        If Ftp.State = FtpState.Disconnected Then
            Console.WriteLine("Not connected.")
            Return
        End If

        Ftp.GetCurrentDirectory()
    End Sub 'Pwd


    Private Sub Chmod(ByVal param As String)
        If Ftp.State = FtpState.Disconnected Then
            Console.WriteLine("Not connected.")
            Return
        End If

        If param Is Nothing OrElse param.Length = 0 Then
            Console.WriteLine("Usage: chmod mode filename")
            Return
        End If

        Ftp.Site(("CHMOD " + param))
    End Sub 'Chmod



    Private Sub Cd(ByVal param As String)
        If Ftp.State = FtpState.Disconnected Then
            Console.WriteLine("Not connected.")
            Return
        End If

        Ftp.ChangeDirectory(param)
    End Sub 'Cd


    Private Sub Quit()
        If Ftp.State <> FtpState.Disconnected Then
            Close()
        End If
        Console.WriteLine()
    End Sub 'Quit


    Private Sub Ascii()
        If Ftp.State = FtpState.Disconnected Then
            Console.WriteLine("Not connected.")
            Return
        End If

        Ftp.TransferType = FtpTransferType.Ascii
    End Sub 'Ascii


    Private Sub Active()
        Ftp.Passive = False
        Console.WriteLine("Passive mode turned off.")
    End Sub 'Active


    Private Sub Passive()
        Ftp.Passive = True
        Console.WriteLine("Passive mode turned on.")
    End Sub 'Passive


    Private Sub [Binary]()
        If Ftp.State = FtpState.Disconnected Then
            Console.WriteLine("Not connected.")
            Return
        End If

        Ftp.TransferType = FtpTransferType.Binary
    End Sub 'Binary

    Private Sub Mkdir(ByVal param As String)
        If Ftp.State = FtpState.Disconnected Then
            Console.WriteLine("Not connected.")
            Exit Sub
        End If

        If param Is Nothing OrElse param.Length = 0 Then
            Console.WriteLine("Usage: mkdir dirname")
            Return
        End If

        Ftp.CreateDirectory(param)
    End Sub 'Mkdir

    Private Sub Rmdir(ByVal param As String)
        If Ftp.State = FtpState.Disconnected Then
            Console.WriteLine("Not connected.")
            Exit Sub
        End If

        If param Is Nothing OrElse param.Length = 0 Then
            Console.WriteLine("Usage: rmdir dirname")
            Return
        End If

        Ftp.RemoveDirectory(param)
    End Sub 'Rmdir

    '' <summary>
    '' Removes a whole directory tree. Use with care.
    '' </summary>
    '' <param name="ftp">An instance of Ftp object.</param>
    '' <param name="path">A path to a directory to remove.</param>
    Public Shared Sub RemoveTree(ByVal ftp As Ftp, ByVal path As String)
        '' Save the current directory and change it to 'path'.
        Dim original As String = ftp.GetCurrentDirectory()
        ftp.ChangeDirectory(path)

        '' Get this list of files and directories
        Dim list As FtpItemCollection = ftp.GetList()

        '' Delete all files and directories in the list recursively
        Dim i As Integer
        For i = 0 To list.Count - 1
            If list(i).IsDirectory Then
                RemoveTree(ftp, list(i).Name)
            Else
                ftp.DeleteFile(list(i).Name)
            End If
        Next i

        '' Go back to the original directory
        ftp.ChangeDirectory(original)

        '' Remove the directory that was just cleaned
        ftp.RemoveDirectory(path)
    End Sub ' RemoveTree


    Private Sub Rmtree(ByVal param As String)
        If Ftp.State = FtpState.Disconnected Then
            Console.WriteLine("Not connected.")
            Exit Sub
        End If

        If param Is Nothing OrElse param.Length = 0 Then
            Console.WriteLine("Usage: rmtree dirname")
            Return
        End If

        RemoveTree(Ftp, param)
    End Sub 'Rmtree

    Private Sub Dir()
        If Ftp.State = FtpState.Disconnected Then
            Console.WriteLine("Not connected.")
            Return
        End If

        lastReport = DateTime.Now
        Dim list As FtpItemCollection = ftp.GetList()

        Dim i As Integer
        For i = 0 To list.Count - 1
            Dim item As FtpItem = list(i)
            If item.IsLink Then
                Console.Write("s ")
            ElseIf item.IsDirectory Then
                Console.Write("d ")
            Else
                Console.Write("- ")
            End If

            If item.Permissions.HasValue Then
                Console.Write((Convert.ToString(CType(item.Permissions, Integer), 8).PadLeft(3, "0"c) + " "))
            Else
                Console.Write("??? ")
            End If
            Console.Write(item.LastWriteTime.Value.ToString("u").Substring(0, 16))
            Console.Write(item.Length.ToString().PadLeft(10, " "c))
            Console.Write(" {0}", item.Name)
            If item.IsLink Then
                Console.Write((" -> " + item.SymlinkPath))
            End If
            Console.WriteLine()
        Next i
    End Sub 'Dir


    Private Sub Ls()
        If Ftp.State = FtpState.Disconnected Then
            Console.WriteLine("Not connected.")
            Return
        End If

        lastReport = DateTime.Now
        Dim list As FtpItemCollection = ftp.GetList()

        Dim i As Integer
        For i = 0 To list.Count - 1
            Console.WriteLine(list(i).Name)
        Next i
    End Sub 'Ls


    Private Sub [Get](ByVal remotePath As String)
        If Ftp.State = FtpState.Disconnected Then
            Console.WriteLine("Not connected.")
            Return
        End If

        If remotePath Is Nothing OrElse remotePath.Length = 0 Then
            Console.WriteLine("Usage: get remotePath")
            Return
        End If

        Try
            lastReport = DateTime.Now
            Ftp.GetFile(remotePath, Path.GetFileName(remotePath))
        Catch e As Exception
            If TypeOf e Is FtpException Then Throw
            Console.WriteLine(e.Message)
        End Try
    End Sub 'Get



    Private Sub GetResume(ByVal remotePath As String)
        If Ftp.State = FtpState.Disconnected Then
            Console.WriteLine("Not connected.")
            Return
        End If

        If remotePath Is Nothing OrElse remotePath.Length = 0 Then
            Console.WriteLine("Usage: getr remotePath")
            Return
        End If

        Dim localStream As FileStream = Nothing
        Try
            ' Get local file name corresponding to remote file name.
            Dim localPath As String = Path.GetFileName(remotePath)

            ' Get the length of remote file. This will throw exception
            ' if the file does not exists.
            Dim remoteLength As Long = Ftp.GetFileLength(remotePath)

            ' Create or open the local file for writing and seek to the end.
            localStream = File.Open(localPath, FileMode.Append, FileAccess.Write, FileShare.Read)

            ' Check lengths too see if the resume makes sense.
            If localStream.Length = remoteLength Then
                Console.WriteLine("File has already been downloaded.")
            ElseIf localStream.Length > remoteLength Then
                Console.WriteLine("Local file is longer than remote file.")
            Else
                ' Display resume info if we are really resuming download.
                If localStream.Length > 0 Then
                    Console.WriteLine(("Download resumed at offset " + localStream.Length + "."))
                End If
                lastReport = DateTime.Now

                ' Download the file. Start at the position of the length of the local stream.
                Ftp.GetFile(remotePath, localStream, localStream.Length)
            End If
        Catch e As Exception
            If TypeOf e Is FtpException Then Throw
            Console.WriteLine(e.Message)
        Finally
            ' Close the stream if it was created.
            If Not (localStream Is Nothing) Then
                localStream.Close()
            End If
        End Try
    End Sub 'GetResume

    '' <summary>
    '' Uploads a local directory with all files and subdirectories to an FTP server.
    '' </summary>
    '' <param name="ftp">Instance of an Ftp object.</param>
    '' <param name="localPath">The local path of the root of the directory structure to be uploaded.</param>
    '' <remarks>
    '' <p>
    '' Ftp object must be connected and should have the directory set to the desired path.
    '' </p>
    '' <p>
    '' Method reports information to console. This behaviour should be changed to reflect actual needs.
    '' </p>
    '' </remarks>
    Public Shared Sub UploadDirectory(ByVal ftp As Ftp, ByVal localPath As String)
        ' strip ending slashes (GetFileName returns empty string otherwise...)
        localPath = localPath.TrimEnd("\"c, "/"c)

        Console.WriteLine(("Uploading directory: '" + localPath + "'"))

        ' get the name of the directory and try to create it
        Dim remotePath As String = Path.GetFileName(localPath)
        Try
            ftp.CreateDirectory(remotePath)
        Catch e As FtpException
            ' throw exception if an error other than ProtocolError occurs
            If e.Status <> FtpExceptionStatus.ProtocolError Then Throw

            ' protocol error - directory probably already exists
            Console.WriteLine(e.Message)
        End Try

        ' change the directory
        ftp.ChangeDirectory(remotePath)

        ' upload all files from the localPath directory
        Dim files As String() = Directory.GetFiles(localPath)
        Dim i As Integer
        For i = 0 To files.Length - 1
            Console.WriteLine(("Uploading file: '" + files(i) + "'"))
            Try
                ftp.PutFile(files(i), Path.GetFileName(files(i)))
            Catch e As FtpException
                ' throw exception if an error other than ProtocolError occurs
                If e.Status <> FtpExceptionStatus.ProtocolError Then Throw

                ' protocol error - report it and continue with the remaining files
                Console.WriteLine(e.Message)
            End Try
        Next i

        ' upload all directories from the localPath directory
        Dim dirs As String() = Directory.GetDirectories(localPath)
        For i = 0 To dirs.Length - 1
            UploadDirectory(ftp, dirs(i))
        Next i
        ftp.ChangeDirectory("..")
    End Sub 'UploadDirectory


    Private Sub PutDir(ByVal localPath As String)
        If Ftp.State = FtpState.Disconnected Then
            Console.WriteLine("Not connected.")
            Return
        End If

        If localPath Is Nothing OrElse localPath.Length = 0 Then
            Console.WriteLine("Usage: putdir localPath")
            Return
        End If

        If Not Directory.Exists(localPath) Then
            Console.WriteLine("Path does not exist.")
            Return
        End If

        lastReport = DateTime.Now
        UploadDirectory(Ftp, localPath)
    End Sub 'PutDir


    Private Shared Sub DownloadDirectory(ByVal ftp As Ftp, ByVal remotePath As String)
        ' strip ending slashes (GetFileName returns empty string otherwise...)
        remotePath = remotePath.TrimEnd("\", "/")

        Console.WriteLine("Downloading directory: '" & remotePath & "'")

        ' get the name of the directory and try to create it
        Dim localPath As String = Path.GetFileName(remotePath)
        Try
            Directory.CreateDirectory(localPath)
        Catch e As Exception
            ' error - directory probably already exists
            Console.WriteLine(e.Message)
        End Try

        ' change the directory
        Directory.SetCurrentDirectory(localPath)
        ftp.ChangeDirectory(remotePath)

        ' download all files from the remotePath directory
        Dim files As FtpItemCollection = ftp.GetList()
        Dim i As Integer
        For i = 0 To files.Count - 1
            If files(i).IsFile Then
                Console.WriteLine("Downloading file: '" & files(i).Name & "'")
                Try
                    If Not File.Exists(files(i).Name) Then
                        ftp.GetFile(files(i).Name, files(i).Name)
                    End If
                Catch e As FtpException
                    ' throw exception if an error other than ProtocolError occurs
                    If e.Status <> FtpExceptionStatus.ProtocolError Then Throw

                    ' protocol error - report it and continue with the remaining files
                    Console.WriteLine(e.Message)
                End Try
            End If
        Next

        ' download all directories from the remotePath directory
        For i = 0 To i < files.Count - 1
            If files(i).IsDirectory Then
                DownloadDirectory(ftp, files(i).Name)
            End If
        Next

        Directory.SetCurrentDirectory("..")
        ftp.ChangeDirectory("..")
    End Sub 'DownloadDirectory

    Private Sub GetDir(ByVal remotePath As String)
        If Ftp.State = FtpState.Disconnected Then
            Console.WriteLine("Not connected.")
            Exit Sub
        End If

        If remotePath = Nothing OrElse remotePath.Length = 0 Then
            Console.WriteLine("Usage: getdir remotePath")
            Exit Sub
        End If

        lastReport = DateTime.Now
        DownloadDirectory(Ftp, remotePath)
    End Sub 'GetDir

    Private Sub Put(ByVal localPath As String)
        If Ftp.State = FtpState.Disconnected Then
            Console.WriteLine("Not connected.")
            Return
        End If

        If localPath Is Nothing OrElse localPath.Length = 0 Then
            Console.WriteLine("Usage: put localPath")
            Return
        End If

        Try
            lastReport = DateTime.Now
            Ftp.PutFile(localPath, Path.GetFileName(localPath))
        Catch e As Exception
            If TypeOf e Is FtpException Then Throw

            Console.WriteLine(e.Message)
        End Try
    End Sub 'Put

    Private Sub PutResume(ByVal localPath As String)
        If Ftp.State = FtpState.Disconnected Then
            Console.WriteLine("Not connected.")
            Return
        End If

        If localPath Is Nothing OrElse localPath.Length = 0 Then
            Console.WriteLine("Usage: putr localPath")
            Return
        End If

        Dim localStream As FileStream = Nothing
        Try
            ' Get local file name corresponding to remote file name.
            Dim remotePath As String = Path.GetFileName(localPath)

            ' Try to get the length of the remote file. If the file
            ' does not exist, it will throw exception with status
            ' of ProtocolError and response code 550. Ignore this
            ' exception and report the others.
            Dim remoteLength As Long = 0
            Try
                remoteLength = Ftp.GetFileLength(remotePath)
            Catch e As FtpException
                If e.Status <> FtpExceptionStatus.ProtocolError OrElse e.Response.Code <> 550 Then Throw
            End Try
            ' Open the local file for reading.
            localStream = File.OpenRead(localPath)

            ' Check lengths too see if the resume makes sense.
            If localStream.Length = remoteLength Then
                Console.WriteLine("File has already been uploaded.")
            ElseIf localStream.Length < remoteLength Then
                Console.WriteLine("Remote file is longer than local file.")
            Else
                ' Display resume info if we are really resuming upload.
                If remoteLength > 0 Then
                    Console.WriteLine(("Upload resumed at offset " + remoteLength + "."))
                End If
                lastReport = DateTime.Now

                ' Upload the file. Start at the position of the length of the remote stream.
                localStream.Position = remoteLength
                Ftp.PutFile(localStream, remotePath, remoteLength, -1)
            End If
        Catch e As Exception
            If TypeOf e Is FtpException Then Throw

            Console.WriteLine(e.Message)
        Finally
            ' Close the stream if it was created.
            If Not (localStream Is Nothing) Then
                localStream.Close()
            End If
        End Try
    End Sub 'PutResume

    Public Sub Run()
        While True
            Console.Write("ftp> ")
            Dim command As String = Console.ReadLine().Trim()
            Dim param As String = Nothing
            Dim i As Integer = command.IndexOf(" "c)
            If i > 0 Then
                param = command.Substring((i + 1))
                command = command.Substring(0, i)
            End If

            Try
                Select Case command.ToLower()
                    Case "!"
                        GoTo Casequit
                    Case "?"
                        GoTo Casehelp
                    Case "bye"
                        GoTo Casequit
                    Case "exit"
                        GoTo Casequit
                    Case "disconnect"
                        GoTo Caseclose
                    Case "quit"
Casequit:

                        Quit()
                        Return
                    Case "close"
Caseclose:

                        Close()
                    Case "open"
                        If Not Open(param) Then
                            Exit Select
                        End If
                        param = Nothing
                        GoTo Caseauth

                    Case "auth"
CaseAuth:
                        If Not Auth() Then Exit Select
                        GoTo Caseuser

                    Case "user"
Caseuser:
                        Login(param)
                    Case "help"
Casehelp:

                        Help()
                    Case "ccc"
                        ClearCommandChannel()
                    Case "ascii"
                        Ascii()
                    Case "binary"
                        [Binary]()
                    Case "active"
                        Active()
                    Case "passive"
                        Passive()
                    Case "cd"
                        Cd(param)
                    Case "pwd"
                        Pwd()
                    Case "chmod"
                        Chmod(param)
                    Case "mkdir"
                        Mkdir(param)
                    Case "rmdir"
                        Rmdir(param)
                    Case "rmtree"
                        Rmtree(param)
                    Case "get"
                        [Get](param)
                    Case "put"
                        Put(param)
                    Case "getr"
                        GetResume(param)
                    Case "putr"
                        PutResume(param)
                    Case "putdir"
                        PutDir(param)
                    Case "getdir"
                        GetDir(param)
                    Case "dir"
                        Dir()
                    Case "ls"
                        If Not (param Is Nothing) AndAlso param.StartsWith("-l") Then
                            Dir()
                        Else
                            Ls()
                        End If
                    Case Else
                        Console.WriteLine("Invalid command.")
                End Select
            Catch e As FtpException
                If e.Status <> FtpExceptionStatus.ProtocolError And e.Status <> FtpExceptionStatus.ConnectionClosed Then
                    Console.WriteLine(e.ToString())
                    Ftp.Disconnect()
                End If
            End Try
        End While
    End Sub 'Run

    Public Shared Sub Main()
            ' set the trial license key
            Rebex.Licensing.Key = Rebex.Samples.TrialKey.Key


        Dim args() As String = Environment.GetCommandLineArgs()

        Dim client As New ConsoleClient

        If args.Length > 1 Then
            Dim param As String = args(1)
            Dim i As Integer
            For i = 2 To args.Length - 1
                param += " " + args(i)
            Next i
            If client.Open(param) Then
                client.Login(Nothing)
            End If
        End If
        client.Run()
    End Sub 'Main
End Class 'ConsoleClient
