End goal:
Users are uploading a large number of files in different sizes to my web site. And i dont want duplicate files on the disk.
The solution i have been using is a simple SH1 hash of the file when it is uploaded. With code like this:
public static string HashFile(string FileName)
{
using (FileStream stream = File.OpenRead(FileName))
{
SHA1Managed sha = new SHA1Managed();
byte[] checksum = sha.ComputeHash(stream);
string sendCheckSum = BitConverter.ToString(checksum).Replace("-",string.Empty);
return sendCheckSum;
}
}
This "works" fine for smaller files, but its a big pain when the file is 30gb. So i would like to hash the file as im reciving it from the client. I get the file from the client in "chunks" and size of the chunk is not always static.
Code that recives the file.
int chunk = context.Request["chunk"] != null ? int.Parse(context.Request["chunk"]) : 0;
int chunks = context.Request["chunks"] != null ? int.Parse(context.Request["chunks"]) : 0;
string fileName = context.Request["name"] != null ? context.Request["name"] : string.Empty;
HttpPostedFile fileUpload = context.Request.Files[0];
string fullFilePath = Path.Combine(SiteSettings.UploadTempFolder, fileName);
using (var fs = new FileStream(fullFilePath, chunk == 0 ? FileMode.Create : FileMode.Append))
{
var buffer = new byte[fileUpload.InputStream.Length];
fileUpload.InputStream.Read(buffer, 0, buffer.Length);
fs.Write(buffer, 0, buffer.Length);
**// Here i want the hash, when i have the file data in memory.**
}
You can always create your own stream :)
public class ActionStream : Stream
{
private readonly Stream _innerStream;
private readonly Action<byte[], int, int> _readAction;
public ActionStream(Stream innerStream, Action<byte[], int, int> readAction)
{
_innerStream = innerStream;
_readAction = readAction;
}
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => _innerStream.Length;
public override long Position
{
get { return _innerStream.Position; }
set { throw new NotSupportedException(); }
}
public override void Flush() { }
public override int Read(byte[] buffer, int offset, int count)
{
var bytesRead = _innerStream.Read(buffer, offset, count);
_readAction(buffer, offset, bytesRead);
return bytesRead;
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotSupportedException();
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_innerStream.Dispose();
}
base.Dispose(disposing);
}
public override void SetLength(long value) { throw new NotSupportedException(); }
public override void Write(byte[] buffer, int offset, int count)
{
throw new NotSupportedException();
}
}
This allows you to bind together the two stream operations you're doing:
using (var fs = new FileStream(path, chunk == 0 ? FileMode.Create : FileMode.Append))
{
var as = new ActionStream(fileUpload.InputStream,
(buffer, offset, bytesRead) =>
{
fs.Write(buffer, offset, bytesRead);
});
var sha = new SHA1Managed();
var checksum = sha.ComputeHash(as);
}
This assumes that SHA1Manager reads through every single byte of the input stream in order - you should check that. I'm pretty sure that is how it works, though :)
This is a cut and paste from:
Compute a hash from a stream of unknown length in C#
MD5, like other hash functions, does not require two passes.
To start:
HashAlgorithm hasher = ..;
hasher.Initialize();
As each block of data arrives:
byte[] buffer = ..;
int bytesReceived = ..;
hasher.TransformBlock(buffer, 0, bytesReceived, null, 0);
To finish and retrieve the hash:
hasher.TransformFinalBlock(new byte[0], 0, 0);
byte[] hash = hasher.Hash;
This pattern works for any type derived from HashAlgorithm, including MD5CryptoServiceProvider and SHA1Managed.
HashAlgorithm also defines a method ComputeHash which takes a Stream object; however, this method will block the thread until the stream is consumed. Using the TransformBlock approach allows an "asynchronous hash" that is computed as data arrives without using up a thread.
I integrated (this)EPUB Reader reader to my project. It is working fine. & I want to load the file from SDCard instead of Isolated storage of device
To open file from Isolated storage we have IsolatedStorageFileStream like this
IsolatedStorageFileStream isfs;
using (IsolatedStorageFile isf = IsolatedStorageFile.GetUserStoreForApplication())
{
try
{
isfs = isf.OpenFile([Path to file], FileMode.Open);
}
catch
{
return;
}
}
ePubView.Source = isfs;
For file in SDcard I tried like this
ExternalStorageDevice sdCard = (await ExternalStorage.GetExternalStorageDevicesAsync()).FirstOrDefault();
// If the SD card is present, get the route from the SD card.
if (sdCard != null)
{
ExternalStorageFile file = await sdCard.GetFileAsync(_sdFilePath);
// _sdFilePath is string that having file path of file in SDCard
// Create a stream for the route.
Stream file = await file.OpenForReadAsync();
// Read the route data.
ePubView.Source = file;
}
Here I am getting exception System.IO.EndOfStreamException
If You want try.. Here is my project sample link
Question : How can I give my file as source to epubView control
Is this is proper way, please give a suggestion regarding this..
Thanks
Although I've not tried your approach, and I cannot say exactly where is an error (maybe file from SD is read async and thus you get EndOfStream, and please keep in mind that as it is said at EPUB Reader Site - it's under heavy developement). Check if after copying the file to ISolatedStorage, you will be able to use it. I would try in this case first copying from SD to Memory stream like this:
ExternalStorageDevice sdCard = (await ExternalStorage.GetExternalStorageDevicesAsync()).FirstOrDefault();
if (sdCard != null)
{
MemoryStream newStream = new MemoryStream();
using (ExternalStorageFile file = await sdCard.GetFileAsync(_sdFilePath))
using (Stream SDfile = await file.OpenForReadAsync())
newStream = await ReadToMemory(SDfile);
ePubView.Source = newStream;
}
And ReadToMemory:
private async Task<MemoryStream> ReadToMemory(Stream streamToRead)
{
MemoryStream targetStream = new MemoryStream();
const int BUFFER_SIZE = 1024;
byte[] buf = new byte[BUFFER_SIZE];
int bytesread = 0;
while ((bytesread = await streamToRead.ReadAsync(buf, 0, BUFFER_SIZE)) > 0)
{
targetStream.Write(buf, 0, bytesread);
}
return targetStream;
}
Maybe it will help.
There's a bug with the stream returned from ExternalStorageFile. There's two options to get around it...
If the file is small then you can simply copy the stream to a MemoryStream:
Stream s = await file.OpenForReadAsync();
MemoryStream ms = new MemoryStream();
s.CopyTo(ms);
However, if the file is too large you'll run in to memory issues so the following stream wrapper class can be used to correct Microsoft's bug (though in future versions of Windows Phone you'll need to disable this fix once the bug has been fixed):
using System;
using System.IO;
namespace WindowsPhoneBugFix
{
/// <summary>
/// Stream wrapper to circumnavigate buggy Stream reading of stream returned by ExternalStorageFile.OpenForReadAsync()
/// </summary>
public sealed class ExternalStorageFileWrapper : Stream
{
private Stream _stream; // Underlying stream
public ExternalStorageFileWrapper(Stream stream)
{
if (stream == null)
throw new ArgumentNullException("stream");
_stream = stream;
}
// Workaround described here - http://stackoverflow.com/a/21538189/250254
public override long Seek(long offset, SeekOrigin origin)
{
ulong uoffset = (ulong)offset;
ulong fix = ((uoffset & 0xffffffffL) << 32) | ((uoffset & 0xffffffff00000000L) >> 32);
return _stream.Seek((long)fix, origin);
}
public override bool CanRead
{
get { return _stream.CanRead; }
}
public override bool CanSeek
{
get { return _stream.CanSeek; }
}
public override bool CanWrite
{
get { return _stream.CanWrite; }
}
public override void Flush()
{
_stream.Flush();
}
public override long Length
{
get { return _stream.Length; }
}
public override long Position
{
get
{
return _stream.Position;
}
set
{
_stream.Position = value;
}
}
public override int Read(byte[] buffer, int offset, int count)
{
return _stream.Read(buffer, offset, count);
}
public override void SetLength(long value)
{
_stream.SetLength(value);
}
public override void Write(byte[] buffer, int offset, int count)
{
_stream.Write(buffer, offset, count);
}
}
}
Code is available here to drop in to your project:
https://github.com/gavinharriss/ExternalStorageFileWrapper-wp8
Example of use:
ExternalStorageFile file = await device.GetFileAsync(filename); // device is an instance of ExternalStorageDevice
Stream streamOriginal = await file.OpenForReadAsync();
ExternalStorageFileWrapper streamToUse = new ExternalStorageFileWrapper(streamOriginal);
i have a file downloader function:
HttpClientHandler aHandler = new HttpClientHandler();
aHandler.ClientCertificateOptions = ClientCertificateOption.Automatic;
HttpClient aClient = new HttpClient(aHandler);
aClient.DefaultRequestHeaders.ExpectContinue = false;
HttpResponseMessage response = await aClient.GetAsync(url);
InMemoryRandomAccessStream randomAccessStream = new InMemoryRandomAccessStream();
// To save downloaded image to local storage
var imageFile = await ApplicationData.Current.LocalFolder.CreateFileAsync(
filename, CreationCollisionOption.ReplaceExisting);
var fs = await imageFile.OpenAsync(FileAccessMode.ReadWrite);
DataWriter writer = new DataWriter(fs.GetOutputStreamAt(0));
writer.WriteBytes(await response.Content.ReadAsByteArrayAsync());
await writer.StoreAsync();
//current.image.SetSource(randomAccessStream);
writer.DetachStream();
await fs.FlushAsync();
How can i realize progress bar functionality?
Maybe i can get the writers bytes written so far? Or something?
P.S. I cant use DownloadOperation(Background transferring) because data from server requests certificate - and this functionality doesn't exist in DownloadOperations.
From .Net 4.5 onwards: Use IProgress<T>
Since .Net 4.5 you can handle asynchronous progress reporting with the IProgress<T> interface. You can write an extension method for downloading files using the HttpClient that can be called like this where progress is the implementation of IProgress<float> for your progress bar or other UI stuff:
// Seting up the http client used to download the data
using (var client = new HttpClient()) {
client.Timeout = TimeSpan.FromMinutes(5);
// Create a file stream to store the downloaded data.
// This really can be any type of writeable stream.
using (var file = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None)) {
// Use the custom extension method below to download the data.
// The passed progress-instance will receive the download status updates.
await client.DownloadAsync(DownloadUrl, file, progress, cancellationToken);
}
}
Implementation
The code for this extension method looks like this. Note that this extension depends on another extension for handling asynchronous stream copying with progress reporting.
public static class HttpClientExtensions
{
public static async Task DownloadAsync(this HttpClient client, string requestUri, Stream destination, IProgress<float> progress = null, CancellationToken cancellationToken = default) {
// Get the http headers first to examine the content length
using (var response = await client.GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead)) {
var contentLength = response.Content.Headers.ContentLength;
using (var download = await response.Content.ReadAsStreamAsync()) {
// Ignore progress reporting when no progress reporter was
// passed or when the content length is unknown
if (progress == null || !contentLength.HasValue) {
await download.CopyToAsync(destination);
return;
}
// Convert absolute progress (bytes downloaded) into relative progress (0% - 100%)
var relativeProgress = new Progress<long>(totalBytes => progress.Report((float)totalBytes / contentLength.Value));
// Use extension method to report progress while downloading
await download.CopyToAsync(destination, 81920, relativeProgress, cancellationToken);
progress.Report(1);
}
}
}
}
With stream extension for the real progress reporting:
public static class StreamExtensions
{
public static async Task CopyToAsync(this Stream source, Stream destination, int bufferSize, IProgress<long> progress = null, CancellationToken cancellationToken = default) {
if (source == null)
throw new ArgumentNullException(nameof(source));
if (!source.CanRead)
throw new ArgumentException("Has to be readable", nameof(source));
if (destination == null)
throw new ArgumentNullException(nameof(destination));
if (!destination.CanWrite)
throw new ArgumentException("Has to be writable", nameof(destination));
if (bufferSize < 0)
throw new ArgumentOutOfRangeException(nameof(bufferSize));
var buffer = new byte[bufferSize];
long totalBytesRead = 0;
int bytesRead;
while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0) {
await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
totalBytesRead += bytesRead;
progress?.Report(totalBytesRead);
}
}
}
Here's a self-contained class that'll do the download, and report back the progress percentage, based on code from TheBlueSky on this SO answer, and eriksendc on this GitHub comment.
public class HttpClientDownloadWithProgress : IDisposable
{
private readonly string _downloadUrl;
private readonly string _destinationFilePath;
private HttpClient _httpClient;
public delegate void ProgressChangedHandler(long? totalFileSize, long totalBytesDownloaded, double? progressPercentage);
public event ProgressChangedHandler ProgressChanged;
public HttpClientDownloadWithProgress(string downloadUrl, string destinationFilePath)
{
_downloadUrl = downloadUrl;
_destinationFilePath = destinationFilePath;
}
public async Task StartDownload()
{
_httpClient = new HttpClient { Timeout = TimeSpan.FromDays(1) };
using (var response = await _httpClient.GetAsync(_downloadUrl, HttpCompletionOption.ResponseHeadersRead))
await DownloadFileFromHttpResponseMessage(response);
}
private async Task DownloadFileFromHttpResponseMessage(HttpResponseMessage response)
{
response.EnsureSuccessStatusCode();
var totalBytes = response.Content.Headers.ContentLength;
using (var contentStream = await response.Content.ReadAsStreamAsync())
await ProcessContentStream(totalBytes, contentStream);
}
private async Task ProcessContentStream(long? totalDownloadSize, Stream contentStream)
{
var totalBytesRead = 0L;
var readCount = 0L;
var buffer = new byte[8192];
var isMoreToRead = true;
using (var fileStream = new FileStream(_destinationFilePath, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true))
{
do
{
var bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length);
if (bytesRead == 0)
{
isMoreToRead = false;
TriggerProgressChanged(totalDownloadSize, totalBytesRead);
continue;
}
await fileStream.WriteAsync(buffer, 0, bytesRead);
totalBytesRead += bytesRead;
readCount += 1;
if (readCount % 100 == 0)
TriggerProgressChanged(totalDownloadSize, totalBytesRead);
}
while (isMoreToRead);
}
}
private void TriggerProgressChanged(long? totalDownloadSize, long totalBytesRead)
{
if (ProgressChanged == null)
return;
double? progressPercentage = null;
if (totalDownloadSize.HasValue)
progressPercentage = Math.Round((double)totalBytesRead / totalDownloadSize.Value * 100, 2);
ProgressChanged(totalDownloadSize, totalBytesRead, progressPercentage);
}
public void Dispose()
{
_httpClient?.Dispose();
}
}
Usage:
var downloadFileUrl = "http://example.com/file.zip";
var destinationFilePath = Path.GetFullPath("file.zip");
using (var client = new HttpClientDownloadWithProgress(downloadFileUrl, destinationFilePath))
{
client.ProgressChanged += (totalFileSize, totalBytesDownloaded, progressPercentage) => {
Console.WriteLine($"{progressPercentage}% ({totalBytesDownloaded}/{totalFileSize})");
};
await client.StartDownload();
}
Result:
7.81% (26722304/342028776)
8.05% (27535016/342028776)
8.28% (28307984/342028776)
8.5% (29086548/342028776)
8.74% (29898692/342028776)
8.98% (30704184/342028776)
9.22% (31522816/342028776)
The best way to go is using Windows.Web.Http.HttpClient instead of System.Net.Http.HttpClient. The first one supports progress.
But if for some reason you want to stick to the System.Net one, you will need to implement your own progress.
Remove the DataWriter, remove the InMemoryRandomAccessStream and add HttpCompletionOption.ResponseHeadersRead to GetAsync call so it returns as soon as headers are received, not when the whole response is received. I.e.:
// Your original code.
HttpClientHandler aHandler = new HttpClientHandler();
aHandler.ClientCertificateOptions = ClientCertificateOption.Automatic;
HttpClient aClient = new HttpClient(aHandler);
aClient.DefaultRequestHeaders.ExpectContinue = false;
HttpResponseMessage response = await aClient.GetAsync(
url,
HttpCompletionOption.ResponseHeadersRead); // Important! ResponseHeadersRead.
// To save downloaded image to local storage
var imageFile = await ApplicationData.Current.LocalFolder.CreateFileAsync(
filename,
CreationCollisionOption.ReplaceExisting);
var fs = await imageFile.OpenAsync(FileAccessMode.ReadWrite);
// New code.
Stream stream = await response.Content.ReadAsStreamAsync();
IInputStream inputStream = stream.AsInputStream();
ulong totalBytesRead = 0;
while (true)
{
// Read from the web.
IBuffer buffer = new Windows.Storage.Streams.Buffer(1024);
buffer = await inputStream.ReadAsync(
buffer,
buffer.Capacity,
InputStreamOptions.None);
if (buffer.Length == 0)
{
// There is nothing else to read.
break;
}
// Report progress.
totalBytesRead += buffer.Length;
System.Diagnostics.Debug.WriteLine("Bytes read: {0}", totalBytesRead);
// Write to file.
await fs.WriteAsync(buffer);
}
inputStream.Dispose();
fs.Dispose();
The simplest way to implement progress tracking for both uploading and downloading is to use ProgressMessageHandler from the Microsoft.AspNet.WebApi.Client nuget package.
Note: this library was originally named System.Net.Http.Formatting, and was renamed to Microsoft.AspNet.WebApi.Client. However, this library is not related to ASP.Net and can be used by any project looking for official Microsoft extensions to HttpClient. The source code is available here.
Example:
var handler = new HttpClientHandler() { AllowAutoRedirect = true };
var ph = new ProgressMessageHandler(handler);
ph.HttpSendProgress += (_, args) =>
{
Console.WriteLine($"upload progress: {(double)args.BytesTransferred / args.TotalBytes}");
};
ph.HttpReceiveProgress += (_, args) =>
{
Console.WriteLine($"download progress: {(double)args.BytesTransferred / args.TotalBytes}");
};
var client = new HttpClient(ph);
await client.SendAsync(...);
Note that this will not report progress if uploading a byte array. The request message content must be a stream.
The following code shows a minimal example of what must be done against the HttpClient api to get download progress.
HttpClient client = //...
// Must use ResponseHeadersRead to avoid buffering of the content
using (var response = await client.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead)){
// You must use as stream to have control over buffering and number of bytes read/received
using (var stream = await response.Content.ReadAsStreamAsync())
{
// Read/process bytes from stream as appropriate
// Calculated by you based on how many bytes you have read. Likely incremented within a loop.
long bytesRecieved = //...
long? totalBytes = response.Content.Headers.ContentLength;
double? percentComplete = (double)bytesRecieved / totalBytes;
// Do what you want with `percentComplete`
}
}
The above does not tell you how to process the stream, how to report the process, or try to provide a direct solution to the code in the original question. However, this answer may be more accessible to future readers who wish to apply having progress to in their code.
same as #René Sackers solution above, but added the ability to cancel the download
class HttpClientDownloadWithProgress : IDisposable
{
private readonly string _downloadUrl;
private readonly string _destinationFilePath;
private readonly CancellationToken? _cancellationToken;
private HttpClient _httpClient;
public delegate void ProgressChangedHandler(long? totalFileSize, long totalBytesDownloaded, double? progressPercentage);
public event ProgressChangedHandler ProgressChanged;
public HttpClientDownloadWithProgress(string downloadUrl, string destinationFilePath, CancellationToken? cancellationToken = null)
{
_downloadUrl = downloadUrl;
_destinationFilePath = destinationFilePath;
_cancellationToken = cancellationToken;
}
public async Task StartDownload()
{
_httpClient = new HttpClient { Timeout = TimeSpan.FromDays(1) };
using (var response = await _httpClient.GetAsync(_downloadUrl, HttpCompletionOption.ResponseHeadersRead))
await DownloadFileFromHttpResponseMessage(response);
}
private async Task DownloadFileFromHttpResponseMessage(HttpResponseMessage response)
{
response.EnsureSuccessStatusCode();
var totalBytes = response.Content.Headers.ContentLength;
using (var contentStream = await response.Content.ReadAsStreamAsync())
await ProcessContentStream(totalBytes, contentStream);
}
private async Task ProcessContentStream(long? totalDownloadSize, Stream contentStream)
{
var totalBytesRead = 0L;
var readCount = 0L;
var buffer = new byte[8192];
var isMoreToRead = true;
using (var fileStream = new FileStream(_destinationFilePath, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true))
{
do
{
int bytesRead;
if (_cancellationToken.HasValue)
{
bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length, _cancellationToken.Value);
}
else
{
bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length);
}
if (bytesRead == 0)
{
isMoreToRead = false;
continue;
}
await fileStream.WriteAsync(buffer, 0, bytesRead);
totalBytesRead += bytesRead;
readCount += 1;
if (readCount % 10 == 0)
TriggerProgressChanged(totalDownloadSize, totalBytesRead);
}
while (isMoreToRead);
}
//the last progress trigger should occur after the file handle has been released or you may get file locked error
TriggerProgressChanged(totalDownloadSize, totalBytesRead);
}
private void TriggerProgressChanged(long? totalDownloadSize, long totalBytesRead)
{
if (ProgressChanged == null)
return;
double? progressPercentage = null;
if (totalDownloadSize.HasValue)
progressPercentage = Math.Round((double)totalBytesRead / totalDownloadSize.Value * 100, 2);
ProgressChanged(totalDownloadSize, totalBytesRead, progressPercentage);
}
public void Dispose()
{
_httpClient?.Dispose();
}
}
René Sackers version is excellent but it could be better. Specifically, it has a subtle race condition caused by TriggerProgressChanged firing before the stream closes. The fix is to fire the event after the stream is explicitly disposed. The version below includes the above change, inherits from HttpClient and adds support for cancellation tokens.
public delegate void ProgressChangedHandler(long? totalFileSize, long totalBytesDownloaded, double? progressPercentage);
public class HttpClientWithProgress : HttpClient
{
private readonly string _DownloadUrl;
private readonly string _DestinationFilePath;
public event ProgressChangedHandler ProgressChanged;
public HttpClientWithProgress(string downloadUrl, string destinationFilePath)
{
_DownloadUrl = downloadUrl;
_DestinationFilePath = destinationFilePath;
}
public async Task StartDownload()
{
using (var response = await GetAsync(_DownloadUrl, HttpCompletionOption.ResponseHeadersRead))
await DownloadFileFromHttpResponseMessage(response);
}
public async Task StartDownload(CancellationToken cancellationToken)
{
using (var response = await GetAsync(_DownloadUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken))
await DownloadFileFromHttpResponseMessage(response);
}
private async Task DownloadFileFromHttpResponseMessage(HttpResponseMessage response)
{
response.EnsureSuccessStatusCode();
long? totalBytes = response.Content.Headers.ContentLength;
using (var contentStream = await response.Content.ReadAsStreamAsync())
await ProcessContentStream(totalBytes, contentStream);
}
private async Task ProcessContentStream(long? totalDownloadSize, Stream contentStream)
{
long totalBytesRead = 0L;
long readCount = 0L;
byte[] buffer = new byte[8192];
bool isMoreToRead = true;
using (FileStream fileStream = new FileStream(_DestinationFilePath, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true))
{
do
{
int bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length);
if (bytesRead == 0)
{
isMoreToRead = false;
continue;
}
await fileStream.WriteAsync(buffer, 0, bytesRead);
totalBytesRead += bytesRead;
readCount += 1;
if (readCount % 10 == 0)
TriggerProgressChanged(totalDownloadSize, totalBytesRead);
}
while (isMoreToRead);
}
TriggerProgressChanged(totalDownloadSize, totalBytesRead);
}
private void TriggerProgressChanged(long? totalDownloadSize, long totalBytesRead)
{
if (ProgressChanged == null)
return;
double? progressPercentage = null;
if (totalDownloadSize.HasValue)
progressPercentage = Math.Round((double)totalBytesRead / totalDownloadSize.Value * 100, 2);
ProgressChanged(totalDownloadSize, totalBytesRead, progressPercentage);
}
}
This is my variation on the answer of René Sackers. Main differences:
A more functional style.
Only one method instead of a whole object.
Can cancel the download
public async static Task Download(
string downloadUrl,
string destinationFilePath,
Func<long?, long, double?, bool> progressChanged)
{
using var httpClient = new HttpClient { Timeout = TimeSpan.FromDays(1) };
using var response = await httpClient.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead);
response.EnsureSuccessStatusCode();
var totalBytes = response.Content.Headers.ContentLength;
using var contentStream = await response.Content.ReadAsStreamAsync();
var totalBytesRead = 0L;
var readCount = 0L;
var buffer = new byte[8192];
var isMoreToRead = true;
static double? calculatePercentage(long? totalDownloadSize, long totalBytesRead) => totalDownloadSize.HasValue ? Math.Round((double)totalBytesRead / totalDownloadSize.Value * 100, 2) : null;
using var fileStream = new FileStream(destinationFilePath, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true);
do
{
var bytesRead = await contentStream.ReadAsync(buffer);
if (bytesRead == 0)
{
isMoreToRead = false;
if (progressChanged(totalBytes, totalBytesRead, calculatePercentage(totalBytes, totalBytesRead)))
{
throw new OperationCanceledException();
}
continue;
}
await fileStream.WriteAsync(buffer.AsMemory(0, bytesRead));
totalBytesRead += bytesRead;
readCount++;
if (readCount % 100 == 0)
{
if (progressChanged(totalBytes, totalBytesRead, calculatePercentage(totalBytes, totalBytesRead)))
{
throw new OperationCanceledException();
}
}
}
while (isMoreToRead);
}
It can be called this way:
// Change this variable to stop the download
// You can use a global variable or some kind of state management
var mustStop = false;
var downloadProgress = (long? _, long __, double? progressPercentage) =>
{
if (progressPercentage.HasValue)
progressBar.Value = progressPercentage.Value;
// In this example only the variable is checked
// You could write other code that evaluates other conditions
return mustStop;
};
SomeClass.Download("https://example.com/bigfile.zip", "c:\downloads\file.zip", downloadProgress);
Hm, you could have another thread check the current size of the stream being written (you'd also pass the expected file size to it) and then update the progress bar accordingly.
This is a modified version of René Sackers answer with the following functional changes:
http client not disposed (because it should not be disposed)
better progress handling
callback to create httpRequest (custom header support)
utilizes ArrayPool to reduce memory footprint
automatic event subscribe+unsubscribe to prevent memory leaks by event handlers
You can also use this nuget package https://www.nuget.org/packages/Amusoft.Toolkit.Http to gain all benefits. Since it supports net462 and above that is probably the easiest way.
Usage:
await DownloadWithProgress.ExecuteAsync(HttpClients.General, assetUrl, downloadFilePath, progressHandler, () =>
{
var requestMessage = new HttpRequestMessage(HttpMethod.Get, assetUrl);
requestMessage.Headers.Accept.TryParseAdd("application/octet-stream");
return requestMessage;
});
I guess i am not the only one who needs custom headers so i figured i would share this rewrite
Implementation:
public delegate void DownloadProgressHandler(long? totalFileSize, long totalBytesDownloaded, double? progressPercentage);
public static class DownloadWithProgress
{
public static async Task ExecuteAsync(HttpClient httpClient, string downloadPath, string destinationPath, DownloadProgressHandler progress, Func<HttpRequestMessage> requestMessageBuilder = null)
{
requestMessageBuilder ??= GetDefaultRequestBuilder(downloadPath);
var download = new HttpClientDownloadWithProgress(httpClient, destinationPath, requestMessageBuilder);
download.ProgressChanged += progress;
await download.StartDownload();
download.ProgressChanged -= progress;
}
private static Func<HttpRequestMessage> GetDefaultRequestBuilder(string downloadPath)
{
return () => new HttpRequestMessage(HttpMethod.Get, downloadPath);
}
}
internal class HttpClientDownloadWithProgress
{
private readonly HttpClient _httpClient;
private readonly string _destinationFilePath;
private readonly Func<HttpRequestMessage> _requestMessageBuilder;
private int _bufferSize = 8192;
public event DownloadProgressHandler ProgressChanged;
public HttpClientDownloadWithProgress(HttpClient httpClient, string destinationFilePath, Func<HttpRequestMessage> requestMessageBuilder)
{
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_destinationFilePath = destinationFilePath ?? throw new ArgumentNullException(nameof(destinationFilePath));
_requestMessageBuilder = requestMessageBuilder ?? throw new ArgumentNullException(nameof(requestMessageBuilder));
}
public async Task StartDownload()
{
using var requestMessage = _requestMessageBuilder.Invoke();
using var response = await _httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead);
await DownloadAsync(response);
}
private async Task DownloadAsync(HttpResponseMessage response)
{
response.EnsureSuccessStatusCode();
var totalBytes = response.Content.Headers.ContentLength;
using (var contentStream = await response.Content.ReadAsStreamAsync())
await ProcessContentStream(totalBytes, contentStream);
}
private async Task ProcessContentStream(long? totalDownloadSize, Stream contentStream)
{
var totalBytesRead = 0L;
var readCount = 0L;
var buffer = ArrayPool<byte>.Shared.Rent(_bufferSize);
var isMoreToRead = true;
using (var fileStream = new FileStream(_destinationFilePath, FileMode.Create, FileAccess.Write, FileShare.None, _bufferSize, true))
{
do
{
var bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length);
if (bytesRead == 0)
{
isMoreToRead = false;
ReportProgress(totalDownloadSize, totalBytesRead);
continue;
}
await fileStream.WriteAsync(buffer, 0, bytesRead);
totalBytesRead += bytesRead;
readCount += 1;
if (readCount % 100 == 0)
ReportProgress(totalDownloadSize, totalBytesRead);
}
while (isMoreToRead);
}
ArrayPool<byte>.Shared.Return(buffer);
}
private void ReportProgress(long? totalDownloadSize, long totalBytesRead)
{
double? progressPercentage = null;
if (totalDownloadSize.HasValue)
progressPercentage = Math.Round((double)totalBytesRead / totalDownloadSize.Value * 100, 2);
ProgressChanged?.Invoke(totalDownloadSize, totalBytesRead, progressPercentage);
}
}
Im not really sure how to measure how the completion logic, but for now this seems to do it.
public event ProgressChangedHandler ProgressChanged;
public event ProgressCompleteHandler DownloadComplete;
...
TriggerProgressChanged(totalDownloadSize, totalBytesRead);
TriggerDownloadComplete(totalBytesRead == totalDownloadSize);
private void TriggerDownloadComplete(bool status)
{
DownloadComplete(status);
}
client.DownloadComplete += (status) =>
{
if (status)
{
// success
}
};
I am using VSTS 2008 + C# + .Net 3.5 + ASP.Net + IIS 7.0 to develop a console application at client side to upload a file, and at server side I receive this file using an aspx file.
From client side, I always notice (from console output) the upload percetage of the file increase from 1% to 50%, then to 100% suddenly. Any ideas what is wrong?
Here is my client side code,
class Program
{
private static WebClient client = new WebClient();
private static ManualResetEvent uploadLock = new ManualResetEvent(false);
private static void Upload()
{
try
{
Uri uri = new Uri("http://localhost/Default.aspx");
String filename = #"C:\test\1.dat";
client.Headers.Add("UserAgent", "TestAgent");
client.UploadProgressChanged += new UploadProgressChangedEventHandler(UploadProgressCallback);
client.UploadFileCompleted += new UploadFileCompletedEventHandler(UploadFileCompleteCallback);
client.UploadFileAsync(uri, "POST", filename);
}
catch (Exception e)
{
Console.WriteLine(e.StackTrace.ToString());
}
}
public static void UploadFileCompleteCallback(object sender, UploadFileCompletedEventArgs e)
{
Console.WriteLine("Completed! ");
uploadLock.Set();
}
private static void UploadProgressCallback(object sender, UploadProgressChangedEventArgs e)
{
Console.WriteLine (e.ProgressPercentage);
}
static void Main(string[] args)
{
Upload();
uploadLock.WaitOne();
return;
}
}
Here is my server side code,
protected void Page_Load(object sender, EventArgs e)
{
string agent = HttpContext.Current.Request.Headers["UserAgent"];
using (FileStream file = new FileStream(#"C:\Test\Agent.txt", FileMode.Append, FileAccess.Write))
{
byte[] buf = Encoding.UTF8.GetBytes(agent);
file.Write(buf, 0, buf.Length);
}
foreach (string f in Request.Files.AllKeys)
{
HttpPostedFile file = Request.Files[f];
file.SaveAs("C:\\Test\\UploadFile.dat");
}
}
thanks in advance,
George
This is a known bug in the WebClient class. It will be fixed in .NET 4.0. Until then you could use HttpWebRequest to implement this functionality.
UPDATE: Here's an example of using synchronous HttpWebRequest to upload a file and track the progress:
public sealed class Uploader
{
public const int CHUNK_SIZE = 1024; // 1 KB
public void Upload(string url, string filename, Stream streamToUpload, Action<int> progress)
{
var request = (HttpWebRequest)WebRequest.Create(url);
request.Method = "POST";
string boundary = string.Format("---------------------{0}", DateTime.Now.Ticks.ToString("x"));
request.ContentType = string.Format("multipart/form-data; boundary={0}", boundary);
request.KeepAlive = true;
using (var requestStream = request.GetRequestStream())
{
var header = string.Format("--{0}\r\nContent-Disposition: form-data; name=\"file\"; filename=\"{1}\"\r\nContent-Type: application/octet-stream\r\n\r\n", boundary, filename);
var headerBytes = Encoding.ASCII.GetBytes(header);
requestStream.Write(headerBytes, 0, headerBytes.Length);
byte[] buffer = new byte[CHUNK_SIZE];
int bytesRead;
long total = streamToUpload.Length;
long totalBytesRead = 0;
while ((bytesRead = streamToUpload.Read(buffer, 0, buffer.Length)) > 0)
{
totalBytesRead += bytesRead;
progress((int)(100 * totalBytesRead / total));
byte[] actual = new byte[bytesRead];
Buffer.BlockCopy(buffer, 0, actual, 0, bytesRead);
requestStream.Write(actual, 0, actual.Length);
}
}
using (var response = request.GetResponse()) { }
}
}
class Program
{
static void Main(string[] args)
{
var url = "http://localhost:2141/Default.aspx";
var filename = "1.dat";
var uploader = new Uploader();
using (var fileStream = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
uploader.Upload(url, filename, fileStream, progress => Console.WriteLine("{0}% of \"{1}\" uploaded to {2}", progress, filename, url));
}
}
}
You can read webclient.UploadProgressChanged code.Then you will know the reason.Code below.
private void PostProgressChanged(AsyncOperation asyncOp, WebClient.ProgressData progress)
{
if (asyncOp == null || progress.BytesSent + progress.BytesReceived <= 0L)
return;
if (progress.HasUploadPhase)
{
int progressPercentage = progress.TotalBytesToReceive >= 0L || progress.BytesReceived != 0L
? (progress.TotalBytesToSend < 0L
? 50
: (progress.TotalBytesToReceive == 0L
? 100
: (int) (50L*progress.BytesReceived/progress.TotalBytesToReceive + 50L)))
: (progress.TotalBytesToSend < 0L
? 0
: (progress.TotalBytesToSend == 0L ? 50 : (int) (50L*progress.BytesSent/progress.TotalBytesToSend)));
asyncOp.Post(this.reportUploadProgressChanged,
(object)
new UploadProgressChangedEventArgs(progressPercentage, asyncOp.UserSuppliedState,
progress.BytesSent, progress.TotalBytesToSend, progress.BytesReceived,
progress.TotalBytesToReceive));
}
else
{
int progressPercentage = progress.TotalBytesToReceive < 0L
? 0
: (progress.TotalBytesToReceive == 0L
? 100
: (int) (100L*progress.BytesReceived/progress.TotalBytesToReceive));
asyncOp.Post(this.reportDownloadProgressChanged,
(object)
new DownloadProgressChangedEventArgs(progressPercentage, asyncOp.UserSuppliedState,
progress.BytesReceived, progress.TotalBytesToReceive));
}
}
This beheavior is by design, its 50% upload the file and 50% the response of the server.
Not a bug.