Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/overhaul httpclientdownloadwithprogress #284

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
101 changes: 76 additions & 25 deletions src/Common/HttpClientDownloadWithProgress.cs
Original file line number Diff line number Diff line change
@@ -1,41 +1,83 @@
/// https://stackoverflow.com/a/43169927/14894786
using System.Threading.Tasks;
using System.IO;
using System.Threading;
using System.Buffers;

namespace System.Net.Http
{
public class HttpClientDownloadWithProgress : IDisposable
public delegate void DownloadProgressHandler(long? totalFileSize, long totalBytesDownloaded, double? progressPercentage);

public static class DownloadWithProgress
{
/// <summary>
/// An amalgamation of StackOverflow Answers:
/// </summary>
/// <remarks>
/// EXAMPLE: <br/>
/// ```cs <br/>
/// await DownloadWithProgress.ExecuteAsync( <br/>
/// HttpClients.General, assetUrl, downloadFilePath, progressHandler, () => <br/>
/// { <br/>
/// var requestMessage = new HttpRequestMessage(HttpMethod.Get, assetUrl); <br/>
/// requestMessage.Headers.Accept.TryParseAdd("application/octet-stream"); <br/>
/// return requestMessage; <br/>
/// }); <br/>
/// ```
/// </remarks>
public static async Task ExecuteAsync(HttpClient httpClient, string downloadPath, string destinationPath, DownloadProgressHandler progress, Func<HttpRequestMessage> requestMessageBuilder = null, CancellationToken? cancellationToken = null)
{
requestMessageBuilder ??= GetDefaultRequestBuilder(downloadPath);
var download = new HttpClientDownloadWithProgress(httpClient, destinationPath, requestMessageBuilder, cancellationToken);
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 string _downloadUrl;
private readonly string _destinationFilePath;
private readonly CancellationToken? _cancellationToken;

private HttpClient _httpClient;
private readonly HttpClient _httpClient;

public delegate void ProgressChangedHandler(long? totalFileSize, long totalBytesDownloaded, double? progressPercentage);

public event ProgressChangedHandler ProgressChanged;
public event DownloadProgressHandler ProgressChanged;

public HttpClientDownloadWithProgress(string downloadUrl, string destinationFilePath)
private readonly Func<HttpRequestMessage> _requestMessageBuilder;
private readonly int _bufferSize = 8192;

public HttpClientDownloadWithProgress(HttpClient httpClient, string destinationFilePath, Func<HttpRequestMessage> requestMessageBuilder, CancellationToken? cancellationToken)
{
_downloadUrl = downloadUrl;
_destinationFilePath = destinationFilePath;
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_destinationFilePath = destinationFilePath ?? throw new ArgumentNullException(nameof(destinationFilePath));
_requestMessageBuilder = requestMessageBuilder ?? throw new ArgumentNullException(nameof(requestMessageBuilder));
_cancellationToken = cancellationToken;
}

/// <summary>
/// TODO. SendAsync is way more complex than GetAsync. I don'tunderstand it at all.
/// </summary>
public async Task StartDownload()
{
_httpClient = new HttpClient { Timeout = TimeSpan.FromDays(1) };

using (var response = await _httpClient.GetAsync(_downloadUrl, HttpCompletionOption.ResponseHeadersRead))
await DownloadFileFromHttpResponseMessage(response);
using (var requestMessage = _requestMessageBuilder.Invoke())
using (var response = await _httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead))
await DownloadAsync(response);
}

private async Task DownloadFileFromHttpResponseMessage(HttpResponseMessage response)
private async Task DownloadAsync(HttpResponseMessage response)
{
response.EnsureSuccessStatusCode();

var totalBytes = response.Content.Headers.ContentLength;


using (Stream contentStream = await response.Content.ReadAsStreamAsync())
await ProcessContentStream(totalBytes, contentStream);
}
Expand All @@ -44,18 +86,27 @@ private async Task ProcessContentStream(long? totalDownloadSize, Stream contentS
{
var totalBytesRead = 0L;
var readCount = 0L;
var buffer = new byte[8192];
var buffer = ArrayPool<byte>.Shared.Rent(_bufferSize);
var isMoreToRead = true;

using (var fileStream = new FileStream(_destinationFilePath, FileMode.Create, FileAccess.Write, FileShare.None, 8192, 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);
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;
TriggerProgressChanged(totalDownloadSize, totalBytesRead);
ReportProgress(totalDownloadSize, totalBytesRead);
continue;
}

Expand All @@ -65,13 +116,18 @@ private async Task ProcessContentStream(long? totalDownloadSize, Stream contentS
readCount += 1;

if (readCount % 100 == 0)
TriggerProgressChanged(totalDownloadSize, totalBytesRead);
ReportProgress(totalDownloadSize, totalBytesRead);
}
while (isMoreToRead);

}

//the last progress trigger should occur after the file handle has been released or you may get file locked error
ReportProgress(totalDownloadSize, totalBytesRead);
ArrayPool<byte>.Shared.Return(buffer);
}

private void TriggerProgressChanged(long? totalDownloadSize, long totalBytesRead)
private void ReportProgress(long? totalDownloadSize, long totalBytesRead)
{
if (ProgressChanged == null)
return;
Expand All @@ -80,12 +136,7 @@ private void TriggerProgressChanged(long? totalDownloadSize, long totalBytesRead
if (totalDownloadSize.HasValue)
progressPercentage = Math.Round((double)totalBytesRead / totalDownloadSize.Value * 100, 2);

ProgressChanged(totalDownloadSize, totalBytesRead, progressPercentage);
}

public void Dispose()
{
_httpClient?.Dispose();
ProgressChanged?.Invoke(totalDownloadSize, totalBytesRead, progressPercentage);
}
}
}