FTP client 2

'
'    This class is a FTP client. It provides limited features and is only supporting passive mode.
'    It works well with logging in to the proxy server.
'
'    Written By: Peter Ng
'    Date Last Modified: 2013.08.01
'
'    Ref:
'    (1) Network Programming in .NET: With C# and Visual Basic .NET by Fiach Reid
'    (2) http://www.codeproject.com/Articles/293391/File-Transfer-Protocol-FTP-Client
'

Imports System
Imports System.Collections.Generic
Imports System.IO
Imports System.Net.Sockets
Imports System.Text

Public Class TinyFTP
    Private ms_host As String
    Private mi_port As Integer
    Private ms_username As String
    Private ms_password As String
    Private mi_timeout As Integer

    Private ms_current_directory As String = String.Empty

    Private m_ftp As _FTP


    Private Class _FTP
        Private Const BUFFER_SIZE As Long = 8192

        Private m_command_port As TcpClient
        Private m_data_port As TcpClient

        Public Sub New(ByVal host As String, ByVal port As Integer, ByVal username As String, ByVal password As String, ByVal timeout As Integer)
            open(host, port, username, password, timeout)
        End Sub

        Protected Sub open(ByVal host As String, ByVal port As Integer, ByVal username As String, ByVal password As String, ByVal timeout As Integer)
            Dim responses As String()

            '
            ' open connection
            '
            m_command_port = New TcpClient(host, port)
            m_command_port.ReceiveTimeout = timeout
            m_command_port.SendTimeout = timeout
            responses = read_ftp_command_responses()
            If responses(0).Substring(0, 3)  "220" Then
                Throw New IOException(String.Join(vbCrLf, responses))
            End If

            '
            ' login
            '
            responses = send_ftp_command("USER " + username)
            Select Case responses(0).Substring(0, 3)
                Case Is = "230"
                    ' user logged in and does not need password

                Case Is = "331"
                    ' password needed 
                    responses = send_ftp_command("PASS " & password)
                    If Not (responses(0).Substring(0, 3) = "230" OrElse responses(0).Substring(0, 3) = "202") Then
                        Throw New IOException(String.Join(vbCrLf, responses))
                    End If

                Case Else
                    Throw New IOException(String.Join(vbCrLf, responses))
            End Select

            '
            ' open data port in passive mode
            '
            responses = send_ftp_command("PASV")
            If responses(0).Substring(0, 3)  "227" Then
                Throw New IOException(String.Join(vbCrLf, responses))
            End If

            ' sample FTP replay
            ' 227 Entering Passive Mode (127,0,0,1,4,147).
            Dim index1 As Integer = responses(0).IndexOf("("c)
            Dim index2 As Integer = responses(0).IndexOf(")"c, index1)
            Dim values As String() = responses(0).Substring(index1 + 1, index2 - index1 - 1).Split(New Char() {","c})

            Dim data_ip As String = values(0) + "." + values(1) + "." + values(2) + "." + values(3)
            Dim high_byte As Integer = Integer.Parse(values(4)) * 256
            Dim low_byte As Integer = Integer.Parse(values(5))
            Dim data_port As Integer = high_byte + low_byte

            m_data_port = New TcpClient(data_ip, data_port)
            m_data_port.ReceiveTimeout = timeout
            m_data_port.SendTimeout = timeout
        End Sub

        Public Function send_ftp_command(ByVal cmd As String) As String()
            Return send_ftp_command(cmd, True)
        End Function

        Public Function send_ftp_command(ByVal cmd As String, ByVal return_responses As Boolean) As String()
            Dim s As NetworkStream = m_command_port.GetStream()
            ' prepare command
            Dim buffer() As Byte = Encoding.ASCII.GetBytes((cmd + vbCrLf).ToCharArray())

            ' send command over the network
            s.Write(buffer, 0, buffer.Length)

            If return_responses Then
                Return read_ftp_command_responses()
            Else
                Return Nothing
            End If
        End Function

        Public Function read_ftp_command_responses() As String()
            Dim c As New List(Of String)
            Dim s As NetworkStream = m_command_port.GetStream()
            Dim sr As StreamReader = New StreamReader(s)

            Do
                c.Add(sr.ReadLine())
            Loop While s.DataAvailable

            Return c.ToArray()
        End Function

        Public Sub read_ftp_data(ByRef s As Stream)
            stream_ftp_data(m_data_port.GetStream(), s)
        End Sub

        Public Sub send_ftp_data(ByRef s As Stream)
            stream_ftp_data(s, m_data_port.GetStream())
        End Sub

        Private Sub stream_ftp_data(ByRef input_stream As Stream, ByRef output_stream As Stream)
            Dim buffer As Byte() = New Byte(BUFFER_SIZE - 1) {}
            Dim bytes_read As Integer

            Do
                bytes_read = input_stream.Read(buffer, 0, BUFFER_SIZE)
                output_stream.Write(buffer, 0, bytes_read)
            Loop While bytes_read > 0
        End Sub

        Public Sub close()
            If m_data_port IsNot Nothing Then
                Try
                    m_data_port.Close()
                Finally
                    m_data_port = Nothing
                End Try
            End If

            If m_command_port IsNot Nothing Then
                Try
                    m_command_port.Close()
                Finally
                    m_command_port = Nothing
                End Try
            End If
        End Sub

        Protected Overrides Sub Finalize()
            close()
            MyBase.Finalize()
        End Sub
    End Class

    Public Sub New(ByVal host As String, ByVal port As Integer, ByVal username As String, ByVal password As String)
        Me.New(host, port, username, password, 5000)
    End Sub

    Public Sub New(ByVal host As String, ByVal port As Integer, ByVal username As String, ByVal password As String, ByVal timeout As Integer)
        ms_host = host
        mi_port = port
        ms_username = username
        ms_password = password
        mi_timeout = timeout

        Connect()
    End Sub

    Public Sub Close()
        If m_ftp IsNot Nothing Then
            Try
                m_ftp.close()
            Catch
                Threading.Thread.Sleep(10000)
            Finally
                m_ftp = Nothing
            End Try
        End If
    End Sub

    Public Sub Connect()
        Close()
        m_ftp = New _FTP(ms_host, mi_port, ms_username, ms_password, mi_timeout)

        If Not String.IsNullOrEmpty(ms_current_directory) Then
            Change_Directory(ms_current_directory)
        End If
    End Sub

    Public Sub Change_Directory(ByVal remote_path As String)
        If Not (remote_path = "" OrElse remote_path = ".") Then
            Dim responses As String() = m_ftp.send_ftp_command("CWD " + remote_path)
            If Not responses(0).Substring(0, 3) = "250" Then
                Throw New IOException(String.Join(vbCrLf, responses))
            End If

            ms_current_directory = remote_path
        End If
    End Sub

    Public Sub Download(ByVal filename As String, ByVal local_path As String)
        Set_Binary()

        ' prepare file stream
        Dim fs As FileStream = New FileStream(local_path, FileMode.CreateNew)

        Dim responses As String()

        Try
            m_ftp.send_ftp_command("RETR " + filename, False)
            ' assuming the responses are good
            m_ftp.read_ftp_data(CType(fs, Stream))
            responses = m_ftp.read_ftp_command_responses()
        Catch ex As Exception
            Throw ex
        Finally
            fs.Close()
        End Try

        For Each s As String In responses
            Select Case s.Length
                Case 0
                    ' fine
                Case Is < 3
                    ' unexpected problems
                    Throw New IOException(String.Join(vbCrLf, responses))
                Case Else
                    Dim m As String = s.Substring(0, 3)
                    If Not (m = "150" OrElse m = "226" OrElse m = "125") Then
                        Throw New IOException(String.Join(vbCrLf, responses))
                    End If
            End Select
        Next

        ' reconnect after using data port
        Connect()
    End Sub

    Public Sub Upload(ByVal local_path As String, ByVal wait_for_complete As Boolean)
        Upload(Path.GetFileName(local_path), local_path, wait_for_complete)
    End Sub

    Public Sub Upload(ByVal local_path As String)
        Upload(Path.GetFileName(local_path), local_path, True)
    End Sub

    Public Sub Upload(ByVal filename As String, ByVal local_path As String)
        Upload(filename, local_path, True)
    End Sub

    Public Sub Upload(ByVal filename As String, ByVal local_path As String, ByVal wait_for_complete As Boolean)
        Set_Binary()

        ' prepare file stream
        Dim fs As FileStream = New FileStream(local_path, FileMode.Open, FileAccess.Read, FileShare.Read)

        Dim responses As String()

        Try
            m_ftp.send_ftp_command("STOR " + filename, False)
            ' assuming the responses are good
            m_ftp.send_ftp_data(CType(fs, Stream))
            responses = m_ftp.read_ftp_command_responses()
        Catch ex As Exception
            Throw ex
        Finally
            fs.Close()
        End Try

        Console.WriteLine(String.Join(vbCrLf, responses))

        For Each s As String In responses
            Select Case s.Length
                Case 0
                    ' fine
                Case Is < 3
                    ' unexpected problems
                    Throw New IOException(String.Join(vbCrLf, responses))
                Case Else
                    Dim m As String = s.Substring(0, 3)
                    If Not (m = "150" OrElse m = "226" OrElse m = "125") Then
                        Throw New IOException(String.Join(vbCrLf, responses))
                    End If
            End Select
        Next
        ' reconnect after using data port
        Connect()

        If wait_for_complete Then
            If Not Wait_for_Upload_Complete(filename, local_path) Then
                Throw New IOException("problem during uploading file '" + local_path + "' to the host")
            End If
        End If
    End Sub

    Private Function Wait_for_Upload_Complete(ByVal filename As String, ByVal local_path As String) As Boolean
        '
        '  Notes: Uploading files via proxy servers may have a lot of problem.  This function
        '         is try to resolve the time related issue.
        '

        Dim result As Boolean = False

        Dim threshold As Integer = 40000
        Const threshold_multiplier As Double = 1.15

        Dim filesize As Long = (New FileInfo(local_path)).Length

        Dim p As Dictionary(Of String, Object)

        Do
            Threading.Thread.Sleep(threshold)

            Try
                If m_ftp Is Nothing Then Connect()

                p = File_Properties(filename)

                If CLng(p("size").ToString()) = filesize Then
                    result = True
                    Exit Do
                End If

            Catch
                Threading.Thread.Sleep(3000)
                Connect()

            Finally
                threshold = CInt(Math.Floor(threshold * threshold_multiplier))
            End Try
        Loop While threshold <= mi_timeout

        Return result
    End Function

    Public Function File_List() As String()
        Return File_List("")
    End Function

    Public Function File_List(ByVal directory As String) As String()
        Dim ms As MemoryStream = New MemoryStream()

        ' prepare FTP command
        Dim cmd As String = "LIST"
        If Not String.IsNullOrEmpty(directory) Then
            cmd = cmd + " " + directory
        End If

        m_ftp.send_ftp_command(cmd, False)
        m_ftp.read_ftp_data(CType(ms, Stream))
        Dim responses As String() = m_ftp.read_ftp_command_responses()

        For Each s As String In responses
            Select Case s.Length
                Case 0
                    ' fine
                Case Is < 3
                    ' unexpected problems
                    Throw New IOException(String.Join(vbCrLf, responses))
                Case Else
                    Dim m As String = s.Substring(0, 3)
                    If Not (m = "150" OrElse m = "226" OrElse m = "125") Then
                        Throw New IOException(String.Join(vbCrLf, responses))
                    End If
            End Select
        Next


        Dim mr As StreamReader = New StreamReader(ms)
        ms.Position = 0
        Dim r As String() = mr.ReadToEnd().Split(New String() {vbCrLf, vbLf}, StringSplitOptions.RemoveEmptyEntries)

        ' reconnect after using data port
        Connect()

        Return r
    End Function

    Public Function Filename_List() As String()
        Return Filename_List("")
    End Function

    Public Function Filename_List(ByVal directory As String) As String()
        Dim ms As MemoryStream = New MemoryStream()

        ' prepare FTP command
        Dim cmd As String = "NLST"
        If Not String.IsNullOrEmpty(directory) Then
            cmd = cmd + " " + directory
        End If

        m_ftp.send_ftp_command(cmd, False)
        m_ftp.read_ftp_data(CType(ms, Stream))
        Dim responses As String() = m_ftp.read_ftp_command_responses()

        For Each s As String In responses
            Select Case s.Length
                Case 0
                    ' fine
                Case Is < 3
                    ' unexpected problems
                    Throw New IOException(String.Join(vbCrLf, responses))
                Case Else
                    Dim m As String = s.Substring(0, 3)
                    If Not (m = "150" OrElse m = "226" OrElse m = "125") Then
                        Throw New IOException(String.Join(vbCrLf, responses))
                    End If
            End Select
        Next


        Dim mr As StreamReader = New StreamReader(ms)
        ms.Position = 0
        Dim r As String() = mr.ReadToEnd().Split(New String() {vbCrLf, vbLf}, StringSplitOptions.RemoveEmptyEntries)

        ' reconnect after using data port
        Connect()

        Return r
    End Function

    Public Function File_Properties(ByVal filename As String) As Dictionary(Of String, Object)
        Dim ms As MemoryStream = New MemoryStream()

        ' validate file name
        If String.IsNullOrEmpty(filename) Then
            Throw New Exception("missing file name")
        ElseIf filename.Contains("*") Then
            Throw New Exception("the method does not support wildcard")
        End If

        ' prepare FTP command
        Dim cmd As String = "LIST" + " " + filename
        Dim responses As String() = m_ftp.send_ftp_command(cmd)

        For Each s As String In responses
            Select Case s.Length
                Case 0
                    ' fine
                Case Is < 3
                    ' unexpected problems
                    Throw New IOException(String.Join(vbCrLf, responses))
                Case Else
                    Dim m As String = s.Substring(0, 3)
                    If Not (m = "150" OrElse m = "226" OrElse m = "125") Then
                        Throw New IOException(String.Join(vbCrLf, responses))
                    End If
            End Select
        Next

        m_ftp.read_ftp_data(CType(ms, Stream))
        Dim mr As StreamReader = New StreamReader(ms)
        ms.Position = 0
        ' expecting only one line
        Dim lines As String() = mr.ReadToEnd().Split(New String() {vbCrLf, vbLf}, StringSplitOptions.RemoveEmptyEntries)
        If lines.Length  1 Then
            Throw New Exception("invalid number of files" + vbCrLf + "    messages: " + String.Join("; ", lines))
        End If

        Dim parts As String() = lines(0).Split(New String() {" "}, StringSplitOptions.RemoveEmptyEntries)
        If parts.Length < 8 Then
            Throw New Exception("unexpected number of results")
        End If

        '
        ' parse data
        '
        Dim c As New Dictionary(Of String, Object)
        c.Add("filename", filename)
        c.Add("size", parts(4))
        If parts(7).Contains(":") Then
            ' in case there is a time
            c.Add("date", DateTime.Parse((parts(5) + " " + parts(6) + " " + DateTime.Now.Year.ToString() + " " + parts(7))))
        Else
            c.Add("date", DateTime.Parse((parts(5) + " " + parts(6) + " " + parts(7))))
        End If

        ' reconnect after using data port
        Connect()

        Return c
    End Function

    Public Sub Set_Binary()
        send_simple_ftp_command("TYPE I")
    End Sub

    Public Sub send_simple_ftp_command(ByVal cmd As String)
        Dim responses As String() = m_ftp.send_ftp_command(cmd)
        If Not responses(0).Substring(0, 3) = "200" Then
            Throw New IOException(String.Join(vbCrLf, responses))
        End If
    End Sub

    Protected Overrides Sub Finalize()
        Me.Close()
        MyBase.Finalize()
    End Sub
End Class
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s