C#: HttpClient, File upload progress when uploading multiple file as MultipartFormDataContent - c#

I'm using this code to upload multiple files and it working very well. It uses modernhttpclient library.
public async Task<string> PostImages (int platform, string url, List<byte []> imageList)
{
try {
int count = 1;
var requestContent = new MultipartFormDataContent ();
foreach (var image in imageList) {
var imageContent = new ByteArrayContent (image);
imageContent.Headers.ContentType = MediaTypeHeaderValue.Parse ("image/jpeg");
requestContent.Add (imageContent, "image" + count, "image.jpg");
count++;
}
var cookieHandler = new NativeCookieHandler ();
var messageHandler = new NativeMessageHandler (false, false, cookieHandler);
cookieHandler.SetCookies (cookies);
using (var client = new HttpClient (messageHandler)) {
client.DefaultRequestHeaders.TryAddWithoutValidation ("User-Agent", GetUserAgent (platform));
using (var r = await client.PostAsync (url, requestContent)) {
string result = await r.Content.ReadAsStringAsync ();
System.Diagnostics.Debug.WriteLine ("PostAsync: " + result);
return result;
}
}
} catch (Exception e) {
System.Diagnostics.Debug.WriteLine (e.Message);
return null;
}
}
Now I need the progress when uploading the files. I searched in google and found I need to use ProgressStreamContent
https://github.com/paulcbetts/ModernHttpClient/issues/80
Since ProgressStreamContent contains a constructor that takes a stream, I converted the MultipartFormDataContent to stream and used it in its constructor. But, its not working. Upload fails. I think its because it is a stream of all the files together which is not what my back end is expecting.
public async Task<string> PostImages (int platform, string url, List<byte []> imageList)
{
try {
int count = 1;
var requestContent = new MultipartFormDataContent ();
// here you can specify boundary if you need---^
foreach (var image in imageList) {
var imageContent = new ByteArrayContent (image);
imageContent.Headers.ContentType = MediaTypeHeaderValue.Parse ("image/jpeg");
requestContent.Add (imageContent, "image" + count, "image.jpg");
count++;
}
var cookieHandler = new NativeCookieHandler ();
var messageHandler = new NativeMessageHandler (false, false, cookieHandler);
cookieHandler.SetCookies (RestApiPaths.cookies);
var stream = await requestContent.ReadAsStreamAsync ();
var client = new HttpClient (messageHandler);
client.DefaultRequestHeaders.TryAddWithoutValidation ("User-Agent", RestApiPaths.GetUserAgent (platform));
var request = new HttpRequestMessage (HttpMethod.Post, url);
var progressContent = new ProgressStreamContent (stream, 4096);
progressContent.Progress = (bytes, totalBytes, totalBytesExpected) => {
Console.WriteLine ("Uploading {0}/{1}", totalBytes, totalBytesExpected);
};
request.Content = progressContent;
var response = await client.SendAsync (request);
string result = await response.Content.ReadAsStringAsync ();
System.Diagnostics.Debug.WriteLine ("PostAsync: " + result);
return result;
} catch (Exception e) {
System.Diagnostics.Debug.WriteLine (e.Message);
return null;
}
}
What should I do here to get this working? Any help is appreciated

I have a working version of ProgressableStreamContent. Please note, I am adding headers in the constructor, this is a bug in original ProgressStreamContent that it does not add headers !!
internal class ProgressableStreamContent : HttpContent
{
/// <summary>
/// Lets keep buffer of 20kb
/// </summary>
private const int defaultBufferSize = 5*4096;
private HttpContent content;
private int bufferSize;
//private bool contentConsumed;
private Action<long,long> progress;
public ProgressableStreamContent(HttpContent content, Action<long,long> progress) : this(content, defaultBufferSize, progress) { }
public ProgressableStreamContent(HttpContent content, int bufferSize, Action<long,long> progress)
{
if (content == null)
{
throw new ArgumentNullException("content");
}
if (bufferSize <= 0)
{
throw new ArgumentOutOfRangeException("bufferSize");
}
this.content = content;
this.bufferSize = bufferSize;
this.progress = progress;
foreach (var h in content.Headers) {
this.Headers.Add(h.Key,h.Value);
}
}
protected override Task SerializeToStreamAsync(Stream stream, TransportContext context)
{
return Task.Run(async () =>
{
var buffer = new Byte[this.bufferSize];
long size;
TryComputeLength(out size);
var uploaded = 0;
using (var sinput = await content.ReadAsStreamAsync())
{
while (true)
{
var length = sinput.Read(buffer, 0, buffer.Length);
if (length <= 0) break;
//downloader.Uploaded = uploaded += length;
uploaded += length;
progress?.Invoke(uploaded, size);
//System.Diagnostics.Debug.WriteLine($"Bytes sent {uploaded} of {size}");
stream.Write(buffer, 0, length);
stream.Flush();
}
}
stream.Flush();
});
}
protected override bool TryComputeLength(out long length)
{
length = content.Headers.ContentLength.GetValueOrDefault();
return true;
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
content.Dispose();
}
base.Dispose(disposing);
}
}
Also note, it expects HttpContent, not stream.
This is how you can use it.
var progressContent = new ProgressableStreamContent (
requestContent,
4096,
(sent,total) => {
Console.WriteLine ("Uploading {0}/{1}", sent, total);
});

Related

Problems with streaming video for IOS Client (Server developed on ASP.NET WEB API 2)

I have a problem with streaming video. I developed the server on ASP.NET Web API 2 and implemented 2 methods:
The first method:
if (Request.Headers.Range != null)
{
try
{
var httpResponce = Request.CreateResponse();
httpResponce.Content =
new PushStreamContent((Action<Stream, HttpContent, TransportContext>) WriteContentToStream);
return httpResponce;
}
catch (Exception ex)
{
return new HttpResponseMessage(HttpStatusCode.InternalServerError);
}
}
else
{
return new HttpResponseMessage(HttpStatusCode.RequestedRangeNotSatisfiable);
}
/*method for streaming*/
private async void WriteContentToStream(Stream outputStream, HttpContent content, TransportContext transportContext)
{
string relativeFilePath = "~/App_Data/Videos/4.mp4";
try
{
var filePath = System.Web.Hosting.HostingEnvironment.MapPath(relativeFilePath);
int bufferSize = 1000;
byte[] buffer = new byte[bufferSize];
using (var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
{
int totalSize = (int)fileStream.Length;
while (totalSize > 0)
{
int count = totalSize > bufferSize ? bufferSize : totalSize;
int sizeOfReadedBuffer = fileStream.Read(buffer, 0, count);
await outputStream.WriteAsync(buffer, 0, sizeOfReadedBuffer);
totalSize -= sizeOfReadedBuffer;
}
}
}
catch (HttpException ex)
{
if (ex.ErrorCode == -2147023667)
{
return;
}
}
finally
{
outputStream.Close();
}
}
2) The second method:
public HttpResponseMessage Test()
{
if (Request.Headers.Range != null)
{
try
{
string relativeFilePath = "~/App_Data/Videos/4.mp4";
var filePath = System.Web.Hosting.HostingEnvironment.MapPath(relativeFilePath);
HttpResponseMessage partialResponse = Request.CreateResponse(HttpStatusCode.PartialContent);
partialResponse.Headers.AcceptRanges.Add("bytes");
var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
partialResponse.Content = new ByteRangeStreamContent(stream, Request.Headers.Range, new MediaTypeHeaderValue("video/mp4"));
return partialResponse;
}
catch (Exception)
{
return new HttpResponseMessage(HttpStatusCode.InternalServerError);
}
}
else
{
return new HttpResponseMessage(HttpStatusCode.RequestedRangeNotSatisfiable);
}
}
Both of these methods worked on Web-client and Android-client, but iOS-client doesn't show video.
I think, that problem may be with codec of video (but I used codecs, which recommend Apple) or http-headers.
I just solved this one, and it was because the Content-Length header had (what iOS considered to be) an invalid value.
My solution was based on method #2 above...
Here's the important part of my code that actually worked.
if (!file.Exists) {
response.StatusCode = HttpStatusCode.NotFound;
response.ReasonPhrase = "Deleted";
} else {
var range = Request.Headers.Range?.Ranges?.FirstOrDefault();
if (range == null) {
using (var stream = new MemoryStream()) {
using (var video = file.OpenRead()) await video.CopyToAsync(stream);
response.Content = new ByteArrayContent(stream.ToArray());
}
response.Content.Headers.ContentType = new MediaTypeHeaderValue("video/mp4");
response.Content.Headers.ContentLength = file.Length;
} else {
var stream = new MemoryStream();
using (var video = file.OpenRead()) await video.CopyToAsync(stream);
response.Content = new ByteRangeStreamContent(
stream,
new RangeHeaderValue(range.From, range.To),
new MediaTypeHeaderValue("video/mp4")
);
// response.Content.Headers.ContentLength = file.Length;
// this is what makes iOS work
response.Content.Headers.ContentLength = (range.To.HasValue ? range.To.Value + 1 : file.Length) - (range.From ?? 0);
}
response.StatusCode = HttpStatusCode.OK;
}
I should probably put in an HTTP 206 (partial content) status when dealing with ranges, but I was working on this for nearly two days before coming up with a solution.
The only problem I have yet to fully track down is that from time-to-time, the Application_EndRequest doesn't fire for some of these. I am able to log the response being sent by the endpoint, but it's like iOS disconnects the connection somewhere and the request hangs until it times out internally.
Check HLS streaming which required for iOS. You can not play video file directly pointing to video file.
https://developer.apple.com/streaming/

Upload a File to WebDAV using HttpClient PUT in Windows Universal App?

I used WebClient for uploading a file with few headers and it worked perfectly. But now I am creating a universal app using HttpClient. I don't understanding how to add file path to request header. Please see the following code:
public async void testUploadFile()
{
string url = "http://localhost/webdav/";
string filepath = #"C:\mydata\file-1.txt";
string resource_name = Path.GetFileName(filepath);
url += resource_name;
HttpMultipartFormDataContent multipart = new HttpMultipartFormDataContent();
multipart.Headers.Add("RequestId", "abc");
multipart.Headers.Add("UserId", "apple");
multipart.Headers.Add("SessionId", "ssdfsd22");
Stream stream = new System.IO.MemoryStream();
HttpStreamContent streamContent = new HttpStreamContent(stream.AsInputStream());
multipart.Add(streamContent);
httpClient = new HttpClient();
HttpResponseMessage respMesg =await httpClient.PutAsync(new Uri(url), multipart);
Debug.WriteLine(respMesg.Content);
}
Can anyone sort out this issue? Thank you in advance!
The following code works for one time upload:
public async void testFileUploadWebDAV()
{
string url = "http://localhost/webdav/";
string userId = "xxx";
string sessionId = "yyy";
var filter = new HttpBaseProtocolFilter();
filter.IgnorableServerCertificateErrors.Add(ChainValidationResult.Untrusted);
filter.IgnorableServerCertificateErrors.Add(ChainValidationResult.IncompleteChain);
var filePicker = new FileOpenPicker();
filePicker.FileTypeFilter.Add("*");
filePicker.SuggestedStartLocation = PickerLocationId.DocumentsLibrary;
StorageFile file = await filePicker.PickSingleFileAsync();
url += file.Name;
httpClient = new HttpClient(filter);
msg = new HttpRequestMessage(new HttpMethod("PUT"), new Uri(url));
httpClient.DefaultRequestHeaders.Add("RequestId", file.DisplayName);
httpClient.DefaultRequestHeaders.Add("UserId", userId);
httpClient.DefaultRequestHeaders.Add("SessionId", sessionId);
httpClient.DefaultRequestHeaders.Add("ContentType", file.ContentType);
Certificate cert = msg.TransportInformation.ServerCertificate;
//-----------------ADD FILE CONTENT TO BODY-----------
HttpStreamContent content = new HttpStreamContent(await file.OpenReadAsync());
try
{
HttpResponseMessage httpResponseContent = await httpClient.PutAsync(new Uri(url), content);
Debug.WriteLine(httpResponseContent.ToString());
if (httpResponseContent.IsSuccessStatusCode)
{
msg.Dispose();
httpClient.Dispose();
}
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
}
#Brajesh's answer was super helpful for me, but I needed a .NET Core solution. In particular, I found there to be some issues with encoding support in .NET Core, so I couldn't just pass an StreamContent into the httpClient.PutAsync method. Please see below for the .NET Core equivalent:
public static async void writeToWebDAV(string sourceFilename, Stream httpStream)
{
//As described above, decoding must be forced as UTF8 default returns some strange results
var content = Encoding.GetEncoding("iso-8859-1").GetString(readToEnd(httpStream));
using (var httpClient = new HttpClient())
{
httpClient.DefaultRequestHeaders.Add("RequestId", sourceFilename);
//Be sure user:pass is in Base64 encoding, can use this resource https://www.base64encode.org/
httpClient.DefaultRequestHeaders.Add("Authorization", "Basic dXNlcjpwYXNzd29yZA==");
StringContent c = new StringContent(content, Encoding.UTF8);
try
{
HttpResponseMessage httpResponseContent = await httpClient.PutAsync(
new Uri(Path.Combine(#"https://randomhost.com:5009/shareFolder", sourceFilename)), c);
if (httpResponseContent.IsSuccessStatusCode)
httpClient.Dispose();
else
{
try
{
//occasionally the server will respond with the WWW-Authenticate header in which case you need to re-PUT the file
//described here: https://stackoverflow.com/questions/32393846/webdav-return-401-how-to-authenticate
HttpResponseMessage httpResponseContent = await httpClient.PutAsync(
new Uri(Path.Combine(#"https://randomhost.com:5009/shareFolder", sourceFilename)), c);
if (httpResponseContent.IsSuccessStatusCode)
httpClient.Dispose();
else if (httpResponseContent.StatusCode.ToString() == "401")
Console.WriteLine("WebDAV Authentication Error...");
}
catch (Exception ex)
{ Console.WriteLine(ex.Message); }
}
}
catch (Exception ex)
{ Console.WriteLine(ex.Message); }
}
}
//Taken from StackOverflow: https://stackoverflow.com/questions/1080442/how-to-convert-an-stream-into-a-byte-in-c
public static byte[] readToEnd(Stream stream)
{
long originalPosition = 0;
if (stream.CanSeek)
{
originalPosition = stream.Position;
stream.Position = 0;
}
try
{
byte[] readBuffer = new byte[4096];
int totalBytesRead = 0;
int bytesRead;
while ((bytesRead = stream.Read(readBuffer, totalBytesRead, readBuffer.Length - totalBytesRead)) > 0)
{
totalBytesRead += bytesRead;
if (totalBytesRead == readBuffer.Length)
{
int nextByte = stream.ReadByte();
if (nextByte != -1)
{
byte[] temp = new byte[readBuffer.Length * 2];
Buffer.BlockCopy(readBuffer, 0, temp, 0, readBuffer.Length);
Buffer.SetByte(temp, totalBytesRead, (byte)nextByte);
readBuffer = temp;
totalBytesRead++;
}
}
}
byte[] buffer = readBuffer;
if (readBuffer.Length != totalBytesRead)
{
buffer = new byte[totalBytesRead];
Buffer.BlockCopy(readBuffer, 0, buffer, 0, totalBytesRead);
}
return buffer;
}
finally
{
if (stream.CanSeek)
stream.Position = originalPosition;
}
}

Create progress bar for download zip

I want to create a progress bar in a backgroud progress when the users download zip's or same. This is my code:
private void DoSincroFit()
{
HttpWebRequest request = HttpWebRequest.CreateHttp(url);
//Add headers to request
request.Headers["Type"] = "sincrofit";
request.Headers["Device"] = "1";
request.Headers["Version"] = "0.000";
request.Headers["Os"] = "WindowsPhone";
request.BeginGetResponse(new AsyncCallback(playResponseAsync), request);
}
public async void playResponseAsync(IAsyncResult asyncResult)
{
//Declaration of variables
HttpWebRequest webRequest = (HttpWebRequest)asyncResult.AsyncState;
try
{
string fileName = "sincrofit.rar";
using (HttpWebResponse webResponse = (HttpWebResponse)webRequest.EndGetResponse(asyncResult))
{
byte[] buffer = new byte[1024];
var newZipFile = await ApplicationData.Current.LocalFolder.CreateFileAsync(fileName, CreationCollisionOption.ReplaceExisting);
using (var writeStream = await newZipFile.OpenAsync(FileAccessMode.ReadWrite))
{
using (var outputStream = writeStream.GetOutputStreamAt(0))
{
using (var dataWriter = new DataWriter(outputStream))
{
using (Stream input = webResponse.GetResponseStream())
{
var totalSize = 0;
for (int size = input.Read(buffer, 0, buffer.Length); size > 0; size = input.Read(buffer, 0, buffer.Length))
{
dataWriter.WriteBytes(buffer);
totalSize += size; //get the progress of download
//I think the progress bar going here!
}
await dataWriter.StoreAsync();
await outputStream.FlushAsync();
dataWriter.DetachStream();
}
}
}
}
}
}
catch
{
CoreDispatcher dispatcher = CoreWindow.GetForCurrentThread().Dispatcher;
dispatcher.RunAsync(CoreDispatcherPriority.Normal, async () =>
{
//Declaration of variables
SMethods.Message_Dialog("Download has stopped!", "Error");
});
}
}
And this is method that call background method
public async Task<string> doSync(ProgressBar bar)
{
//Declaration of variables
string response = await DoRequest("CHECK", "1", "0.000", "WindowsPhone");
pBar = bar;
//When is 1, the checkConnection will connect
if (response == "1")
{
response = response + "," + await DoRequest("SIZEFIT", "1", "0.000", "WindowsPhone");
DoSincroFit();
response += "," + await DoRequest("DELSINC", "1", "0.000", "WindowsPhone");
return response;
}
return "0,0,0";
}
How can I create progress bar when this is a external class? Exactly, doSync and DoSincroFit belong a Sync.cs and my UI is MyPage.Xaml.cs.
Thanks in advance!
If you are using MVVM you might create properties that notify when they are changed. Then, your external code which shouldn't really know anything about UI representation simply updates such properties.
Then, in your UI code you bind controls to those properties.
Without knowing more about your application, I cannot offer anything concrete here.
Finally i can solve this! My progress bar do work now!
private void DoSincroFit()
{
HttpWebRequest request = HttpWebRequest.CreateHttp(url);
//Add headers to request
request.Headers["Type"] = "sincrofit";
request.Headers["Device"] = "1";
request.Headers["Version"] = "0.000";
request.Headers["Os"] = "WindowsPhone";
//Windows Cache Problems
request.Headers["Cache-Control"] = "no-cache";
request.Headers["Pragma"] = "no-cache";
dispatcher = CoreWindow.GetForCurrentThread().Dispatcher;
request.BeginGetResponse(new AsyncCallback(playResponseAsync), request);
}
public async void playResponseAsync(IAsyncResult asyncResult)
{
//Declaration of variables
HttpWebRequest webRequest = (HttpWebRequest)asyncResult.AsyncState;
try
{
//For download file with stream
string fileName = "sincrofit.rar";
using (HttpWebResponse webResponse = (HttpWebResponse)webRequest.EndGetResponse(asyncResult))
{
byte[] buffer = new byte[1];
//For acces Local folder of phone device
var newZipFile = await ApplicationData.Current.LocalFolder.CreateFileAsync(fileName, CreationCollisionOption.ReplaceExisting);
//Progress bar and their update
using (var writeStream = await newZipFile.OpenAsync(FileAccessMode.ReadWrite))
{
using (var outputStream = writeStream.GetOutputStreamAt(0))
{
using (var dataWriter = new DataWriter(outputStream))
{
using (Stream input = webResponse.GetResponseStream())
{
var totalSize = 0;
int read;
uint zeroUint = Convert.ToUInt32(0);
uint readUint;
while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
{
totalSize += read;
await dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
//Declaration of variables
pBar.Value = totalSize * 100 / sizeFit;
});
readUint = Convert.ToUInt32(read);
IBuffer ibuffer = buffer.AsBuffer();
dataWriter.WriteBuffer(ibuffer, zeroUint, readUint);
}
await dataWriter.StoreAsync();
await outputStream.FlushAsync();
dataWriter.DetachStream();
}
}
}
}
}
}
catch
{
//For control errors stopped progress bar
dispatcher.RunAsync(CoreDispatcherPriority.Normal, async () =>
{
//Declaration of variables
SMethods.Message_Dialog("Download has stopped!", "Error");
});
}
}
This is a test progress bar but if u need download a big zip's then use buffer with 1024 or same... Because with 1 byte can be terrible! Thanks for all! :)

Adding pause and continue ability in my downloader

I am creating a downloader in C#. I am using WebClient class. To pause downloading on a button click I could just think of using Thread. So when I created thread and attached it with my file downloading as below
WebClient web = new WebLCient();
Thread dwnd_thread = new Thread(Program.web.DownloadFileAsync(new Uri(Program.src), Program.dest));
it is giving me following errors:
"The best overloaded method match for 'System.Threading.Thread.Thread(System.Threading.ThreadStart)' has some invalid arguments"
and
"Argument '1': cannot convert from 'void' to 'System.Threading.ThreadStart'".
Then I thought if I pause my system main thread then it could block my whole process for that I used below line of code
System.Threading.Thread.Sleep(100);
but it is doing nothing at all. Can somebody tell me what could be a better approach for pause/downloading and how to use thread to pause my downloading process.
As there's no standard way of pause/resume a download request, you'll have to implement your own mechanism. Below is a block of code, containing an example of how such a mechanism could look. The class FileDownload takes 3 parameters:
source - url to the file, to download.
destination - where to save the file.
chunkSize - how many bytes to read, before checking whether to pause or continue the download.
public class FileDownload
{
private volatile bool _allowedToRun;
private string _source;
private string _destination;
private int _chunkSize;
private Lazy<int> _contentLength;
public int BytesWritten { get; private set; }
public int ContentLength { get { return _contentLength.Value; } }
public bool Done { get { return ContentLength == BytesWritten; } }
public FileDownload(string source, string destination, int chunkSize)
{
_allowedToRun = true;
_source = source;
_destination = destination;
_chunkSize = chunkSize;
_contentLength = new Lazy<int>(() => Convert.ToInt32(GetContentLength()));
BytesWritten = 0;
}
private long GetContentLength()
{
var request = (HttpWebRequest)WebRequest.Create(_source);
request.Method = "HEAD";
using (var response = request.GetResponse())
return response.ContentLength;
}
private async Task Start(int range)
{
if (!_allowedToRun)
throw new InvalidOperationException();
var request = (HttpWebRequest)WebRequest.Create(_source);
request.Method = "GET";
request.UserAgent = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; .NET CLR 1.0.3705;)";
request.AddRange(range);
using (var response = await request.GetResponseAsync())
{
using (var responseStream = response.GetResponseStream())
{
using (var fs = new FileStream(_destination, FileMode.Append, FileAccess.Write, FileShare.ReadWrite))
{
while (_allowedToRun)
{
var buffer = new byte[_chunkSize];
var bytesRead = await responseStream.ReadAsync(buffer, 0, buffer.Length);
if (bytesRead == 0) break;
await fs.WriteAsync(buffer, 0, bytesRead);
BytesWritten += bytesRead;
}
await fs.FlushAsync();
}
}
}
}
public Task Start()
{
_allowedToRun = true;
return Start(BytesWritten);
}
public void Pause()
{
_allowedToRun = false;
}
}
Usage:
static void Main(string[] args)
{
var fw = new FileDownload("http://download.microsoft.com/download/E/E/2/EE2D29A1-2D5C-463C-B7F1-40E4170F5E2C/KinectSDK-v1.0-Setup.exe", #"D:\KinetSDK.exe", 5120);
// Display progress...
Task.Factory.StartNew(() =>
{
while (!fw.Done)
{
Console.SetCursorPosition(0, Console.CursorTop);
Console.Write(string.Format("ContentLength: {0} | BytesWritten: {1}", fw.ContentLength, fw.BytesWritten));
}
});
// Start the download...
fw.Start();
// Simulate pause...
Thread.Sleep(500);
fw.Pause();
Thread.Sleep(2000);
// Start the download from where we left, and when done print to console.
fw.Start().ContinueWith(t => Console.WriteLine("Done"));
Console.ReadKey();
}
I've taken #ebb's solution (which works great!) and adapted it a bit. It now:
Can accept a stream as an input
Can report progress via IProgress
Some other minor tweaks
public class FileDownload
{
private volatile bool _allowedToRun;
private Stream _sourceStream;
private string _sourceUrl;
private string _destination;
private bool _disposeOnCompletion;
private int _chunkSize;
private IProgress<double> _progress;
private Lazy<long> _contentLength;
public long BytesWritten { get; private set; }
public long ContentLength { get { return _contentLength.Value; } }
public bool Done { get { return ContentLength == BytesWritten; } }
public FileDownload(Stream source, string destination, bool disposeOnCompletion = true, int chunkSizeInBytes = 10000 /*Default to 0.01 mb*/, IProgress<double> progress = null)
{
_allowedToRun = true;
_sourceStream = source;
_destination = destination;
_disposeOnCompletion = disposeOnCompletion;
_chunkSize = chunkSizeInBytes;
_contentLength = new Lazy<int>(() => Convert.ToInt32(GetContentLength()));
_progress = progress;
BytesWritten = 0;
}
public FileDownload(string source, string destination, int chunkSizeInBytes = 10000 /*Default to 0.01 mb*/, IProgress<double> progress = null)
{
_allowedToRun = true;
_sourceUrl = source;
_destination = destination;
_chunkSize = chunkSizeInBytes;
_contentLength = new Lazy<int>(() => Convert.ToInt32(GetContentLength()));
_progress = progress;
BytesWritten = 0;
}
private long GetContentLength()
{
if (_sourceStream != null)
return _sourceStream.Length;
else
{
var request = (HttpWebRequest)WebRequest.Create(_sourceUrl);
request.Method = "HEAD";
using (var response = request.GetResponse())
return response.ContentLength;
}
}
private async Task Start(int range)
{
if (!_allowedToRun)
throw new InvalidOperationException();
if (_sourceStream != null)
{
using (var fs = new FileStream(_destination, FileMode.Append, FileAccess.Write, FileShare.ReadWrite))
{
while (_allowedToRun)
{
var buffer = new byte[_chunkSize];
var bytesRead = await _sourceStream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
if (bytesRead == 0) break;
await fs.WriteAsync(buffer, 0, bytesRead);
BytesWritten += bytesRead;
_progress?.Report((double)BytesWritten / ContentLength);
}
await fs.FlushAsync();
}
//Control whether the stream should be disposed here or outside of this class
if (BytesWritten == ContentLength && _disposeOnCompletion)
_sourceStream?.Dispose();
}
else
{
var request = (HttpWebRequest)WebRequest.Create(_sourceUrl);
request.Method = "GET";
request.UserAgent = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; .NET CLR 1.0.3705;)";
request.AddRange(range);
using (var response = await request.GetResponseAsync())
{
using (var responseStream = response.GetResponseStream())
{
using (var fs = new FileStream(_destination, FileMode.Append, FileAccess.Write, FileShare.ReadWrite))
{
while (_allowedToRun)
{
var buffer = new byte[_chunkSize];
var bytesRead = await responseStream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
if (bytesRead == 0) break;
await fs.WriteAsync(buffer, 0, bytesRead);
BytesWritten += bytesRead;
_progress?.Report((double)BytesWritten / ContentLength);
}
await fs.FlushAsync();
}
}
}
}
}
public Task Start()
{
_allowedToRun = true;
return Start(BytesWritten);
}
public void Pause()
{
_allowedToRun = false;
}
}
I have adapted it in order to deal with files > 2GB and deals with broken download, so that for any reasons the download was stopped and you need to try again, at next download attempt, instead of starting again from scratch it will continue where it was stopped.
All above solutions from #derekantrican, #ebb and myself will only work if server supports Range requests.
You might need to test first if server accepts range requests before to proceed with this solution.
https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests
public class FileDownload
{
private volatile bool _allowedToRun;
private readonly string _sourceUrl;
private readonly string _destination;
private readonly int _chunkSize;
private readonly IProgress<double> _progress;
private readonly Lazy<long> _contentLength;
public long BytesWritten { get; private set; }
public long ContentLength => _contentLength.Value;
public bool Done => ContentLength == BytesWritten;
public FileDownload(string source, string destination, int chunkSizeInBytes = 10000 /*Default to 0.01 mb*/, IProgress<double> progress = null)
{
if(string.IsNullOrEmpty(source))
throw new ArgumentNullException("source is empty");
if (string.IsNullOrEmpty(destination))
throw new ArgumentNullException("destination is empty");
_allowedToRun = true;
_sourceUrl = source;
_destination = destination;
_chunkSize = chunkSizeInBytes;
_contentLength = new Lazy<long>(GetContentLength);
_progress = progress;
if (!File.Exists(destination))
BytesWritten = 0;
else
{
try
{
BytesWritten = new FileInfo(destination).Length;
}
catch
{
BytesWritten = 0;
}
}
}
private long GetContentLength()
{
var request = (HttpWebRequest)WebRequest.Create(_sourceUrl);
request.Method = "HEAD";
using (var response = request.GetResponse())
return response.ContentLength;
}
private async Task Start(long range)
{
if (!_allowedToRun)
throw new InvalidOperationException();
if(Done)
//file has been found in folder destination and is already fully downloaded
return;
var request = (HttpWebRequest)WebRequest.Create(_sourceUrl);
request.Method = "GET";
request.UserAgent = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; .NET CLR 1.0.3705;)";
request.AddRange(range);
using (var response = await request.GetResponseAsync())
{
using (var responseStream = response.GetResponseStream())
{
using (var fs = new FileStream(_destination, FileMode.Append, FileAccess.Write, FileShare.ReadWrite))
{
while (_allowedToRun)
{
var buffer = new byte[_chunkSize];
var bytesRead = await responseStream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
if (bytesRead == 0) break;
await fs.WriteAsync(buffer, 0, bytesRead);
BytesWritten += bytesRead;
_progress?.Report((double)BytesWritten / ContentLength);
}
await fs.FlushAsync();
}
}
}
}
public Task Start()
{
_allowedToRun = true;
return Start(BytesWritten);
}
public void Pause()
{
_allowedToRun = false;
}
}
unfortunately WebClient has no ways to pause downloading. So, You must use WebRequest on background thread and pause getting response stream with flag. Here is sample code. But make sure that you can't pause unlimited, because TCP connection will be closed if there has nothing transfer for a while. So if resume downloading fail, you must restart downloading again.
public class DownloadJob {
public delegate void DownloadCompletedDelegate(Stream stream);
//completion download event
public event DownloadCompletedDelegate OnDownloadCompleted;
//sync object
private object _lock = new object();
//pause flag
private bool _bPause = false;
//Pause download
public void Pause() {
lock (_lock) {
_bPause = true;
}
}
//Resume download
public void Resume() {
lock (_lock) {
_bPause = false;
}
}
//Begin download with URI
public void BeginDowload(Uri uri) {
//Create Background thread
Thread downLoadThread = new Thread(
delegate() {
WebRequest pWebReq = WebRequest.Create(uri);
WebResponse pWebRes = pWebReq.GetResponse();
using (MemoryStream pResultStream = new MemoryStream())
using (Stream pWebStream = pWebRes.GetResponseStream()) {
byte[] buffer = new byte[256];
int readCount = 1;
while (readCount > 0) {
//Read download stream
readCount = pWebStream.Read(buffer, 0, buffer.Length);
//Write to result MemoryStream
pResultStream.Write(buffer, 0, readCount);
//Waiting 100msec while _bPause is true
while (true) {
lock (_lock) {
if (_bPause == true) {
Thread.Sleep(100);
}
else {
break;
}
}
}
pResultStream.Flush();
}
//Fire Completion event
if (OnDownloadCompleted != null) {
OnDownloadCompleted(pResultStream);
}
}
}
);
//Start background thread job
downLoadThread.Start();
}
}

Retrying asynchronous file upload on error

I'm trying to upload files in a WPF-application. Everything works fine if the server responds but the application will be used in an environment with "unsafe" internet connection. So I want to retry uploading after a short break if the first attempt failed.
I tried several things with async/await and ended up with the following code.
If the server is running everything is fine, but if not the program fails with an ObjectDisposedException in the second iteration of the while-loop.
Any ideas?
private void UploadButton_Click(object sender, RoutedEventArgs e)
{
// build content to send
content = new MultipartFormDataContent();
var filestream = new FileStream(filePath, FileMode.Open);
var fileName = System.IO.Path.GetFileName(filePath);
content.Add(new StreamContent(filestream), "file", fileName);
content.Add(new StringContent(terminal_id.ToString()), "terminal_id");
UploadTask(content);
/*var task_a = new Task(() => UploadTask(content));
task_a.Start();*/
}
private async void UploadTask(HttpContent content)
{
bool success = false;
int counter = 0;
while (counter < 3 && !success)
{
Debug.WriteLine("starting upload");
success = await UploadFileAsync(content);
Debug.WriteLine("finished upload. result " + success.ToString());
//if (!success) System.Threading.Thread.Sleep(5000);
counter++;
}
}
private async Task<bool> UploadFileAsync(HttpContent content)
{
var message = new HttpRequestMessage();
message.Method = HttpMethod.Post;
message.Content = content;
message.RequestUri = new Uri(target_url);
using (HttpClient client = new HttpClient())
{
try
{
HttpResponseMessage res = await client.SendAsync(message);
if (res.IsSuccessStatusCode) return true;
}
catch (HttpRequestException hre)
{
Debug.WriteLine(hre.ToString());
}
return false;
}
}
Seems like your file stream gets disposed/closed. You need to retry from the very beginning (content = new MultipartFormDataContent(); etc).
I think issue could be that the content is going out of scope? Try creating content inside the UploadTask method. Also, it might be worth returning a Task<bool> from UploadTask and caching it is a class level variable (so you don't have to return void).
For example:
Task<bool> newTask;
private void UploadButton_Click(object sender, RoutedEventArgs e)
{
newTask = UploadTask();
}
private async Task<bool> UploadTask()
{
bool success = false;
int counter = 0;
// build content to send
HttpContent content = new MultipartFormDataContent();
var filestream = new FileStream(filePath, FileMode.Open);
var fileName = System.IO.Path.GetFileName(filePath);
content.Add(new StreamContent(filestream), "file", fileName);
content.Add(new StringContent(terminal_id.ToString()), "terminal_id");
while (counter < 3 && !success)
{
Debug.WriteLine("starting upload");
success = await UploadFileAsync(content);
Debug.WriteLine("finished upload. result " + success.ToString());
counter++;
}
return success;
}
After moving the creation of the content into UploadFileAsync() it works. Result:
Task<bool> newTask;
private void UploadButton_Click(object sender, RoutedEventArgs e)
{
newTask = UploadTask();
}
private async Task<bool> UploadTask()
{
bool success = false;
int counter = 0;
while (counter < 3 && !success)
{
Debug.WriteLine("starting upload");
success = await UploadFileAsync();
Debug.WriteLine("finished upload. result " + success.ToString());
if (!success) System.Threading.Thread.Sleep(5000);
counter++;
}
return success;
}
private async Task<bool> UploadFileAsync()
{
MultipartFormDataContent content = new MultipartFormDataContent();
var filestream = new FileStream(filePath, FileMode.Open);
var fileName = System.IO.Path.GetFileName(filePath);
content.Add(new StreamContent(filestream), "file", fileName);
content.Add(new StringContent(terminal_id.ToString()), "terminal_id");
var message = new HttpRequestMessage();
message.Method = HttpMethod.Post;
message.Content = content;
message.RequestUri = new Uri(target_url);
using (HttpClient client = new HttpClient())
{
try
{
HttpResponseMessage res = await client.SendAsync(message);
if (res.IsSuccessStatusCode) return true;
}
catch (HttpRequestException hre)
{
Debug.WriteLine(hre.ToString());
}
return false;
}
}

Categories

Resources