Blog Archives

Copy File With Update Progress in .Net

The process of copying a file in .Net is of course very simple and straightforward.

//in a real world solution, use Environment.SpecialFolders instead of hard coding c drive letter
string inputpath = "C:\\in\\";
string outpath = "C:\\out\\";
string filename = "foo.txt";
System.IO.File.Copy(inputpath + filename, outputpath + filename, true);
//file copied!.. but if foo was very large, this block of code following would be delayed execution until the copy completes..

If you want to copy multiple files and update the end user on the progress of a large file copy, or copy files asynchronously while performing other tasks in your application, you’re getting into slightly hairy territory.

There are many ways to achieve this with varying results, and you will often see many application installers and various other large file copy processes inaccurately estimate remaining time and incorrectly report progress, or lock up close to the end.

To correct this, my advice would be to go back to basics – copying bytes using FileStream. See example below.

(applies to winform since update progress is displayed to user, but can easily be adapted to console or web)

using System.IO;

 public class ThreadedFormFileCopy : Form
    {

        // Class to report exception {
        private class UIError
        {
            public UIError(Exception ex, string path_)
            {
                msg = ex.Message; path = path_; result = DialogResult.Cancel;
            }
            public string msg;
            public string path;
            public DialogResult result;
        }

        #region "winforms specific"
        public ThreadedFormFileCopy()
        {
            InitializeComponent();
        }

        private void btnCopy_Click(object sender, EventArgs e)
        {
            StartCopy();
        }
        #endregion

        #region "variable declarations"
        FileInfo currentfile { get; set; } //keep track of current file being copied
        long maxbytes { get; set; } //shared with file agent, background worker and form thread to keep track of bytes

        private System.Timers.Timer fileagent; //we'll be using this thread to update the progress bar as bytes are copied

        //would also declare OnChange delegate and progresschange object here as well if byte level progress not needed and just want to update UI for each file copied
        private BackgroundWorker mCopier;
        private delegate void CopyError(UIError err);
        private CopyError OnError;
        #endregion


        public void StartCopy()
        {
            LoadFileAgent(); //instantiate and update file agent parameters such as internal and elapsed handler
            PrepareBackgroundWorker(); //instantiate and update background worker parameters and handlers
            UpdateFormLabels(false); //change labels on form to indicate copy process has begun
            ToggleBackgroundWorker(); //start background worker, can be used with a button or UI element to cancel as well
        }

        private void LoadFileAgent()
        {
            fileagent = new System.Timers.Timer();
            fileagent.Interval = 100; //1ss
            fileagent.Elapsed += new System.Timers.ElapsedEventHandler(FileAgent_Process);
        }

        private void FileAgent_Process(object source, System.Timers.ElapsedEventArgs e)
        {
            try
            {
                label1.Text = currentfile.Name;
                progressBar1.Value = (int)(100 * maxbytes / currentfile.Length);
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.ToString()); //may want to include in debug only
            }
        }

        private void Copier_DoWork(object sender, DoWorkEventArgs e)
        {
            string[] allowextensions = { ".wmv", ".flv", ".f4v", ".mov", ".mp4", ".mpeg", ".mpg", ".mp3", ".wma" }; //etc, videos can get big and might want progress..
            List<FileInfo> files = new List<FileInfo>();

            //don't forget your Environment.SpecialFolder
            string path = "\\path_containing_files\\";
            DirectoryInfo dir = new DirectoryInfo(path);
            foreach (string ext in theExtensions)
            {
                FileInfo[] folder = dir.GetFiles(ext, SearchOption.AllDirectories);
                foreach (FileInfo file in folder)
                {
                    if ((file.Attributes & FileAttributes.Directory) != 0) continue;
                    files.Add(file);
                    maxbytes += file.Length;
                }
            }

            //and now for the good stuff
            int bytesread = 0;
            foreach (FileInfo file in files)
            {
                try
                {
                    currentfile = file;
                    fileagent.Start();

                    string outputpath =  "\\destination_path\\";

                    FileInfo destfile = new FileInfo(outputpath + file.Name);

                    byte[] buffer = new byte[4096]; //4MB buffer
                    FileStream fsread = file.Open(FileMode.Open, FileAccess.Read);
                    FileStream fswrite = destfile.Open(FileMode.OpenOrCreate, FileAccess.ReadWrite);
                    maxbytes = 0;

                    while ((bytesread = fsread.Read(buffer, 0, buffer.Length)) > 0)
                    {
                        fswrite.Write(buffer, 0, bytesread);
                        maxbytes = maxbytes + bytesread;
                    }
                    fsread.Flush();
                    fswrite.Flush();
                    fsread.Close();
                    fswrite.Close();
                    fsread.Dispose();
                    fswrite.Dispose();
                    //-------------------
                    System.IO.File.SetAttributes(outputpath + file.Name, FileAttributes.Normal);
                    fileagent.Stop();

                }
                catch (Exception ex)
                {
                    UIError err = new UIError(ex, file.FullName);
                    this.Invoke(OnError, new object[] { err });
                    if (err.result == DialogResult.Cancel) break;
                }
                //could update bytes here also
            }

        }

        private void PrepareBackgroundWorker()
        {
            mCopier = new BackgroundWorker();
            mCopier.DoWork += Copier_DoWork; //all the heavy lifting is done here
            mCopier.RunWorkerCompleted += Copier_RunWorkerCompleted;
            mCopier.WorkerSupportsCancellation = true;
            //if you wanted to add an onchange event for the background worker, you could do this here and it would fire after each complete copy, though not necessary this is viable alternative for medium file sizes
            OnError += Copier_Error;
        }

        private void UpdateFormLabels(bool copying)
        {
            label1.Visible = copying;
            progressBar1.Visible = copying;
            label1.Text = "Starting copy...";
            progressBar1.Value = 0;
        }

        private void ToggleBackgroundWorker()
        {
            bool copying = true;
            UpdateFormLabels(copying);
            if (copying)
            {
                mCopier.RunWorkerAsync();
            }
            else
            {
                mCopier.CancelAsync();
            }
        }

        private void Copier_Error(UIError err)
        {
            string msg = string.Format("Error copying file. Details: " + err.Message);
            err.result = MessageBox.Show(msg, "Copy error", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
            Environment.ExitCode = 1;
            Application.Exit(); //log error, do something, close, or continue if it's not critical or used for long unattended copies
        }

        private void Copier_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            label1.Text = "Complete!";
            progressBar1.Value = 100;
            this.Invoke(new MethodInvoker(delegate { FinishedCopying(); }));
            this.Close();
        }

        private void FinishedCopying()
        {
            //execute something when worker finished
        }

    }

References
StackOverflow, “Read/Write bytes in Chunks”, http://stackoverflow.com/questions/5654298/filestream-read-write-methods-limitation
Java2s, “Write Bytes using Filestream”, http://www.java2s.com/Tutorial/VB/0240__Stream-File/WriteBytesusingFileStream.htm
MSDN, “FileStream.WriteByte”,
MSDN, “FileStream.ReadByte”,
http://msdn.microsoft.com/en-us/library/system.io.filestream.readbyte.aspx
extremevbtalk.com, “Read Binary Files into a Buffer”, http://www.xtremevbtalk.com/showthread.php?t=259085
Java2s, “Read into a Buffer”, http://www.java2s.com/Tutorial/VB/0240__Stream-File/Readintoabuffer.htm
StackOverflow, http://stackoverflow.com/questions/1261570/c-filestream-read-doesnt-read-the-file-up-to-the-end-but-returns-0
StackOverflow, “Asynchronous stream read/write”, http://stackoverflow.com/questions/1540658/net-asynchronous-stream-read-write
xtremevbtalk.com, “Background Worker Progress Changed and Progress Bar”, http://www.xtremevbtalk.com/showthread.php?t=294040
StackOverflow, “Copy Stream to byte array”, http://stackoverflow.com/questions/950513/how-to-copy-one-stream-to-a-byte-array-with-the-smallest-c-code

Advertisement

Download File with Update Progress in .Net

Next best thing to wget on Windows. 😉

VB

Imports System.Net
Imports System.IO

Namespace SoftwareAgent
    Public Class WebDownload
        Public Event FileDownloadProgressChanged(ByVal iNewProgress As Long)
        Public Event FileDownloadSizeObtained(ByVal iFileSize As Long)
        Public Event FileDownloadComplete()
        Public Event FileDownloadFailed(ByVal ex As Exception)

        Private mCurrentFile As String = ""
        Public ReadOnly Property FileName() As String
            Get
                Return mCurrentFile
            End Get
        End Property

        Public Sub New(ByVal URL As String)
            Dim wRemote As System.Net.WebRequest
            wRemote = System.Net.WebRequest.Create(URL)
            Dim HeaderDataCollection As System.Net.WebHeaderCollection = wRemote.GetResponse.Headers
            If Not HeaderDataCollection("Content-Disposition") Is Nothing Then
                mCurrentFile = HeaderDataCollection("Content-Disposition").Split("=")(1)
            Else
                If URL.IndexOf("/"c) = -1 Then : mCurrentFile = ""
                Else : mCurrentFile = "\" & URL.Substring(URL.LastIndexOf("/"c) + 1) : End If
            End If
        End Sub

        Public Function DownloadFile(ByVal URL As String, ByVal Location As String) As Boolean
            Try
                Dim WC As New WebClient
                WC.DownloadFile(URL, Location)
                RaiseEvent FileDownloadComplete()
                Return True
            Catch ex As Exception
                RaiseEvent FileDownloadFailed(ex)
                Return False
            End Try
        End Function

        Private Function GetFileName(ByVal URL As String) As String
            Try
                Return URL.Substring(URL.LastIndexOf("/") + 1)
            Catch ex As Exception
                Return URL
            End Try
        End Function

        'TODO: consolidate with download file and remove URL required param change to constructor
        Public Function DownloadFileWithProgress(ByVal URL As String, ByVal Location As String) As Boolean
            Dim FS As FileStream
            Try
                Dim wRemote As WebRequest
                Dim bBuffer As Byte()
                ReDim bBuffer(256)
                Dim iBytesRead As Integer
                Dim iTotalBytesRead As Integer

                FS = New FileStream(Location, FileMode.Create, FileAccess.Write)
                wRemote = WebRequest.Create(URL)
                Dim myWebResponse As WebResponse = wRemote.GetResponse

                RaiseEvent FileDownloadSizeObtained(myWebResponse.ContentLength)
                Dim sChunks As Stream = myWebResponse.GetResponseStream
                Do
                    iBytesRead = sChunks.Read(bBuffer, 0, 256)
                    FS.Write(bBuffer, 0, iBytesRead)
                    iTotalBytesRead += iBytesRead
                    If myWebResponse.ContentLength < iTotalBytesRead Then
                        RaiseEvent FileDownloadProgressChanged(myWebResponse.ContentLength)
                    Else
                        RaiseEvent FileDownloadProgressChanged(iTotalBytesRead)
                    End If
                Loop While Not iBytesRead = 0
                sChunks.Close()
                FS.Close()
                RaiseEvent FileDownloadComplete()
                Return True
            Catch ex As Exception
                If Not (FS Is Nothing) Then
                    FS.Close()
                    FS = Nothing
                End If
                RaiseEvent FileDownloadFailed(ex)
                Return False
            End Try
        End Function

        Public Shared Function FormatFileSize(ByVal Size As Long) As String
            Try
                Dim KB As Integer = 1024
                Dim MB As Integer = KB * KB
                ' Return size of file in kilobytes.
                If Size < KB Then
                    Return (Size.ToString("D") & " bytes")
                Else
                    Select Case Size / KB
                        Case Is < 1000
                            Return (Size / KB).ToString("N") & "KB"
                        Case Is < 1000000
                            Return (Size / MB).ToString("N") & "MB"
                        Case Is < 10000000
                            Return (Size / MB / KB).ToString("N") & "GB"
                    End Select
                End If
            Catch ex As Exception
                Return Size.ToString
            End Try
        End Function
    End Class
End Namespace

C# (note this is autoconversion and actual project is in VB):

using Microsoft.VisualBasic;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Net;
using System.IO;

	public class WebDownload
	{
		public event FileDownloadProgressChangedEventHandler FileDownloadProgressChanged;
		public delegate void FileDownloadProgressChangedEventHandler(long iNewProgress);
		public event FileDownloadSizeObtainedEventHandler FileDownloadSizeObtained;
		public delegate void FileDownloadSizeObtainedEventHandler(long iFileSize);
		public event FileDownloadCompleteEventHandler FileDownloadComplete;
		public delegate void FileDownloadCompleteEventHandler();
		public event FileDownloadFailedEventHandler FileDownloadFailed;
		public delegate void FileDownloadFailedEventHandler(Exception ex);

		private string mCurrentFile = "";
		public string FileName {
			get { return mCurrentFile; }
		}

		public WebDownload(string URL)
		{
			System.Net.WebRequest wRemote = null;
			wRemote = System.Net.WebRequest.Create(URL);
			System.Net.WebHeaderCollection HeaderDataCollection = wRemote.GetResponse().Headers;
			if ((HeaderDataCollection["Content-Disposition"] != null)) {
				mCurrentFile = HeaderDataCollection["Content-Disposition"].Split("=")[1];
			} else {
				if (URL.IndexOf('/') == -1) {
					mCurrentFile = "";
				} else {
					mCurrentFile = "\\" + URL.Substring(URL.LastIndexOf('/') + 1);
				}
			}
		}

		public bool DownloadFile(string URL, string Location)
		{
			try {
				WebClient WC = new WebClient();
				WC.DownloadFile(URL, Location);
				if (FileDownloadComplete != null) {
					FileDownloadComplete();
				}
				return true;
			} catch (Exception ex) {
				if (FileDownloadFailed != null) {
					FileDownloadFailed(ex);
				}
				return false;
			}
		}

		private string GetFileName(string URL)
		{
			try {
				return URL.Substring(URL.LastIndexOf("/") + 1);
			} catch (Exception ex) {
				return URL;
			}
		}

		//TODO: consolidate with download file and remove URL required param change to constructor
		public bool DownloadFileWithProgress(string URL, string Location)
		{
			FileStream FS = null;
			try {
				WebRequest wRemote = null;
				byte[] bBuffer = null;
				bBuffer = new byte[257];
				int iBytesRead = 0;
				int iTotalBytesRead = 0;

				FS = new FileStream(Location, FileMode.Create, FileAccess.Write);
				wRemote = WebRequest.Create(URL);
				WebResponse myWebResponse = wRemote.GetResponse();

				if (FileDownloadSizeObtained != null) {
					FileDownloadSizeObtained(myWebResponse.ContentLength);
				}
				Stream sChunks = myWebResponse.GetResponseStream();
				do {
					iBytesRead = sChunks.Read(bBuffer, 0, 256);
					FS.Write(bBuffer, 0, iBytesRead);
					iTotalBytesRead += iBytesRead;
					if (myWebResponse.ContentLength < iTotalBytesRead) {
						if (FileDownloadProgressChanged != null) {
							FileDownloadProgressChanged(myWebResponse.ContentLength);
						}
					} else {
						if (FileDownloadProgressChanged != null) {
							FileDownloadProgressChanged(iTotalBytesRead);
						}
					}
				} while (!(iBytesRead == 0));
				sChunks.Close();
				FS.Close();
				if (FileDownloadComplete != null) {
					FileDownloadComplete();
				}
				return true;
			} catch (Exception ex) {
				if ((FS != null)) {
					FS.Close();
					FS = null;
				}
				if (FileDownloadFailed != null) {
					FileDownloadFailed(ex);
				}
				return false;
			}
		}

		public static string FormatFileSize(long Size)
		{
			try {
				int KB = 1024;
				int MB = KB * KB;
				// Return size of file in kilobytes.
				if (Size < KB) {
					return (Size.ToString("D") + " bytes");
				} else {
					switch (Size / KB) {
						case  // ERROR: Case labels with binary operators are unsupported : LessThan
1000:
							return (Size / KB).ToString("N") + "KB";
						case  // ERROR: Case labels with binary operators are unsupported : LessThan
1000000:
							return (Size / MB).ToString("N") + "MB";
						case  // ERROR: Case labels with binary operators are unsupported : LessThan
10000000:
							return (Size / MB / KB).ToString("N") + "GB";
					}
				}
			} catch (Exception ex) {
				return Size.ToString();
			}
		}
	}