I am trying to implement download a zip file and unzip it with progressbar. Roughly below how my code looks like
var handler = new HttpClientHandler() { AllowAutoRedirect = true };
var ph = new ProgressMessageHandler(handler);
ph.HttpReceiveProgress += (_, args) => { GetProgress(args.ProgressPercentage); };
var httpClient = new HttpClient(ph);
var response = await _httpClient.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead, cancellationToken.Token);
response.EnsureSuccessStatusCode();
using (var zipInputStream = new ZipInputStream(response.Content.ReadAsStreamAsync()))
{
while (zipInputStream.GetNextEntry() is { } zipEntry)
{
var entryFileName = zipEntry.Name;
var buffer = new byte[4096];
var directoryName = Path.GetDirectoryName(fullZipToPath);
if (directoryName?.Length > 0)
{
Directory.CreateDirectory(directoryName);
}
if (Path.GetFileName(fullZipToPath).Length == 0)
{
continue;
}
using (var streamWriter = File.Create(fullZipToPath))
{
StreamUtils.Copy(zipInputStream, streamWriter, buffer);
}
}
}
My problem here is when I use ResponseHeadersRead
instead of ResponseContentRead
, ProgressMessageHandler
is not reporting progress, using ResponseContentRead
I can see the progress incrementing correctly.
It also works fine using ResponseHeadersRead and copy the stream directly to a file as below.
await using (var fs = new FileStream(pathToNewFile + "/test.zip", FileMode.Create))
{
await response.Content.CopyToAsync(fs);
}
But I feel like this way is waste to download zip to a temp file and unzip again with another stream while i can directly pass the stream to ZipInputStream like I do above. I believe I do something wrong here as I possible misunderstand the usage of ZipInputStream or ResponseHeadersRead? Does ZipInputStream require entire stream loaded at once while ResponseHeadersRead can gradually download the stream, so at the end I cannot directly pass the stream like that?
Please give me a suggestion if that is bad usage or i miss something?
EDIT: Problem seems to be because StreamUtils.Copy
is sync, and Progress is only reported when this line is executed completed but it is already 100% once it is done. It looks like that ZipInputStream
doesn't provide any async option to copy stream into a file. I need to probably find an alternative.
EDIT 2: I have changed the code using the built in ZipArchive, but also implements as Sync
using (var zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Read))
{
zipArchive.ExtractToDirectory(directoryName, true)
}
EDIT 3 Working solution: like I said if I just copy the response first to filestream and write as zip file
await using (var fs = new FileStream(pathToNewFile + "/test.zip", FileMode.Create))
{
await response.Content.CopyToAsync(fs);
}
then read this zip file into stream and use this stream as below. it works, I can see the progress.
var fileToDecompress = new FileInfo(_pathToNewFile + $"/test.zip");
var stream = fileToDecompress.OpenRead();
using (var zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Read))
{
zipArchive.ExtractToDirectory(directoryName, true)
}
As you have found, the UI will not update if the copying is done synchronously.
Unfortunately, there is no async version of
ExtractToDirectory
as yet. Ther is an open GitHub issue for this.In the meantime, you can use the following code. Most of it is taken from the original source code:
Note that if the base stream is not seekable then
ZipArchive
will synchronously buffer it into aMemoryStream
. To avoid that, you can buffer it yourself