From 534a444ab3c60e3a209132de8046c3e6e5db5d15 Mon Sep 17 00:00:00 2001 From: Albie Date: Mon, 11 Jan 2021 10:39:13 +0000 Subject: [PATCH] add file download progress reporting --- .../Files/FileDownloadTests.cs | 23 ++++++++++++++----- DragonFruit.Common.Data/ApiClient.cs | 15 ++++++++---- DragonFruit.Common.Data/ApiFileRequest.cs | 17 ++++++++++++-- 3 files changed, 43 insertions(+), 12 deletions(-) diff --git a/DragonFruit.Common.Data.Tests/Files/FileDownloadTests.cs b/DragonFruit.Common.Data.Tests/Files/FileDownloadTests.cs index aa174f7..8f8690d 100644 --- a/DragonFruit.Common.Data.Tests/Files/FileDownloadTests.cs +++ b/DragonFruit.Common.Data.Tests/Files/FileDownloadTests.cs @@ -1,6 +1,7 @@ // DragonFruit.Common Copyright 2020 DragonFruit Network // Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details +using System; using System.IO; using DragonFruit.Common.Data.Basic; using NUnit.Framework; @@ -10,19 +11,29 @@ namespace DragonFruit.Common.Data.Tests.Files [TestFixture] public class FileDownloadTests : ApiTest { - [TestCase] - public void FileDownloadTest() + [TestCase("https://github.com/ppy/osu/archive/2020.1121.0.zip", 19018589)] + public void FileDownloadTest(string path, long expectedFileSize) { - var request = new BasicApiFileRequest("https://github.com/ppy/osu/archive/2020.1121.0.zip", Path.GetTempPath()); + var request = new BasicApiFileRequest(path, Path.GetTempPath()); - Assert.IsFalse(File.Exists(request.Destination)); + if (File.Exists(request.Destination)) + { + try + { + File.Delete(request.Destination); + } + catch + { + Assert.Inconclusive("Failed to remove file needed for test"); + } + } try { - Client.Perform(request); + Client.Perform(request, (progress, total) => TestContext.Out.WriteLine($"Progress: {progress:n0}/{total:n0} ({Convert.ToSingle(progress) / Convert.ToSingle(total):F2}%)")); Assert.IsTrue(File.Exists(request.Destination)); - Assert.IsTrue(new FileInfo(request.Destination).Length > 5000); + Assert.GreaterOrEqual(new FileInfo(request.Destination).Length, expectedFileSize); } finally { diff --git a/DragonFruit.Common.Data/ApiClient.cs b/DragonFruit.Common.Data/ApiClient.cs index 2f0d65a..8153145 100644 --- a/DragonFruit.Common.Data/ApiClient.cs +++ b/DragonFruit.Common.Data/ApiClient.cs @@ -244,7 +244,7 @@ public virtual T Perform(HttpRequestMessage request, CancellationToken token /// Download a file with an . /// Bypasses /// - public virtual void Perform(ApiFileRequest request, CancellationToken token = default) + public virtual void Perform(ApiFileRequest request, Action progressUpdated = null, CancellationToken token = default) { //check request data is valid ValidateRequest(request); @@ -256,18 +256,25 @@ public virtual void Perform(ApiFileRequest request, CancellationToken token = de HttpResponseMessage CopyProcess(HttpResponseMessage response) { - //validate + // validate response.EnsureSuccessStatusCode(); // create a new filestream and copy all data into using var stream = File.Open(request.Destination, request.FileCreationMode); - #if NET5_0 using var networkStream = response.Content.ReadAsStreamAsync(token).Result; #else using var networkStream = response.Content.ReadAsStreamAsync().Result; #endif - networkStream.CopyTo(stream); + // create a buffer for progress reporting + var buffer = new byte[request.BufferSize]; + int count; + + while ((count = networkStream.Read(buffer, 0, buffer.Length)) > 0) + { + stream.Write(buffer, 0, count); + progressUpdated?.Invoke(stream.Length, response.Content.Headers.ContentLength); + } // flush and return stream.Flush(); diff --git a/DragonFruit.Common.Data/ApiFileRequest.cs b/DragonFruit.Common.Data/ApiFileRequest.cs index bfa15fc..3743b80 100644 --- a/DragonFruit.Common.Data/ApiFileRequest.cs +++ b/DragonFruit.Common.Data/ApiFileRequest.cs @@ -7,10 +7,23 @@ namespace DragonFruit.Common.Data { public abstract class ApiFileRequest : ApiRequest { - protected override Methods Method => Methods.Get; - + /// + /// The location, on the disk, to put the resultant file + /// public abstract string Destination { get; } + /// + /// The mode of file creation + /// public virtual FileMode FileCreationMode => FileMode.Create; + + /// + /// Length, in bytes, of the buffer used for copying data from the network stream to the file. + /// Defaults to 32kb + /// + /// + /// This should be less than 85KiB (87040 bytes) to avoid potential memory leaks + /// + public virtual int BufferSize => 32 * 1024; } }