Copy files in C# with progress report
On my last project I had need for my application to copy files or folders (including sub dir structure). I right away had idea how simple that is, I’m sure you are thinking it too File.Copy() would suffice. Using that would give me simple copying routine and I could go along dir structure with recursion and just copy files and create directories, for a progress report I could be using ReportProgress() of the BackgroundWorker to say copying of file is done and then can iterate scroll bar with max set to total number of files. And this would work nice but I wondered if I could do it more elegant, what if user copies one big file the scrollbar would just sit there like stuck until that one file copying is finished, I needed a way to report progress how much byes of single file is copied…
So basically now that I knew what I need I was on a quest to search the Internet for something that would help me. After a few hours all I could find was a way how to do it using COM interop and its fine but i really wanted to do it solely in managed code i have found many ideas but nothing complete so I was then behind a drawing board trying to come up with something.
Basically idea that came to my mind was to split file and do copying one part at a time so then I could have copying progress reported after each part is copied and it would give me what I wanted report on how many bytes was copied out of file that is currently being copied. Here how I implemented it.
Because I wanted for UI to remain responsive whole copying is happening in a separate thread from the UI. Note that this is form that has two progress bars named progressBar1 and progressBar2 and three labels for file name currently copied, amount of KBs currently copied and nr of files remaining I don’t display code for adding progress bars or labels to the form because I assume you know this already or can use simple drag and drop from the Toolbox in Visual Studio.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
BackgroundWorker bw;
string source = "";
string target = "";
bool isfile = false;
int filecount = 0;
int currentFileNr = 1;
string newFilename = "";
bool firstdir = true;
int maxbytes = 0;
public ShowProgress(string from, string to, bool isf)
{
InitializeComponent();
source = from;
target = to;
isfile = isf;
bw = new BackgroundWorker();
bw.DoWork += new DoWorkEventHandler(bw_DoWork);
bw.RunWorkerCompleted += new RunWorkerCompletedEventHandler(bw_RunWorkerCompleted);
bw.ProgressChanged += new ProgressChangedEventHandler(bw_ProgressChanged);
bw.WorkerReportsProgress = true;
GetFileData();
} |
Basically here I initialise BackgroundWorker along and parse some data required by the constructor like source, target and isfile is a variable that indicates is the source file or is it a directory (this is just because of specific use of application it can be removed for your use). So after initialisation I call GetFileData() func.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
private void GetFileData()
{
if (isfile)
{
FileInfo fi = new FileInfo(source);
maxbytes = Convert.ToInt32(fi.Length);
//
//set progress bar length
//
progressBar1.Maximum = maxbytes;
progressBar2.Maximum = 1;
bw.RunWorkerAsync();
}
else
{
GetDirectoryInfo(source);
//
//set progress bar length
//
progressBar1.Maximum = maxbytes;
progressBar2.Maximum = filecount;
bw.RunWorkerAsync();
}
} |
Ok here I collect some general information about what is to be copied, if (isfile) then i just get info about file to be copied set progressBar1 maximum size to bytesize of file and set progressBar2 maximum size to 1 because its only one file in question and run thread. If its not file in question then I call GetDirectoryInfo(source) function that collects info about source dir.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
private void GetDirectoryInfo(string source)
{
string[] files = Directory.GetFiles(source);
foreach (string file in files)
{
FileInfo fi = new FileInfo(file);
maxbytes += Convert.ToInt32(fi.Length);
filecount += 1;
}
string[] folders = Directory.GetDirectories(source);
foreach (string folder in folders)
{
GetDirectoryInfo(folder);
}
} |
GetDirectoryInfo() function is recursive function that collects info about source dir. it gets all files from dir then for each file it gets bytecount and stores it in a maxbytes variable that will be used as maximum value for progressBar1 and for each file it increments filecount variable counting how many files are in the source location, filecount variable is then passed as maximum of progressBar2, after all this is done function GetFileData sets maximum for progress bars and calls a new thread to begin executing copying routine.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
void bw_DoWork(object sender, DoWorkEventArgs e)
{
if (isfile)
{
FileStream fs = new FileStream(source, FileMode.Open);
long FileSize = fs.Length;
FileInfo fi = new FileInfo(source);
byte[] bBuffer = new byte[(int)FileSize];
fs.Read(bBuffer, 0, (int)FileSize);
fs.Close();
UpdateLabels(fi.FullName);
newFilename = fi.Name;
if (File.Exists(target + "\\" + fi.Name))
{
Random rand = new Random();
string[] nfn = newFilename.Split('.');
newFilename = nfn[0] + "_" + rand.Next(1, 10000) + "." + nfn[1];
}
FileStream fss = new FileStream(target + "\\" + newFilename, FileMode.CreateNew);
BinaryWriter biwr = new BinaryWriter(fss);
for (int i = 0; i < bBuffer.Length; i+=5000)
{
if (i + 5000 < bBuffer.Length)
{
biwr.Write(bBuffer, i, 5000);
bw.ReportProgress(5000);
}
else
{
biwr.Write(bBuffer, i, bBuffer.Length - i);
bw.ReportProgress(bBuffer.Length - i);
}
}
biwr.Close();
fss.Close();
}
else
{
string[] temp = source.Split('\\');
target += "\\" + temp[temp.Count() - 1];
DirectoryInfo s = new DirectoryInfo(source);
DirectoryInfo t = new DirectoryInfo(target);
CopyDirectory(s, t);
}
} |
here is where most of it happens first I check is it file. If its file I use FileStream to open it and then read a file in to byte array then close the FileStream so basically I have the file put in to byte array then all that is left to check if a file exists already on the target location in my case for the needs of this specific application if file already exist on target location I append random number to file name but easily you can show some dialogue and ask user what to do. Then I create new file on target location and use BinaryWriter to write data from byte array to it using a for loop writing 5000 bytes at a time I just do checks if a file is smaller than that or if what’s left to be written is smaller than that and if it is the whole file will be written at once. You can of course change this 5000 bytes to whatever you feel like but keep it reasonable I tested this method of copying 5000 bytes at a time with regular file copying in windows and the times I get are equal sometimes maybe one or two seconds slower, since this application is planned to work in local area network testing was done copying file over network, if it comes slower at copying files on one computer you can probably increase copying chunks to 10000 or more and it should give you same results.
Now if the source in question is directory and not file the copying is done using CopyDirectory() function that is called from background workers do work method.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
public void CopyDirectory(DirectoryInfo di_source, DirectoryInfo di_target)
{
if (Directory.Exists(di_target.FullName) == false)
{
Directory.CreateDirectory(di_target.FullName);
}
else
{
if (firstdir)
{
Random rand = new Random();
string newdir = di_target.FullName + "_" + rand.Next(1, 10000);
Directory.CreateDirectory(newdir);
di_target = new DirectoryInfo(newdir);
firstdir = false;
}
}
foreach (FileInfo fi in di_source.GetFiles())
{
newFilename = fi.Name;
FileStream fs = new FileStream(fi.FullName, FileMode.Open);
long FileSize = fs.Length;
byte[] bBuffer = new byte[(int)FileSize];
fs.Read(bBuffer, 0, (int)FileSize);
fs.Close();
UpdateLabels(fi.FullName);
if (File.Exists(di_target.ToString() + "\\" + fi.Name))
{
Random rand = new Random();
newFilename = newFilename + "_" + rand.Next(1, 10000);
}
FileStream fss = new FileStream(di_target.ToString() + "\\" + newFilename, FileMode.CreateNew );
BinaryWriter biwr = new BinaryWriter(fss);
for (int i = 0; i < bBuffer.Length; i += 500000)
{
if (i + 500000 < bBuffer.Length)
{
biwr.Write(bBuffer, i, 500000);
bw.ReportProgress(500000);
}
else
{
biwr.Write(bBuffer, i, bBuffer.Length - i);
bw.ReportProgress(bBuffer.Length - i);
}
}
biwr.Close();
fss.Close();
}
foreach (DirectoryInfo di_SourceSubDir in di_source.GetDirectories())
{
DirectoryInfo nextSubDir = di_target.CreateSubdirectory(di_SourceSubDir.Name);
CopyDirectory(di_SourceSubDir, nextSubDir);
}
} |
This is the function that copies complete directory structure from source to target it does so by using recursion. First part of function checks if a directory allreday exists if it does exist it appends a random number to directory name before creating it (Again this is needed by application I am using this copy routine in you can simply exchange it with something else like asking user what to do). It will rename only first directory and all other subdirs will remain intact so at first run it checks if dir exists in target if it does it checks if its the first directory by checking the status of firstdir variable if its true it appends random number to directory name and sets firstdir variable to false so subdirs of directory that is being copied are not renamed.
Then function basically does the same routine like if its case of one file, for each file in target dir it checks if file exists if it exists it appends to its name random number (this is leftover while working with previous versions of this copying routine now because it changes the name of directory if it allreday exists there is no need for this check and you can remove it if you like). Afterwards it simply repeats same routine like with single file, reads it in byte array and writes it in chunks of 5000 bytes. After it has written all the files in source dir it calls it self (recursion) for each subdir in source dir and repeats the process.
Each time one chunk of 5000 bytes is written bw.ReportProgress() is called so I can update progress bars.
|
1 2 3 4 5 6 7 |
void bw_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
progressBar1.Value += e.ProgressPercentage;
int copied = progressBar1.Value / 1024;
int total = maxbytes / 1024;
lbl_kbscopied.Text = copied + "/" + total;
} |
This updates progressBar1 value incrementing it and shows in label how many kilobytes are copied so far.
For updating label texts and progressBar2 that shows how many files are copied so far I use delegate.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
delegate void UpdateLabelsDelegate(string filename);
void UpdateLabels(string fname)
{
if (!InvokeRequired)
{
lbl_filename.Text = "Copying: " + fname;
lbl_filenr.Text = "File: " + currentFileNr + "/" + filecount;
currentFileNr++;
progressBar2.Value += 1;
}
else
{
Invoke(new UpdateLabelsDelegate(UpdateLabels), new object[] { fname });
}
} |
So I can set labels for filename currently copied and display how many files are left to be copied.
That is it, I hope this helps you on your projects I know this is probably not most efficient code but its a prototype and it works ok i know this because application that this is coded for is used daily at my work its been in testing phase for two weeks now and there is no reported problems with copying be it speed wise or some other bug.
All comments, suggestions are more than welcome. Thank you for bearing with me through this one.





Hi,
Very useful code but one thing is missing…Where is the code for this function “bw_RunWorkerCompleted”.
Please provide complete code for this function because without this it’s not useful at all for me.
If you provide me because I need this for my appliction.
Thanks.
You can do what ever you want in bw_RunWorkerCompleted it does all work in bw_DoWork and reports it to bw_ProgressChanged. When it finishes it calls bw_RunWorkerCompleted so you can do what ever you want like pop a message, call some other function, everything you need is already there.