I have run into some problems using C# WebClient DownloadFileAsync and hope that you would help me.
Here's my scenario:
I am downloading many files at the same time (http://example.com/abc/1.jpg, http://example.com/abc/efg/2.jpg, etc.) using WebClient DownloadFileAsync.
My current example code is:
while (theres still files in http://example.com/abc/) {
// filename will be something like /abc/1.jpg, /abc/efg/2.jpg
if (!file.exists(directory.getcurrentdirectory()+filename.Replace('/', '\\'))) {
WebClient webClient = new WebClient();
webClient.DownloadProgressChanged += new DownloadProgressChangedEventHandler(downloadProgressChanged);
webClient.DownloadFileCompleted += new AsyncCompletedEventHandler(downloadCompleted);
webClient.DownloadFileAsync(new Uri("http://example.com"+filename), Directory.GetCurrentDirectory()+filename.Replace('/', '\\'));
}
How do I make it so that all the files being downloaded is shown in one progress bar?
eg. 1.jpg is 50kb, 2.jpg is 50kb, when 1.jpg completes download the progress bar will show 50%, and 2.jpg will be from 51% to 100% in the progress bar.
Also if the filename is /abc/1.jpg, if my current directory do not have a folder called abc, the download will not work. How do I make it create the folder automatically depending on filename? (eg. /random123/3.jpg, /anotherrandom/4.jpg, etc.)
To give an overall progress counter for you will need a centralised routine that gathers the amount-completed-so-far and total-amount-to-download for each of the in-progress transfers and sums them to calculate the overall totals. Because the transfers are running in parallel you can't just assign a chunk of the progress bar (e.g 51-100%) to each file.
With most APIs you will need to create folders for yourself before writing files into them. Just do this to create all missing folders on the path before you start writing the file:
Directory.CreateDirectory(Path.GetDirectoryName(filename));
To do the progress bar thing, you can use a queue. When something finishes downloading, update the progress bar, and start the next download.
However, I don't think the WebClient class can tell you how much of the download finished, only if it is finished or not.
To verify if the directory exists, use:
if (!System.IO.Directory.Exists(folder_name)) System.IO.Directory.Create(folder_name);
You can get the directory from a path, using the System.IO.Path class.
Related
I am using dotnet 6-windows.
I want to copy a large amount of files/folders at once.
The problem with an approach like the following is that it is
noticeably slower for large amount of files compared to copying using Windows file explorer. I tested it by copying ca. 500 images. The beneath code needed over a minute while the file explorer finished in just a few seconds.
does not show a progress bar for all files as a whole like the file explorer
foreach (string filePath in paths)
{
FileSystem.CopyFile(filePath, destination, UIOption.AllDialogs);
}
The problem is that it hands over one copying task after the other to the operating system instead of one task for all of the files.
Is there any library or built-in method that achieves this (something like FileSystem.CopyMultipleFiles(arrayOfPaths, destination, UIOption.AllDialogs)? Or do I have to use native windows APIs and if so, which?
As far as I understood your question, you want to copy files in parallel. You may use a perfect multithreading abstraction — class System.Threading.Tasks.Parallel. Static method Parallel.ForEach takes a collection, takes an Action<TCollectionElement> and for each item in the collection runs the action, passing it the item. All actions (if there's not too many of them) run in parallel. The order is not specified, but it's not a problem in your case.
Here's an example for your case:
using System.Threading.Tasks;
// ...
Parallel.ForEach(paths, p => {
FileSystem.CopyFile(p, destination, UIOption.AllDialogs);
});
By the way, if each action increases the value of your progress bar according to its progress, and each action thinks it can fill only a fixed area in the bar (in the sum), the effect will be as if the bar represented the progress of work in whole. The fixed area should be 1/x of the whole bar, where x is the number of your files.
Yes, in C# you can use the System.IO namespace to copy multiple files and folders as one task. One way to accomplish this is by using the Directory.GetFiles() method to get a list of files in a directory, and then using the File.Copy() method to copy each file to a different location. Similarly, you can use the Directory.GetDirectories() method to get a list of subdirectories, and then use the Directory.CreateDirectory() method to create those subdirectories in the new location.
You can also use the File.Move() method to move the files and folders.
You could use a loop to iterate through the files and folders and copy them one by one.
Here is an example of copying multiple files and folders in one task :
string sourcePath = #"C:\example\source";
string targetPath = #"C:\example\target";
// Copy all files from source to target
foreach (string file in Directory.GetFiles(sourcePath))
{
string targetFile = Path.Combine(targetPath, Path.GetFileName(file));
File.Copy(file, targetFile, true);
}
// Copy all subdirectories from source to target
foreach (string dir in Directory.GetDirectories(sourcePath))
{
string targetDir = Path.Combine(targetPath, Path.GetFileName(dir));
Directory.CreateDirectory(targetDir);
CopyAll(dir, targetDir);
}
This example uses a helper method CopyAll() that recursively copy all the files and folders from source to target.
You can also use the Microsoft's System.IO.Compression namespace to Zip and Unzip files and folders.
I have logic that downloads a group of files as a zip. The issue is there is no progress so the user does not know how far along the download is.
This Zip file doesn't exist before hand, the user selects the files they want to download and then I use the SharpZipLib nuget package to create a zip
and stream it to the response.
It seems I need to set the Content-Length header for the browser to show a total size progress indicator. The issue I'm having is it seems
this value has to be exact, if its too low or too high by 1 byte the file does not get downloaded properly. I can get an approximate
end value size by adding all the files size together and setting there to be no compressions level but I don't see a way I can calculate the final zip size exactly.
I hoped I could of just overesitmated the final size a bit and the browser would allow that but that doesn't work, the file isn't downloaded properly so you cant access it.
Here are some possible solution I've come up with but they have there own issues.
1 - I can create the zip on the server first and then stream it, therefore knowing the exact size I can set the Content-length. Issue with this
is the user will have to wait for all the files to be streamed to the web server, the zip to be created and then I can start streaming it to the user. While this is going on the user wont even see the file download as being started. This also results in more memory usage of the web server as it has to persist the entire zip file in memory.
2 - I can come up with my own progress UI, I will use the combined file sizes to get a rough final size estimation and then as the files are streamed I push updates to the user via signalR indicating the progress.
3- I show the user the total file size before download begins, this way they will at least have a way to assess themselves how far along it is. But the browser has no indication of how far along it is so if they may forget and when they look at the browser download progress there will be no indication how far along it is
These all have their own drawbacks. Is there a better way do this, ideally so its all handled by the browser?
Below is my ZipFilesToRepsonse method. It uses some objects that aren't shown here for simplicity sake. It also streams the files from azure blob storage
public void ZipFilesToResponse(HttpResponseBase response, IEnumerable<Tuple<string,string>> filePathNames, string zipFileName)
{
using (var zipOutputStream = new ZipOutputStream(response.OutputStream))
{
zipOutputStream.SetLevel(0); // 0 - store only to 9 - means best compression
response.BufferOutput = false;
response.AddHeader("Content-Disposition", "attachment; filename=" + zipFileName);
response.ContentType = "application/octet-stream";
Dictionary<string,long> sizeDictionary = new Dictionary<string, long>();
long totalSize = 0;
foreach (var file in filePathNames)
{
long size = GetBlobProperties(file.Item1).Length;
totalSize += size;
sizeDictionary.Add(file.Item1,size);
}
//Zip files breaks if we dont have exact content length
//and it isn't nesccarily the total lengths of the contents
//dont see a simple way to get it set correctly without downloading entire file to server first
//so for now we wont include a content length
//response.AddHeader("Content-Length",totalSize.ToString());
foreach (var file in filePathNames)
{
long size = sizeDictionary[file.Item1];
var entry = new ZipEntry(file.Item2)
{
DateTime = DateTime.Now,
Size = size
};
zipOutputStream.PutNextEntry(entry);
Container.GetBlockBlobReference(file.Item1).DownloadToStream(zipOutputStream);
response.Flush();
if (!response.IsClientConnected)
{
break;
}
}
zipOutputStream.Finish();
zipOutputStream.Close();
}
response.End();
}
Currently, I download byte arrays as files using JsInterop.
Here is my JS file:
function downloadFile(fileName, base64String) {
const url = "data:application/octet-stream;base64," + base64String;
const anchorElement = document.createElement('a');
anchorElement.href = url;
anchorElement.download = fileName ?? '';
anchorElement.click();
anchorElement.remove();
}
And here is a method in my razor component:
async Task DownloadFile(byte[] file)
{
ShowMessage("Start");
await JSRuntime.InvokeVoidAsync("downloadFile", "FileName", Convert.ToBase64String(file));
ShowMessage("End");
}
This code works, and I am able to download files. My issue is that I cannot implement the progress bar, or even show the loading spinner because await JSRuntime has no idea about an actual file download size and its progress. JSRuntime only launches the process of downloading and immediately continues to the next line of code.
In the code above ShowMessage("Start") and ShowMessage("End") are both shown one after another as soon as I click the download button, but the file in the browser downloads much later (depending on the file size).
How may I await the download process and execute relevant code only when the file has been downloaded? And it would be even better if I could read downloaded bytes to show a progress bar with percentages.
Update: for test purposes, I upload the file from the browser and store it in a byte[] variable. Then I download the same file from the variable using JS. Even though I store the file in the memory, it still takes time to download the file. I suppose that when I store a file in memory, it is already on my PC (client), and should download immediately. But instead, my window gets frozen for the duration of downloading the file. Tested on 6 - 11- 20 MB files. The bigger file, the more I have to wait for it to download.
I suggest you should be show message ShowMessage("Start") and ShowMessage("End"); in function downloadFile at JS
In one of our project we need the functionality to download a file from server to client location.
For this we are using an ashx handler to do the operation. Its working perfectly and we are able to download files.
Now we need a requirement like we need to update a field when a download is started and completed. Is there any way to do this.
Once we click the download link the Save as dialog box will appear and after that i think we don't have any control to check the progress. I think we even don't know which button is clicked ie we don't know whether the user is clicked a 'Yes' or 'No'.
Can anyone please suggest a method to know when the download is started and when it has been completed? We are using Asp.Net 2.0 with c#.
The handler used for download is given below
string fileUrl = string.Empty;
if (context.Request["fileUrl"] != null)
{
fileUrl = context.Request["fileUrl"].ToString();
}
string filename = System.IO.Path.GetFileName(fileUrl);
context.Response.ClearContent();
context.Response.ContentType = "application/exe";
context.Response.AddHeader("content-disposition", String.Format("attachment; filename={0}", filename));
context.Response.TransmitFile(fileUrl);
context.Response.Flush();
The file is downloaded from an aspx page method like
private void DownloadExe()
{
string downloadUrl = "Test.exe");
Response.Redirect("Test.ashx?fileUrl=" + downloadUrl, false);
}
Your ASHX handler knwos if download started (since it is actually get called) and when download is completed (end of handler is reached). You may even get some progress server side if you are writing response manually in chunks, this way you also may be able to detect some cases when user cancels download (if writing to response stream fails at some point).
Depending on your needs you may be able to transfer this information to other pages (i.e. via session state) or simply store in some database.
How about this:
Response.BufferOutput = false;
Response.TransmitFile(fileUrl);
//download complete code
If you disable response output buffering then it won't move past the line of code that sends the file to the client until the client has finished receiving it. If they cancel the download half way through it throws a HttpException so the download complete code doesn't get run.
You could also place your download complete code after your call to flush the buffer. But it's better not to enable buffering when sending large binary files to save on server memory.
Ok I had the same problem and jumped over this site:
Check over coockies
This works great for me.
Whenever I click a button in my GUI this code gets executed
this.file_name = #"c:\temp\file_" + DateTime.Now.Ticks / 10000 +".pdf";
client.DownloadFileCompleted +=
new AsyncCompletedEventHandler(pdfDownloadComplete);
client.DownloadFileAsync(new Uri(uri), file_name);
It's supposed to download a pdf file. It works fine the first time I click it, but the second time all it does is it creates an empty file in the temp directory and downloads nothing, I click the same button afterwards nothing new happens.
I cannot figure it out why it won't download more than once.
Later Edit
This is the complete code which is needed http://pastie.org/private/y7na2f4fjdu6anzteoa
I noticed that if I remove the download that checks for content type, the app downloads files without a problem
client.HeadOnly = true;
byte[] body = client.DownloadData(uri); // note should be 0-length
string type = client.ResponseHeaders["content-type"];
client.HeadOnly = false;
Still, I need to know if I'm getting text of a file from that URL so I need to make that call.
Your code has at least one issue that might be root of the problem:
You do an async download inside a using block. I don't really know what happens if the download is still running when the scope of the using block is left, but I guess that it is canceled. You should avoid that problem by using DownloadFile instead of DownloadFileAsync.
Additionally, please check whether it works, when you use the normal WebClient class and not your MyClient class.