Search code examples
vb.netwinformsftpprogress-barbackgroundworker

Get File Size on FTP Server and put it on a Label


I'm trying to get the size of a file that is hosted on a FTP Server and put it in a Label while the `BackgroundWorker works in the background.

I'm using "Try" to get the value, however the value is caught on the first attempt. After downloading, if I press to try to get it again then it works.

Note: The progress bar also does not work on the first try.

Image

enter image description here

What I have tried:

Private Sub BWorkerD_DoWork(sender As Object, e As System.ComponentModel.DoWorkEventArgs) Handles BWorkerD.DoWork

    Dim buffer(1023) As Byte
    Dim bytesIn As Integer
    Dim totalBytesIn As Integer
    Dim output As IO.Stream
    Dim flLength As Integer

    ''TRY TO GET FILE SIZE''

    Try
        Dim FTPRequest As FtpWebRequest = DirectCast(WebRequest.Create(txtFilePathD.Text), FtpWebRequest)
        FTPRequest.Credentials = New NetworkCredential(txtFTPUsernameD.Text, txtFTPPasswordD.Text)
        FTPRequest.Method = Net.WebRequestMethods.Ftp.GetFileSize

        flLength = CInt(FTPRequest.GetResponse.ContentLength)
        lblFileSizeD.Text = flLength & " bytes"

    Catch ex As Exception

    End Try

    Try
        Dim FTPRequest As FtpWebRequest = DirectCast(WebRequest.Create(txtFilePathD.Text), FtpWebRequest)
        FTPRequest.Credentials = New NetworkCredential(txtFTPUsernameD.Text, txtFTPPasswordD.Text)
        FTPRequest.Method = WebRequestMethods.Ftp.DownloadFile
        Dim stream As IO.Stream = FTPRequest.GetResponse.GetResponseStream
        Dim OutputFilePath As String = txtSavePathD.Text & "\" & IO.Path.GetFileName(txtFilePathD.Text)
        output = IO.File.Create(OutputFilePath)
        bytesIn = 1

        Do Until bytesIn < 1
            bytesIn = stream.Read(buffer, 0, 1024)
            If bytesIn > 0 Then
                output.Write(buffer, 0, bytesIn)
                totalBytesIn += bytesIn
                lblDownloadedBytesD.Text = totalBytesIn.ToString & " bytes"
                If flLength > 0 Then
                    Dim perc As Integer = (totalBytesIn / flLength) * 100
                    BWorkerD.ReportProgress(perc)
                End If
            End If
        Loop

        output.Close()
        stream.Close()

    Catch ex As Exception
        MessageBox.Show(ex.Message)
    End Try
End Sub

''UPDATE EVERY PROGRESS - DONT WORK ON FIRST TRY''

Private Sub BWorkerD_ProgressChanged(sender As Object, e As System.ComponentModel.ProgressChangedEventArgs) Handles BWorkerD.ProgressChanged

    pBarD.Value = e.ProgressPercentage
    lblPercentD.Text = e.ProgressPercentage & " %"
End Sub

Solution

  • The main problems (set Option Strict On to find more):
    You can't access the UI objects from a thread different than the UI Thread.

    The error you receive is:

    Cross-thread operation not valid:
    Control lblFileSizeD accessed from a thread other than the thread it was created on

    Then, the same error for lblDownloadedBytesD.

    Also, you are eating up your Error messages using an empty handler with:

    Catch ex As Exception
    
    End Try
    

    This nullifies any handling, because there's none. You are simply letting the code run past it without taking any action. The handlers are there to, well, handle the errors, not to let them go unchecked.

    When you need to access and update some UI component property, use the BackGroundWorker ReportProgress() method. This method has an overload that accepts a parameter of type Object. Meaning, you can feed it anything. This Object will be the e.UserState property in the ReportProgress ProgressChangedEventArgs class.

    The .RunWorkerAsync() method also accepts an Object parameter. This Object will become the e.Argument property of the BackgroundWorker.DoWork Event. This gives some flexibility in relation to the parameters you can actually pass to your BackGroundWorker.

    One more problem: the Ftp Download procedure does not support cancellation. When run, a user can't stop it.

    Last problem: as reported in the documentation, you should never reference the BackGroundWorker object you instantiated in your UI thread (the Form) in its DoWork event. Use the sender object and cast it to the BackGroundWorker class.

    In this example, all the UI references are delegated to a Class object that is passed to the DoWork event through the RunWorkerAsync(Object) method (using the e.Argument property).
    The Class object is updated with progress details and then fed to the ReportProgress(Int32, Object) method, which runs in the original Synchronization Context (the UI thread, where the RunWorkerAsync method is called).
    The UI can be updated safely. No cross-thread operations can occur.

    A cancellation method is also implemented. This allows to abort the download procedure and to delete a partial downloaded file, if one is created.

    The error handling is minimal, but this is something you need to integrate with your own tools.

    (I've used the same names for the UI Controls, it should be easier to test.)

    Imports System.ComponentModel
    Imports System.Globalization
    Imports System.IO
    Imports System.Net
    Imports System.Net.Security
    Imports System.Security.Cryptography.X509Certificates
    
    Public Class frmBGWorkerDownload
    
        Friend WithEvents BWorkerD As BackgroundWorker
        Public Sub New()
            InitializeComponent()
            BWorkerD = New BackgroundWorker()
            BWorkerD.WorkerReportsProgress = True
            BWorkerD.WorkerSupportsCancellation = True
    
            AddHandler BWorkerD.DoWork, AddressOf BWorkerD_DoWork
            AddHandler BWorkerD.ProgressChanged, AddressOf BWorkerD_ProgressChanged
            AddHandler BWorkerD.RunWorkerCompleted, AddressOf BWorkerD_RunWorkerCompleted
            BWorkerD.RunWorkerAsync(BGWorkerObj)
        End Sub
    
        Private Class BGWorkerObject
            Public Property UserName As String
            Public Property Password As String
            Public Property ResourceURI As String
            Public Property FilePath As String
            Public Property FileLength As Long
            Public Property DownloadedBytes As Long
            Public Property BytesToDownload As Long
        End Class
    
        Private Sub btnDownload_Click(sender As Object, e As EventArgs) Handles btnDownload.Click
            pBarD.Value = 0
            Dim BGWorkerObj As BGWorkerObject = New BGWorkerObject With {
                .ResourceURI = txtFilePathD.Text,
                .FilePath = Path.Combine(txtSavePathD.Text, Path.GetFileName(txtFilePathD.Text)),
                .UserName = txtFTPUsernameD.Text,
                .Password = txtFTPPasswordD.Text
            }
        End Sub
    
        Private Sub BWorkerD_DoWork(sender As Object, e As DoWorkEventArgs)
            Dim BGW As BackgroundWorker = TryCast(sender, BackgroundWorker)
            Dim BGWorkerObj As BGWorkerObject = TryCast(e.Argument, BGWorkerObject)
            Dim FTPRequest As FtpWebRequest
            Dim BufferSize As Integer = 131072
    
            ' Windows 7 / Windows 10 lacking maintenance    
            ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12
    
            FTPRequest = DirectCast(WebRequest.Create(BGWorkerObj.ResourceURI), FtpWebRequest)
            FTPRequest.Credentials = New NetworkCredential(BGWorkerObj.UserName, BGWorkerObj.Password)
            'FTPRequest.Method = WebRequestMethods.Ftp.GetFileSize
            '----------------------- UPDATE  ------------------------
            FTPRequest.Method = WebRequestMethods.Ftp.ListDirectoryDetails
            '--------------------- END UPDATE  ------------------------
            FTPRequest.EnableSsl = True
    
            '----------------------- UPDATE  ------------------------
            Using FtpResponse As WebResponse = FTPRequest.GetResponse, 
              DirListStream As Stream = FtpResponse.GetResponseStream(), 
              listReader As StreamReader = New StreamReader(DirListStream)
                While Not listReader.EndOfStream
                    Dim DirContent As String = listReader.ReadLine()
                    If DirContent.Contains(Path.GetFileNameWithoutExtension(BGWorkerObj.ResourceURI)) Then
                        BGWorkerObj.FileLength = Convert.ToInt64(DirContent.Split(New String() {" "}, StringSplitOptions.RemoveEmptyEntries)(4))
                        BGW.ReportProgress(0, BGWorkerObj)
                        Exit While
                    End If
                End While
            End Using
            '----------------------- END UPDATE  ------------------------
    
            'Using FtpResponse As WebResponse = FTPRequest.GetResponse
            '    BGWorkerObj.FileLength = Convert.ToInt64(FtpResponse.ContentLength)
            '    BGW.ReportProgress(0, BGWorkerObj)
            'End Using
    
            If BGW.CancellationPending Then e.Cancel = True
    
            Try
                FTPRequest = CType(WebRequest.Create(BGWorkerObj.ResourceURI), FtpWebRequest)
                FTPRequest.EnableSsl = True
                FTPRequest.Credentials = New NetworkCredential(BGWorkerObj.UserName, BGWorkerObj.Password)
                FTPRequest.Method = WebRequestMethods.Ftp.DownloadFile
    
                Using Response As FtpWebResponse = DirectCast(FTPRequest.GetResponse, FtpWebResponse)
                    If Response.StatusCode > 299 Then
                        e.Result = 0
                        Throw New Exception("The Ftp Server rejected the request. StatusCode: " &
                                            Response.StatusCode.ToString(),
                                            New InvalidOperationException(Response.StatusCode.ToString()))
                        Exit Sub
                    End If
                    Using stream = Response.GetResponseStream(), 
                      fileStream As FileStream = File.Create(BGWorkerObj.FilePath)
                        Dim read As Integer
                        Dim buffer As Byte() = New Byte(BufferSize - 1) {}
                        Do
                            read = stream.Read(buffer, 0, buffer.Length)
                            fileStream.Write(buffer, 0, read)
                            BGWorkerObj.DownloadedBytes += read
                            BGWorkerObj.BytesToDownload = BGWorkerObj.FileLength - BGWorkerObj.DownloadedBytes
    
                            If BGW.CancellationPending Then
                                e.Cancel = True
                                Exit Do
                            Else
                                BGW.ReportProgress(CInt((CSng(BGWorkerObj.DownloadedBytes) / BGWorkerObj.FileLength) * 100), BGWorkerObj)
                            End If
                        Loop While read > 0
                    End Using
                End Using
    
            Catch ex As Exception
                If e.Cancel = False Then Throw
            Finally
                If e.Cancel = True Then
                    If File.Exists(BGWorkerObj.FilePath) Then
                        File.Delete(BGWorkerObj.FilePath)
                    End If
                End If
            End Try
    
        End Sub
    
        Private Sub BWorkerD_ProgressChanged(sender As Object, e As ProgressChangedEventArgs)
             pBarD.Value = e.ProgressPercentage
            lblPercentD.Text = e.ProgressPercentage.ToString() & " %"
    
            If lblFileSizeD.Text.Length = 0 Then
                lblFileSizeD.Text = CType(e.UserState, BGWorkerObject).FileLength.ToString("N0", CultureInfo.CurrentUICulture.NumberFormat)
            End If
            lblDownloadedBytesD.Text = CType(e.UserState, BGWorkerObject).DownloadedBytes.ToString("N0", CultureInfo.CurrentUICulture.NumberFormat)
            If e.ProgressPercentage <= 15 Then
                lblDownloadedBytesD.ForeColor = Color.Red
            ElseIf e.ProgressPercentage <= 66 Then
                lblDownloadedBytesD.ForeColor = Color.Orange
            Else
                lblDownloadedBytesD.ForeColor = Color.LightGreen
            End If
        End Sub
    
        Private Sub BWorkerD_RunWorkerCompleted(sender As Object, e As RunWorkerCompletedEventArgs)
            Dim DownloadAborted As Boolean = False
            If e.Error IsNot Nothing Then
                DownloadAborted = True
                lblDownloadedBytesD.ForeColor = Color.Red
                lblDownloadedBytesD.Text = "Error!"
            ElseIf e.Cancelled Then
                DownloadAborted = True
                lblDownloadedBytesD.ForeColor = Color.Yellow
                lblDownloadedBytesD.Text = "Cancelled!"
                pBarD.Value = 0
                lblPercentD.Text = "0%"
            Else
                lblDownloadedBytesD.ForeColor = Color.LightGreen
                lblDownloadedBytesD.Text = "Download completed"
            End If
        End Sub
    
        Private Sub btnAbortDownload_Click(sender As Object, e As EventArgs) Handles btnAbortDownload.Click
            BWorkerD.CancelAsync()
        End Sub
    End Class
    

    A visual result of the operation described:

    frmBGWorkerDownload Image

    A PasteBin of the Form's Designer + Code