Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Download file from FTP with Progress - TotalBytesToReceive is always -1?

I am trying to download a file from an FTP server with a progress bar.

The file is downloading, and the ProgressChanged event is calling, except in the event args TotalBytesToReceive is always -1. TotalBytes increases, but I am unable to calculate the percentage without the total.

I imagine I could find the file size through other ftp commands, but I wonder why this doesn't work?

My code:

FTPClient request = new FTPClient();
request.Credentials = credentials;
request.DownloadProgressChanged += new DownloadProgressChangedEventHandler(request_DownloadProgressChanged);
//request.DownloadDataCompleted += new DownloadDataCompletedEventHandler(request_DownloadDataCompleted);
request.DownloadDataAsync(new Uri(folder + file));
while (request.IsBusy) ;

....

static void request_DownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e)
{
    if (e.TotalBytesToReceive == -1)
    {
        l.reportProgress(-1, FormatBytes(e.BytesReceived) + " out of ?" );
    }
    else
    {
        l.reportProgress(e.ProgressPercentage, "Downloaded " + FormatBytes(e.BytesReceived) + " out of " + FormatBytes(e.TotalBytesToReceive) + " (" + e.ProgressPercentage + "%)");
    }
}

....

class FTPClient : WebClient
{
    protected override WebRequest GetWebRequest(System.Uri address)
    {
        FtpWebRequest req = (FtpWebRequest)base.GetWebRequest(address);
        req.UsePassive = false;
        return req;
    }
}

Thanks.

like image 712
Mitch Avatar asked Jan 04 '11 06:01

Mitch


2 Answers

So I had the same issue. I got around it by retrieving the file size first.

        // Get the object used to communicate with the server.
        FtpWebRequest request = (FtpWebRequest)WebRequest.Create("URL");
        request.Method = WebRequestMethods.Ftp.GetFileSize;
        request.Credentials = networkCredential;
        FtpWebResponse response = (FtpWebResponse)request.GetResponse();

        Stream responseStream = response.GetResponseStream();
        bytes_total = response.ContentLength; //this is an int member variable stored for later
        Console.WriteLine("Fetch Complete, ContentLength {0}", response.ContentLength);
        response.Close();

        webClient = new MyWebClient();
        webClient.Credentials = networkCredential; ;
        webClient.DownloadDataCompleted += new DownloadDataCompletedEventHandler(FTPDownloadCompleted);
        webClient.DownloadProgressChanged += new DownloadProgressChangedEventHandler(FTPDownloadProgressChanged);
        webClient.DownloadDataAsync(new Uri("URL"));

Then do the math in the callback.

private void FTPDownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e)
{
     progressBar.Value = (int)(((float)e.BytesReceived / (float)bytes_total) * 100.0);
}
like image 156
Django Avatar answered Nov 20 '22 01:11

Django


With FTP protocol, WebClient in general does not know total download size. So you commonly get -1 with FTP.

Note that the behavior actually contradicts the .NET documentation, which says for FtpWebResponse.ContentLength (where the value of TotalBytesToReceive comes from):

For requests that use the DownloadFile method, the property is greater than zero if the downloaded file contained data and is zero if it was empty.

But you will easily find out many of questions about this, effectively showing that the behavior is not always as documented. The FtpWebResponse.ContentLength has a meaningful value for GetFileSize method only.

The FtpWebRequest/WebClient makes no explicit attempt to find out a size of the file that it is downloading. All it does is that it tries to look for (xxx bytes). string in 125/150 responses to RETR command. No FTP RFC mandates that the server should include such information. ProFTPD (see data_pasv_open in src/data.c) and vsftpd (see handle_retr in postlogin.c) seem to include this information. Other common FTP servers (IIS, FileZilla) do not do this.


If your server does not provide size information, you have to query for size yourself before download. A complete solution using FtpWebRequest and Task:

private void button1_Click(object sender, EventArgs e)
{
    // Run Download on background thread
    Task.Run(() => Download());
}

private void Download()
{
    try
    {
        const string url = "ftp://ftp.example.com/remote/path/file.zip";
        NetworkCredential credentials = new NetworkCredential("username", "password");

        // Query size of the file to be downloaded
        WebRequest sizeRequest = WebRequest.Create(url);
        sizeRequest.Credentials = credentials;
        sizeRequest.Method = WebRequestMethods.Ftp.GetFileSize;
        int size = (int)sizeRequest.GetResponse().ContentLength;

        progressBar1.Invoke(
            (MethodInvoker)(() => progressBar1.Maximum = size));

        // Download the file
        WebRequest request = WebRequest.Create(url);
        request.Credentials = credentials;
        request.Method = WebRequestMethods.Ftp.DownloadFile;

        using (Stream ftpStream = request.GetResponse().GetResponseStream())
        using (Stream fileStream = File.Create(@"C:\local\path\file.zip"))
        {
            byte[] buffer = new byte[10240];
            int read;
            while ((read = ftpStream.Read(buffer, 0, buffer.Length)) > 0)
            {
                fileStream.Write(buffer, 0, read);
                int position = (int)fileStream.Position;
                progressBar1.Invoke(
                    (MethodInvoker)(() => progressBar1.Value = position));
            }
        }
    }
    catch (Exception e)
    {
        MessageBox.Show(e.Message);
    }
}

enter image description here

The core download code is based on:
Upload and download a binary file to/from FTP server in C#/.NET

like image 23
Martin Prikryl Avatar answered Nov 20 '22 02:11

Martin Prikryl