diff --git a/.github/workflows/dotnet-ubuntu.yml b/.github/workflows/dotnet-ubuntu.yml index 114c224e..08256381 100644 --- a/.github/workflows/dotnet-ubuntu.yml +++ b/.github/workflows/dotnet-ubuntu.yml @@ -1,45 +1,25 @@ name: Ubuntu x64 -on: [push] +on: [ push ] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - - name: Setup .NET Core 3.1 - uses: actions/setup-dotnet@v3 - with: - dotnet-version: 3.1.x - - - name: Setup .NET Core 6.0 - uses: actions/setup-dotnet@v3 - with: - dotnet-version: 6.0.x - - - name: Setup .NET Core 8.0 - uses: actions/setup-dotnet@v3 - with: - dotnet-version: 8.0.x - - - name: Install Downloader Dependencies - run: dotnet restore ./src/Downloader/Downloader.csproj - - - name: Build Downloader Project - run: dotnet build ./src/Downloader/Downloader.csproj - - - name: Install Downloader.DummyHttpServer Dependencies - run: dotnet restore ./src/Downloader.DummyHttpServer/Downloader.DummyHttpServer.csproj - - - name: Build Downloader.DummyHttpServer Project - run: dotnet build ./src/Downloader.DummyHttpServer/Downloader.DummyHttpServer.csproj + - name: Checkout code + uses: actions/checkout@v3 - - name: Install Downloader.Test Dependencies - run: dotnet restore ./src/Downloader.Test/Downloader.Test.csproj + - name: Setup .NET Core 8.0 + uses: actions/setup-dotnet@v3 + with: + dotnet-version: 8.0.x - - name: Build Downloader.Test Project - run: dotnet build ./src/Downloader.Test/Downloader.Test.csproj + - name: Build solution + run: dotnet build -c Release ./src --verbosity minimal - - name: Test - run: dotnet test ./src/Downloader.Test/Downloader.Test.csproj --no-build --verbosity detailed \ No newline at end of file + - name: Run tests + run: dotnet test -c Release ./src --verbosity normal --no-build --no-restore \ No newline at end of file diff --git a/.github/workflows/dotnet-windows.yml b/.github/workflows/dotnet-windows.yml index 22917acd..0af4dc16 100644 --- a/.github/workflows/dotnet-windows.yml +++ b/.github/workflows/dotnet-windows.yml @@ -1,45 +1,25 @@ name: Windows x64 -on: [push] +on: [ push ] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: build: runs-on: windows-latest steps: - - uses: actions/checkout@v3 - - - name: Setup .NET Core 3.1 - uses: actions/setup-dotnet@v3 - with: - dotnet-version: 3.1.x - - - name: Setup .NET Core 6.0 - uses: actions/setup-dotnet@v3 - with: - dotnet-version: 6.0.x - - - name: Setup .NET Core 8.0 - uses: actions/setup-dotnet@v3 - with: - dotnet-version: 8.0.x - - - name: Install Downloader Dependencies - run: dotnet restore ./src/Downloader/Downloader.csproj - - - name: Build Downloader Project - run: dotnet build ./src/Downloader/Downloader.csproj - - - name: Install Downloader.DummyHttpServer Dependencies - run: dotnet restore ./src/Downloader.DummyHttpServer/Downloader.DummyHttpServer.csproj - - - name: Build Downloader.DummyHttpServer Project - run: dotnet build ./src/Downloader.DummyHttpServer/Downloader.DummyHttpServer.csproj + - name: Checkout code + uses: actions/checkout@v3 - - name: Install Downloader.Test Dependencies - run: dotnet restore ./src/Downloader.Test/Downloader.Test.csproj + - name: Setup .NET Core 8.0 + uses: actions/setup-dotnet@v3 + with: + dotnet-version: 8.0.x - - name: Build Downloader.Test Project - run: dotnet build ./src/Downloader.Test/Downloader.Test.csproj + - name: Build solution + run: dotnet build -c Release ./src --verbosity minimal - - name: Test - run: dotnet test ./src/Downloader.Test/Downloader.Test.csproj --no-build --verbosity detailed \ No newline at end of file + - name: Run tests + run: dotnet test -c Release ./src --verbosity normal --no-build --no-restore \ No newline at end of file diff --git a/README.md b/README.md index bedf2f9d..95b686a0 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ [![Windows x64](https://github.com/bezzad/Downloader/workflows/Windows%20x64/badge.svg)](https://github.com/bezzad/Downloader/actions/workflows/dotnet-windows.yml) [![Ubuntu x64](https://github.com/bezzad/Downloader/workflows/Ubuntu%20x64/badge.svg)](https://github.com/bezzad/Downloader/actions/workflows/dotnet-ubuntu.yml) +[![Build Status](https://ci.appveyor.com/api/projects/status/github/bezzad/downloader?branch=master&svg=true)](https://ci.appveyor.com/project/bezzad/downloader) [![codecov](https://codecov.io/gh/bezzad/downloader/branch/master/graph/badge.svg)](https://codecov.io/gh/bezzad/downloader) [![NuGet](https://img.shields.io/nuget/dt/downloader.svg)](https://www.nuget.org/packages/downloader) [![NuGet](https://img.shields.io/nuget/vpre/downloader.svg)](https://www.nuget.org/packages/downloader) @@ -15,9 +16,9 @@ :rocket: Fast, cross-platform and reliable multipart downloader with **.Net Core** supporting :rocket: Downloader is a modern, fluent, asynchronous, testable and portable library for .NET. This is a multipart downloader with asynchronous progress events. -This library can be added to your `.Net Core v2` and later or `.Net Framework v4.5` or later projects. +This library can be added to your `.Net 8` or later projects. -Downloader is compatible with .NET Standard 2.0 and above, running on Windows, Linux, and macOS, in full .NET Framework or .NET Core. +Downloader is running on Windows, Linux, and macOS. > For a complete example see [Downloader.Sample](https://github.com/bezzad/Downloader/blob/master/src/Samples/Downloader.Sample/Program.cs) project from this repository. @@ -111,7 +112,7 @@ var downloadOpt = new DownloadConfiguration() // minimum size of chunking to download a file in multiple parts, the default value is 512 MinimumSizeOfChunking = 1024, // Before starting the download, reserve the storage space of the file as file size, the default value is false - ReserveStorageSpaceBeforeStartingDownload = true; + ReserveStorageSpaceBeforeStartingDownload = true, // config and customize request headers RequestConfiguration = { diff --git a/src/.idea/.idea.Downloader/.idea/.name b/src/.idea/.idea.Downloader/.idea/.name new file mode 100644 index 00000000..69e108a1 --- /dev/null +++ b/src/.idea/.idea.Downloader/.idea/.name @@ -0,0 +1 @@ +Downloader \ No newline at end of file diff --git a/src/Downloader.DummyHttpServer/Downloader.DummyHttpServer.csproj b/src/Downloader.DummyHttpServer/Downloader.DummyHttpServer.csproj index 49d5e429..c8b4925f 100644 --- a/src/Downloader.DummyHttpServer/Downloader.DummyHttpServer.csproj +++ b/src/Downloader.DummyHttpServer/Downloader.DummyHttpServer.csproj @@ -1,7 +1,6 @@  - netcoreapp3.1;net6.0; latestMajor true true @@ -11,6 +10,7 @@ True True sgKey.snk + net8.0 diff --git a/src/Downloader.DummyHttpServer/DummyApiException.cs b/src/Downloader.DummyHttpServer/DummyApiException.cs index e881cf75..29049252 100644 --- a/src/Downloader.DummyHttpServer/DummyApiException.cs +++ b/src/Downloader.DummyHttpServer/DummyApiException.cs @@ -1,7 +1,9 @@ -using System.Net; +using System.Diagnostics.CodeAnalysis; +using System.Net; namespace Downloader.DummyHttpServer; +[ExcludeFromCodeCoverage] public class DummyApiException : WebException { public DummyApiException(string message) diff --git a/src/Downloader.DummyHttpServer/DummyApiExceptionFilterAttribute.cs b/src/Downloader.DummyHttpServer/DummyApiExceptionFilterAttribute.cs index ce35ade4..388e897c 100644 --- a/src/Downloader.DummyHttpServer/DummyApiExceptionFilterAttribute.cs +++ b/src/Downloader.DummyHttpServer/DummyApiExceptionFilterAttribute.cs @@ -1,10 +1,12 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; +using System.Diagnostics.CodeAnalysis; using System.Net; using static System.Console; namespace Downloader.DummyHttpServer; +[ExcludeFromCodeCoverage] public class DummyApiExceptionFilterAttribute : ExceptionFilterAttribute { public override void OnException(ExceptionContext context) diff --git a/src/Downloader.DummyHttpServer/DummyData.cs b/src/Downloader.DummyHttpServer/DummyData.cs index a248391f..66a0ba8d 100644 --- a/src/Downloader.DummyHttpServer/DummyData.cs +++ b/src/Downloader.DummyHttpServer/DummyData.cs @@ -1,13 +1,15 @@ using System; +using System.Diagnostics.CodeAnalysis; namespace Downloader.DummyHttpServer; /// /// Class with helper methods to create random data /// +[ExcludeFromCodeCoverage] public static class DummyData { - private static Random _rand = new Random(DateTime.Now.GetHashCode()); + private static Random Rand = new Random(DateTime.Now.GetHashCode()); /// /// Generates random bytes @@ -19,7 +21,7 @@ public static byte[] GenerateRandomBytes(int length) throw new ArgumentException("length has to be > 0"); byte[] buffer = new byte[length]; - _rand.NextBytes(buffer); + Rand.NextBytes(buffer); return buffer; } diff --git a/src/Downloader.DummyHttpServer/DummyFileHelper.cs b/src/Downloader.DummyHttpServer/DummyFileHelper.cs index ae1ab0ff..3123b9eb 100644 --- a/src/Downloader.DummyHttpServer/DummyFileHelper.cs +++ b/src/Downloader.DummyHttpServer/DummyFileHelper.cs @@ -1,8 +1,10 @@ -using System.IO; +using System.Diagnostics.CodeAnalysis; +using System.IO; using System.Linq; namespace Downloader.DummyHttpServer; +[ExcludeFromCodeCoverage] public static class DummyFileHelper { public const string TempFilesExtension = ".temp"; diff --git a/src/Downloader.DummyHttpServer/DummyLazyStream.cs b/src/Downloader.DummyHttpServer/DummyLazyStream.cs index 9b64d840..dbafa2a9 100644 --- a/src/Downloader.DummyHttpServer/DummyLazyStream.cs +++ b/src/Downloader.DummyHttpServer/DummyLazyStream.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.IO; namespace Downloader.DummyHttpServer; @@ -10,6 +11,7 @@ public enum DummyDataType Single } +[ExcludeFromCodeCoverage] public class DummyLazyStream : Stream { private readonly Random _random; diff --git a/src/Downloader.DummyHttpServer/HttpServer.cs b/src/Downloader.DummyHttpServer/HttpServer.cs index 91d86b0c..f6cb7e4e 100644 --- a/src/Downloader.DummyHttpServer/HttpServer.cs +++ b/src/Downloader.DummyHttpServer/HttpServer.cs @@ -2,17 +2,18 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Hosting; using System; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace Downloader.DummyHttpServer; +[ExcludeFromCodeCoverage] public class HttpServer { - private static IMemoryCache _cache = new MemoryCache(new MemoryCacheOptions()); + private static IMemoryCache Cache = new MemoryCache(new MemoryCacheOptions()); private static IWebHost Server; public static int Port { get; set; } = 3333; public static CancellationTokenSource CancellationToken { get; set; } @@ -30,7 +31,7 @@ public static void Run(int port) if (CancellationToken.IsCancellationRequested) return; - Server ??= _cache.GetOrCreate("DownloaderWebHost", e => { + Server ??= Cache.GetOrCreate("DownloaderWebHost", e => { var host = CreateHostBuilder(port); host.RunAsync(CancellationToken.Token).ConfigureAwait(false); return host; diff --git a/src/Downloader.DummyHttpServer/MockMemoryStream.cs b/src/Downloader.DummyHttpServer/MockMemoryStream.cs index c76148ea..362f9e4c 100644 --- a/src/Downloader.DummyHttpServer/MockMemoryStream.cs +++ b/src/Downloader.DummyHttpServer/MockMemoryStream.cs @@ -1,10 +1,12 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Threading; using System.Threading.Tasks; namespace Downloader.DummyHttpServer; +[ExcludeFromCodeCoverage] public class MockMemoryStream : MemoryStream { private readonly long _failureOffset = 0; diff --git a/src/Downloader.DummyHttpServer/Startup.cs b/src/Downloader.DummyHttpServer/Startup.cs index bd1f4ce0..0ce661d1 100644 --- a/src/Downloader.DummyHttpServer/Startup.cs +++ b/src/Downloader.DummyHttpServer/Startup.cs @@ -1,8 +1,10 @@ using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; +using System.Diagnostics.CodeAnalysis; namespace Downloader.DummyHttpServer; +[ExcludeFromCodeCoverage] internal class Startup { /// diff --git a/src/Downloader.Test/Downloader.Test.csproj b/src/Downloader.Test/Downloader.Test.csproj index df50e2b0..06945fe8 100644 --- a/src/Downloader.Test/Downloader.Test.csproj +++ b/src/Downloader.Test/Downloader.Test.csproj @@ -1,7 +1,6 @@  - net6.0;net8.0; false true Sign.snk @@ -9,6 +8,7 @@ True Downloader Tests latestMajor + net8.0 @@ -25,19 +25,20 @@ all - + all runtime; build; native; contentfiles; analyzers; buildtransitive + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Downloader.Test/Helper/FileLogger.cs b/src/Downloader.Test/Helper/FileLogger.cs index b16c58f6..a2392548 100644 --- a/src/Downloader.Test/Helper/FileLogger.cs +++ b/src/Downloader.Test/Helper/FileLogger.cs @@ -1,5 +1,7 @@ -using System; +using Microsoft.Extensions.Logging; +using System; using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Runtime.CompilerServices; using System.Threading; @@ -7,10 +9,11 @@ namespace Downloader.Extensions.Logging; +[ExcludeFromCodeCoverage] public class FileLogger : ILogger, IDisposable { private volatile bool _disposed; - private SemaphoreSlim _semaphore; + private readonly SemaphoreSlim _semaphore; protected readonly ConcurrentQueue LogQueue; protected string LogPath; protected StreamWriter LogStream; @@ -39,51 +42,51 @@ public FileLogger(string logPath) public void LogDebug(string message) { - Log(nameof(LogDebug), message); + Log(LogLevel.Information, message); } public void LogInfo(string message) { - Log(nameof(LogInfo), message); + Log(LogLevel.Information, message); } public void LogWarning(string message) { - Log(nameof(LogWarning), message); + Log(LogLevel.Warning, message); } public void LogError(string message) { - Log(nameof(LogError), message); + Log(LogLevel.Error, message); } public void LogError(Exception exception, string message) { - Log(nameof(LogError), message, exception); + Log(LogLevel.Error, message, exception); } public void LogCritical(string message) { - Log(nameof(LogCritical), message); + Log(LogLevel.Critical, message); } public void LogCritical(Exception exception, string message) { - Log(nameof(LogCritical), message, exception); + Log(LogLevel.Critical, message, exception); } - protected void Log(string logType, string message, Exception exception = null) + protected void Log(LogLevel logLevel, string message, Exception exception = null) { if (!_disposed) { - LogQueue.Enqueue(Formatter(logType, message, exception)); + LogQueue.Enqueue(Formatter(logLevel, message, exception)); _semaphore.Release(); } } - public virtual string Formatter(string logType, string message, Exception exception) + public virtual string Formatter(LogLevel logLevel, string message, Exception exception) { - var log = $"{DateTime.Now:s} | {logType} | {message}"; + var log = $"{DateTime.Now:s} | {logLevel} | {message}"; if (exception is not null) { log += " | " + exception.Message + ": " + exception.StackTrace; @@ -136,4 +139,27 @@ private static Stream CreateFile(string filename) return new FileStream(filename, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete); } + + public IDisposable BeginScope(TState state) where TState : notnull + { + throw new NotImplementedException(); + } + + public bool IsEnabled(LogLevel logLevel) + { + return true; + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + if (!IsEnabled(logLevel)) + { + return; + } + + var logMessage = formatter(state, exception); + var logEntry = $"{DateTime.UtcNow:yyyy-MM-ddTHH:mm:ss.fffZ} [{logLevel}] {logMessage}{Environment.NewLine}"; + + Log(logLevel, logEntry, exception); + } } diff --git a/src/Downloader.Test/HelperTests/AssertHelperTest.cs b/src/Downloader.Test/HelperTests/AssertHelperTest.cs index ace572d5..dc658c3d 100644 --- a/src/Downloader.Test/HelperTests/AssertHelperTest.cs +++ b/src/Downloader.Test/HelperTests/AssertHelperTest.cs @@ -66,10 +66,10 @@ public void TestChunksAreNotEquals() }; // act - void testAssertHelper() => AssertHelper.AreEquals(chunk1, chunk2); + void TestAssertHelper() => AssertHelper.AreEquals(chunk1, chunk2); // assert - Assert.ThrowsAny(testAssertHelper); + Assert.ThrowsAny(TestAssertHelper); Assert.NotEqual(chunk1, chunk2); } diff --git a/src/Downloader.Test/HelperTests/DummyDataTest.cs b/src/Downloader.Test/HelperTests/DummyDataTest.cs index 57fbcc8d..3a1d9021 100644 --- a/src/Downloader.Test/HelperTests/DummyDataTest.cs +++ b/src/Downloader.Test/HelperTests/DummyDataTest.cs @@ -29,10 +29,10 @@ public void GenerateOrderedBytesLessThan1Test() int size = 0; // act - void act() => DummyData.GenerateOrderedBytes(size); + void Act() => DummyData.GenerateOrderedBytes(size); // assert - Assert.ThrowsAny(act); + Assert.ThrowsAny(Act); } [Fact] @@ -56,10 +56,10 @@ public void GenerateRandomBytesLessThan1Test() int size = 0; // act - void act() => DummyData.GenerateRandomBytes(size); + void Act() => DummyData.GenerateRandomBytes(size); // assert - Assert.ThrowsAny(act); + Assert.ThrowsAny(Act); } [Fact] diff --git a/src/Downloader.Test/HelperTests/DummyFileControllerTest.cs b/src/Downloader.Test/HelperTests/DummyFileControllerTest.cs index 8773bf40..6a2b666e 100644 --- a/src/Downloader.Test/HelperTests/DummyFileControllerTest.cs +++ b/src/Downloader.Test/HelperTests/DummyFileControllerTest.cs @@ -10,8 +10,8 @@ namespace Downloader.Test.HelperTests; public class DummyFileControllerTest { - private readonly string contentType = "application/octet-stream"; - private WebHeaderCollection headers; + private readonly string _contentType = "application/octet-stream"; + private WebHeaderCollection _headers; [Fact] public void GetFileTest() @@ -27,8 +27,8 @@ public void GetFileTest() // assert Assert.True(dummyData.SequenceEqual(bytes)); - Assert.Equal(size.ToString(), headers["Content-Length"]); - Assert.Equal(contentType, headers["Content-Type"]); + Assert.Equal(size.ToString(), _headers["Content-Length"]); + Assert.Equal(_contentType, _headers["Content-Type"]); } [Fact] @@ -46,8 +46,8 @@ public void GetFileWithNameTest() // assert Assert.True(dummyData.SequenceEqual(bytes)); - Assert.Equal(size.ToString(), headers["Content-Length"]); - Assert.Equal(contentType, headers["Content-Type"]); + Assert.Equal(size.ToString(), _headers["Content-Length"]); + Assert.Equal(_contentType, _headers["Content-Type"]); } [Fact] @@ -67,8 +67,8 @@ public void GetSingleByteFileWithNameTest() // assert Assert.True(bytes.All(i => i == fillByte)); Assert.True(dummyData.SequenceEqual(bytes)); - Assert.Equal(size.ToString(), headers["Content-Length"]); - Assert.Equal(contentType, headers["Content-Type"]); + Assert.Equal(size.ToString(), _headers["Content-Length"]); + Assert.Equal(_contentType, _headers["Content-Type"]); } [Fact] @@ -86,8 +86,8 @@ public void GetFileWithoutHeaderTest() // assert Assert.True(dummyData.SequenceEqual(bytes)); - Assert.Null(headers["Content-Length"]); - Assert.Null(headers["Content-Type"]); + Assert.Null(_headers["Content-Length"]); + Assert.Null(_headers["Content-Type"]); } [Fact] @@ -107,8 +107,8 @@ public void GetSingleByteFileWithoutHeaderTest() // assert Assert.True(bytes.All(i => i == fillByte)); Assert.True(dummyData.SequenceEqual(bytes)); - Assert.Null(headers["Content-Length"]); - Assert.Null(headers["Content-Type"]); + Assert.Null(_headers["Content-Length"]); + Assert.Null(_headers["Content-Type"]); } [Fact] @@ -126,9 +126,9 @@ public void GetFileWithContentDispositionTest() // assert Assert.True(dummyData.SequenceEqual(bytes)); - Assert.Equal(size.ToString(), headers["Content-Length"]); - Assert.Equal(contentType, headers["Content-Type"]); - Assert.Contains($"filename={filename};", headers["Content-Disposition"]); + Assert.Equal(size.ToString(), _headers["Content-Length"]); + Assert.Equal(_contentType, _headers["Content-Type"]); + Assert.Contains($"filename={filename};", _headers["Content-Disposition"]); } [Fact] @@ -148,9 +148,9 @@ public void GetSingleByteFileWithContentDispositionTest() // assert Assert.True(bytes.All(i => i == fillByte)); Assert.True(dummyData.SequenceEqual(bytes)); - Assert.Equal(size.ToString(), headers["Content-Length"]); - Assert.Equal(contentType, headers["Content-Type"]); - Assert.Contains($"filename={filename};", headers["Content-Disposition"]); + Assert.Equal(size.ToString(), _headers["Content-Length"]); + Assert.Equal(_contentType, _headers["Content-Type"]); + Assert.Contains($"filename={filename};", _headers["Content-Disposition"]); } [Fact] @@ -167,10 +167,10 @@ public void GetFileWithRangeTest() // assert Assert.True(dummyData.Take(512).SequenceEqual(bytes.Take(512))); - Assert.Equal(contentType, headers["Content-Type"]); - Assert.Equal("512", headers["Content-Length"]); - Assert.Equal("bytes 0-511/1024", headers["Content-Range"]); - Assert.Equal("bytes", headers["Accept-Ranges"]); + Assert.Equal(_contentType, _headers["Content-Type"]); + Assert.Equal("512", _headers["Content-Length"]); + Assert.Equal("bytes 0-511/1024", _headers["Content-Range"]); + Assert.Equal("bytes", _headers["Accept-Ranges"]); } [Fact] @@ -188,9 +188,9 @@ public void GetFileWithNoAcceptRangeTest() // assert Assert.True(dummyData.SequenceEqual(bytes)); - Assert.Equal(size.ToString(), headers["Content-Length"]); - Assert.Equal(contentType, headers["Content-Type"]); - Assert.Null(headers["Accept-Ranges"]); + Assert.Equal(size.ToString(), _headers["Content-Length"]); + Assert.Equal(_contentType, _headers["Content-Type"]); + Assert.Null(_headers["Accept-Ranges"]); } [Fact] @@ -210,9 +210,9 @@ public void GetSingleByteFileWithNoAcceptRangeTest() // assert Assert.True(bytes.All(i => i == fillByte)); Assert.True(dummyData.SequenceEqual(bytes)); - Assert.Equal(size.ToString(), headers["Content-Length"]); - Assert.Equal(contentType, headers["Content-Type"]); - Assert.Null(headers["Accept-Ranges"]); + Assert.Equal(size.ToString(), _headers["Content-Length"]); + Assert.Equal(_contentType, _headers["Content-Type"]); + Assert.Null(_headers["Accept-Ranges"]); } [Fact] @@ -230,9 +230,9 @@ public void GetFileWithNameOnRedirectTest() // assert Assert.True(dummyData.SequenceEqual(bytes)); - Assert.Equal(size.ToString(), headers["Content-Length"]); - Assert.Equal(contentType, headers["Content-Type"]); - Assert.NotEqual(url, headers[nameof(WebResponse.ResponseUri)]); + Assert.Equal(size.ToString(), _headers["Content-Length"]); + Assert.Equal(_contentType, _headers["Content-Type"]); + Assert.NotEqual(url, _headers[nameof(WebResponse.ResponseUri)]); } [Fact] @@ -245,12 +245,12 @@ public void GetFileWithFailureAfterOffsetTest() string url = DummyFileHelper.GetFileWithFailureAfterOffset(size, failureOffset); // act - void getHeaders() => ReadAndGetHeaders(url, bytes, false); + void GetHeaders() => ReadAndGetHeaders(url, bytes, false); // assert - Assert.ThrowsAny(getHeaders); - Assert.Equal(size.ToString(), headers["Content-Length"]); - Assert.Equal(contentType, headers["Content-Type"]); + Assert.ThrowsAny(GetHeaders); + Assert.Equal(size.ToString(), _headers["Content-Length"]); + Assert.Equal(_contentType, _headers["Content-Type"]); Assert.Equal(0, bytes[size - 1]); } @@ -264,12 +264,12 @@ public void GetFileWithTimeoutAfterOffsetTest() string url = DummyFileHelper.GetFileWithTimeoutAfterOffset(size, timeoutOffset); // act - void getHeaders() => ReadAndGetHeaders(url, bytes, false); + void GetHeaders() => ReadAndGetHeaders(url, bytes, false); // assert - Assert.ThrowsAny(getHeaders); - Assert.Equal(size.ToString(), headers["Content-Length"]); - Assert.Equal(contentType, headers["Content-Type"]); + Assert.ThrowsAny(GetHeaders); + Assert.Equal(size.ToString(), _headers["Content-Length"]); + Assert.Equal(_contentType, _headers["Content-Type"]); Assert.Equal(0, bytes[size - 1]); } @@ -287,7 +287,7 @@ private void ReadAndGetHeaders(string url, byte[] bytes, bool justFirst512Bytes // keep response headers downloadResponse.Headers.Add(nameof(WebResponse.ResponseUri), downloadResponse.ResponseUri.ToString()); - headers = downloadResponse.Headers; + _headers = downloadResponse.Headers; // read stream data var readCount = 1; diff --git a/src/Downloader.Test/HelperTests/DummyLazyStreamTest.cs b/src/Downloader.Test/HelperTests/DummyLazyStreamTest.cs index bf447fd2..1f576c86 100644 --- a/src/Downloader.Test/HelperTests/DummyLazyStreamTest.cs +++ b/src/Downloader.Test/HelperTests/DummyLazyStreamTest.cs @@ -31,10 +31,10 @@ public void GenerateOrderedBytesStreamLessThan1Test() int size = 0; // act - void act() => new DummyLazyStream(DummyDataType.Order, size); + void Act() => new DummyLazyStream(DummyDataType.Order, size); // assert - Assert.ThrowsAny(act); + Assert.ThrowsAny(Act); } [Fact] @@ -58,10 +58,10 @@ public void GenerateRandomBytesLessThan1Test() int size = 0; // act - void act() => new DummyLazyStream(DummyDataType.Random, size); + void Act() => new DummyLazyStream(DummyDataType.Random, size); // assert - Assert.ThrowsAny(act); + Assert.ThrowsAny(Act); } [Fact] diff --git a/src/Downloader.Test/IntegrationTests/DownloadIntegrationTest.cs b/src/Downloader.Test/IntegrationTests/DownloadIntegrationTest.cs index d1928ec4..65d26dac 100644 --- a/src/Downloader.Test/IntegrationTests/DownloadIntegrationTest.cs +++ b/src/Downloader.Test/IntegrationTests/DownloadIntegrationTest.cs @@ -8,7 +8,6 @@ using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; -using FileLogger = Downloader.Extensions.Logging.FileLogger; namespace Downloader.Test.IntegrationTests; @@ -16,7 +15,7 @@ public abstract class DownloadIntegrationTest : IDisposable { protected static byte[] FileData { get; set; } protected readonly ITestOutputHelper Output; - protected string URL { get; set; } + protected string Url { get; set; } protected int FileSize { get; set; } protected string Filename { get; set; } protected string FilePath { get; set; } @@ -30,7 +29,7 @@ public DownloadIntegrationTest(ITestOutputHelper output) FilePath = Path.Combine(Path.GetTempPath(), Filename); FileSize = DummyFileHelper.FileSize16Kb; FileData ??= DummyFileHelper.File16Kb; - URL = DummyFileHelper.GetFileWithNameUrl(Filename, FileSize); + Url = DummyFileHelper.GetFileWithNameUrl(Filename, FileSize); } public void Dispose() @@ -43,7 +42,7 @@ protected void DownloadFileCompleted(object sender, System.ComponentModel.AsyncC { if (e.Error is not null) { - Output.WriteLine("Error when completed: " + e.Error.Message.ToString()); + Output.WriteLine("Error when completed: " + e.Error.Message); } } @@ -53,7 +52,7 @@ public async Task DownloadUrlWithFilenameOnMemoryTest() // arrange var downloadCompletedSuccessfully = false; var resultMessage = ""; - Downloader.DownloadFileCompleted += (s, e) => { + Downloader.DownloadFileCompleted += (_, e) => { if (e.Cancelled == false && e.Error == null) { downloadCompletedSuccessfully = true; @@ -65,7 +64,7 @@ public async Task DownloadUrlWithFilenameOnMemoryTest() }; // act - using var memoryStream = await Downloader.DownloadFileTaskAsync(URL); + await using var memoryStream = await Downloader.DownloadFileTaskAsync(Url); // assert Assert.True(downloadCompletedSuccessfully, resultMessage); @@ -85,7 +84,7 @@ public async Task DownloadAndReadFileOnDownloadFileCompletedEventTest() var destFilename = FilePath; byte[] downloadedBytes = null; var downloadCompletedSuccessfully = false; - Downloader.DownloadFileCompleted += (s, e) => { + Downloader.DownloadFileCompleted += (_, e) => { if (e.Cancelled == false && e.Error == null) { // Execute the downloaded file within completed event @@ -99,7 +98,7 @@ public async Task DownloadAndReadFileOnDownloadFileCompletedEventTest() }; // act - await Downloader.DownloadFileTaskAsync(URL, destFilename); + await Downloader.DownloadFileTaskAsync(Url, destFilename); // assert Assert.True(downloadCompletedSuccessfully); @@ -119,7 +118,7 @@ public async Task Download16KbWithoutFilenameOnDirectoryTest() var dir = new DirectoryInfo(Path.GetTempPath()); // act - await Downloader.DownloadFileTaskAsync(URL, dir); + await Downloader.DownloadFileTaskAsync(Url, dir); // assert Assert.True(Downloader.Package.IsSaveComplete); @@ -137,7 +136,7 @@ public async Task Download16KbWithoutFilenameOnDirectoryTest() public async Task Download16KbWithFilenameTest() { // act - await Downloader.DownloadFileTaskAsync(URL, Path.GetTempFileName()); + await Downloader.DownloadFileTaskAsync(Url, Path.GetTempFileName()); // assert Assert.True(File.Exists(Downloader.Package.FileName)); @@ -179,7 +178,7 @@ public async Task Download1KbWhenAnotherBiggerFileExistTest() public async Task Download16KbOnMemoryTest() { // act - var fileBytes = await Downloader.DownloadFileTaskAsync(URL); + var fileBytes = await Downloader.DownloadFileTaskAsync(Url); // assert Assert.Equal(expected: FileSize, actual: Downloader.Package.TotalFileSize); @@ -193,10 +192,10 @@ public async Task DownloadProgressChangedTest() // arrange var progressChangedCount = (int)Math.Ceiling((double)FileSize / Config.BufferBlockSize); var progressCounter = 0; - Downloader.DownloadProgressChanged += (s, e) => Interlocked.Increment(ref progressCounter); + Downloader.DownloadProgressChanged += (_, _) => Interlocked.Increment(ref progressCounter); // act - await Downloader.DownloadFileTaskAsync(URL); + await Downloader.DownloadFileTaskAsync(Url); // assert // Note: some times received bytes on read stream method was less than block size! @@ -215,7 +214,7 @@ public async Task StopResumeDownloadTest() var cancellationsOccurrenceCount = 0; var downloadFileExecutionCounter = 0; var downloadCompletedSuccessfully = false; - Downloader.DownloadFileCompleted += (s, e) => { + Downloader.DownloadFileCompleted += (_, e) => { if (e.Cancelled && e.Error != null) { cancellationsOccurrenceCount++; @@ -235,13 +234,13 @@ public async Task StopResumeDownloadTest() }; // act - await Downloader.DownloadFileTaskAsync(URL, Path.GetTempFileName()); + await Downloader.DownloadFileTaskAsync(Url, Path.GetTempFileName()); while (expectedStopCount > downloadFileExecutionCounter++) { // resume download from stopped point. await Downloader.DownloadFileTaskAsync(Downloader.Package); } - var stream = File.ReadAllBytes(Downloader.Package.FileName); + var stream = await File.ReadAllBytesAsync(Downloader.Package.FileName); // assert Assert.True(File.Exists(Downloader.Package.FileName)); @@ -261,7 +260,7 @@ public async Task PauseResumeDownloadTest() var expectedPauseCount = 2; var pauseCount = 0; var downloadCompletedSuccessfully = false; - Downloader.DownloadFileCompleted += (s, e) => { + Downloader.DownloadFileCompleted += (_, e) => { if (e.Cancelled == false && e.Error is null) downloadCompletedSuccessfully = true; }; @@ -276,7 +275,7 @@ public async Task PauseResumeDownloadTest() }; // act - await Downloader.DownloadFileTaskAsync(URL, Path.GetTempFileName()); + await Downloader.DownloadFileTaskAsync(Url, Path.GetTempFileName()); var stream = File.ReadAllBytes(Downloader.Package.FileName); // assert @@ -300,7 +299,9 @@ public async Task StopResumeDownloadFromLastPositionTest() var totalProgressedByteSize = 0L; var totalReceivedBytes = 0L; Config.BufferBlockSize = 1024; - Downloader.DownloadProgressChanged += (s, e) => { + Config.EnableLiveStreaming = true; + + Downloader.DownloadProgressChanged += (_, e) => { totalProgressedByteSize += e.ProgressedByteSize; totalReceivedBytes += e.ReceivedBytes.Length; if (expectedStopCount > stopCount) @@ -312,7 +313,7 @@ public async Task StopResumeDownloadFromLastPositionTest() }; // act - await Downloader.DownloadFileTaskAsync(URL); + await Downloader.DownloadFileTaskAsync(Url); while (expectedStopCount > downloadFileExecutionCounter++) { // resume download from stopped point. @@ -332,8 +333,9 @@ public async Task StopResumeDownloadOverFirstPackagePositionTest() var cancellationCount = 4; var isSavingStateOnCancel = false; var isSavingStateBeforCancel = false; - - Downloader.DownloadProgressChanged += async (s, e) => { + Config.EnableLiveStreaming = true; + + Downloader.DownloadProgressChanged += async (_, _) => { isSavingStateBeforCancel |= Downloader.Package.IsSaving; if (--cancellationCount > 0) { @@ -343,7 +345,7 @@ public async Task StopResumeDownloadOverFirstPackagePositionTest() }; // act - var result = await Downloader.DownloadFileTaskAsync(URL); + var result = await Downloader.DownloadFileTaskAsync(Url); // check point of package for once time var firstCheckPointPackage = JsonConvert.SerializeObject(Downloader.Package); @@ -374,7 +376,8 @@ public async Task TestTotalReceivedBytesWhenResumeDownload() var lastProgressPercentage = 0.0; Config.BufferBlockSize = 1024; Config.ChunkCount = 1; - Downloader.DownloadProgressChanged += async (s, e) => { + Config.EnableLiveStreaming = true; + Downloader.DownloadProgressChanged += async (_, e) => { totalDownloadSize += e.ReceivedBytes.Length; lastProgressPercentage = e.ProgressPercentage; if (canStopDownload && totalDownloadSize > FileSize / 2) @@ -386,7 +389,7 @@ public async Task TestTotalReceivedBytesWhenResumeDownload() }; // act - await Downloader.DownloadFileTaskAsync(URL); + await Downloader.DownloadFileTaskAsync(Url); await Downloader.DownloadFileTaskAsync(Downloader.Package); // resume download from stopped point. // assert @@ -406,7 +409,7 @@ public async Task TestTotalReceivedBytesOnResumeDownloadWhenLostDownloadedData() var lastProgressPercentage = 0.0; Config.BufferBlockSize = 1024; Config.ChunkCount = 1; - Downloader.DownloadProgressChanged += (s, e) => { + Downloader.DownloadProgressChanged += (_, e) => { totalDownloadSize = e.ReceivedBytesSize; lastProgressPercentage = e.ProgressPercentage; if (canStopDownload && totalDownloadSize > FileSize / 2) @@ -418,7 +421,7 @@ public async Task TestTotalReceivedBytesOnResumeDownloadWhenLostDownloadedData() }; // act - await Downloader.DownloadFileTaskAsync(URL); + await Downloader.DownloadFileTaskAsync(Url); Downloader.Package.Storage.Dispose(); // set position to zero await Downloader.DownloadFileTaskAsync(Downloader.Package); // resume download from stopped point. @@ -439,13 +442,13 @@ public async Task SpeedLimitTest() Config.BufferBlockSize = 1024; Config.MaximumBytesPerSecond = 2048; // Byte/s - Downloader.DownloadProgressChanged += (s, e) => { + Downloader.DownloadProgressChanged += (_, e) => { averageSpeed = ((averageSpeed * progressCounter) + e.BytesPerSecondSpeed) / (progressCounter + 1); progressCounter++; }; // act - await Downloader.DownloadFileTaskAsync(URL); + await Downloader.DownloadFileTaskAsync(Url); // assert Assert.Equal(FileSize, Downloader.Package.TotalFileSize); @@ -457,7 +460,7 @@ public async Task DynamicSpeedLimitTest() { // arrange double upperTolerance = 1.5; // 50% upper than expected avg speed - double expectedAverageSpeed = FileSize / 32; // == (256*16 + 512*8 + 1024*4 + 2048*2) / 32 + long expectedAverageSpeed = FileSize / 32; // == (256*16 + 512*8 + 1024*4 + 2048*2) / 32 double averageSpeed = 0; var progressCounter = 0; const int oneSpeedStepSize = 4096; // FileSize / 4 @@ -465,7 +468,8 @@ public async Task DynamicSpeedLimitTest() Config.MaximumBytesPerSecond = 256; // Byte/s - Downloader.DownloadProgressChanged += (s, e) => { + Downloader.DownloadProgressChanged += (_, e) => { + // ReSharper disable once AccessToModifiedClosure averageSpeed += e.BytesPerSecondSpeed; progressCounter++; @@ -474,7 +478,7 @@ public async Task DynamicSpeedLimitTest() }; // act - await Downloader.DownloadFileTaskAsync(URL); + await Downloader.DownloadFileTaskAsync(Url); averageSpeed /= progressCounter; // assert @@ -491,7 +495,7 @@ public async Task TestSizeWhenDownloadOnMemoryStream() // act - using var stream = await Downloader.DownloadFileTaskAsync(URL); + await using var stream = await Downloader.DownloadFileTaskAsync(Url); // assert Assert.Equal(FileSize, Downloader.Package.TotalFileSize); @@ -505,7 +509,7 @@ public async Task TestTypeWhenDownloadOnMemoryStream() // act - using var stream = await Downloader.DownloadFileTaskAsync(URL); + await using var stream = await Downloader.DownloadFileTaskAsync(Url); // assert Assert.True(stream is MemoryStream); @@ -515,11 +519,11 @@ public async Task TestTypeWhenDownloadOnMemoryStream() public async Task TestContentWhenDownloadOnMemoryStream() { // act - using var stream = await Downloader.DownloadFileTaskAsync(URL); - var data = (stream as MemoryStream).ToArray(); + await using var stream = await Downloader.DownloadFileTaskAsync(Url); + var data = (stream as MemoryStream)?.ToArray(); // assert - Assert.True(FileData.SequenceEqual(data)); + Assert.True(data != null && FileData.SequenceEqual(data)); } [Fact(Timeout = 60_000)] @@ -533,7 +537,7 @@ public async Task Download256BytesRangeOfFileTest() // act - using var stream = await Downloader.DownloadFileTaskAsync(URL); + await using var stream = await Downloader.DownloadFileTaskAsync(Url); // assert Assert.NotNull(stream); @@ -558,7 +562,7 @@ public async Task DownloadNegetiveRangeOfFileTest() // act - using var stream = await Downloader.DownloadFileTaskAsync(URL); + await using var stream = await Downloader.DownloadFileTaskAsync(Url); var bytes = ((MemoryStream)stream).ToArray(); // assert @@ -578,12 +582,12 @@ public async Task TestDownloadParallelVsHalfOfChunks() Config.ParallelCount = maxParallelCountTasks; var actualMaxParallelCountTasks = 0; - Downloader.ChunkDownloadProgressChanged += (s, e) => { + Downloader.ChunkDownloadProgressChanged += (_, e) => { actualMaxParallelCountTasks = Math.Max(actualMaxParallelCountTasks, e.ActiveChunks); }; // act - using var stream = await Downloader.DownloadFileTaskAsync(URL); + await using var stream = await Downloader.DownloadFileTaskAsync(Url); var bytes = ((MemoryStream)stream).ToArray(); // assert @@ -604,8 +608,8 @@ public async Task TestResumeImmediatelyAfterCanceling() var lastProgressPercentage = 0d; bool? stopped = null; - Downloader.DownloadFileCompleted += (s, e) => stopped ??= e.Cancelled; - Downloader.DownloadProgressChanged += (s, e) => { + Downloader.DownloadFileCompleted += (_, e) => stopped ??= e.Cancelled; + Downloader.DownloadProgressChanged += (_, e) => { if (canStopDownload && e.ProgressPercentage > 50) { canStopDownload = false; @@ -618,8 +622,8 @@ public async Task TestResumeImmediatelyAfterCanceling() }; // act - await Downloader.DownloadFileTaskAsync(URL); - using var stream = await Downloader.DownloadFileTaskAsync(Downloader.Package); // resume + await Downloader.DownloadFileTaskAsync(Url); + await using var stream = await Downloader.DownloadFileTaskAsync(Downloader.Package); // resume // assert Assert.True(stopped); @@ -653,7 +657,7 @@ public async Task KeepOrRemoveFileWhenDownloadFailedTest(bool clearFileAfterFail [Theory] [InlineData(true)] // Test Retry Download After Timeout [InlineData(false)] // Test Retry Download After Failure - public async Task testRetryDownloadAfterFailure(bool timeout) + public async Task TestRetryDownloadAfterFailure(bool timeout) { // arrange Exception error = null; @@ -668,7 +672,7 @@ public async Task testRetryDownloadAfterFailure(bool timeout) var url = timeout ? DummyFileHelper.GetFileWithTimeoutAfterOffset(fileSize, failureOffset) : DummyFileHelper.GetFileWithFailureAfterOffset(fileSize, failureOffset); - downloadService.DownloadFileCompleted += (s, e) => error = e.Error; + downloadService.DownloadFileCompleted += (_, e) => error = e.Error; // act var stream = await downloadService.DownloadFileTaskAsync(url); @@ -717,8 +721,8 @@ public async Task TestStopDownloadWithCancellationToken() var downloadCancelled = false; var cts = new CancellationTokenSource(); - Downloader.DownloadFileCompleted += (s, e) => downloadCancelled = e.Cancelled; - Downloader.DownloadProgressChanged += (s, e) => { + Downloader.DownloadFileCompleted += (_, e) => downloadCancelled = e.Cancelled; + Downloader.DownloadProgressChanged += (_, e) => { downloadProgress = e.ProgressPercentage; if (e.ProgressPercentage > 10) { @@ -728,7 +732,7 @@ public async Task TestStopDownloadWithCancellationToken() }; // act - await Downloader.DownloadFileTaskAsync(URL, cts.Token); + await Downloader.DownloadFileTaskAsync(Url, cts.Token); // assert Assert.True(downloadCancelled); @@ -747,7 +751,7 @@ public async Task TestResumeDownloadWithAnotherUrl() var totalDownloadSize = 0L; Config.BufferBlockSize = 1024; Config.ChunkCount = 4; - Downloader.DownloadProgressChanged += (s, e) => { + Downloader.DownloadProgressChanged += (_, e) => { totalDownloadSize = e.ReceivedBytesSize; if (canStopDownload && totalDownloadSize > FileSize / 2) { @@ -785,7 +789,7 @@ public async Task DownloadAFileFromMultipleUrlsWithMultipleChunksTest(int urlsCo .ToArray(); // act - using var stream = await Downloader.DownloadFileTaskAsync(urls); + await using var stream = await Downloader.DownloadFileTaskAsync(urls); var bytes = ((MemoryStream)stream).ToArray(); // assert @@ -809,12 +813,12 @@ public async Task DownloadBigFileOnDisk() Config.ChunkCount = 8; Config.ParallelCount = 8; Config.MaximumBytesPerSecond = 0; - URL = DummyFileHelper.GetFileWithNameUrl(Filename, totalSize); + Url = DummyFileHelper.GetFileWithNameUrl(Filename, totalSize); //Downloader.AddLogger(FileLogger.Factory("D:\\TestDownload")); var actualFile = DummyData.GenerateOrderedBytes(totalSize); // act - await Downloader.DownloadFileTaskAsync(URL, FilePath); + await Downloader.DownloadFileTaskAsync(Url, FilePath); var file = await File.ReadAllBytesAsync(FilePath); // assert @@ -834,11 +838,11 @@ public async Task DownloadBigFileOnMemory() Config.ChunkCount = 8; Config.ParallelCount = 8; Config.MaximumBytesPerSecond = 0; - URL = DummyFileHelper.GetFileWithNameUrl(Filename, totalSize); + Url = DummyFileHelper.GetFileWithNameUrl(Filename, totalSize); var actualFile = DummyData.GenerateOrderedBytes(totalSize); // act - using var stream = await Downloader.DownloadFileTaskAsync(URL); + await using var stream = await Downloader.DownloadFileTaskAsync(Url); // assert Assert.Equal(totalSize, Downloader.Package.TotalFileSize); @@ -857,12 +861,12 @@ public async Task DownloadBigFileWithMemoryLimitationOnDisk() Config.ParallelCount = 16; Config.MaximumBytesPerSecond = 0; Config.MaximumMemoryBufferBytes = 1024 * 1024 * 50; // 50MB - URL = DummyFileHelper.GetFileWithNameUrl(Filename, totalSize, fillByte); + Url = DummyFileHelper.GetFileWithNameUrl(Filename, totalSize, fillByte); //Downloader.AddLogger(FileLogger.Factory("D:\\TestDownload")); // act - await Downloader.DownloadFileTaskAsync(URL, FilePath); - using var fileStream = File.Open(FilePath, FileMode.Open, FileAccess.Read); + await Downloader.DownloadFileTaskAsync(Url, FilePath); + await using var fileStream = File.Open(FilePath, FileMode.Open, FileAccess.Read); // assert Assert.Equal(totalSize, Downloader.Package.TotalFileSize); diff --git a/src/Downloader.Test/IntegrationTests/DownloadServiceTest.cs b/src/Downloader.Test/IntegrationTests/DownloadServiceTest.cs index 58ab570b..b51f726c 100644 --- a/src/Downloader.Test/IntegrationTests/DownloadServiceTest.cs +++ b/src/Downloader.Test/IntegrationTests/DownloadServiceTest.cs @@ -58,8 +58,8 @@ public async Task CancelAsyncTest() AsyncCompletedEventArgs eventArgs = null; string address = DummyFileHelper.GetFileUrl(DummyFileHelper.FileSize16Kb); Options = GetDefaultConfig(); - DownloadStarted += (s, e) => CancelAsync(); - DownloadFileCompleted += (s, e) => eventArgs = e; + DownloadStarted += (_, _) => CancelAsync(); + DownloadFileCompleted += (_, e) => eventArgs = e; // act await DownloadFileTaskAsync(address); @@ -68,7 +68,7 @@ public async Task CancelAsyncTest() Assert.True(IsCancelled); Assert.NotNull(eventArgs); Assert.True(eventArgs.Cancelled); - Assert.Equal(typeof(TaskCanceledException), eventArgs.Error.GetType()); + Assert.Equal(typeof(TaskCanceledException), eventArgs.Error?.GetType()); } [Fact] @@ -78,8 +78,8 @@ public async Task CancelTaskAsyncTest() AsyncCompletedEventArgs eventArgs = null; string address = DummyFileHelper.GetFileUrl(DummyFileHelper.FileSize16Kb); Options = GetDefaultConfig(); - DownloadStarted += async (s, e) => await CancelTaskAsync(); - DownloadFileCompleted += (s, e) => eventArgs = e; + DownloadStarted += async (_, _) => await CancelTaskAsync(); + DownloadFileCompleted += (_, e) => eventArgs = e; // act await DownloadFileTaskAsync(address); @@ -88,7 +88,7 @@ public async Task CancelTaskAsyncTest() Assert.True(IsCancelled); Assert.NotNull(eventArgs); Assert.True(eventArgs.Cancelled); - Assert.Equal(typeof(TaskCanceledException), eventArgs.Error.GetType()); + Assert.Equal(typeof(TaskCanceledException), eventArgs.Error?.GetType()); } [Fact(Timeout = 30_000)] @@ -100,7 +100,7 @@ public async Task CompletesWithErrorWhenBadUrlTest() Filename = Path.GetTempFileName(); Options = GetDefaultConfig(); Options.MaxTryAgainOnFailover = 0; - DownloadFileCompleted += (s, e) => { + DownloadFileCompleted += (_, e) => { onCompletionException = e.Error; }; @@ -174,7 +174,7 @@ public async Task TestPackageChunksDataAfterDispose() for (int i = 0; i < Package.Chunks.Length; i++) { var buffer = new byte[chunkSize]; - await stream.ReadAsync(buffer, 0, chunkSize); + _ = await stream.ReadAsync(buffer, 0, chunkSize); Assert.True(dummyData.SequenceEqual(buffer)); } } @@ -187,11 +187,11 @@ public async Task CancelPerformanceTest() var watch = new Stopwatch(); string address = DummyFileHelper.GetFileUrl(DummyFileHelper.FileSize16Kb); Options = GetDefaultConfig(); - DownloadProgressChanged += async (s, e) => { + DownloadProgressChanged += async (_, _) => { watch.Start(); await CancelTaskAsync(); }; - DownloadFileCompleted += (s, e) => eventArgs = e; + DownloadFileCompleted += (_, e) => eventArgs = e; // act await DownloadFileTaskAsync(address); @@ -213,8 +213,8 @@ public async Task ResumePerformanceTest() var isCancelled = false; string address = DummyFileHelper.GetFileUrl(DummyFileHelper.FileSize16Kb); Options = GetDefaultConfig(); - DownloadFileCompleted += (s, e) => eventArgs = e; - DownloadProgressChanged += async (s, e) => { + DownloadFileCompleted += (_, e) => eventArgs = e; + DownloadProgressChanged += async (_, _) => { if (isCancelled == false) { await CancelTaskAsync(); @@ -248,10 +248,10 @@ public async Task PauseResumeTest() var cancelled = false; string address = DummyFileHelper.GetFileUrl(DummyFileHelper.FileSize16Kb); Options = GetDefaultConfig(); - DownloadFileCompleted += (s, e) => eventArgs = e; + DownloadFileCompleted += (_, e) => eventArgs = e; // act - DownloadProgressChanged += (s, e) => { + DownloadProgressChanged += (_, _) => { Pause(); cancelled = IsCancelled; paused = IsPaused; @@ -277,10 +277,10 @@ public async Task CancelAfterPauseTest() var cancelStateAfterCancel = false; string address = DummyFileHelper.GetFileUrl(DummyFileHelper.FileSize16Kb); Options = GetDefaultConfig(); - DownloadFileCompleted += (s, e) => eventArgs = e; + DownloadFileCompleted += (_, e) => eventArgs = e; // act - DownloadProgressChanged += async (s, e) => { + DownloadProgressChanged += async (_, _) => { Pause(); cancelStateBeforeCancel = IsCancelled; pauseStateBeforeCancel = IsPaused; @@ -310,8 +310,8 @@ public async Task DownloadParallelNotSupportedUrlTest() AsyncCompletedEventArgs eventArgs = null; string address = DummyFileHelper.GetFileWithNoAcceptRangeUrl("test.dat", DummyFileHelper.FileSize16Kb); Options = GetDefaultConfig(); - DownloadFileCompleted += (s, e) => eventArgs = e; - DownloadStarted += (s, e) => { + DownloadFileCompleted += (_, e) => eventArgs = e; + DownloadStarted += (_, _) => { actualChunksCount = Package.Chunks.Length; }; @@ -340,8 +340,8 @@ public async Task ResumeNotSupportedUrlTest() var maxProgressPercentage = 0d; var address = DummyFileHelper.GetFileWithNoAcceptRangeUrl("test.dat", DummyFileHelper.FileSize16Kb); Options = GetDefaultConfig(); - DownloadFileCompleted += (s, e) => eventArgs = e; - DownloadProgressChanged += async (s, e) => { + DownloadFileCompleted += (_, e) => eventArgs = e; + DownloadProgressChanged += async (_, e) => { if (cancelOnProgressNo == progressCount++) { await CancelTaskAsync(); @@ -379,7 +379,7 @@ public async Task ActiveChunksTest() Options = GetDefaultConfig(); // act - DownloadProgressChanged += (s, e) => { + DownloadProgressChanged += (_, e) => { allActiveChunksCount.Add(e.ActiveChunks); }; await DownloadFileTaskAsync(address); @@ -402,7 +402,7 @@ public async Task ActiveChunksWithRangeNotSupportedUrlTest() Options = GetDefaultConfig(); // act - DownloadProgressChanged += (s, e) => { + DownloadProgressChanged += (_, e) => { allActiveChunksCount.Add(e.ActiveChunks); }; await DownloadFileTaskAsync(address); @@ -427,7 +427,7 @@ public async Task ActiveChunksAfterCancelResumeWithNotSupportedUrlTest() var cancelOnProgressNo = 6; var address = DummyFileHelper.GetFileWithNoAcceptRangeUrl("test.dat", DummyFileHelper.FileSize16Kb); Options = GetDefaultConfig(); - DownloadProgressChanged += async (s, e) => { + DownloadProgressChanged += async (_, e) => { allActiveChunksCount.Add(e.ActiveChunks); if (cancelOnProgressNo == progressCount++) { @@ -488,8 +488,8 @@ public async Task TestPackageStatusAfterCompletionWithSuccess() var resumeStatus = DownloadStatus.None; var completedStatus = DownloadStatus.None; - DownloadStarted += (s, e) => createdStatus = Package.Status; - DownloadProgressChanged += (s, e) => { + DownloadStarted += (_, _) => createdStatus = Package.Status; + DownloadProgressChanged += (_, e) => { runningStatus = Package.Status; if (e.ProgressPercentage > 50 && e.ProgressPercentage < 70) { @@ -499,7 +499,7 @@ public async Task TestPackageStatusAfterCompletionWithSuccess() resumeStatus = Package.Status; } }; - DownloadFileCompleted += (s, e) => completedStatus = Package.Status; + DownloadFileCompleted += (_, _) => completedStatus = Package.Status; // act await DownloadFileTaskAsync(url); @@ -526,8 +526,8 @@ public async Task TestSerializePackageAfterCancel(bool onMemory) var packageText = string.Empty; var url = DummyFileHelper.GetFileUrl(DummyFileHelper.FileSize16Kb); Options = GetDefaultConfig(); - ChunkDownloadProgressChanged += (s, e) => CancelAsync(); - DownloadFileCompleted += (s, e) => { + ChunkDownloadProgressChanged += (_, _) => CancelAsync(); + DownloadFileCompleted += (_, e) => { package = e.UserState as DownloadPackage; if (package!.Status != DownloadStatus.Completed) packageText = System.Text.Json.JsonSerializer.Serialize(package!); @@ -561,14 +561,14 @@ public async Task TestResumeFromSerializedPackage(bool onMemory) var packageText = string.Empty; var url = DummyFileHelper.GetFileUrl(DummyFileHelper.FileSize16Kb); Options = GetDefaultConfig(); - ChunkDownloadProgressChanged += async (s, e) => { + ChunkDownloadProgressChanged += async (_, _) => { if (isCancelOccurred == false) { isCancelOccurred = true; await CancelTaskAsync(); } }; - DownloadFileCompleted += (s, e) => { + DownloadFileCompleted += (_, e) => { package = e.UserState as DownloadPackage; if (package!.Status != DownloadStatus.Completed) packageText = System.Text.Json.JsonSerializer.Serialize(package!); @@ -607,8 +607,8 @@ public async Task TestPackageStatusAfterCancellation() var cancelledStatus = DownloadStatus.None; var completedStatus = DownloadStatus.None; - DownloadStarted += (s, e) => createdStatus = Package.Status; - DownloadProgressChanged += async (s, e) => { + DownloadStarted += (_, _) => createdStatus = Package.Status; + DownloadProgressChanged += async (_, e) => { runningStatus = Package.Status; if (e.ProgressPercentage > 50 && e.ProgressPercentage < 70) { @@ -616,7 +616,7 @@ public async Task TestPackageStatusAfterCancellation() cancelledStatus = Package.Status; } }; - DownloadFileCompleted += (s, e) => completedStatus = Package.Status; + DownloadFileCompleted += (_, _) => completedStatus = Package.Status; // act await DownloadFileTaskAsync(url); @@ -640,10 +640,10 @@ public async Task TestResumeDownloadImmediatelyAfterCancellationAsync() var secondStartProgressPercent = -1d; var url = DummyFileHelper.GetFileWithNameUrl(DummyFileHelper.SampleFile16KbName, DummyFileHelper.FileSize16Kb); var tcs = new TaskCompletionSource(); - DownloadFileCompleted += (s, e) => completedState = Package.Status; + DownloadFileCompleted += (_, _) => completedState = Package.Status; // act - DownloadProgressChanged += async (s, e) => { + DownloadProgressChanged += async (_, e) => { if (secondStartProgressPercent < 0) { if (checkProgress) @@ -676,10 +676,10 @@ public async Task TestStopDownloadOnClearWhenRunning() // arrange var completedState = DownloadStatus.None; var url = DummyFileHelper.GetFileWithNameUrl(DummyFileHelper.SampleFile16KbName, DummyFileHelper.FileSize16Kb); - DownloadFileCompleted += (s, e) => completedState = Package.Status; + DownloadFileCompleted += (_, _) => completedState = Package.Status; // act - DownloadProgressChanged += async (s, e) => { + DownloadProgressChanged += async (_, e) => { if (e.ProgressPercentage > 50 && e.ProgressPercentage < 60) await Clear(); }; @@ -698,11 +698,11 @@ public async Task TestStopDownloadOnClearWhenPaused() // arrange var completedState = DownloadStatus.None; var url = DummyFileHelper.GetFileWithNameUrl(DummyFileHelper.SampleFile16KbName, DummyFileHelper.FileSize16Kb); - DownloadFileCompleted += (s, e) => completedState = Package.Status; + DownloadFileCompleted += (_, _) => completedState = Package.Status; // act - DownloadProgressChanged += async (s, e) => { - if (e.ProgressPercentage > 50 && e.ProgressPercentage < 60) + DownloadProgressChanged += async (_, e) => { + if (e.ProgressPercentage is > 50 and < 60) { Pause(); await Clear(); @@ -728,7 +728,7 @@ public async Task TestMinimumSizeOfChunking() var activeChunks = 0; int? chunkCounts = null; var progressIds = new Dictionary(); - ChunkDownloadProgressChanged += (s, e) => { + ChunkDownloadProgressChanged += (_, e) => { activeChunks = Math.Max(activeChunks, e.ActiveChunks); progressIds[e.ProgressId] = true; chunkCounts ??= Package.Chunks.Length; diff --git a/src/Downloader.Test/IntegrationTests/ThrottledStreamTest.cs b/src/Downloader.Test/IntegrationTests/ThrottledStreamTest.cs index 7b0c2470..21dc89eb 100644 --- a/src/Downloader.Test/IntegrationTests/ThrottledStreamTest.cs +++ b/src/Downloader.Test/IntegrationTests/ThrottledStreamTest.cs @@ -31,7 +31,7 @@ public async Task TestReadStreamSpeed(int speedX, bool asAsync) var buffer = new byte[maxBytesPerSecond / 8]; var readSize = 1; var totalReadSize = 0L; - using ThrottledStream stream = new ThrottledStream(new MemoryStream(bytes), maxBytesPerSecond); + await using ThrottledStream stream = new ThrottledStream(new MemoryStream(bytes), maxBytesPerSecond); var stopWatcher = Stopwatch.StartNew(); // act @@ -86,7 +86,7 @@ public async Task TestStreamWriteSpeedAsync() var tolerance = 50; // 50 ms var expectedTime = size / bytesPerSecond * 1000; // 4000 Milliseconds var randomBytes = DummyData.GenerateRandomBytes(size); - using Stream stream = new ThrottledStream(new MemoryStream(), bytesPerSecond); + await using Stream stream = new ThrottledStream(new MemoryStream(), bytesPerSecond); var stopWatcher = Stopwatch.StartNew(); // act @@ -141,7 +141,7 @@ public void TestStreamIntegrity(int streamSize, long maximumBytesPerSecond) // act stream.Write(data, 0, data.Length); stream.Seek(0, SeekOrigin.Begin); - stream.Read(copiedData, 0, copiedData.Length); + _ = stream.Read(copiedData, 0, copiedData.Length); // assert Assert.Equal(streamSize, data.Length); diff --git a/src/Downloader.Test/UnitTests/ChunkDownloaderOnFileTest.cs b/src/Downloader.Test/UnitTests/ChunkDownloaderOnFileTest.cs index af45829a..36557bba 100644 --- a/src/Downloader.Test/UnitTests/ChunkDownloaderOnFileTest.cs +++ b/src/Downloader.Test/UnitTests/ChunkDownloaderOnFileTest.cs @@ -1,5 +1,4 @@ -using Downloader.DummyHttpServer; -using System.IO; +using System.IO; namespace Downloader.Test.UnitTests; diff --git a/src/Downloader.Test/UnitTests/ChunkDownloaderOnMemoryTest.cs b/src/Downloader.Test/UnitTests/ChunkDownloaderOnMemoryTest.cs index 07be0a82..c7d0d241 100644 --- a/src/Downloader.Test/UnitTests/ChunkDownloaderOnMemoryTest.cs +++ b/src/Downloader.Test/UnitTests/ChunkDownloaderOnMemoryTest.cs @@ -10,7 +10,7 @@ public ChunkDownloaderOnMemoryTest() ParallelDownload = true, MaxTryAgainOnFailover = 100, MinimumSizeOfChunking = 16, - Timeout = 100, + Timeout = 100 }; Storage = new ConcurrentStream(null); } diff --git a/src/Downloader.Test/UnitTests/ChunkDownloaderTest.cs b/src/Downloader.Test/UnitTests/ChunkDownloaderTest.cs index 1810a8e0..5f683238 100644 --- a/src/Downloader.Test/UnitTests/ChunkDownloaderTest.cs +++ b/src/Downloader.Test/UnitTests/ChunkDownloaderTest.cs @@ -81,6 +81,7 @@ public async Task ReadStreamProgressEventsTest() var source = DummyData.GenerateRandomBytes(Size); using var sourceMemoryStream = new MemoryStream(source); var chunk = new Chunk(0, Size - 1) { Timeout = 100 }; + Configuration.EnableLiveStreaming = true; var chunkDownloader = new ChunkDownloader(chunk, Configuration, Storage); chunkDownloader.DownloadProgressChanged += (s, e) => { eventCount++; @@ -126,7 +127,7 @@ public async Task ReadStreamTimeoutExceptionTest() var chunk = new Chunk(0, Size - 1) { Timeout = 0 }; var chunkDownloader = new ChunkDownloader(chunk, Configuration, Storage); using var memoryStream = new MemoryStream(randomlyBytes); - using var slowStream = new ThrottledStream(memoryStream, Configuration.BufferBlockSize); + await using var slowStream = new ThrottledStream(memoryStream, Configuration.BufferBlockSize); // act async Task CallReadStream() => await chunkDownloader @@ -156,7 +157,7 @@ public async Task CancelReadStreamTest() } }; - async Task act() + async Task Act() { try { @@ -170,8 +171,8 @@ async Task act() } // act - await Assert.ThrowsAnyAsync(act); - using var chunkStream = Storage.OpenRead(); + await Assert.ThrowsAnyAsync(Act); + await using var chunkStream = Storage.OpenRead(); // assert Assert.False(memoryStream.CanRead); // stream has been closed diff --git a/src/Downloader.Test/UnitTests/ChunkTest.cs b/src/Downloader.Test/UnitTests/ChunkTest.cs index d5e48c85..efafe115 100644 --- a/src/Downloader.Test/UnitTests/ChunkTest.cs +++ b/src/Downloader.Test/UnitTests/ChunkTest.cs @@ -185,4 +185,24 @@ public void ChunkSerializationTest() chunk.Clear(); } -} + + [Fact] + public void TestCanWriteWhenChunkIsNotFull() + { + // arrange + var chunk = new Chunk(0, 1000) { Position = 120 }; + + // assert + Assert.True(chunk.CanWrite); + } + + [Fact] + public void TestCanWriteWhenChunkIsFull() + { + // arrange + var chunk = new Chunk(0, 1000) { Position = 1000 }; + + // assert + Assert.False(chunk.CanWrite); + } +} \ No newline at end of file diff --git a/src/Downloader.Test/UnitTests/DownloadBuilderTest.cs b/src/Downloader.Test/UnitTests/DownloadBuilderTest.cs index 47a77471..6a729377 100644 --- a/src/Downloader.Test/UnitTests/DownloadBuilderTest.cs +++ b/src/Downloader.Test/UnitTests/DownloadBuilderTest.cs @@ -9,18 +9,18 @@ namespace Downloader.Test.UnitTests; public class DownloadBuilderTest { // arrange - private string url; - private string filename; - private string folder; - private string path; + private string _url; + private string _filename; + private string _folder; + private string _path; public DownloadBuilderTest() { // arrange - url = DummyFileHelper.GetFileUrl(DummyFileHelper.FileSize16Kb); - filename = Path.GetRandomFileName(); - folder = Path.GetTempPath().TrimEnd('\\', '/'); - path = Path.Combine(folder, filename); + _url = DummyFileHelper.GetFileUrl(DummyFileHelper.FileSize16Kb); + _filename = Path.GetRandomFileName(); + _folder = Path.GetTempPath().TrimEnd('\\', '/'); + _path = Path.Combine(_folder, _filename); } [Fact] @@ -28,16 +28,16 @@ public void TestCorrect() { // act IDownload download = DownloadBuilder.New() - .WithUrl(url) - .WithFileLocation(path) + .WithUrl(_url) + .WithFileLocation(_path) .Configure(config => { config.ParallelDownload = true; }) .Build(); // assert - Assert.Equal(folder, download.Folder); - Assert.Equal(filename, download.Filename); + Assert.Equal(_folder, download.Folder); + Assert.Equal(_filename, download.Filename); } [Fact] @@ -45,14 +45,14 @@ public void TestSetFolderAndName() { // act IDownload download = DownloadBuilder.New() - .WithUrl(url) - .WithDirectory(folder) - .WithFileName(filename) + .WithUrl(_url) + .WithDirectory(_folder) + .WithFileName(_filename) .Build(); // assert - Assert.Equal(folder, download.Folder); - Assert.Equal(filename, download.Filename); + Assert.Equal(_folder, download.Folder); + Assert.Equal(_filename, download.Filename); } [Fact] @@ -63,7 +63,7 @@ public void TestSetFolder() // act IDownload download = DownloadBuilder.New() - .WithUrl(url) + .WithUrl(_url) .WithDirectory(dir) .Build(); @@ -77,21 +77,21 @@ public void TestSetName() { // act IDownload download = DownloadBuilder.New() - .WithUrl(url) - .WithFileLocation(path) - .WithFileName(filename) + .WithUrl(_url) + .WithFileLocation(_path) + .WithFileName(_filename) .Build(); // assert - Assert.Equal(folder, download.Folder); - Assert.Equal(filename, download.Filename); + Assert.Equal(_folder, download.Folder); + Assert.Equal(_filename, download.Filename); } [Fact] public void TestUrlless() { // act - Action act = () => DownloadBuilder.New().WithFileLocation(path).Build(); + Action act = () => DownloadBuilder.New().WithFileLocation(_path).Build(); // assert Assert.ThrowsAny(act); @@ -102,7 +102,7 @@ public void TestPathless() { // act IDownload result = DownloadBuilder.New() - .WithUrl(url) + .WithUrl(_url) .Build(); // assert @@ -115,7 +115,7 @@ public async Task TestPackageWhenNewUrl() // arrange DownloadPackage beforePackage = null; IDownload download = DownloadBuilder.New() - .WithUrl(url) + .WithUrl(_url) .Build(); // act @@ -134,7 +134,7 @@ public async Task TestPackageWhenResume() { // arrange DownloadPackage package = new DownloadPackage() { - Urls = new[] { url }, + Urls = new[] { _url }, IsSupportDownloadInRange = true }; IDownload download = DownloadBuilder.New().Build(package); @@ -157,8 +157,8 @@ public async Task TestPauseAndResume() // arrange var pauseCount = 0; var downloader = DownloadBuilder.New() - .WithUrl(url) - .WithFileLocation(path) + .WithUrl(_url) + .WithFileLocation(_path) .Build(); downloader.DownloadProgressChanged += (s, e) => { @@ -176,10 +176,10 @@ public async Task TestPauseAndResume() // assert Assert.True(downloader.Package?.IsSaveComplete); Assert.Equal(10, pauseCount); - Assert.True(File.Exists(path)); + Assert.True(File.Exists(_path)); // clean up - File.Delete(path); + File.Delete(_path); } [Fact] @@ -188,23 +188,23 @@ public async Task TestOverwriteFileWithDownloadSameLocation() // arrange var content = "THIS IS TEST CONTENT WHICH MUST BE OVERWRITE WITH THE DOWNLOADER"; var downloader = DownloadBuilder.New() - .WithUrl(url) - .WithFileLocation(path) + .WithUrl(_url) + .WithFileLocation(_path) .Build(); // act - await File.WriteAllTextAsync(path, content); // create file + await File.WriteAllTextAsync(_path, content); // create file await downloader.StartAsync(); // overwrite file - var file = await File.ReadAllTextAsync(path); + var file = await File.ReadAllTextAsync(_path); // assert Assert.True(downloader.Package?.IsSaveComplete); - Assert.True(File.Exists(path)); + Assert.True(File.Exists(_path)); Assert.False(file.StartsWith(content)); Assert.Equal(DummyFileHelper.FileSize16Kb, Encoding.ASCII.GetByteCount(file)); // clean up - File.Delete(path); + File.Delete(_path); } [Fact] @@ -213,23 +213,23 @@ public async Task TestOverwriteFileWithDownloadSameFileName() // arrange var content = "THIS IS TEST CONTENT WHICH MUST BE OVERWRITE WITH THE DOWNLOADER"; var downloader = DownloadBuilder.New() - .WithUrl(url) - .WithDirectory(folder) - .WithFileName(filename) + .WithUrl(_url) + .WithDirectory(_folder) + .WithFileName(_filename) .Build(); // act - await File.WriteAllTextAsync(path, content); // create file + await File.WriteAllTextAsync(_path, content); // create file await downloader.StartAsync(); // overwrite file - var file = await File.ReadAllTextAsync(path); + var file = await File.ReadAllTextAsync(_path); // assert Assert.True(downloader.Package?.IsSaveComplete); - Assert.True(File.Exists(path)); + Assert.True(File.Exists(_path)); Assert.False(file.StartsWith(content)); Assert.Equal(DummyFileHelper.FileSize16Kb, Encoding.ASCII.GetByteCount(file)); // clean up - File.Delete(path); + File.Delete(_path); } } diff --git a/src/Downloader.Test/UnitTests/DownloadPackageTest.cs b/src/Downloader.Test/UnitTests/DownloadPackageTest.cs index b8140108..7317620a 100644 --- a/src/Downloader.Test/UnitTests/DownloadPackageTest.cs +++ b/src/Downloader.Test/UnitTests/DownloadPackageTest.cs @@ -1,6 +1,5 @@ using Downloader.DummyHttpServer; using Downloader.Test.Helper; -using System; using System.Linq; using System.Threading.Tasks; using Xunit; @@ -37,7 +36,7 @@ public void PackageSerializationTest() Package.Storage.Dispose(); var deserialized = Newtonsoft.Json.JsonConvert.DeserializeObject(serialized); var destData = new byte[deserialized.TotalFileSize]; - deserialized.Storage.OpenRead().Read(destData, 0, destData.Length); + _ = deserialized.Storage.OpenRead().Read(destData, 0, destData.Length); // assert AssertHelper.AreEquals(Package, deserialized); diff --git a/src/Downloader.Test/UnitTests/DownloadPackageTestOnFile.cs b/src/Downloader.Test/UnitTests/DownloadPackageTestOnFile.cs index 81b714e7..ec783aeb 100644 --- a/src/Downloader.Test/UnitTests/DownloadPackageTestOnFile.cs +++ b/src/Downloader.Test/UnitTests/DownloadPackageTestOnFile.cs @@ -31,7 +31,7 @@ public override async Task DisposeAsync() [Theory] [InlineData(true)] // BuildStorageWithReserveSpaceTest [InlineData(false)] // BuildStorageTest - public void BuildStorageTest(bool reserveSpace) + public async Task BuildStorageTest(bool reserveSpace) { // arrange _path = Path.GetTempFileName(); @@ -43,7 +43,7 @@ public void BuildStorageTest(bool reserveSpace) // act Package.BuildStorage(reserveSpace, 1024 * 1024); - using var stream = Package.Storage.OpenRead(); + await using var stream = Package.Storage.OpenRead(); // assert Assert.IsType(stream); diff --git a/src/Downloader.Test/UnitTests/PacketTest.cs b/src/Downloader.Test/UnitTests/PacketTest.cs index f02c0b9f..80d6c026 100644 --- a/src/Downloader.Test/UnitTests/PacketTest.cs +++ b/src/Downloader.Test/UnitTests/PacketTest.cs @@ -1,5 +1,4 @@ using Downloader.DummyHttpServer; -using System; using System.Linq; using Xunit; diff --git a/src/Downloader.Test/UnitTests/PauseTokenTest.cs b/src/Downloader.Test/UnitTests/PauseTokenTest.cs index 3901506c..7c616c00 100644 --- a/src/Downloader.Test/UnitTests/PauseTokenTest.cs +++ b/src/Downloader.Test/UnitTests/PauseTokenTest.cs @@ -7,7 +7,7 @@ namespace Downloader.Test.UnitTests; public class PauseTokenTest { private PauseTokenSource _pauseTokenSource; - private volatile int actualPauseCount = 0; + private volatile int _actualPauseCount = 0; public PauseTokenTest() { @@ -40,20 +40,20 @@ public async Task TestPauseTaskWithPauseToken() for (var i = 0; i < 10; i++) { await Task.Delay(1); - tasksAlreadyPaused &= (actualPauseCount == expectedCount); + tasksAlreadyPaused &= (_actualPauseCount == expectedCount); pts.Resume(); checkTokenStateIsNotPaused |= pts.IsPaused; await Task.Delay(1); pts.Pause(); checkTokenStateIsPaused &= pts.IsPaused; await Task.Delay(1); - hasRunningTask &= (actualPauseCount > expectedCount); - expectedCount = actualPauseCount; + hasRunningTask &= (_actualPauseCount > expectedCount); + expectedCount = _actualPauseCount; } cts.Cancel(); // assert - Assert.True(expectedCount >= actualPauseCount, $"Expected: {expectedCount}, Actual: {actualPauseCount}"); + Assert.True(expectedCount >= _actualPauseCount, $"Expected: {expectedCount}, Actual: {_actualPauseCount}"); Assert.True(pts.IsPaused); Assert.True(checkTokenStateIsPaused); Assert.False(checkTokenStateIsNotPaused); @@ -66,7 +66,7 @@ private async Task IncreaseAsync(PauseToken pause, CancellationToken cancel) while (cancel.IsCancellationRequested == false) { await pause.WaitWhilePausedAsync(); - actualPauseCount++; + _actualPauseCount++; await Task.Yield(); } } diff --git a/src/Downloader.Test/UnitTests/StorageTestOnFile.cs b/src/Downloader.Test/UnitTests/StorageTestOnFile.cs index 93466507..2d5193bd 100644 --- a/src/Downloader.Test/UnitTests/StorageTestOnFile.cs +++ b/src/Downloader.Test/UnitTests/StorageTestOnFile.cs @@ -6,18 +6,18 @@ namespace Downloader.Test.UnitTests; public class StorageTestOnFile : StorageTest { - private string path; + private string _path; protected override void CreateStorage(int initialSize) { - path = Path.GetTempFileName(); - Storage = new ConcurrentStream(path, initialSize); + _path = Path.GetTempFileName(); + Storage = new ConcurrentStream(_path, initialSize); } public override void Dispose() { base.Dispose(); - File.Delete(path); + File.Delete(_path); } [Fact] @@ -27,7 +27,7 @@ public void TestInitialSizeOnFileStream() CreateStorage(DataLength); // assert - Assert.Equal(DataLength, new FileInfo(path).Length); + Assert.Equal(DataLength, new FileInfo(_path).Length); Assert.Equal(DataLength, Storage.Length); } @@ -41,7 +41,7 @@ public async Task TestInitialSizeWithNegativeNumberOnFileStream() await Storage.FlushAsync(); // create lazy stream // assert - Assert.Equal(0, new FileInfo(path).Length); + Assert.Equal(0, new FileInfo(_path).Length); Assert.Equal(0, Storage.Length); } @@ -62,7 +62,7 @@ public async Task TestWriteSizeOverflowOnFileStream() var readerStream = Storage.OpenRead(); // assert - Assert.Equal(actualSize, new FileInfo(path).Length); + Assert.Equal(actualSize, new FileInfo(_path).Length); Assert.Equal(actualSize, Storage.Length); for (int i = 0; i < actualSize; i++) Assert.Equal(1, readerStream.ReadByte()); @@ -85,7 +85,7 @@ public async Task TestAccessMoreThanSizeOnFileStream() var readerStream = Storage.OpenRead(); // assert - Assert.Equal(actualSize, new FileInfo(path).Length); + Assert.Equal(actualSize, new FileInfo(_path).Length); Assert.Equal(actualSize, Storage.Length); for (int i = 0; i < size + jumpStepCount; i++) Assert.Equal(0, readerStream.ReadByte()); // empty spaces diff --git a/src/Downloader.sln.DotSettings b/src/Downloader.sln.DotSettings deleted file mode 100644 index 8a235ead..00000000 --- a/src/Downloader.sln.DotSettings +++ /dev/null @@ -1,5 +0,0 @@ - - True - True - True - True \ No newline at end of file diff --git a/src/Downloader/AbstractDownloadService.cs b/src/Downloader/AbstractDownloadService.cs index 2ef3f6bf..27824444 100644 --- a/src/Downloader/AbstractDownloadService.cs +++ b/src/Downloader/AbstractDownloadService.cs @@ -1,54 +1,135 @@ using Downloader.Extensions.Helpers; -using Downloader.Extensions.Logging; +using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.Linq; using System.Net; -using System.Net.Security; using System.Threading; using System.Threading.Tasks; namespace Downloader; -public abstract class AbstractDownloadService : IDownloadService, IDisposable +/// +/// Abstract base class for download services implementing and . +/// +public abstract class AbstractDownloadService : IDownloadService, IDisposable, IAsyncDisposable { - protected ILogger _logger; - protected SemaphoreSlim _parallelSemaphore; - protected readonly SemaphoreSlim _singleInstanceSemaphore = new SemaphoreSlim(1, 1); - protected CancellationTokenSource _globalCancellationTokenSource; - protected TaskCompletionSource _taskCompletion; - protected readonly PauseTokenSource _pauseTokenSource; - protected ChunkHub _chunkHub; - protected List _requestInstances; - protected readonly Bandwidth _bandwidth; + /// + /// Logger instance for logging messages. + /// + protected ILogger Logger; + + /// + /// Semaphore to control parallel downloads. + /// + protected SemaphoreSlim ParallelSemaphore; + + /// + /// Semaphore to ensure single instance operations. + /// + protected readonly SemaphoreSlim SingleInstanceSemaphore = new SemaphoreSlim(1, 1); + + /// + /// Global cancellation token source for managing download cancellation. + /// + protected CancellationTokenSource GlobalCancellationTokenSource; + + /// + /// Task completion source for managing asynchronous operations. + /// + protected TaskCompletionSource TaskCompletion; + + /// + /// Pause token source for managing download pausing. + /// + protected readonly PauseTokenSource PauseTokenSource; + + /// + /// Chunk hub for managing download chunks. + /// + protected ChunkHub ChunkHub; + + /// + /// List of request instances for download operations. + /// + protected List RequestInstances; + + /// + /// Bandwidth tracker for download speed calculations. + /// + protected readonly Bandwidth Bandwidth; + + /// + /// Configuration options for the download service. + /// protected DownloadConfiguration Options { get; set; } + /// + /// Indicates whether the download service is currently busy. + /// public bool IsBusy => Status == DownloadStatus.Running; - public bool IsCancelled => _globalCancellationTokenSource?.IsCancellationRequested == true; - public bool IsPaused => _pauseTokenSource.IsPaused; + + /// + /// Indicates whether the download operation has been cancelled. + /// + public bool IsCancelled => GlobalCancellationTokenSource?.IsCancellationRequested == true; + + /// + /// Indicates whether the download operation is paused. + /// + public bool IsPaused => PauseTokenSource.IsPaused; + + /// + /// The download package containing the necessary information for the download. + /// public DownloadPackage Package { get; set; } + + /// + /// The current status of the download operation. + /// public DownloadStatus Status { get => Package?.Status ?? DownloadStatus.None; set => Package.Status = value; } + + /// + /// Event triggered when the download file operation is completed. + /// public event EventHandler DownloadFileCompleted; + + /// + /// Event triggered when the download progress changes. + /// public event EventHandler DownloadProgressChanged; + + /// + /// Event triggered when the progress of a chunk download changes. + /// public event EventHandler ChunkDownloadProgressChanged; + + /// + /// Event triggered when the download operation starts. + /// public event EventHandler DownloadStarted; - public AbstractDownloadService(DownloadConfiguration options) + /// + /// Initializes a new instance of the class with the specified options. + /// + /// The configuration options for the download service. + protected AbstractDownloadService(DownloadConfiguration options) { - _pauseTokenSource = new PauseTokenSource(); - _bandwidth = new Bandwidth(); + PauseTokenSource = new PauseTokenSource(); + Bandwidth = new Bandwidth(); Options = options ?? new DownloadConfiguration(); Package = new DownloadPackage(); // This property selects the version of the Secure Sockets Layer (SSL) or // existing connections aren't changed. - ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12; + ServicePointManager.SecurityProtocol = + SecurityProtocolType.Tls | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12; // Accept the request for POST, PUT and PATCH verbs ServicePointManager.Expect100Continue = false; @@ -62,87 +143,170 @@ public AbstractDownloadService(DownloadConfiguration options) // garbage collection and cannot be used by the ServicePointManager object. ServicePointManager.MaxServicePointIdleTime = 10000; - ServicePointManager.ServerCertificateValidationCallback = - new RemoteCertificateValidationCallback(ExceptionHelper.CertificateValidationCallBack); + ServicePointManager.ServerCertificateValidationCallback = ExceptionHelper.CertificateValidationCallBack; } + /// + /// Downloads a file asynchronously using the specified and optional . + /// + /// The download package containing the necessary information for the download. + /// A cancellation token that can be used to cancel the download. + /// A task that represents the asynchronous download operation. The task result contains the downloaded stream. public Task DownloadFileTaskAsync(DownloadPackage package, CancellationToken cancellationToken = default) { return DownloadFileTaskAsync(package, package.Urls, cancellationToken); } - public Task DownloadFileTaskAsync(DownloadPackage package, string address, CancellationToken cancellationToken = default) + /// + /// Downloads a file asynchronously using the specified and , with an optional . + /// + /// The download package containing the necessary information for the download. + /// The URL address of the file to download. + /// A cancellation token that can be used to cancel the download. + /// A task that represents the asynchronous download operation. The task result contains the downloaded stream. + public Task DownloadFileTaskAsync(DownloadPackage package, string address, + CancellationToken cancellationToken = default) { return DownloadFileTaskAsync(package, new[] { address }, cancellationToken); } - public virtual async Task DownloadFileTaskAsync(DownloadPackage package, string[] urls, CancellationToken cancellationToken = default) + /// + /// Downloads a file asynchronously using the specified and , with an optional . + /// + /// The download package containing the necessary information for the download. + /// The array of URL addresses of the file to download. + /// A cancellation token that can be used to cancel the download. + /// A task that represents the asynchronous download operation. The task result contains the downloaded stream. + public virtual async Task DownloadFileTaskAsync(DownloadPackage package, string[] urls, + CancellationToken cancellationToken = default) { Package = package; - await InitialDownloader(cancellationToken, urls); + await InitialDownloader(cancellationToken, urls).ConfigureAwait(false); return await StartDownload().ConfigureAwait(false); } + /// + /// Downloads a file asynchronously using the specified and optional . + /// + /// The URL address of the file to download. + /// A cancellation token that can be used to cancel the download. + /// A task that represents the asynchronous download operation. The task result contains the downloaded stream. public Task DownloadFileTaskAsync(string address, CancellationToken cancellationToken = default) { return DownloadFileTaskAsync(new[] { address }, cancellationToken); } - public virtual async Task DownloadFileTaskAsync(string[] urls, CancellationToken cancellationToken = default) + /// + /// Downloads a file asynchronously using the specified and optional . + /// + /// The array of URL addresses of the file to download. + /// A cancellation token that can be used to cancel the download. + /// A task that represents the asynchronous download operation. The task result contains the downloaded stream. + public virtual async Task DownloadFileTaskAsync(string[] urls, + CancellationToken cancellationToken = default) { - await InitialDownloader(cancellationToken, urls); + await InitialDownloader(cancellationToken, urls).ConfigureAwait(false); return await StartDownload().ConfigureAwait(false); } + /// + /// Downloads a file asynchronously using the specified and saves it to the specified , with an optional . + /// + /// The URL address of the file to download. + /// The name of the file to save the download as. + /// A cancellation token that can be used to cancel the download. + /// A task that represents the asynchronous download operation. public Task DownloadFileTaskAsync(string address, string fileName, CancellationToken cancellationToken = default) { return DownloadFileTaskAsync(new[] { address }, fileName, cancellationToken); } - public virtual async Task DownloadFileTaskAsync(string[] urls, string fileName, CancellationToken cancellationToken = default) + /// + /// Downloads a file asynchronously using the specified and saves it to the specified , with an optional . + /// + /// The array of URL addresses of the file to download. + /// The name of the file to save the download as. + /// A cancellation token that can be used to cancel the download. + /// A task that represents the asynchronous download operation. + public virtual async Task DownloadFileTaskAsync(string[] urls, string fileName, + CancellationToken cancellationToken = default) { - await InitialDownloader(cancellationToken, urls); + await InitialDownloader(cancellationToken, urls).ConfigureAwait(false); await StartDownload(fileName).ConfigureAwait(false); } - public Task DownloadFileTaskAsync(string address, DirectoryInfo folder, CancellationToken cancellationToken = default) + /// + /// Downloads a file asynchronously using the specified and saves it to the specified , with an optional . + /// + /// The URL address of the file to download. + /// The directory to save the downloaded file in. + /// A cancellation token that can be used to cancel the download. + /// A task that represents the asynchronous download operation. + public Task DownloadFileTaskAsync(string address, DirectoryInfo folder, + CancellationToken cancellationToken = default) { return DownloadFileTaskAsync(new[] { address }, folder, cancellationToken); } - public virtual async Task DownloadFileTaskAsync(string[] urls, DirectoryInfo folder, CancellationToken cancellationToken = default) + + /// + /// Downloads a file asynchronously using the specified and saves it to the specified , with an optional . + /// + /// The array of URL addresses of the file to download. + /// The directory to save the downloaded file in. + /// A cancellation token that can be used to cancel the download. + /// A task that represents the asynchronous download operation. + public virtual async Task DownloadFileTaskAsync(string[] urls, DirectoryInfo folder, + CancellationToken cancellationToken = default) { - await InitialDownloader(cancellationToken, urls); - var name = await _requestInstances.First().GetFileName().ConfigureAwait(false); + await InitialDownloader(cancellationToken, urls).ConfigureAwait(false); + var name = await RequestInstances.First().GetFileName().ConfigureAwait(false); var filename = Path.Combine(folder.FullName, name); await StartDownload(filename).ConfigureAwait(false); } + /// + /// Cancels the current download operation. + /// public virtual void CancelAsync() { - _globalCancellationTokenSource?.Cancel(true); + GlobalCancellationTokenSource?.Cancel(true); Resume(); Status = DownloadStatus.Stopped; } + /// + /// Cancels the current download operation asynchronously. + /// + /// A task that represents the asynchronous cancellation operation. public virtual async Task CancelTaskAsync() { CancelAsync(); - if (_taskCompletion != null) - await _taskCompletion.Task.ConfigureAwait(false); + if (TaskCompletion != null) + await TaskCompletion.Task.ConfigureAwait(false); } + /// + /// Resumes the paused download operation. + /// public virtual void Resume() { Status = DownloadStatus.Running; - _pauseTokenSource.Resume(); + PauseTokenSource.Resume(); } + /// + /// Pauses the current download operation. + /// public virtual void Pause() { - _pauseTokenSource.Pause(); + PauseTokenSource.Pause(); Status = DownloadStatus.Paused; } + /// + /// Clears the current download operation, including cancellation and disposal of resources. + /// + /// A task that represents the asynchronous clear operation. public virtual async Task Clear() { try @@ -150,56 +314,82 @@ public virtual async Task Clear() if (IsBusy || IsPaused) await CancelTaskAsync().ConfigureAwait(false); - await _singleInstanceSemaphore?.WaitAsync(); + await SingleInstanceSemaphore.WaitAsync().ConfigureAwait(false); - _parallelSemaphore?.Dispose(); - _globalCancellationTokenSource?.Dispose(); - _bandwidth.Reset(); - _requestInstances = null; + ParallelSemaphore?.Dispose(); + GlobalCancellationTokenSource?.Dispose(); + Bandwidth.Reset(); + RequestInstances = null; - if (_taskCompletion != null) + if (TaskCompletion != null) { - if (_taskCompletion.Task.IsCompleted == false) - _taskCompletion.TrySetCanceled(); + if (TaskCompletion.Task.IsCompleted == false) + TaskCompletion.TrySetCanceled(); - _taskCompletion = null; + TaskCompletion = null; } // Note: don't clear package from `DownloadService.Dispose()`. // Because maybe it will be used at another time. } finally { - _singleInstanceSemaphore?.Release(); + SingleInstanceSemaphore?.Release(); } } + /// + /// Initializes the downloader with the specified and . + /// + /// A cancellation token that can be used to cancel the download. + /// The array of URL addresses of the file to download. + /// A task that represents the asynchronous initialization operation. protected async Task InitialDownloader(CancellationToken cancellationToken, params string[] addresses) { - await Clear(); + await Clear().ConfigureAwait(false); Status = DownloadStatus.Created; - _globalCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - _taskCompletion = new TaskCompletionSource(); - _requestInstances = addresses.Select(url => new Request(url, Options.RequestConfiguration)).ToList(); - Package.Urls = _requestInstances.Select(req => req.Address.OriginalString).ToArray(); - _chunkHub = new ChunkHub(Options); - _parallelSemaphore = new SemaphoreSlim(Options.ParallelCount, Options.ParallelCount); + GlobalCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + TaskCompletion = new TaskCompletionSource(); + RequestInstances = addresses.Select(url => new Request(url, Options.RequestConfiguration)).ToList(); + Package.Urls = RequestInstances.Select(req => req.Address.OriginalString).ToArray(); + ChunkHub = new ChunkHub(Options); + ParallelSemaphore = new SemaphoreSlim(Options.ParallelCount, Options.ParallelCount); } + /// + /// Starts the download operation and saves it to the specified . + /// + /// The name of the file to save the download as. + /// A task that represents the asynchronous download operation. protected async Task StartDownload(string fileName) { - Package.FileName = fileName; - Directory.CreateDirectory(Path.GetDirectoryName(fileName)); // ensure the folder is exist - - if (File.Exists(fileName)) + if (!string.IsNullOrWhiteSpace(fileName)) { - File.Delete(fileName); + Package.FileName = fileName; + string dirName = Path.GetDirectoryName(fileName); + if (dirName != null) + { + Directory.CreateDirectory(dirName); // ensure the folder is existing + } + + if (File.Exists(fileName)) + { + File.Delete(fileName); + } } await StartDownload().ConfigureAwait(false); } + /// + /// Starts the download operation. + /// + /// A task that represents the asynchronous download operation. The task result contains the downloaded stream. protected abstract Task StartDownload(); + /// + /// Raises the event. + /// + /// The event arguments for the download started event. protected void OnDownloadStarted(DownloadStartedEventArgs e) { Status = DownloadStatus.Running; @@ -207,6 +397,10 @@ protected void OnDownloadStarted(DownloadStartedEventArgs e) DownloadStarted?.Invoke(this, e); } + /// + /// Raises the event. + /// + /// The event arguments for the download file completed event. protected void OnDownloadFileCompleted(AsyncCompletedEventArgs e) { Package.IsSaving = false; @@ -237,24 +431,29 @@ protected void OnDownloadFileCompleted(AsyncCompletedEventArgs e) Package.Storage = null; } - _taskCompletion.TrySetResult(e); + TaskCompletion.TrySetResult(e); DownloadFileCompleted?.Invoke(this, e); } + /// + /// Raises the and events. + /// + /// The sender of the event. + /// The event arguments for the download progress changed event. protected void OnChunkDownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e) { if (e.ReceivedBytesSize > Package.TotalFileSize) Package.TotalFileSize = e.ReceivedBytesSize; - _bandwidth.CalculateSpeed(e.ProgressedByteSize); + Bandwidth.CalculateSpeed(e.ProgressedByteSize); var totalProgressArg = new DownloadProgressChangedEventArgs(nameof(DownloadService)) { TotalBytesToReceive = Package.TotalFileSize, ReceivedBytesSize = Package.ReceivedBytesSize, - BytesPerSecondSpeed = _bandwidth.Speed, - AverageBytesPerSecondSpeed = _bandwidth.AverageSpeed, + BytesPerSecondSpeed = Bandwidth.Speed, + AverageBytesPerSecondSpeed = Bandwidth.AverageSpeed, ProgressedByteSize = e.ProgressedByteSize, ReceivedBytes = e.ReceivedBytes, - ActiveChunks = Options.ParallelCount - _parallelSemaphore.CurrentCount, + ActiveChunks = Options.ParallelCount - ParallelSemaphore.CurrentCount, }; Package.SaveProgress = totalProgressArg.ProgressPercentage; e.ActiveChunks = totalProgressArg.ActiveChunks; @@ -262,13 +461,28 @@ protected void OnChunkDownloadProgressChanged(object sender, DownloadProgressCha DownloadProgressChanged?.Invoke(this, totalProgressArg); } + /// + /// Adds a logger to the download service. + /// + /// The logger instance to add. public void AddLogger(ILogger logger) { - _logger = logger; + Logger = logger; } - public virtual void Dispose() + /// + /// Disposes of the download service, including clearing the current download operation. + /// + public void Dispose() { Clear().Wait(); } + + /// + /// Disposes asynchronously of the download service, including clearing the current download operation. + /// + public async ValueTask DisposeAsync() + { + await Clear().ConfigureAwait(false); + } } \ No newline at end of file diff --git a/src/Downloader/Bandwidth.cs b/src/Downloader/Bandwidth.cs index b98a08b2..c7cd5348 100644 --- a/src/Downloader/Bandwidth.cs +++ b/src/Downloader/Bandwidth.cs @@ -3,6 +3,9 @@ namespace Downloader; +/// +/// Represents a class for calculating and managing bandwidth usage during a download operation. +/// public class Bandwidth { private const double OneSecond = 1000; // millisecond @@ -10,16 +13,35 @@ public class Bandwidth private int _lastSecondCheckpoint; private long _lastTransferredBytesCount; private int _speedRetrieveTime; + + /// + /// Gets the current download speed in bytes per second. + /// public double Speed { get; private set; } + + /// + /// Gets the average download speed in bytes per second. + /// public double AverageSpeed { get; private set; } + + /// + /// Gets or sets the bandwidth limit in bytes per second. + /// public long BandwidthLimit { get; set; } + /// + /// Initializes a new instance of the class with default settings. + /// public Bandwidth() { BandwidthLimit = long.MaxValue; Reset(); } + /// + /// Calculates the current download speed based on the received bytes count. + /// + /// The number of bytes received since the last calculation. public void CalculateSpeed(long receivedBytesCount) { int elapsedTime = Environment.TickCount - _lastSecondCheckpoint + 1; @@ -36,16 +58,23 @@ public void CalculateSpeed(long receivedBytesCount) if (momentSpeed >= BandwidthLimit) { - var expectedTime = receivedBytesCount * OneSecond / BandwidthLimit; + double expectedTime = receivedBytesCount * OneSecond / BandwidthLimit; Interlocked.Add(ref _speedRetrieveTime, (int)expectedTime - elapsedTime); } } + /// + /// Retrieves and resets the speed retrieve time. + /// + /// The speed retrieve time in milliseconds. public int PopSpeedRetrieveTime() { return Interlocked.Exchange(ref _speedRetrieveTime, 0); } + /// + /// Resets the bandwidth calculation. + /// public void Reset() { SecondCheckpoint(); @@ -54,9 +83,12 @@ public void Reset() AverageSpeed = 0; } + /// + /// Sets the last second checkpoint to the current time and resets the transferred bytes count. + /// private void SecondCheckpoint() { Interlocked.Exchange(ref _lastSecondCheckpoint, Environment.TickCount); Interlocked.Exchange(ref _lastTransferredBytesCount, 0); } -} +} \ No newline at end of file diff --git a/src/Downloader/Chunk.cs b/src/Downloader/Chunk.cs index 7ecd3a6c..99b74a43 100644 --- a/src/Downloader/Chunk.cs +++ b/src/Downloader/Chunk.cs @@ -3,88 +3,106 @@ namespace Downloader; /// -/// Chunk data structure +/// Represents a chunk of data in a file download operation. /// public class Chunk { /// - /// Chunk unique identity name + /// Gets or sets the unique identifier for the chunk. /// public string Id { get; set; } - + /// - /// Start offset of the chunk in the file bytes + /// Gets or sets the start offset of the chunk in the file bytes. /// public long Start { get; set; } /// - /// End offset of the chunk in the file bytes + /// Gets or sets the end offset of the chunk in the file bytes. /// public long End { get; set; } /// - /// Current write offset of the chunk + /// Gets or sets the current write offset of the chunk. /// public long Position { get; set; } /// - /// How many times to try again after the error + /// Gets or sets the maximum number of times to try again after an error. /// public int MaxTryAgainOnFailover { get; set; } /// - /// How many milliseconds to wait for a response from the server? + /// Gets or sets the timeout in milliseconds to wait for a response from the server. /// public int Timeout { get; set; } /// - /// How many times has downloading the chunk failed? + /// Gets the number of times downloading the chunk has failed. /// public int FailoverCount { get; private set; } /// - /// Length of current chunk. + /// Gets the length of the current chunk. /// When the chunk length is zero, the file is open to receive new bytes /// until no more bytes are received from the server. /// public long Length => End - Start + 1; /// - /// Unused length of current chunk. + /// Gets the unused length of the current chunk. /// When the chunk length is zero, the file is open to receive new bytes /// until no more bytes are received from the server. /// public long EmptyLength => Length > 0 ? Length - Position : long.MaxValue; /// - /// Can write more data on this chunk according to the chunk situations? + /// Gets a value indicating whether more data can be written to this chunk according to the chunk's situation. /// - public bool CanWrite => Length > 0 ? Start + Position < End : true; - + public bool CanWrite => Length <= 0 || Start + Position < End; + /// + /// Initializes a new instance of the class with default values. + /// public Chunk() { Timeout = 1000; Id = Guid.NewGuid().ToString("N"); } + /// + /// Initializes a new instance of the class with the specified start and end positions. + /// + /// The start offset of the chunk in the file bytes. + /// The end offset of the chunk in the file bytes. public Chunk(long start, long end) : this() { Start = start; End = end; } + /// + /// Determines whether the chunk can be retried on failover. + /// + /// True if the chunk can be retried; otherwise, false. public bool CanTryAgainOnFailover() { return FailoverCount++ < MaxTryAgainOnFailover; } + /// + /// Clears the chunk's position and failover count. + /// public void Clear() { Position = 0; FailoverCount = 0; } + /// + /// Determines whether the download of the chunk is completed. + /// + /// True if the download is completed; otherwise, false. public bool IsDownloadCompleted() { var isNoneEmptyFile = Length > 0; @@ -93,6 +111,10 @@ public bool IsDownloadCompleted() return isNoneEmptyFile && isChunkedFilledWithBytes; } + /// + /// Determines whether the current position of the chunk is valid. + /// + /// True if the position is valid; otherwise, false. public bool IsValidPosition() { return Length == 0 || (Position >= 0 && Position <= Length); diff --git a/src/Downloader/ChunkDownloader.cs b/src/Downloader/ChunkDownloader.cs index b173c8b9..c8053e82 100644 --- a/src/Downloader/ChunkDownloader.cs +++ b/src/Downloader/ChunkDownloader.cs @@ -1,5 +1,5 @@ using Downloader.Extensions.Helpers; -using Downloader.Extensions.Logging; +using Microsoft.Extensions.Logging; using System; using System.ComponentModel; using System.IO; @@ -16,9 +16,8 @@ internal class ChunkDownloader private readonly DownloadConfiguration _configuration; private readonly int _timeoutIncrement = 10; private ThrottledStream _sourceStream; - private ConcurrentStream _storage; + private readonly ConcurrentStream _storage; internal Chunk Chunk { get; set; } - public event EventHandler DownloadProgressChanged; public ChunkDownloader(Chunk chunk, DownloadConfiguration config, ConcurrentStream storage, ILogger logger = null) @@ -79,7 +78,7 @@ public async Task Download(Request downloadRequest, PauseToken pause, Can private async Task ContinueWithDelay(Request request, PauseToken pause, CancellationToken cancelToken) { _logger?.LogDebug($"ContinueWithDelay of the chunk {Chunk.Id}"); - await request.ThrowIfIsNotSupportDownloadInRange(); + await request.ThrowIfIsNotSupportDownloadInRange().ConfigureAwait(false); await Task.Delay(Chunk.Timeout, cancelToken).ConfigureAwait(false); // Increasing reading timeout to reduce stress and conflicts Chunk.Timeout += _timeoutIncrement; @@ -96,33 +95,32 @@ private async Task DownloadChunk(Request downloadRequest, PauseToken pauseToken, HttpWebRequest request = downloadRequest.GetRequest(); SetRequestRange(request); using HttpWebResponse downloadResponse = request.GetResponse() as HttpWebResponse; - if (downloadResponse.StatusCode == HttpStatusCode.OK || - downloadResponse.StatusCode == HttpStatusCode.PartialContent || - downloadResponse.StatusCode == HttpStatusCode.Created || - downloadResponse.StatusCode == HttpStatusCode.Accepted || - downloadResponse.StatusCode == HttpStatusCode.ResetContent) + if (downloadResponse?.StatusCode == HttpStatusCode.OK || + downloadResponse?.StatusCode == HttpStatusCode.PartialContent || + downloadResponse?.StatusCode == HttpStatusCode.Created || + downloadResponse?.StatusCode == HttpStatusCode.Accepted || + downloadResponse?.StatusCode == HttpStatusCode.ResetContent) { - _logger?.LogDebug($"DownloadChunk of the chunk {Chunk.Id} with response status: {downloadResponse.StatusCode}"); + _logger?.LogDebug( + $"DownloadChunk of the chunk {Chunk.Id} with response status: {downloadResponse.StatusCode}"); _configuration.RequestConfiguration.CookieContainer = request.CookieContainer; - using Stream responseStream = downloadResponse?.GetResponseStream(); - if (responseStream != null) + await using Stream responseStream = downloadResponse.GetResponseStream(); + await using (_sourceStream = new ThrottledStream(responseStream, _configuration.MaximumSpeedPerChunk)) { - using (_sourceStream = new ThrottledStream(responseStream, _configuration.MaximumSpeedPerChunk)) - { - await ReadStream(_sourceStream, pauseToken, cancelToken).ConfigureAwait(false); - } + await ReadStream(_sourceStream, pauseToken, cancelToken).ConfigureAwait(false); } } else { - throw new WebException($"Download response status of the chunk {Chunk.Id} was {downloadResponse.StatusCode}: {downloadResponse.StatusDescription}"); + throw new WebException($"Download response status of the chunk {Chunk.Id} was " + + $"{downloadResponse?.StatusCode}: " + downloadResponse?.StatusDescription); } } } private void SetRequestRange(HttpWebRequest request) { - var startOffset = Chunk.Start + Chunk.Position; + long startOffset = Chunk.Start + Chunk.Position; // has limited range if (Chunk.End > 0 && @@ -143,7 +141,7 @@ internal async Task ReadStream(Stream stream, PauseToken pauseToken, Cancellatio try { // close stream on cancellation because, it's not work on .Net Framework - using var _ = cancelToken.Register(stream.Close); + await using var _ = cancelToken.Register(stream.Close); while (readSize > 0 && Chunk.CanWrite) { cancelToken.ThrowIfCancellationRequested(); @@ -152,7 +150,7 @@ internal async Task ReadStream(Stream stream, PauseToken pauseToken, Cancellatio using var innerCts = CancellationTokenSource.CreateLinkedTokenSource(cancelToken); innerToken = innerCts.Token; innerCts.CancelAfter(Chunk.Timeout); - using (innerToken.Value.Register(stream.Close)) + await using (innerToken.Value.Register(stream.Close)) { // if innerToken timeout occurs, close the stream just during the reading stream readSize = await stream.ReadAsync(buffer, 0, buffer.Length, innerToken.Value).ConfigureAwait(false); @@ -162,7 +160,8 @@ internal async Task ReadStream(Stream stream, PauseToken pauseToken, Cancellatio readSize = (int)Math.Min(Chunk.EmptyLength, readSize); if (readSize > 0) { - await _storage.WriteAsync(Chunk.Start + Chunk.Position - _configuration.RangeLow, buffer, readSize).ConfigureAwait(false); + await _storage.WriteAsync(Chunk.Start + Chunk.Position - _configuration.RangeLow, buffer, readSize) + .ConfigureAwait(false); _logger?.LogDebug($"Write {readSize}bytes in the chunk {Chunk.Id}"); Chunk.Position += readSize; _logger?.LogDebug($"The chunk {Chunk.Id} current position is: {Chunk.Position} of {Chunk.Length}"); @@ -171,14 +170,17 @@ internal async Task ReadStream(Stream stream, PauseToken pauseToken, Cancellatio TotalBytesToReceive = Chunk.Length, ReceivedBytesSize = Chunk.Position, ProgressedByteSize = readSize, - ReceivedBytes = buffer.Take(readSize).ToArray() + ReceivedBytes = _configuration.EnableLiveStreaming + ? buffer.Take(readSize).ToArray() + : [] }); } } } catch (ObjectDisposedException exp) // When closing stream manually, ObjectDisposedException will be thrown { - _logger?.LogError(exp, $"ReadAsync of the chunk {Chunk.Id} stream was canceled or closed forcibly from server"); + _logger?.LogError(exp, + $"ReadAsync of the chunk {Chunk.Id} stream was canceled or closed forcibly from server"); cancelToken.ThrowIfCancellationRequested(); if (innerToken?.IsCancellationRequested == true) { diff --git a/src/Downloader/ChunkHub.cs b/src/Downloader/ChunkHub.cs index 67471355..c9a08ea2 100644 --- a/src/Downloader/ChunkHub.cs +++ b/src/Downloader/ChunkHub.cs @@ -2,18 +2,29 @@ namespace Downloader; +/// +/// Manages the creation and validation of chunks for a download package. +/// public class ChunkHub { private readonly DownloadConfiguration _config; - private int _chunkCount = 0; - private long _chunkSize = 0; - private long _startOffset = 0; + private int _chunkCount; + private long _chunkSize; + private long _startOffset; + /// + /// Initializes a new instance of the class with the specified configuration. + /// + /// The download configuration. public ChunkHub(DownloadConfiguration config) { _config = config; } + /// + /// Sets the file chunks for the specified download package. + /// + /// The download package to set the chunks for. public void SetFileChunks(DownloadPackage package) { Validate(package); @@ -34,6 +45,10 @@ public void SetFileChunks(DownloadPackage package) } } + /// + /// Validates the download package and sets the chunk count, chunk size, and start offset. + /// + /// The download package to validate. private void Validate(DownloadPackage package) { _chunkCount = _config.ChunkCount; @@ -57,9 +72,16 @@ private void Validate(DownloadPackage package) _chunkSize = package.TotalFileSize / _chunkCount; } + /// + /// Creates a new chunk with the specified ID, start position, and end position. + /// + /// The unique identifier for the chunk. + /// The start position of the chunk. + /// The end position of the chunk. + /// A new instance of the class. private Chunk GetChunk(string id, long start, long end) { - var chunk = new Chunk(start, end) { + Chunk chunk = new(start, end) { Id = id, MaxTryAgainOnFailover = _config.MaxTryAgainOnFailover, Timeout = _config.Timeout diff --git a/src/Downloader/ConcurrentPacketBuffer.cs b/src/Downloader/ConcurrentPacketBuffer.cs index 17a75b3e..ef3729f0 100644 --- a/src/Downloader/ConcurrentPacketBuffer.cs +++ b/src/Downloader/ConcurrentPacketBuffer.cs @@ -1,4 +1,4 @@ -using Downloader.Extensions.Logging; +using Microsoft.Extensions.Logging; using System; using System.Collections; using System.Collections.Concurrent; @@ -13,27 +13,26 @@ namespace Downloader; /// Represents a thread-safe, ordered collection of objects. /// With thread-safe multi-thread adding and single-thread consuming methodology (N producers - 1 consumer) /// -/// Specifies the type of elements in the ConcurrentDictionary. /// +/// Specifies the type of elements in the ConcurrentDictionary. +/// [DebuggerTypeProxy(typeof(IReadOnlyCollection<>))] [DebuggerDisplay("Count = {Count}")] -internal class ConcurrentPacketBuffer : IReadOnlyCollection, IDisposable where T : class, ISizeableObject +internal class ConcurrentPacketBuffer(ILogger logger = null) : IReadOnlyCollection, IDisposable + where T : class, ISizeableObject { - private volatile bool _disposed = false; + private volatile bool _disposed; private long _bufferSize = long.MaxValue; - protected readonly ILogger _logger; - protected readonly SemaphoreSlim _queueConsumeLocker = new SemaphoreSlim(0); - protected readonly PauseTokenSource _addingBlocker = new PauseTokenSource(); - protected readonly PauseTokenSource _flushBlocker = new PauseTokenSource(); - protected readonly ConcurrentQueue _queue; + protected readonly ILogger Logger = logger; + protected readonly SemaphoreSlim QueueConsumeLocker = new SemaphoreSlim(0); + protected readonly PauseTokenSource AddingBlocker = new PauseTokenSource(); + protected readonly PauseTokenSource FlushBlocker = new PauseTokenSource(); + protected readonly ConcurrentQueue Queue = new(); public long BufferSize { get => _bufferSize; - set - { - _bufferSize = (value <= 0) ? long.MaxValue : value; - } + set => _bufferSize = (value <= 0) ? long.MaxValue : value; } public ConcurrentPacketBuffer(long size, ILogger logger = null) : this(logger) @@ -41,15 +40,10 @@ public ConcurrentPacketBuffer(long size, ILogger logger = null) : this(logger) BufferSize = size; } - public ConcurrentPacketBuffer(ILogger logger = null) - { - _queue = new ConcurrentQueue(); - _logger = logger; - } - public IEnumerator GetEnumerator() { - return _queue.GetEnumerator(); + // ReSharper disable once NotDisposedResourceIsReturned + return Queue.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() @@ -57,23 +51,23 @@ IEnumerator IEnumerable.GetEnumerator() return GetEnumerator(); } - public int Count => _queue.Count; - public bool IsAddingCompleted => _addingBlocker.IsPaused; - public bool IsEmpty => _queue.Count == 0; + public int Count => Queue.Count; + public bool IsAddingCompleted => AddingBlocker.IsPaused; + public bool IsEmpty => Queue.Count == 0; public T[] ToArray() { - return _queue.ToArray(); + return Queue.ToArray(); } public async Task TryAdd(T item) { try { - await _addingBlocker.WaitWhilePausedAsync().ConfigureAwait(false); - _flushBlocker.Pause(); - _queue.Enqueue(item); - _queueConsumeLocker.Release(); + await AddingBlocker.WaitWhilePausedAsync().ConfigureAwait(false); + FlushBlocker.Pause(); + Queue.Enqueue(item); + QueueConsumeLocker.Release(); StopAddingIfLimitationExceeded(item.Length); return true; } @@ -87,8 +81,8 @@ public async Task WaitTryTakeAsync(CancellationToken cancellation, Func { try { - await _queueConsumeLocker.WaitAsync(cancellation).ConfigureAwait(false); - if (_queue.TryDequeue(out var item) && item != null) + await QueueConsumeLocker.WaitAsync(cancellation).ConfigureAwait(false); + if (Queue.TryDequeue(out var item) && item != null) { await callbackTask(item).ConfigureAwait(false); } @@ -103,7 +97,8 @@ private void StopAddingIfLimitationExceeded(long packetSize) { if (BufferSize < packetSize * Count) { - _logger?.LogDebug($"ConcurrentPacketBuffer: Stop writing packets to the queue on size {packetSize * Count}bytes until the memory is free"); + Logger?.LogDebug($"ConcurrentPacketBuffer: Stop writing packets to the queue on " + + $"size {packetSize * Count}bytes until the memory is free"); StopAdding(); } } @@ -112,36 +107,36 @@ private void ResumeAddingIfEmpty() { if (IsEmpty) { - _flushBlocker.Resume(); + FlushBlocker.Resume(); ResumeAdding(); } } public async Task WaitToComplete() { - await _flushBlocker.WaitWhilePausedAsync().ConfigureAwait(false); + await FlushBlocker.WaitWhilePausedAsync().ConfigureAwait(false); } public void StopAdding() { - _logger?.LogDebug("ConcurrentPacketBuffer: stop writing new items to the list by blocking writer threads"); - _addingBlocker.Pause(); + Logger?.LogDebug("ConcurrentPacketBuffer: stop writing new items to the list by blocking writer threads"); + AddingBlocker.Pause(); } public void ResumeAdding() { - _logger?.LogDebug("ConcurrentPacketBuffer: resume writing new item to the list"); - _addingBlocker.Resume(); + Logger?.LogDebug("ConcurrentPacketBuffer: resume writing new item to the list"); + AddingBlocker.Resume(); } public void Dispose() { - if (_disposed) - return; - - _disposed = true; - StopAdding(); - _queueConsumeLocker.Dispose(); - _addingBlocker.Resume(); + if (!_disposed) + { + _disposed = true; + StopAdding(); + QueueConsumeLocker.Dispose(); + AddingBlocker.Resume(); + } } -} +} \ No newline at end of file diff --git a/src/Downloader/ConcurrentStream.cs b/src/Downloader/ConcurrentStream.cs index 8eabcd93..1a1eec9e 100644 --- a/src/Downloader/ConcurrentStream.cs +++ b/src/Downloader/ConcurrentStream.cs @@ -1,4 +1,4 @@ -using Downloader.Extensions.Logging; +using Microsoft.Extensions.Logging; using System; using System.IO; using System.Threading; @@ -6,7 +6,10 @@ namespace Downloader; -public class ConcurrentStream : TaskStateManagement, IDisposable +/// +/// Represents a stream that supports concurrent read and write operations with optional memory buffering. +/// +public class ConcurrentStream : TaskStateManagement, IDisposable, IAsyncDisposable { private ConcurrentPacketBuffer _inputBuffer; private volatile bool _disposed; @@ -14,6 +17,9 @@ public class ConcurrentStream : TaskStateManagement, IDisposable private string _path; private CancellationTokenSource _watcherCancelSource; + /// + /// Gets or sets the path of the file associated with the stream. + /// public string Path { get => _path; @@ -26,6 +32,10 @@ public string Path } } } + + /// + /// Gets the data of the stream as a byte array if the stream is a MemoryStream. + /// public byte[] Data { get @@ -49,38 +59,86 @@ public byte[] Data } } } + + /// + /// Gets a value indicating whether the stream supports reading. + /// public bool CanRead => _stream?.CanRead == true; + + /// + /// Gets a value indicating whether the stream supports seeking. + /// public bool CanSeek => _stream?.CanSeek == true; + + /// + /// Gets a value indicating whether the stream supports writing. + /// public bool CanWrite => _stream?.CanWrite == true; + + /// + /// Gets the length of the stream in bytes. + /// public long Length => _stream?.Length ?? 0; + + /// + /// Gets or sets the current position within the stream. + /// public long Position { get => _stream?.Position ?? 0; set => _stream.Position = value; } + + /// + /// Gets or sets the maximum amount of memory, in bytes, that the stream is allowed to allocate for buffering. + /// public long MaxMemoryBufferBytes { get => _inputBuffer.BufferSize; set => _inputBuffer.BufferSize = value; } - // parameterless constructor for deserialization - public ConcurrentStream() : this(0, null) { } + /// + /// Initializes a new instance of the class with default settings. + /// + public ConcurrentStream() : this(0) { } + /// + /// Initializes a new instance of the class with the specified logger. + /// + /// The logger to use for logging. public ConcurrentStream(ILogger logger) : this(0, logger) { } + /// + /// Initializes a new instance of the class with the specified maximum memory buffer size and logger. + /// + /// The maximum amount of memory, in bytes, that the stream is allowed to allocate for buffering. + /// The logger to use for logging. public ConcurrentStream(long maxMemoryBufferBytes = 0, ILogger logger = null) : base(logger) { _stream = new MemoryStream(); Initial(maxMemoryBufferBytes); } + /// + /// Initializes a new instance of the class with the specified stream and maximum memory buffer size. + /// + /// The stream to use. + /// The maximum amount of memory, in bytes, that the stream is allowed to allocate for buffering. + /// The logger to use for logging. public ConcurrentStream(Stream stream, long maxMemoryBufferBytes = 0, ILogger logger = null) : base(logger) { _stream = stream; Initial(maxMemoryBufferBytes); } + /// + /// Initializes a new instance of the class with the specified file path, initial size, and maximum memory buffer size. + /// + /// The path of the file to use. + /// The initial size of the file. + /// The maximum amount of memory, in bytes, that the stream is allowed to allocate for buffering. + /// The logger to use for logging. public ConcurrentStream(string filename, long initSize, long maxMemoryBufferBytes = 0, ILogger logger = null) : base(logger) { _path = filename; @@ -92,6 +150,11 @@ public ConcurrentStream(string filename, long initSize, long maxMemoryBufferByte Initial(maxMemoryBufferBytes); } + /// + /// Initializes the stream with the specified maximum memory buffer size. + /// + /// The maximum amount of memory, in bytes, that the stream is allowed to allocate for buffering. + /// The logger to use for logging. private void Initial(long maxMemoryBufferBytes, ILogger logger = null) { _inputBuffer = new ConcurrentPacketBuffer(maxMemoryBufferBytes, logger); @@ -106,18 +169,37 @@ private void Initial(long maxMemoryBufferBytes, ILogger logger = null) task.Unwrap(); } + /// + /// Opens the stream for reading. + /// + /// The stream for reading. public Stream OpenRead() { Seek(0, SeekOrigin.Begin); return _stream; } + /// + /// Reads a sequence of bytes from the stream and advances the position within the stream by the number of bytes read. + /// + /// An array of bytes to store the read data. + /// The zero-based byte offset in buffer at which to begin storing the data read from the stream. + /// The maximum number of bytes to be read from the stream. + /// The total number of bytes read into the buffer. public int Read(byte[] buffer, int offset, int count) { var stream = OpenRead(); return stream.Read(buffer, offset, count); } + /// + /// Writes a sequence of bytes to the stream asynchronously at the specified position. + /// + /// The position within the stream to write the data. + /// The data to write to the stream. + /// The number of bytes to write. + /// A value indicating whether to wait for the write operation to complete. + /// A task that represents the asynchronous write operation. public async Task WriteAsync(long position, byte[] bytes, int length, bool fireAndForget = true) { if (bytes.Length < length) @@ -126,7 +208,7 @@ public async Task WriteAsync(long position, byte[] bytes, int length, bool fireA if (IsFaulted && Exception is not null) throw Exception; - await _inputBuffer.TryAdd(new Packet(position, bytes, length)); + await _inputBuffer.TryAdd(new Packet(position, bytes, length)).ConfigureAwait(false); if (fireAndForget == false) { @@ -135,6 +217,10 @@ public async Task WriteAsync(long position, byte[] bytes, int length, bool fireA } } + /// + /// Watches for incoming packets and writes them to the stream. + /// + /// A task that represents the asynchronous watch operation. private async Task Watcher() { try @@ -161,6 +247,12 @@ private async Task Watcher() } } + /// + /// Sets the position within the current stream. + /// + /// A byte offset relative to the origin parameter. + /// A value of type SeekOrigin indicating the reference point used to obtain the new position. + /// The new position within the current stream. public long Seek(long offset, SeekOrigin origin) { if (offset != Position && CanSeek) @@ -171,11 +263,20 @@ public long Seek(long offset, SeekOrigin origin) return Position; } + /// + /// Sets the length of the current stream. + /// + /// The desired length of the current stream in bytes. public void SetLength(long value) { _stream.SetLength(value); } + /// + /// Writes a packet to the stream. + /// + /// The packet to write. + /// A task that represents the asynchronous write operation. private async Task WritePacketOnFile(Packet packet) { // seek with SeekOrigin.Begin is so faster than SeekOrigin.Current @@ -184,6 +285,10 @@ private async Task WritePacketOnFile(Packet packet) packet.Dispose(); } + /// + /// Flushes the stream asynchronously. + /// + /// A task that represents the asynchronous flush operation. public async Task FlushAsync() { await _inputBuffer.WaitToComplete().ConfigureAwait(false); @@ -196,9 +301,12 @@ public async Task FlushAsync() GC.Collect(); } + /// + /// Releases the unmanaged resources used by the ConcurrentStream and optionally releases the managed resources. + /// public void Dispose() { - if (_disposed == false) + if (!_disposed) { _disposed = true; _watcherCancelSource.Cancel(); // request the cancellation @@ -206,4 +314,19 @@ public void Dispose() _inputBuffer.Dispose(); } } -} + + /// + /// Asynchronously releases the unmanaged resources used by the ConcurrentStream. + /// + /// A task that represents the asynchronous dispose operation. + public async ValueTask DisposeAsync() + { + if (!_disposed) + { + _disposed = true; + await _watcherCancelSource.CancelAsync().ConfigureAwait(false); // request the cancellation + await _stream.DisposeAsync().ConfigureAwait(false); + _inputBuffer.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/Downloader/Download.cs b/src/Downloader/Download.cs index 7f9e663f..202e589a 100644 --- a/src/Downloader/Download.cs +++ b/src/Downloader/Download.cs @@ -9,57 +9,57 @@ namespace Downloader; internal class Download : IDownload { - private readonly IDownloadService downloadService; + private readonly IDownloadService _downloadService; public string Url { get; } public string Folder { get; } public string Filename { get; } - public long DownloadedFileSize => downloadService?.Package?.ReceivedBytesSize ?? 0; - public long TotalFileSize => downloadService?.Package?.TotalFileSize ?? DownloadedFileSize; + public long DownloadedFileSize => _downloadService?.Package?.ReceivedBytesSize ?? 0; + public long TotalFileSize => _downloadService?.Package?.TotalFileSize ?? DownloadedFileSize; public DownloadPackage Package { get; private set; } public DownloadStatus Status => Package?.Status ?? DownloadStatus.None; public event EventHandler ChunkDownloadProgressChanged { - add { downloadService.ChunkDownloadProgressChanged += value; } - remove { downloadService.ChunkDownloadProgressChanged -= value; } + add => _downloadService.ChunkDownloadProgressChanged += value; + remove => _downloadService.ChunkDownloadProgressChanged -= value; } public event EventHandler DownloadFileCompleted { - add { downloadService.DownloadFileCompleted += value; } - remove { downloadService.DownloadFileCompleted -= value; } + add => _downloadService.DownloadFileCompleted += value; + remove => _downloadService.DownloadFileCompleted -= value; } public event EventHandler DownloadProgressChanged { - add { downloadService.DownloadProgressChanged += value; } - remove { downloadService.DownloadProgressChanged -= value; } + add => _downloadService.DownloadProgressChanged += value; + remove => _downloadService.DownloadProgressChanged -= value; } public event EventHandler DownloadStarted { - add { downloadService.DownloadStarted += value; } - remove { downloadService.DownloadStarted -= value; } + add => _downloadService.DownloadStarted += value; + remove => _downloadService.DownloadStarted -= value; } public Download(string url, string path, string filename, DownloadConfiguration configuration) { - downloadService = new DownloadService(configuration); + _downloadService = new DownloadService(configuration); Url = url; Folder = path; Filename = filename; - Package = downloadService.Package; + Package = _downloadService.Package; } public Download(DownloadPackage package, DownloadConfiguration configuration) { - downloadService = new DownloadService(configuration); + _downloadService = new DownloadService(configuration); Package = package; } public Download(DownloadPackage package, string address, DownloadConfiguration configuration) { - downloadService = new DownloadService(configuration); + _downloadService = new DownloadService(configuration); Package = package; Url = address; } @@ -68,45 +68,49 @@ public async Task StartAsync(CancellationToken cancellationToken = defau { if (string.IsNullOrWhiteSpace(Package?.Urls?.FirstOrDefault())) { - if (string.IsNullOrWhiteSpace(Folder) && string.IsNullOrWhiteSpace(Filename)) + if (string.IsNullOrWhiteSpace(Filename)) { - return await downloadService.DownloadFileTaskAsync(Url, cancellationToken); - } - else if (string.IsNullOrWhiteSpace(Filename)) - { - await downloadService.DownloadFileTaskAsync(Url, new DirectoryInfo(Folder), cancellationToken); - return null; + if (string.IsNullOrWhiteSpace(Folder)) + { + // store on memory stream so return stream + return await _downloadService.DownloadFileTaskAsync(Url, cancellationToken).ConfigureAwait(false); + } + + // store on a file with the given path and url fetching name + await _downloadService.DownloadFileTaskAsync(Url, + new DirectoryInfo(Folder), cancellationToken).ConfigureAwait(false); } else { - // with Folder and Filename - await downloadService.DownloadFileTaskAsync(Url, Path.Combine(Folder, Filename), cancellationToken); - return null; + // // store on a file with the given name and folder path + await _downloadService.DownloadFileTaskAsync(Url, Path.Combine(Folder, Filename), cancellationToken) + .ConfigureAwait(false); } + + return Stream.Null; } - else if(string.IsNullOrWhiteSpace(Url)) - { - return await downloadService.DownloadFileTaskAsync(Package, cancellationToken); - } - else + + if (string.IsNullOrWhiteSpace(Url)) { - return await downloadService.DownloadFileTaskAsync(Package, Url, cancellationToken); + return await _downloadService.DownloadFileTaskAsync(Package, cancellationToken).ConfigureAwait(false); } + + return await _downloadService.DownloadFileTaskAsync(Package, Url, cancellationToken).ConfigureAwait(false); } public void Stop() { - downloadService.CancelTaskAsync().Wait(); + _downloadService.CancelTaskAsync().Wait(); } public void Pause() { - downloadService.Pause(); + _downloadService.Pause(); } public void Resume() { - downloadService.Resume(); + _downloadService.Resume(); } public override bool Equals(object obj) @@ -123,9 +127,15 @@ public override int GetHashCode() return hashCode; } - public async void Dispose() + public async ValueTask DisposeAsync() + { + await _downloadService.Clear().ConfigureAwait(false); + Package = null; + } + + public void Dispose() { - await downloadService.Clear(); + _downloadService.Clear().Wait(); Package = null; } -} +} \ No newline at end of file diff --git a/src/Downloader/DownloadBuilder.cs b/src/Downloader/DownloadBuilder.cs index 363c3c13..eadf952f 100644 --- a/src/Downloader/DownloadBuilder.cs +++ b/src/Downloader/DownloadBuilder.cs @@ -3,75 +3,137 @@ namespace Downloader; +/// +/// A builder class for configuring and creating download instances. +/// public class DownloadBuilder { - private string url; - private string directoryPath; - private string filename; - private DownloadConfiguration downloadConfiguration; - + private string _url; + private string _directoryPath; + private string _filename; + private DownloadConfiguration _downloadConfiguration; + + /// + /// Creates a new instance of the class. + /// + /// A new instance of the class. public static DownloadBuilder New() { return new DownloadBuilder(); } + /// + /// Sets the URL for the download. + /// + /// The URL to download from. + /// The current instance. public DownloadBuilder WithUrl(string url) { - this.url = url; + this._url = url; return this; } + /// + /// Sets the URL for the download using a object. + /// + /// The URL to download from. + /// The current instance. public DownloadBuilder WithUrl(Uri url) { return WithUrl(url.AbsoluteUri); } + /// + /// Sets the file location for the download. + /// + /// The full path where the file will be saved. + /// The current instance. public DownloadBuilder WithFileLocation(string fullPath) { fullPath = Path.GetFullPath(fullPath); - filename = Path.GetFileName(fullPath); - directoryPath = Path.GetDirectoryName(fullPath); + _filename = Path.GetFileName(fullPath); + _directoryPath = Path.GetDirectoryName(fullPath); return this; } + /// + /// Sets the file location for the download using a object. + /// + /// The URI representing the file location. + /// The current instance. public DownloadBuilder WithFileLocation(Uri uri) { return WithFileLocation(uri.LocalPath); } + /// + /// Sets the file location for the download using a object. + /// + /// The file information representing the file location. + /// The current instance. public DownloadBuilder WithFileLocation(FileInfo fileInfo) { return WithFileLocation(fileInfo.FullName); } + /// + /// Sets the directory path for the download. + /// + /// The directory path where the file will be saved. + /// The current instance. public DownloadBuilder WithDirectory(string directoryPath) { - this.directoryPath = directoryPath; + this._directoryPath = directoryPath; return this; } + /// + /// Sets the directory path for the download using a object. + /// + /// The URI representing the directory path. + /// The current instance. public DownloadBuilder WithFolder(Uri folderUri) { return WithDirectory(folderUri.LocalPath); } + /// + /// Sets the directory path for the download using a object. + /// + /// The directory information representing the directory path. + /// The current instance. public DownloadBuilder WithFolder(DirectoryInfo folder) { return WithDirectory(folder.FullName); } + /// + /// Sets the file name for the download. + /// + /// The name of the file to be saved. + /// The current instance. public DownloadBuilder WithFileName(string name) { - this.filename = name; + this._filename = name; return this; } + /// + /// Sets the configuration for the download. + /// + /// The download configuration. + /// The current instance. public DownloadBuilder WithConfiguration(DownloadConfiguration configuration) { - downloadConfiguration = configuration; + _downloadConfiguration = configuration; return this; } + /// + /// Configures the download with the specified configuration action. + /// + /// The action to configure the download. + /// The current instance. public DownloadBuilder Configure(Action configure) { var configuration = new DownloadConfiguration(); @@ -79,23 +141,39 @@ public DownloadBuilder Configure(Action configure) return WithConfiguration(configuration); } + /// + /// Builds and returns a new instance with the configured settings. + /// + /// A new instance. + /// Thrown when the URL has not been declared. public IDownload Build() { - if (string.IsNullOrWhiteSpace(url)) + if (string.IsNullOrWhiteSpace(_url)) { - throw new ArgumentNullException($"{nameof(url)} has not been declared."); + throw new ArgumentNullException($"{nameof(_url)} has not been declared."); } - return new Download(url, directoryPath, filename, downloadConfiguration); + return new Download(_url, _directoryPath, _filename, _downloadConfiguration); } + /// + /// Builds and returns a new instance with the specified package. + /// + /// The download package. + /// A new instance. public IDownload Build(DownloadPackage package) { - return new Download(package, url, downloadConfiguration); + return new Download(package, _url, _downloadConfiguration); } + /// + /// Builds and returns a new instance with the specified package and configuration. + /// + /// The download package. + /// The download configuration. + /// A new instance. public IDownload Build(DownloadPackage package, DownloadConfiguration downloadConfiguration) { - return new Download(package, url, downloadConfiguration); + return new Download(package, _url, downloadConfiguration); } -} +} \ No newline at end of file diff --git a/src/Downloader/DownloadConfiguration.cs b/src/Downloader/DownloadConfiguration.cs index 2882c286..3febfde9 100644 --- a/src/Downloader/DownloadConfiguration.cs +++ b/src/Downloader/DownloadConfiguration.cs @@ -4,57 +4,45 @@ namespace Downloader; +/// +/// Represents the configuration settings for a download operation. +/// public class DownloadConfiguration : ICloneable, INotifyPropertyChanged { - private int _bufferBlockSize; - private int _chunkCount; - private long _maximumBytesPerSecond; - private int _maximumTryAgainOnFailover; + private int _bufferBlockSize = 1024; // usually, hosts support max to 8000 bytes + private int _chunkCount = 1; // file parts to download + private long _maximumBytesPerSecond = ThrottledStream.Infinite; // No-limitation in download speed + private int _maximumTryAgainOnFailover = int.MaxValue; // the maximum number of times to fail. private long _maximumMemoryBufferBytes; - private bool _checkDiskSizeBeforeDownload; - private bool _parallelDownload; - private int _parallelCount; - private int _timeout; - private bool _rangeDownload; - private long _rangeLow; - private long _rangeHigh; - private bool _clearPackageOnCompletionWithFailure; - private long _minimumSizeOfChunking; - private bool _reserveStorageSpaceBeforeStartingDownload; - + private bool _checkDiskSizeBeforeDownload = true; // check disk size for temp and file path + private bool _parallelDownload; // download parts of file as parallel or not + private int _parallelCount; // number of parallel downloads + private int _timeout = 1000; // timeout (millisecond) per stream block reader + private bool _rangeDownload; // enable ranged download + private long _rangeLow; // starting byte offset + private long _rangeHigh; // ending byte offset + private bool _clearPackageOnCompletionWithFailure; // Clear package and downloaded data when download completed with failure + private long _minimumSizeOfChunking = 512; // minimum size of chunking to download a file in multiple parts + private bool _reserveStorageSpaceBeforeStartingDownload; // Before starting the download, reserve the storage space of the file as file size. + private bool _enableLiveStreaming; // Get on demand downloaded data with ReceivedBytes on downloadProgressChanged event + + /// + /// To bind view models to fire changes in MVVM pattern + /// public event PropertyChangedEventHandler PropertyChanged = delegate { }; - public DownloadConfiguration() - { - RequestConfiguration = new RequestConfiguration(); // default requests configuration - _maximumTryAgainOnFailover = int.MaxValue; // the maximum number of times to fail. - _parallelDownload = false; // download parts of file as parallel or not - _parallelCount = 0; // number of parallel downloads - _chunkCount = 1; // file parts to download - _timeout = 1000; // timeout (millisecond) per stream block reader - _bufferBlockSize = 1024; // usually, hosts support max to 8000 bytes - _maximumBytesPerSecond = ThrottledStream.Infinite; // No-limitation in download speed - _checkDiskSizeBeforeDownload = true; // check disk size for temp and file path - _rangeDownload = false; // enable ranged download - _rangeLow = 0; // starting byte offset - _rangeHigh = 0; // ending byte offset - _clearPackageOnCompletionWithFailure = false; // Clear package and downloaded data when download completed with failure - _minimumSizeOfChunking = 512; // minimum size of chunking to download a file in multiple parts - _reserveStorageSpaceBeforeStartingDownload = false; // Before starting the download, reserve the storage space of the file as file size. - } - /// - /// Create the OnPropertyChanged method to raise the event + /// Raises the PropertyChanged event. /// The calling member's name will be used as the parameter. /// - /// changed property name + /// The name of the property that changed. protected void OnPropertyChanged([CallerMemberName] string name = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); } /// - /// Stream buffer size which is used for size of blocks + /// Gets or sets the stream buffer size which is used for the size of blocks. /// public int BufferBlockSize { @@ -67,7 +55,7 @@ public int BufferBlockSize } /// - /// Check disk available size for download file before starting the download. + /// Gets or sets a value indicating whether to check the disk available size for the download file before starting the download. /// public bool CheckDiskSizeBeforeDownload { @@ -80,7 +68,7 @@ public bool CheckDiskSizeBeforeDownload } /// - /// File chunking parts count + /// Gets or sets the file chunking parts count. /// public int ChunkCount { @@ -93,7 +81,7 @@ public int ChunkCount } /// - /// The maximum bytes per second that can be transferred through the base stream. + /// Gets or sets the maximum bytes per second that can be transferred through the base stream. /// public long MaximumBytesPerSecond { @@ -106,15 +94,15 @@ public long MaximumBytesPerSecond } /// - /// The maximum bytes per second that can be transferred through the base stream at each chunk downloader. - /// This Property is ReadOnly. + /// Gets the maximum bytes per second that can be transferred through the base stream at each chunk downloader. + /// This property is read-only. /// public long MaximumSpeedPerChunk => ParallelDownload ? MaximumBytesPerSecond / Math.Min(ChunkCount, ParallelCount) : MaximumBytesPerSecond; /// - /// How many time try again to download on failed + /// Gets or sets the maximum number of times to try again to download on failure. /// public int MaxTryAgainOnFailover { @@ -127,7 +115,7 @@ public int MaxTryAgainOnFailover } /// - /// Download file chunks as Parallel or Serial? + /// Gets or sets a value indicating whether to download file chunks in parallel or serially. /// public bool ParallelDownload { @@ -140,8 +128,8 @@ public bool ParallelDownload } /// - /// Count of chunks to download in parallel. - /// If ParallelCount is <=0, then ParallelCount is equal to ChunkCount. + /// Gets or sets the count of chunks to download in parallel. + /// If ParallelCount is less than or equal to 0, then ParallelCount is equal to ChunkCount. /// public int ParallelCount { @@ -154,7 +142,7 @@ public int ParallelCount } /// - /// Download a range of byte + /// Gets or sets a value indicating whether to download a range of bytes. /// public bool RangeDownload { @@ -167,7 +155,7 @@ public bool RangeDownload } /// - /// The starting byte offset for ranged download + /// Gets or sets the starting byte offset for ranged download. /// public long RangeLow { @@ -180,7 +168,7 @@ public long RangeLow } /// - /// The ending byte offset for ranged download + /// Gets or sets the ending byte offset for ranged download. /// public long RangeHigh { @@ -193,12 +181,12 @@ public long RangeHigh } /// - /// Custom body of your requests + /// Gets or sets the custom body of your requests. /// - public RequestConfiguration RequestConfiguration { get; set; } + public RequestConfiguration RequestConfiguration { get; set; } = new(); // default requests configuration /// - /// Download timeout per stream file blocks + /// Gets or sets the download timeout per stream file blocks. /// public int Timeout { @@ -211,7 +199,7 @@ public int Timeout } /// - /// Clear package and downloaded data when download completed with failure + /// Gets or sets a value indicating whether to clear the package and downloaded data when the download completes with failure. /// public bool ClearPackageOnCompletionWithFailure { @@ -224,7 +212,7 @@ public bool ClearPackageOnCompletionWithFailure } /// - /// Minimum size of chunking and multiple part downloading + /// Gets or sets the minimum size of chunking and multiple part downloading. /// public long MinimumSizeOfChunking { @@ -237,7 +225,7 @@ public long MinimumSizeOfChunking } /// - /// Before starting the download, reserve the storage space of the file as file size. + /// Gets or sets a value indicating whether to reserve the storage space of the file as file size before starting the download. /// Default value is false. /// public bool ReserveStorageSpaceBeforeStartingDownload @@ -272,7 +260,26 @@ public long MaximumMemoryBufferBytes OnPropertyChanged(); } } + + /// + /// Gets or sets a value indicating whether live-streaming is enabled or not. If it's enabled, get the on-demand downloaded data + /// with ReceivedBytes on the downloadProgressChanged event. + /// Note: This option may consume more memory because it copies each block of downloaded data into ReceivedBytes. + /// + public bool EnableLiveStreaming + { + get => _enableLiveStreaming; + set + { + _enableLiveStreaming = value; + OnPropertyChanged(); + } + } + /// + /// Creates a shallow copy of the current object. + /// + /// A shallow copy of the current object. public object Clone() { return MemberwiseClone(); diff --git a/src/Downloader/DownloadPackage.cs b/src/Downloader/DownloadPackage.cs index f1e5c150..0041c5dc 100644 --- a/src/Downloader/DownloadPackage.cs +++ b/src/Downloader/DownloadPackage.cs @@ -1,26 +1,78 @@ -using Downloader.Extensions.Logging; +using Microsoft.Extensions.Logging; using System; using System.Linq; -using System.Threading; using System.Threading.Tasks; namespace Downloader; -public class DownloadPackage : IDisposable +/// +/// Represents a package containing information about a download operation. +/// +public class DownloadPackage : IDisposable, IAsyncDisposable { + /// + /// Gets or sets a value indicating whether the package is currently being saved. + /// public bool IsSaving { get; set; } + + /// + /// Gets or sets a value indicating whether the save operation is complete. + /// public bool IsSaveComplete { get; set; } + + /// + /// Gets or sets the progress of the save operation. + /// public double SaveProgress { get; set; } + + /// + /// Gets or sets the status of the download operation. + /// public DownloadStatus Status { get; set; } = DownloadStatus.None; + + /// + /// Gets or sets the URLs from which the file is being downloaded. + /// public string[] Urls { get; set; } + + /// + /// Gets or sets the total size of the file to be downloaded. + /// public long TotalFileSize { get; set; } + + /// + /// Gets or sets the name of the file to be saved. + /// public string FileName { get; set; } + + /// + /// Gets or sets the chunks of the file being downloaded. + /// public Chunk[] Chunks { get; set; } + + /// + /// Gets the total size of the received bytes. + /// public long ReceivedBytesSize => Chunks?.Sum(chunk => chunk.Position) ?? 0; + + /// + /// Gets or sets a value indicating whether the download supports range requests. + /// public bool IsSupportDownloadInRange { get; set; } = true; + + /// + /// Gets a value indicating whether the download is being stored in memory. + /// public bool InMemoryStream => string.IsNullOrWhiteSpace(FileName); + + /// + /// Gets or sets the storage for the download. + /// public ConcurrentStream Storage { get; set; } + /// + /// Clears the chunks and resets the package. + /// public void Clear() { if (Chunks != null) @@ -28,15 +80,23 @@ public void Clear() foreach (Chunk chunk in Chunks) chunk.Clear(); } + Chunks = null; } + /// + /// Flushes the storage asynchronously. + /// + /// A task that represents the asynchronous flush operation. public async Task FlushAsync() { if (Storage?.CanWrite == true) await Storage.FlushAsync().ConfigureAwait(false); } + /// + /// Flushes the storage synchronously. + /// [Obsolete("This method has been deprecated. Please use FlushAsync instead.")] public void Flush() { @@ -44,6 +104,9 @@ public void Flush() Storage?.FlushAsync().Wait(); } + /// + /// Validates the chunks and ensures they are in the correct position. + /// public void Validate() { foreach (var chunk in Chunks) @@ -62,17 +125,37 @@ public void Validate() } } + /// + /// Builds the storage for the download package. + /// + /// Indicates whether to reserve the file size. + /// The maximum size of the memory buffer in bytes. + /// The logger to use for logging. public void BuildStorage(bool reserveFileSize, long maxMemoryBufferBytes = 0, ILogger logger = null) { - if (string.IsNullOrWhiteSpace(FileName)) - Storage = new ConcurrentStream(maxMemoryBufferBytes, logger); - else - Storage = new ConcurrentStream(FileName, reserveFileSize ? TotalFileSize : 0, maxMemoryBufferBytes, logger); + Storage = string.IsNullOrWhiteSpace(FileName) + ? new ConcurrentStream(maxMemoryBufferBytes, logger) + : new ConcurrentStream(FileName, reserveFileSize ? TotalFileSize : 0, maxMemoryBufferBytes, logger); } + /// + /// Disposes of the download package, clearing the chunks and disposing of the storage. + /// public void Dispose() { Clear(); Storage?.Dispose(); } + + /// + /// Disposes of the download package, clearing the chunks and disposing of the storage. + /// + public async ValueTask DisposeAsync() + { + Clear(); + if (Storage is not null) + { + await Storage.DisposeAsync().ConfigureAwait(false); + } + } } \ No newline at end of file diff --git a/src/Downloader/DownloadProgressChangedEventArgs.cs b/src/Downloader/DownloadProgressChangedEventArgs.cs index fa5eba3a..10b447cc 100644 --- a/src/Downloader/DownloadProgressChangedEventArgs.cs +++ b/src/Downloader/DownloadProgressChangedEventArgs.cs @@ -3,63 +3,68 @@ namespace Downloader; /// -/// Provides any information about progress, like progress percentage, speed, -/// total received bytes and received bytes array to live streaming, for the DownloadService.DownloadProgressChanged event of a -/// DownloadService. +/// Provides any information about progress, like progress percentage, speed, +/// total received bytes and received bytes array to live streaming, for the DownloadService.DownloadProgressChanged event of a +/// DownloadService. /// public class DownloadProgressChangedEventArgs : EventArgs { + /// + /// Initializes a new instance of the class. + /// + /// The unique identity of the progress. public DownloadProgressChangedEventArgs(string id) { ProgressId = id ?? "Main"; } /// - /// Progress unique identity + /// Gets the progress unique identity. /// public string ProgressId { get; } /// - /// Gets the asynchronous task progress percentage. + /// Gets the asynchronous task progress percentage. /// /// A percentage value indicating the asynchronous task progress. public double ProgressPercentage => TotalBytesToReceive == 0 ? 0 : ((double)ReceivedBytesSize * 100) / TotalBytesToReceive; /// - /// Gets the number of received bytes. + /// Gets the number of received bytes. /// /// An System.Int64 value that indicates the number of received bytes. public long ReceivedBytesSize { get; internal set; } /// - /// Gets the total number of bytes in a System.Net.WebClient data download operation. + /// Gets the total number of bytes in a System.Net.WebClient data download operation. /// /// An System.Int64 value that indicates the number of bytes that will be received. public long TotalBytesToReceive { get; internal set; } /// - /// How many bytes downloaded per second + /// Gets the number of bytes downloaded per second. /// public double BytesPerSecondSpeed { get; internal set; } /// - /// Average download speed + /// Gets the average download speed. /// public double AverageBytesPerSecondSpeed { get; internal set; } /// - /// How many bytes progressed per this time + /// Gets the number of bytes progressed per this time. /// public long ProgressedByteSize { get; internal set; } /// - /// Gets the received bytes. + /// Gets the received bytes. + /// This property is filled when the EnableLiveStreaming option is true. /// /// A byte array that indicates the received bytes. public byte[] ReceivedBytes { get; internal set; } - + /// - /// Gets the number of chunks being downloaded currently. + /// Gets the number of chunks being downloaded currently. /// public int ActiveChunks { get; internal set; } } \ No newline at end of file diff --git a/src/Downloader/DownloadService.cs b/src/Downloader/DownloadService.cs index 89af2b5d..1a114d94 100644 --- a/src/Downloader/DownloadService.cs +++ b/src/Downloader/DownloadService.cs @@ -9,32 +9,47 @@ namespace Downloader; +/// +/// Concrete implementation of the class. +/// public class DownloadService : AbstractDownloadService { + /// + /// Initializes a new instance of the class with the specified options. + /// + /// The configuration options for the download service. public DownloadService(DownloadConfiguration options) : base(options) { } + + /// + /// Initializes a new instance of the class with default options. + /// public DownloadService() : base(null) { } + /// + /// Starts the download operation. + /// + /// A task that represents the asynchronous download operation. The task result contains the downloaded stream. protected override async Task StartDownload() { try { - await _singleInstanceSemaphore.WaitAsync(); - Package.TotalFileSize = await _requestInstances.First().GetFileSize().ConfigureAwait(false); - Package.IsSupportDownloadInRange = await _requestInstances.First().IsSupportDownloadInRange().ConfigureAwait(false); + await SingleInstanceSemaphore.WaitAsync().ConfigureAwait(false); + Package.TotalFileSize = await RequestInstances.First().GetFileSize().ConfigureAwait(false); + Package.IsSupportDownloadInRange = await RequestInstances.First().IsSupportDownloadInRange().ConfigureAwait(false); Package.BuildStorage(Options.ReserveStorageSpaceBeforeStartingDownload, Options.MaximumMemoryBufferBytes); ValidateBeforeChunking(); - _chunkHub.SetFileChunks(Package); + ChunkHub.SetFileChunks(Package); // firing the start event after creating chunks OnDownloadStarted(new DownloadStartedEventArgs(Package.FileName, Package.TotalFileSize)); if (Options.ParallelDownload) { - await ParallelDownload(_pauseTokenSource.Token).ConfigureAwait(false); + await ParallelDownload(PauseTokenSource.Token).ConfigureAwait(false); } else { - await SerialDownload(_pauseTokenSource.Token).ConfigureAwait(false); + await SerialDownload(PauseTokenSource.Token).ConfigureAwait(false); } await SendDownloadCompletionSignal(DownloadStatus.Completed).ConfigureAwait(false); @@ -49,23 +64,31 @@ protected override async Task StartDownload() } finally { - _singleInstanceSemaphore.Release(); + SingleInstanceSemaphore.Release(); await Task.Yield(); } return Package.Storage?.OpenRead(); } + /// + /// Sends the download completion signal with the specified and optional . + /// + /// The state of the download operation. + /// The exception that caused the download to fail, if any. + /// A task that represents the asynchronous operation. private async Task SendDownloadCompletionSignal(DownloadStatus state, Exception error = null) { var isCancelled = state == DownloadStatus.Stopped; Package.IsSaveComplete = state == DownloadStatus.Completed; Status = state; await (Package?.Storage?.FlushAsync() ?? Task.FromResult(0)).ConfigureAwait(false); - await (_logger?.FlushAsync() ?? Task.FromResult(0)).ConfigureAwait(false); OnDownloadFileCompleted(new AsyncCompletedEventArgs(error, isCancelled, Package)); } + /// + /// Validates the download configuration before chunking the file. + /// private void ValidateBeforeChunking() { CheckSingleChunkDownload(); @@ -74,6 +97,9 @@ private void ValidateBeforeChunking() CheckSizes(); } + /// + /// Sets the range sizes for the download operation. + /// private void SetRangedSizes() { if (Options.RangeDownload) @@ -111,6 +137,9 @@ private void SetRangedSizes() } } + /// + /// Checks if there is enough disk space before starting the download. + /// private void CheckSizes() { if (Options.CheckDiskSizeBeforeDownload && !Package.InMemoryStream) @@ -119,6 +148,9 @@ private void CheckSizes() } } + /// + /// Checks if the download should be handled as a single chunk. + /// private void CheckSingleChunkDownload() { if (Package.TotalFileSize <= 1) @@ -128,19 +160,30 @@ private void CheckSingleChunkDownload() SetSingleChunkDownload(); } + /// + /// Checks if the server supports download in a specific range. + /// private void CheckSupportDownloadInRange() { if (Package.IsSupportDownloadInRange == false) SetSingleChunkDownload(); } + /// + /// Sets the download configuration to handle the file as a single chunk. + /// private void SetSingleChunkDownload() { Options.ChunkCount = 1; Options.ParallelCount = 1; - _parallelSemaphore = new SemaphoreSlim(1, 1); + ParallelSemaphore = new SemaphoreSlim(1, 1); } + /// + /// Downloads the file in parallel chunks. + /// + /// The pause token for pausing the download. + /// A task that represents the asynchronous operation. private async Task ParallelDownload(PauseToken pauseToken) { var tasks = GetChunksTasks(pauseToken); @@ -153,6 +196,11 @@ private async Task ParallelDownload(PauseToken pauseToken) } } + /// + /// Downloads the file in serial chunks. + /// + /// The pause token for pausing the download. + /// A task that represents the asynchronous operation. private async Task SerialDownload(PauseToken pauseToken) { var tasks = GetChunksTasks(pauseToken); @@ -160,20 +208,33 @@ private async Task SerialDownload(PauseToken pauseToken) await task.ConfigureAwait(false); } + /// + /// Gets the tasks for downloading the chunks. + /// + /// The pause token for pausing the download. + /// An enumerable collection of tasks representing the chunk downloads. private IEnumerable GetChunksTasks(PauseToken pauseToken) { for (int i = 0; i < Package.Chunks.Length; i++) { - var request = _requestInstances[i % _requestInstances.Count]; - yield return DownloadChunk(Package.Chunks[i], request, pauseToken, _globalCancellationTokenSource); + var request = RequestInstances[i % RequestInstances.Count]; + yield return DownloadChunk(Package.Chunks[i], request, pauseToken, GlobalCancellationTokenSource); } } + /// + /// Downloads a specific chunk of the file. + /// + /// The chunk to download. + /// The request to use for the download. + /// The pause token for pausing the download. + /// The cancellation token source for cancelling the download. + /// A task that represents the asynchronous operation. The task result contains the downloaded chunk. private async Task DownloadChunk(Chunk chunk, Request request, PauseToken pause, CancellationTokenSource cancellationTokenSource) { - ChunkDownloader chunkDownloader = new ChunkDownloader(chunk, Options, Package.Storage, _logger); + ChunkDownloader chunkDownloader = new ChunkDownloader(chunk, Options, Package.Storage, Logger); chunkDownloader.DownloadProgressChanged += OnChunkDownloadProgressChanged; - await _parallelSemaphore.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false); + await ParallelSemaphore.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false); try { cancellationTokenSource.Token.ThrowIfCancellationRequested(); @@ -191,7 +252,7 @@ private async Task DownloadChunk(Chunk chunk, Request request, PauseToken } finally { - _parallelSemaphore.Release(); + ParallelSemaphore.Release(); } } } \ No newline at end of file diff --git a/src/Downloader/DownloadStartedEventArgs.cs b/src/Downloader/DownloadStartedEventArgs.cs index eec2b419..d9e03641 100644 --- a/src/Downloader/DownloadStartedEventArgs.cs +++ b/src/Downloader/DownloadStartedEventArgs.cs @@ -3,11 +3,15 @@ namespace Downloader; /// -/// Provides data for the DownloadService.DownloadProgressChanged event of a -/// DownloadService. +/// Provides data for the DownloadService.DownloadProgressChanged event of a DownloadService. /// public class DownloadStartedEventArgs : EventArgs { + /// + /// Initializes a new instance of the class. + /// + /// The name of the file being downloaded. + /// The total number of bytes to be received. public DownloadStartedEventArgs(string fileName, long totalBytes) { FileName = fileName; @@ -15,13 +19,13 @@ public DownloadStartedEventArgs(string fileName, long totalBytes) } /// - /// Gets the total number of bytes in a System.Net.WebClient data download operation. + /// Gets the total number of bytes in a System.Net.WebClient data download operation. /// - /// An System.Int64 value that indicates the number of bytes that will be received. + /// A System.Int64 value that indicates the number of bytes that will be received. public long TotalBytesToReceive { get; } /// - /// The name of file which is downloading + /// Gets the name of the file which is being downloaded. /// public string FileName { get; } } \ No newline at end of file diff --git a/src/Downloader/DownloadStatus.cs b/src/Downloader/DownloadStatus.cs index 0052c2f2..d0520bcd 100644 --- a/src/Downloader/DownloadStatus.cs +++ b/src/Downloader/DownloadStatus.cs @@ -1,12 +1,42 @@ namespace Downloader; +/// +/// Represents the status of a download operation. +/// public enum DownloadStatus { + /// + /// The download has not started. + /// None = 0, + + /// + /// The download has been created but not yet started. + /// Created = 1, + + /// + /// The download is currently running. + /// Running = 2, - Stopped = 3, // Cancelled + + /// + /// The download has been stopped or cancelled. + /// + Stopped = 3, + + /// + /// The download has been paused. + /// Paused = 4, + + /// + /// The download has completed successfully. + /// Completed = 5, + + /// + /// The download has failed. + /// Failed = 6 -} +} \ No newline at end of file diff --git a/src/Downloader/Downloader.csproj b/src/Downloader/Downloader.csproj index 27d356ef..7d9e0fd0 100644 --- a/src/Downloader/Downloader.csproj +++ b/src/Downloader/Downloader.csproj @@ -1,6 +1,6 @@  - netstandard2.0;netstandard2.1;netcoreapp3.1;net462;net6.0;net8.0; + net8.0 latestMajor 3.1.2 Downloader @@ -22,8 +22,6 @@ Downloader.snk Copyright (C) 2019-2023 Behzad Khosravifar true - - LICENSE downloader.png git README.md @@ -34,10 +32,13 @@ true + 3.1.2 + 3.1.2 + https://raw.githubusercontent.com/bezzad/Downloader/master/LICENSE - + bin\Release\net8.0\Downloader.xml @@ -65,11 +66,12 @@ True - + \ + diff --git a/src/Downloader/Extensions/Helpers/ExceptionHelper.cs b/src/Downloader/Extensions/Helpers/ExceptionHelper.cs index cc5cf445..8d2d8338 100644 --- a/src/Downloader/Extensions/Helpers/ExceptionHelper.cs +++ b/src/Downloader/Extensions/Helpers/ExceptionHelper.cs @@ -10,9 +10,7 @@ internal static class ExceptionHelper { internal static bool IsMomentumError(this Exception error) { - if (error.HasSource("System.Net.Http", - "System.Net.Sockets", - "System.Net.Security")) + if (error.HasSource("System.Net.Http", "System.Net.Sockets", "System.Net.Security")) return true; if (error.HasTypeOf(typeof(WebException), typeof(SocketException))) @@ -83,11 +81,11 @@ internal static bool CertificateValidationCallBack(object sender, // If the error is for certificate expiration then it can be continued return true; } - else if (certificate.Subject == certificate.Issuer && - status.Status == X509ChainStatusFlags.UntrustedRoot) + + if (status.Status == X509ChainStatusFlags.UntrustedRoot && + certificate.Subject == certificate.Issuer) { // Self-signed certificates with an untrusted root are valid. - continue; } else if (status.Status != X509ChainStatusFlags.NoError) { @@ -109,4 +107,4 @@ internal static bool CertificateValidationCallBack(object sender, return false; } } -} +} \ No newline at end of file diff --git a/src/Downloader/Extensions/Helpers/FileHelper.cs b/src/Downloader/Extensions/Helpers/FileHelper.cs index 80ad13b1..18f41a87 100644 --- a/src/Downloader/Extensions/Helpers/FileHelper.cs +++ b/src/Downloader/Extensions/Helpers/FileHelper.cs @@ -18,12 +18,10 @@ public static Stream CreateFile(string filename) Directory.CreateDirectory(directory); } - return new FileStream(filename, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete); - } - public static string GetTempFile() - { - return GetTempFile(Path.GetTempPath(), string.Empty); + return new FileStream(filename, FileMode.OpenOrCreate, FileAccess.Write, + FileShare.ReadWrite | FileShare.Delete); } + public static string GetTempFile(string baseDirectory, string fileExtension) { if (string.IsNullOrWhiteSpace(baseDirectory)) @@ -41,13 +39,8 @@ public static long GetAvailableFreeSpaceOnDisk(string directory) { try { - var drive = new DriveInfo(directory); - if (drive.IsReady) - { - return drive.AvailableFreeSpace; - } - - return 0L; + DriveInfo drive = new(directory); + return drive.IsReady ? drive.AvailableFreeSpace : 0L; } catch (ArgumentException) { @@ -58,15 +51,16 @@ public static long GetAvailableFreeSpaceOnDisk(string directory) public static void ThrowIfNotEnoughSpace(long actualNeededSize, params string[] directories) { - if (directories != null) + if (directories == null) + return; + + foreach (string directory in directories) { - foreach (string directory in directories) + long availableFreeSpace = GetAvailableFreeSpaceOnDisk(directory); + if (availableFreeSpace > 0 && availableFreeSpace < actualNeededSize) { - var availableFreeSpace = GetAvailableFreeSpaceOnDisk(directory); - if (availableFreeSpace > 0 && availableFreeSpace < actualNeededSize) - { - throw new IOException($"There is not enough space on the disk `{directory}` with {availableFreeSpace} bytes"); - } + throw new IOException($"There is not enough space on the disk `{directory}` " + + $"with {availableFreeSpace} bytes"); } } } diff --git a/src/Downloader/Extensions/Logging/ILogger.cs b/src/Downloader/Extensions/Logging/ILogger.cs deleted file mode 100644 index 030766de..00000000 --- a/src/Downloader/Extensions/Logging/ILogger.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace Downloader.Extensions.Logging; - -public interface ILogger -{ - void LogDebug(string message); - void LogInfo(string message); - void LogWarning(string message); - void LogError(string message); - void LogError(Exception exception, string message); - void LogCritical(string message); - void LogCritical(Exception exception, string message); - string Formatter(string logType, string message, Exception exception); - Task FlushAsync(); -} diff --git a/src/Downloader/IDownload.cs b/src/Downloader/IDownload.cs index 742d60f3..3306547c 100644 --- a/src/Downloader/IDownload.cs +++ b/src/Downloader/IDownload.cs @@ -1,28 +1,90 @@ -using System.ComponentModel; -using System; -using System.Threading.Tasks; +using System; +using System.ComponentModel; using System.IO; using System.Threading; +using System.Threading.Tasks; namespace Downloader; -public interface IDownload : IDisposable +/// +/// Represents an interface for managing file downloads. +/// +public interface IDownload : IDisposable, IAsyncDisposable { + /// + /// Gets the URL of the file to be downloaded. + /// public string Url { get; } + + /// + /// Gets the folder where the downloaded file will be saved. + /// public string Folder { get; } + + /// + /// Gets the name of the file to be saved. + /// public string Filename { get; } + + /// + /// Gets the size of the downloaded portion of the file. + /// public long DownloadedFileSize { get; } + + /// + /// Gets the total size of the file to be downloaded. + /// public long TotalFileSize { get; } + + /// + /// Gets the download package containing information about the download. + /// public DownloadPackage Package { get; } + + /// + /// Gets the current status of the download. + /// public DownloadStatus Status { get; } + /// + /// Occurs when the progress of a chunk download changes. + /// public event EventHandler ChunkDownloadProgressChanged; + + /// + /// Occurs when the download file operation is completed. + /// public event EventHandler DownloadFileCompleted; + + /// + /// Occurs when the overall download progress changes. + /// public event EventHandler DownloadProgressChanged; + + /// + /// Occurs when the download operation starts. + /// public event EventHandler DownloadStarted; + /// + /// Starts the download operation asynchronously. + /// + /// A cancellation token that can be used to cancel the download. + /// A task that represents the asynchronous download operation. The task result contains the downloaded stream. public Task StartAsync(CancellationToken cancellationToken = default); + + /// + /// Stops the download operation. + /// public void Stop(); + + /// + /// Pauses the download operation. + /// public void Pause(); + + /// + /// Resumes the paused download operation. + /// public void Resume(); -} +} \ No newline at end of file diff --git a/src/Downloader/IDownloadService.cs b/src/Downloader/IDownloadService.cs index 6065d771..f927aa70 100644 --- a/src/Downloader/IDownloadService.cs +++ b/src/Downloader/IDownloadService.cs @@ -1,4 +1,4 @@ -using Downloader.Extensions.Logging; +using Microsoft.Extensions.Logging; using System; using System.ComponentModel; using System.IO; @@ -7,6 +7,9 @@ namespace Downloader; +/// +/// Interface of download service which provide all downloader operations +/// public interface IDownloadService { /// diff --git a/src/Downloader/Packet.cs b/src/Downloader/Packet.cs index d4d8d68e..dd3140c3 100644 --- a/src/Downloader/Packet.cs +++ b/src/Downloader/Packet.cs @@ -2,24 +2,15 @@ namespace Downloader; -internal class Packet : IDisposable, ISizeableObject +internal class Packet(long position, byte[] data, int len) : IDisposable, ISizeableObject { - public volatile bool IsDisposed = false; - public byte[] Data { get; set; } - public int Length { get; set; } - public long Position { get; set; } + public byte[] Data { get; set; } = data; + public int Length { get; set; } = len; + public long Position { get; set; } = position; public long EndOffset => Position + Length; - public Packet(long position, byte[] data, int len) - { - Position = position; - Data = data; - Length = len; - } - public void Dispose() { - IsDisposed = true; Data = null; Position = 0; } diff --git a/src/Downloader/PauseToken.cs b/src/Downloader/PauseToken.cs index 02863cfe..337dc762 100644 --- a/src/Downloader/PauseToken.cs +++ b/src/Downloader/PauseToken.cs @@ -2,20 +2,35 @@ namespace Downloader; -public struct PauseToken +/// +/// Represents a pause token that can be used to pause and resume operations. +/// +public record PauseToken { - private readonly PauseTokenSource tokenSource; - public bool IsPaused => tokenSource?.IsPaused == true; + private readonly PauseTokenSource _tokenSource; + /// + /// Gets a value indicating whether the operation is paused. + /// + public bool IsPaused => _tokenSource?.IsPaused == true; + + /// + /// Initializes a new instance of the class. + /// + /// The pause token source. internal PauseToken(PauseTokenSource source) { - tokenSource = source; + _tokenSource = source; } + /// + /// Waits asynchronously while the operation is paused. + /// + /// A task that represents the asynchronous wait operation. public Task WaitWhilePausedAsync() { return IsPaused - ? tokenSource.WaitWhilePausedAsync() + ? _tokenSource.WaitWhilePausedAsync() : Task.FromResult(true); } -} +} \ No newline at end of file diff --git a/src/Downloader/PauseTokenSource.cs b/src/Downloader/PauseTokenSource.cs index b06f14a0..3f8c07d3 100644 --- a/src/Downloader/PauseTokenSource.cs +++ b/src/Downloader/PauseTokenSource.cs @@ -3,19 +3,35 @@ namespace Downloader; +/// +/// Represents a source for creating and managing pause tokens. +/// public class PauseTokenSource { - private volatile TaskCompletionSource tcsPaused; + private volatile TaskCompletionSource _tcsPaused; + /// + /// Gets the pause token associated with this source. + /// public PauseToken Token => new PauseToken(this); - public bool IsPaused => tcsPaused != null; + /// + /// Gets a value indicating whether the operation is paused. + /// + public bool IsPaused => _tcsPaused != null; + + /// + /// Pauses the operation by creating a new task completion source. + /// public void Pause() { // if (tcsPause == null) tcsPause = new TaskCompletionSource(); - Interlocked.CompareExchange(ref tcsPaused, new TaskCompletionSource(), null); + Interlocked.CompareExchange(ref _tcsPaused, new TaskCompletionSource(), null); } + /// + /// Resumes the operation by setting the result of the task completion source and resetting it. + /// public void Resume() { // we need to do this in a standard compare-exchange loop: @@ -24,13 +40,13 @@ public void Resume() // and the time we did the compare-exchange, repeat. while (true) { - var tcs = tcsPaused; + var tcs = _tcsPaused; if (tcs == null) return; // if(tcsPaused == tcs) tcsPaused = null; - if (Interlocked.CompareExchange(ref tcsPaused, null, tcs) == tcs) + if (Interlocked.CompareExchange(ref _tcsPaused, null, tcs) == tcs) { tcs.SetResult(true); return; @@ -38,8 +54,12 @@ public void Resume() } } + /// + /// Waits asynchronously while the operation is paused. + /// + /// A task that represents the asynchronous wait operation. internal Task WaitWhilePausedAsync() { - return tcsPaused?.Task ?? Task.FromResult(true); + return _tcsPaused?.Task ?? Task.FromResult(true); } -} +} \ No newline at end of file diff --git a/src/Downloader/Request.cs b/src/Downloader/Request.cs index 6d1f10fb..4132b874 100644 --- a/src/Downloader/Request.cs +++ b/src/Downloader/Request.cs @@ -9,6 +9,9 @@ namespace Downloader; +/// +/// Represents a class for making HTTP requests and handling response headers. +/// public class Request { private const string GetRequestMethod = "GET"; @@ -19,11 +22,24 @@ public class Request private readonly RequestConfiguration _configuration; private readonly Dictionary _responseHeaders; private readonly Regex _contentRangePattern; + + /// + /// Gets the URI address of the request. + /// public Uri Address { get; private set; } + /// + /// Initializes a new instance of the class with the specified address. + /// + /// The URL address to create the request for. public Request(string address) : this(address, new RequestConfiguration()) { } + /// + /// Initializes a new instance of the class with the specified address and configuration. + /// + /// The URL address to create the request for. + /// The configuration for the request. public Request(string address, RequestConfiguration config) { if (Uri.TryCreate(address, UriKind.Absolute, out Uri uri) == false) @@ -37,6 +53,11 @@ public Request(string address, RequestConfiguration config) _contentRangePattern = new Regex(@"bytes\s*((?\d*)\s*-\s*(?\d*)|\*)\s*\/\s*(?\d+|\*)", RegexOptions.Compiled); } + /// + /// Creates an HTTP request with the specified method. + /// + /// The HTTP method to use for the request. + /// An instance of representing the HTTP request. private HttpWebRequest GetRequest(string method) { HttpWebRequest request = WebRequest.CreateHttp(Address); @@ -78,11 +99,21 @@ private HttpWebRequest GetRequest(string method) return request; } + + /// + /// Creates an HTTP GET request. + /// + /// An instance of representing the HTTP GET request. public HttpWebRequest GetRequest() { return GetRequest(GetRequestMethod); } + /// + /// Fetches the response headers asynchronously. + /// + /// Indicates whether to add a range header to the request. + /// A task that represents the asynchronous operation. private async Task FetchResponseHeaders(bool addRange = true) { try @@ -99,7 +130,7 @@ private async Task FetchResponseHeaders(bool addRange = true) using WebResponse response = await request.GetResponseAsync().ConfigureAwait(false); EnsureResponseAddressIsSameWithOrigin(response); - if (response?.SupportsHeaders == true) + if (response.SupportsHeaders) { foreach (string headerKey in response.Headers.AllKeys) { @@ -128,6 +159,11 @@ exp.Response is HttpWebResponse response && } } + /// + /// Ensures that the response address is the same as the original address. + /// + /// The web response to check. + /// True if the response address is the same as the original address; otherwise, false. private bool EnsureResponseAddressIsSameWithOrigin(WebResponse response) { var redirectUri = GetRedirectUrl(response); @@ -140,6 +176,11 @@ private bool EnsureResponseAddressIsSameWithOrigin(WebResponse response) return true; } + /// + /// Gets the redirect URL from the web response. + /// + /// The web response to get the redirect URL from. + /// The redirect URL. public Uri GetRedirectUrl(WebResponse response) { // https://github.com/dotnet/runtime/issues/23264 @@ -156,9 +197,13 @@ public Uri GetRedirectUrl(WebResponse response) return Address; } + /// + /// Gets the file size asynchronously. + /// + /// A task that represents the asynchronous operation. The task result contains the file size. public async Task GetFileSize() { - if (await IsSupportDownloadInRange()) + if (await IsSupportDownloadInRange().ConfigureAwait(false)) { return GetTotalSizeFromContentRange(_responseHeaders); } @@ -166,6 +211,10 @@ public async Task GetFileSize() return GetTotalSizeFromContentLength(_responseHeaders); } + /// + /// Throws an exception if the download in range is not supported. + /// + /// A task that represents the asynchronous operation. public async Task ThrowIfIsNotSupportDownloadInRange() { var isSupport = await IsSupportDownloadInRange().ConfigureAwait(false); @@ -175,6 +224,10 @@ public async Task ThrowIfIsNotSupportDownloadInRange() } } + /// + /// Checks if the download in range is supported. + /// + /// A task that represents the asynchronous operation. The task result contains a boolean indicating whether the download in range is supported. public async Task IsSupportDownloadInRange() { await FetchResponseHeaders().ConfigureAwait(false); @@ -198,6 +251,11 @@ public async Task IsSupportDownloadInRange() return false; } + /// + /// Gets the total size from the content range headers. + /// + /// The headers to get the total size from. + /// The total size of the content. public long GetTotalSizeFromContentRange(Dictionary headers) { if (headers.TryGetValue(HeaderContentRangeKey, out string contentRange) && @@ -215,6 +273,11 @@ public long GetTotalSizeFromContentRange(Dictionary headers) return -1L; } + /// + /// Gets the total size from the content length headers. + /// + /// The headers to get the total size from. + /// The total size of the content. public long GetTotalSizeFromContentLength(Dictionary headers) { if (headers.TryGetValue(HeaderContentLengthKey, out string contentLengthText) && @@ -226,6 +289,10 @@ public long GetTotalSizeFromContentLength(Dictionary headers) return -1L; } + /// + /// Gets the file name asynchronously. + /// + /// A task that represents the asynchronous operation. The task result contains the file name. public async Task GetFileName() { var filename = await GetUrlDispositionFilenameAsync().ConfigureAwait(false); @@ -241,6 +308,10 @@ public async Task GetFileName() return filename; } + /// + /// Gets the file name from the URL. + /// + /// The file name extracted from the URL. public string GetFileNameFromUrl() { string filename = Path.GetFileName(Address.LocalPath); @@ -253,6 +324,10 @@ public string GetFileNameFromUrl() return filename; } + /// + /// Gets the file name from the URL disposition header asynchronously. + /// + /// A task that represents the asynchronous operation. The task result contains the file name. public async Task GetUrlDispositionFilenameAsync() { try @@ -289,10 +364,14 @@ public async Task GetUrlDispositionFilenameAsync() return null; } + /// + /// Converts the specified text from 'latin-1' encoding to 'utf-8' encoding. + /// + /// The text to convert. + /// The converted text in 'utf-8' encoding. public string ToUnicode(string otherEncodedText) { // decode 'latin-1' to 'utf-8' - string unicode = Encoding.UTF8.GetString(Encoding.GetEncoding("iso-8859-1").GetBytes(otherEncodedText)); - return unicode; + return Encoding.UTF8.GetString(Encoding.GetEncoding("iso-8859-1").GetBytes(otherEncodedText)); } } \ No newline at end of file diff --git a/src/Downloader/RequestConfiguration.cs b/src/Downloader/RequestConfiguration.cs index fd18249e..1eeee458 100644 --- a/src/Downloader/RequestConfiguration.cs +++ b/src/Downloader/RequestConfiguration.cs @@ -8,8 +8,14 @@ namespace Downloader; +/// +/// Represents the configuration settings for an HTTP request. +/// public class RequestConfiguration { + /// + /// Initializes a new instance of the class with default settings. + /// public RequestConfiguration() { Headers = new WebHeaderCollection(); @@ -22,171 +28,171 @@ public RequestConfiguration() MaximumAutomaticRedirections = 50; Pipelined = true; ProtocolVersion = HttpVersion.Version11; - Timeout = 30 * 1000; // 30 second - UserAgent = $"{nameof(Downloader)}/{Assembly.GetExecutingAssembly().GetName().Version.ToString(3)}"; + Timeout = 30 * 1000; // 30 seconds + UserAgent = $"{nameof(Downloader)}/{Assembly.GetExecutingAssembly().GetName().Version?.ToString(3)}"; } /// - /// A containing the value of the HTTP Accept header. The default value of this property is null. - /// Note: For additional information see section 14.1 of IETF RFC 2616 - HTTP/1.1. + /// A containing the value of the HTTP Accept header. The default value of this property is null. + /// Note: For additional information see section 14.1 of IETF RFC 2616 - HTTP/1.1. /// public string Accept { get; set; } /// - /// Set to true to allow the current request to - /// automatically - /// follow HTTP redirection headers to the new location of a resource. Default value is true. - /// The maximum number of redirections to follow is set by the - /// property. + /// Set to true to allow the current request to automatically + /// follow HTTP redirection headers to the new location of a resource. Default value is true. + /// The maximum number of redirections to follow is set by the + /// property. /// public bool AllowAutoRedirect { get; set; } /// - /// Gets or sets values indicating the level of authentication and impersonation used for this request. + /// Gets or sets values indicating the level of authentication and impersonation used for this request. /// public AuthenticationLevel AuthenticationLevel { get; set; } /// - /// A object that indicates the type of decompression that is used. - /// Default value is None; + /// A object that indicates the type of decompression that is used. + /// Default value is None; /// /// The object's current state does not allow this property to be set. public DecompressionMethods AutomaticDecompression { get; set; } /// - /// Gets or sets the cache policy for this request. + /// Gets or sets the cache policy for this request. /// public RequestCachePolicy CachePolicy { get; set; } /// - /// When overridden in a descendant class, gets or sets the name of the connection group for the request. + /// When overridden in a descendant class, gets or sets the name of the connection group for the request. /// public string ConnectionGroupName { get; set; } /// - /// The that contains the security certificates associated with this request. + /// The that contains the security certificates associated with this request. /// /// - /// The Framework caches SSL sessions as they are created and attempts to reuse a cached session for a new request, if - /// possible. - /// When attempting to reuse an SSL session, the Framework uses the first element of - /// (if there is one), - /// or tries to reuse an anonymous sessions if is empty. - /// For performance reasons, you shouldn't add a client certificate to a - /// unless you know the server will ask for it. + /// The Framework caches SSL sessions as they are created and attempts to reuse a cached session for a new request, if + /// possible. + /// When attempting to reuse an SSL session, the Framework uses the first element of + /// (if there is one), + /// or tries to reuse an anonymous sessions if is empty. + /// For performance reasons, you shouldn't add a client certificate to a + /// unless you know the server will ask for it. /// /// The value specified for a set operation is null. public X509CertificateCollection ClientCertificates { get; set; } /// - /// The ContentType property contains the media type of the request. - /// Values assigned to the ContentType property replace any existing contents - /// when the request sends the Content-type HTTP header. - /// The default value is null. + /// The ContentType property contains the media type of the request. + /// Values assigned to the ContentType property replace any existing contents + /// when the request sends the Content-type HTTP header. + /// The default value is null. /// public string ContentType { get; set; } /// - /// Gets or sets the cookies associated with the request. + /// Gets or sets the cookies associated with the request. /// public CookieContainer CookieContainer { get; set; } /// - /// A object containing the authentication - /// credentials associated with the current instance. The default is null. + /// A object containing the authentication + /// credentials associated with the current instance. The default is null. /// /// - /// The property contains authentication - /// information to identify the client making the request. The - /// property - /// can be either an instance of NetworkCredential, in which case the user, password, - /// and domain information contained in the NetworkCredential instance is used to authenticate the request, - /// or it can be an instance of CredentialCache, in which case the uniform resource identifier (URI) - /// of the request is used to determine the user, password, and domain information to use to authenticate the request. + /// The property contains authentication + /// information to identify the client making the request. The + /// property + /// can be either an instance of NetworkCredential, in which case the user, password, + /// and domain information contained in the NetworkCredential instance is used to authenticate the request, + /// or it can be an instance of CredentialCache, in which case the uniform resource identifier (URI) + /// of the request is used to determine the user, password, and domain information to use to authenticate the request. /// public ICredentials Credentials { get; set; } /// - /// A that contains the contents of the HTTP Expect header. The default value is null. - /// - /// The value specified for a set operation is "100-continue". This value is case insensitive. - /// + /// A that contains the contents of the HTTP Expect header. The default value is null. + /// + /// The value specified for a set operation is "100-continue". This value is case insensitive. + /// /// public string Expect { get; set; } /// - /// When overridden in a descendant class, gets or sets the collection of - /// header name/value pairs associated with the request. + /// When overridden in a descendant class, gets or sets the collection of + /// header name/value pairs associated with the request. /// public WebHeaderCollection Headers { get; set; } /// - /// A that contains the contents of the HTTP If-Modified-Since header. - /// The default value is the current date and time of the system. - /// - /// Note: For additional information see section 14.25 of IETF RFC 2616 - HTTP/1.1. - /// + /// A that contains the contents of the HTTP If-Modified-Since header. + /// The default value is the current date and time of the system. + /// + /// Note: For additional information see section 14.25 of IETF RFC 2616 - HTTP/1.1. + /// /// public DateTime? IfModifiedSince { get; set; } /// - /// Gets or sets the impersonation level for the current request. + /// Gets or sets the impersonation level for the current request. /// public TokenImpersonationLevel ImpersonationLevel { get; set; } /// - /// An application uses to - /// indicate a preference for persistent connections. When this property is true, - /// the application makes persistent connections to the servers that support them. + /// An application uses to + /// indicate a preference for persistent connections. When this property is true, + /// the application makes persistent connections to the servers that support them. /// public bool KeepAlive { get; set; } /// - /// A value that indicates the maximum number of redirection responses that the current instance - /// will follow. - /// The default value is implementation-specific. - /// - /// The value specified for a set operation is less than or equal to zero. - /// + /// A value that indicates the maximum number of redirection responses that the current instance + /// will follow. + /// The default value is implementation-specific. + /// + /// The value specified for a set operation is less than or equal to zero. + /// /// public int MaximumAutomaticRedirections { get; set; } /// - /// A that identifies the media type of the current request. - /// The default value is null. + /// A that identifies the media type of the current request. + /// The default value is null. /// /// - /// The value of this property affects the property. - /// When this property is set in the current instance, - /// the corresponding media type is chosen from the list of - /// character sets returned in the response HTTP Content-type header. + /// The value of this property affects the property. + /// When this property is set in the current instance, + /// the corresponding media type is chosen from the list of + /// character sets returned in the response HTTP Content-type header. /// public string MediaType { get; set; } + /// - /// An application uses this property to indicate a preference for pipelined connections. - /// If is true , - /// an application makes pipelined connections to servers that support them. The default is true. - /// Pipelined connections are made only when the property is - /// true. + /// An application uses this property to indicate a preference for pipelined connections. + /// If is true, + /// an application makes pipelined connections to servers that support them. The default is true. + /// Pipelined connections are made only when the property is + /// true. /// public bool Pipelined { get; set; } /// - /// Gets or sets a Boolean value that indicates whether to send HTTP preauthentication header - /// information with current instance without waiting for an authentication challenge - /// from the requested resource. - /// true to send a HTTP WWW-authenticate header with the current instance - /// without waiting for an authentication challenge from the requested resource; - /// otherwise, false . The default is false . + /// Gets or sets a Boolean value that indicates whether to send HTTP preauthentication header + /// information with current instance without waiting for an authentication challenge + /// from the requested resource. + /// true to send a HTTP WWW-authenticate header with the current instance + /// without waiting for an authentication challenge from the requested resource; + /// otherwise, false. The default is false. /// public bool PreAuthenticate { get; set; } /// - /// A that represents the HTTP version to use for the request. - /// The default is . - /// The class supports only versions 1.0 and 1.1 of HTTP. - /// Setting to a different version - /// causes a ArgumentException exception to be thrown. + /// A that represents the HTTP version to use for the request. + /// The default is . + /// The class supports only versions 1.0 and 1.1 of HTTP. + /// Setting to a different version + /// causes a ArgumentException exception to be thrown. /// /// /// The HTTP version is set to a value other than 1.0 or 1.1. @@ -194,73 +200,73 @@ public RequestConfiguration() public Version ProtocolVersion { get; set; } /// - /// The property identifies - /// the WebProxy instance to use to communicate with the destination server. - /// To specify that no proxy should be used, set the . - /// Default value is null. + /// The property identifies + /// the WebProxy instance to use to communicate with the destination server. + /// To specify that no proxy should be used, set the . + /// Default value is null. /// - /// A set operation was requested and the specified value was null . + /// A set operation was requested and the specified value was null. /// - /// A set operation was requested but data has already been sent to the request - /// stream. + /// A set operation was requested but data has already been sent to the request stream. /// /// The caller does not have permission for the requested operation. public IWebProxy Proxy { get; set; } /// - /// A containing the value of the HTTP Referer header. The default value is null. - /// Note: For additional information see section 14.36 of IETF RFC 2616 - HTTP/1.1. + /// A containing the value of the HTTP Referer header. The default value is null. + /// Note: For additional information see section 14.36 of IETF RFC 2616 - HTTP/1.1. /// public string Referer { get; set; } /// - /// When System.Net.HttpWebRequest.SendChunked is true , the request sends data to the destination in segments. - /// The destination server is required to support receiving chunked data. The default value is false. - /// Set this property to true only if the server specified by the System.Net.HttpWebRequest. - /// Address property of the current instance accepts chunked data (i.e. is HTTP/1.1 or greater in compliance). - /// If the server does not accept chunked data, buffer all data to be written and send a HTTP Content-Length header - /// with the buffered data. + /// When System.Net.HttpWebRequest.SendChunked is true, the request sends data to the destination in segments. + /// The destination server is required to support receiving chunked data. The default value is false. + /// Set this property to true only if the server specified by the System.Net.HttpWebRequest. + /// Address property of the current instance accepts chunked data (i.e. is HTTP/1.1 or greater in compliance). + /// If the server does not accept chunked data, buffer all data to be written and send a HTTP Content-Length header + /// with the buffered data. /// /// - /// A set operation was requested but data has already been written to the - /// request data stream. + /// A set operation was requested but data has already been written to the request data stream. /// public bool SendChunked { get; set; } + /// + /// Gets or sets the timeout value in milliseconds for the and + /// methods. + /// public int Timeout { get; set; } /// - /// A String that contains the value of the HTTP Transfer-encoding header. The default value is null. - /// Clearing by setting it to null has no effect - /// on the value of . - /// Values assigned to the property replace any existing - /// contents. - /// For additional information see section 14.41 of IETF RFC 2616 - HTTP/1.1. + /// A String that contains the value of the HTTP Transfer-encoding header. The default value is null. + /// Clearing by setting it to null has no effect + /// on the value of . + /// Values assigned to the property replace any existing contents. + /// For additional information see section 14.41 of IETF RFC 2616 - HTTP/1.1. /// /// - /// is set when - /// is false. + /// is set when + /// is false. /// /// - /// is set to the value "Chunked". This value is case - /// insensitive. + /// is set to the value "Chunked". This value is case insensitive. /// public string TransferEncoding { get; set; } /// - /// true if the default credentials are used; otherwise, false. The default value is false. - /// Set this property to true when requests made by this object should, - /// if requested by the server, be authenticated using the credentials of the currently logged on user. - /// For client applications, this is the desired behavior in most scenarios. For middle-tier applications, - /// such as ASP.NET applications, instead of using this property, you would typically - /// set the Credentials property to the credentials of the client on whose behalf the request is made. + /// true if the default credentials are used; otherwise, false. The default value is false. + /// Set this property to true when requests made by this object should, + /// if requested by the server, be authenticated using the credentials of the currently logged on user. + /// For client applications, this is the desired behavior in most scenarios. For middle-tier applications, + /// such as ASP.NET applications, instead of using this property, you would typically + /// set the Credentials property to the credentials of the client on whose behalf the request is made. /// /// You attempted to set this property after the request was sent. public bool UseDefaultCredentials { get; set; } /// - /// A containing the value of the HTTP User-agent header. - /// The default value is "/{}". + /// A containing the value of the HTTP User-agent header. + /// The default value is "/{}". /// public string UserAgent { get; set; } } \ No newline at end of file diff --git a/src/Downloader/TaskStateManagement.cs b/src/Downloader/TaskStateManagement.cs index 715327e0..d3610f3a 100644 --- a/src/Downloader/TaskStateManagement.cs +++ b/src/Downloader/TaskStateManagement.cs @@ -1,4 +1,4 @@ -using Downloader.Extensions.Logging; +using Microsoft.Extensions.Logging; using System; using System.Collections.Concurrent; using System.Runtime.CompilerServices; @@ -6,58 +6,84 @@ namespace Downloader; +/// +/// Manages the state and exceptions of a task. +/// public class TaskStateManagement { private readonly ConcurrentQueue _exceptions = new ConcurrentQueue(); + + /// + /// inject from DI in upper layers by user + /// protected readonly ILogger Logger; /// - /// Gets the System.AggregateException that caused the ConcurrentStream - /// to end prematurely. If the ConcurrentStream completed successfully - /// or has not yet thrown any exceptions, this will return null. + /// Gets the that caused the task to end prematurely. + /// If the task completed successfully or has not yet thrown any exceptions, this will return null. /// public AggregateException Exception { get; private set; } /// - /// Gets a value that indicates whether the task has completed. - /// Result is true if the task has completed (that is, the task is in one of the three final - /// states: TaskStatus.RanToCompletion, TaskStatus.Faulted or TaskStatus.Canceled); - /// otherwise, false. + /// Gets a value that indicates whether the task has completed. + /// Result is true if the task has completed (that is, the task is in one of the three final + /// states: , , or ); + /// otherwise, false. /// public bool IsCompleted => Status == TaskStatus.RanToCompletion || Status == TaskStatus.Faulted || Status == TaskStatus.Canceled; /// - /// Gets whether this ConcurrentStream.Task instance has completed execution - /// due to being canceled. + /// Gets whether this task instance has completed execution due to being canceled. /// Result is true if the task has completed due to being canceled; otherwise false. /// public bool IsCanceled => Status == TaskStatus.Canceled; /// /// Gets whether the task ran to completion. - /// Result is true if the task ran to completion; otherwise false. + /// Result is true if the task ran to completion; otherwise false. /// public bool IsCompletedSuccessfully => Status == TaskStatus.RanToCompletion; /// - /// Gets whether the ConcurrentStream.Task completed due to an unhandled exception. - /// Reslt is true if the task has thrown an unhandled exception; otherwise false. - /// + /// Gets whether the task completed due to an unhandled exception. + /// Result is true if the task has thrown an unhandled exception; otherwise false. + /// public bool IsFaulted => Status == TaskStatus.Faulted; /// - /// Gets the TaskStatus of this task. + /// Gets the of this task. /// public TaskStatus Status { get; private set; } = TaskStatus.Created; + /// + /// Initializes a new instance of the class. + /// + /// The logger to use for logging exceptions. public TaskStateManagement(ILogger logger = null) { Logger = logger; } + /// + /// Sets the state of the task to . + /// internal void StartState() => Status = TaskStatus.Running; + + /// + /// Sets the state of the task to . + /// internal void CompleteState() => Status = TaskStatus.RanToCompletion; + + /// + /// Sets the state of the task to . + /// internal void CancelState() => Status = TaskStatus.Canceled; + + /// + /// Sets the state of the task to and records the exception. + /// + /// The exception that caused the task to fault. + /// The name of the caller method (automatically populated). internal void SetException(Exception exp, [CallerMemberName] string callerName = null) { Logger?.LogCritical(exp, $"TaskStateManagement: SetException catch an exception on {callerName}"); @@ -65,4 +91,4 @@ internal void SetException(Exception exp, [CallerMemberName] string callerName = _exceptions.Enqueue(exp); Exception = new AggregateException(_exceptions); } -} +} \ No newline at end of file diff --git a/src/Samples/Downloader.Sample.Framework/App.config b/src/Samples/Downloader.Sample.Framework/App.config deleted file mode 100644 index 8d234373..00000000 --- a/src/Samples/Downloader.Sample.Framework/App.config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/src/Samples/Downloader.Sample.Framework/DownloadList.json b/src/Samples/Downloader.Sample.Framework/DownloadList.json deleted file mode 100644 index 0dc6d695..00000000 --- a/src/Samples/Downloader.Sample.Framework/DownloadList.json +++ /dev/null @@ -1,10 +0,0 @@ -[ - { - "FileName": "D:\\TestDownload\\Hello.mp4", - "Url": "https://cdn.pixabay.com/vimeo/576083058/Hello%20-%2081605.mp4?width=1920&hash=e6f56273dcd2f28fd1a9fe6e77f66d7e157b33f6&download=1" - }, - { - "FileName": "D:\\TestDownload\\VS.exe", - "Url": "https://c2rsetup.officeapps.live.com/c2r/downloadVS.aspx?sku=community&channel=Release&version=VS2022&source=VSLandingPage&includeRecommended=true&cid=2030:9bf2104738684908988ca7dcd5dafed1" - } -] \ No newline at end of file diff --git a/src/Samples/Downloader.Sample.Framework/Downloader.Sample.Framework.csproj b/src/Samples/Downloader.Sample.Framework/Downloader.Sample.Framework.csproj deleted file mode 100644 index 9fa029b9..00000000 --- a/src/Samples/Downloader.Sample.Framework/Downloader.Sample.Framework.csproj +++ /dev/null @@ -1,111 +0,0 @@ - - - - - Debug - AnyCPU - {DFA570F0-FFB5-4790-86E1-22D8AD2F77B4} - Exe - Downloader.Sample.Framework - Downloader.Sample.Framework - v4.6.2 - 512 - true - true - - publish\ - true - Disk - false - Foreground - 7 - Days - false - false - true - 0 - 1.0.0.%2a - false - false - true - - - AnyCPU - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - AnyCPU - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - ..\..\packages\Newtonsoft.Json.13.0.1\lib\net45\Newtonsoft.Json.dll - - - ..\..\packages\ShellProgressBar.5.2.0\lib\net461\ShellProgressBar.dll - - - - - ..\..\packages\System.Runtime.InteropServices.RuntimeInformation.4.3.0\lib\net45\System.Runtime.InteropServices.RuntimeInformation.dll - - - ..\..\packages\System.Text.Encoding.CodePages.4.0.1\lib\net46\System.Text.Encoding.CodePages.dll - - - - - - - - - - - DownloadItem.cs - - - Helper.cs - - - Program.cs - - - - - - - Always - - - - - - {cc0c931b-bb7e-42d2-836e-219908c501c9} - Downloader - - - - - False - Microsoft .NET Framework 4.6.2 %28x86 and x64%29 - true - - - False - .NET Framework 3.5 SP1 - false - - - - \ No newline at end of file diff --git a/src/Samples/Downloader.Sample.Framework/Properties/AssemblyInfo.cs b/src/Samples/Downloader.Sample.Framework/Properties/AssemblyInfo.cs deleted file mode 100644 index c4c97af4..00000000 --- a/src/Samples/Downloader.Sample.Framework/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("Downloader.Sample.Framework")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("Downloader.Sample.Framework")] -[assembly: AssemblyCopyright("Copyright © 2022")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("dfa570f0-ffb5-4790-86e1-22d8ad2f77b4")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/Samples/Downloader.Sample.Framework/packages.config b/src/Samples/Downloader.Sample.Framework/packages.config deleted file mode 100644 index 756996b1..00000000 --- a/src/Samples/Downloader.Sample.Framework/packages.config +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/src/Samples/Downloader.Sample/DownloadItem.cs b/src/Samples/Downloader.Sample/DownloadItem.cs index 47a7b461..bf0cf99f 100644 --- a/src/Samples/Downloader.Sample/DownloadItem.cs +++ b/src/Samples/Downloader.Sample/DownloadItem.cs @@ -1,10 +1,12 @@ -using System.IO; +using System.Diagnostics.CodeAnalysis; +using System.IO; namespace Downloader.Sample; +[ExcludeFromCodeCoverage] public class DownloadItem { - public string _folderPath; + private string _folderPath; public string FolderPath { get => _folderPath ?? Path.GetDirectoryName(FileName); set => _folderPath = value; } public string FileName { get; set; } diff --git a/src/Samples/Downloader.Sample/Downloader.Sample.csproj b/src/Samples/Downloader.Sample/Downloader.Sample.csproj index efe0837c..660938b8 100644 --- a/src/Samples/Downloader.Sample/Downloader.Sample.csproj +++ b/src/Samples/Downloader.Sample/Downloader.Sample.csproj @@ -3,7 +3,7 @@ latestMajor Exe - netcoreapp3.1;net6.0; + net8.0 diff --git a/src/Samples/Downloader.Sample/Helper.cs b/src/Samples/Downloader.Sample/Helper.cs index 01795068..9ee022a6 100644 --- a/src/Samples/Downloader.Sample/Helper.cs +++ b/src/Samples/Downloader.Sample/Helper.cs @@ -1,7 +1,9 @@ using System; +using System.Diagnostics.CodeAnalysis; namespace Downloader.Sample; +[ExcludeFromCodeCoverage] public static class Helper { public static string CalcMemoryMensurableUnit(this long bytes) @@ -27,7 +29,7 @@ public static string CalcMemoryMensurableUnit(this double bytes) return result; } - public static void UpdateTitleInfo(this DownloadProgressChangedEventArgs e, bool isPaused) + public static string UpdateTitleInfo(this DownloadProgressChangedEventArgs e, bool isPaused) { int estimateTime = (int)Math.Ceiling((e.TotalBytesToReceive - e.ReceivedBytesSize) / e.AverageBytesPerSecondSpeed); string timeLeftUnit = "seconds"; @@ -49,12 +51,12 @@ public static void UpdateTitleInfo(this DownloadProgressChangedEventArgs e, bool string progressPercentage = $"{e.ProgressPercentage:F3}".Replace("/", "."); string usedMemory = GC.GetTotalMemory(false).CalcMemoryMensurableUnit(); - Console.Title = $"{estimateTime} {timeLeftUnit} left - " + - $"{speed}/s (avg: {avgSpeed}/s) - " + - $"{progressPercentage}% - " + - $"[{bytesReceived} of {totalBytesToReceive}] " + - $"Active Chunks: {e.ActiveChunks} - " + - $"[{usedMemory} memory] " + - (isPaused ? " - Paused" : ""); + return $"{estimateTime} {timeLeftUnit} left - " + + $"{speed}/s (avg: {avgSpeed}/s) - " + + $"{progressPercentage}% - " + + $"[{bytesReceived} of {totalBytesToReceive}] " + + $"Active Chunks: {e.ActiveChunks} - " + + $"[{usedMemory} memory] " + + (isPaused ? " - Paused" : ""); } } diff --git a/src/Samples/Downloader.Sample/Program.Config.cs b/src/Samples/Downloader.Sample/Program.Config.cs index bc2b6ad3..b21b2a15 100644 --- a/src/Samples/Downloader.Sample/Program.Config.cs +++ b/src/Samples/Downloader.Sample/Program.Config.cs @@ -15,7 +15,7 @@ private static DownloadConfiguration GetDownloadConfiguration() ChunkCount = 8, // file parts to download, default value is 1 MaximumBytesPerSecond = 1024 * 1024 * 10, // download speed limited to 10MB/s, default values is zero or unlimited MaxTryAgainOnFailover = 5, // the maximum number of times to fail - MaximumMemoryBufferBytes = 1024 * 1024 * 200, // release memory buffer after each 200MB + MaximumMemoryBufferBytes = 1024 * 1024 * 500, // release memory buffer after each 500MB ParallelDownload = true, // download parts of file as parallel or not. Default value is false ParallelCount = 8, // number of parallel downloads. The default value is the same as the chunk count Timeout = 3000, // timeout (millisecond) per stream block reader, default value is 1000 @@ -25,6 +25,7 @@ private static DownloadConfiguration GetDownloadConfiguration() ClearPackageOnCompletionWithFailure = true, // Clear package and downloaded data when download completed with failure, default value is false MinimumSizeOfChunking = 1024, // minimum size of chunking to download a file in multiple parts, default value is 512 ReserveStorageSpaceBeforeStartingDownload = false, // Before starting the download, reserve the storage space of the file as file size, default value is false + EnableLiveStreaming = false, // Get on demand downloaded data with ReceivedBytes on downloadProgressChanged event RequestConfiguration = { // config and customize request headers diff --git a/src/Samples/Downloader.Sample/Program.cs b/src/Samples/Downloader.Sample/Program.cs index d4575e78..c01e082e 100644 --- a/src/Samples/Downloader.Sample/Program.cs +++ b/src/Samples/Downloader.Sample/Program.cs @@ -1,4 +1,4 @@ -using Downloader.Extensions.Logging; +using Microsoft.Extensions.Logging; using Newtonsoft.Json; using ShellProgressBar; using System; @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -13,6 +14,7 @@ namespace Downloader.Sample; +[ExcludeFromCodeCoverage] public partial class Program { private const string DownloadListFile = "download.json"; @@ -30,9 +32,7 @@ private static async Task Main() { try { -#if NETCOREAPP DummyHttpServer.HttpServer.Run(3333); -#endif await Task.Delay(1000); Console.Clear(); Initial(); @@ -42,18 +42,17 @@ private static async Task Main() catch (Exception e) { Console.Clear(); - Console.Error.WriteLine(e); + await Console.Error.WriteLineAsync(e.Message); Debugger.Break(); } finally { -#if NETCOREAPP await DummyHttpServer.HttpServer.Stop(); -#endif } - Console.WriteLine("END"); + await Console.Out.WriteLineAsync("END"); } + private static void Initial() { CancelAllTokenSource = new CancellationTokenSource(); @@ -65,7 +64,6 @@ private static void Initial() ForegroundColorDone = ConsoleColor.DarkGreen, BackgroundColor = ConsoleColor.DarkGray, BackgroundCharacter = '\u2593', - EnableTaskBarProgress = true, ProgressBarOnBottom = false, ProgressCharacter = '#' }; @@ -76,18 +74,26 @@ private static void Initial() ProgressBarOnBottom = true }; } + private static void KeyboardHandler() { - ConsoleKeyInfo cki; - Console.CancelKeyPress += CancelAll; + Console.CancelKeyPress += (_, _) => CancelAll(); while (true) { - cki = Console.ReadKey(true); - if (CurrentDownloadConfiguration != null) + while (Console.KeyAvailable) { + ConsoleKeyInfo cki = Console.ReadKey(true); switch (cki.Key) { + case ConsoleKey.C: + if (cki.Modifiers == ConsoleModifiers.Control) + { + CancelAll(); + return; + } + + break; case ConsoleKey.P: CurrentDownloadService?.Pause(); Console.Beep(); @@ -99,16 +105,19 @@ private static void KeyboardHandler() CurrentDownloadService?.CancelAsync(); break; case ConsoleKey.UpArrow: - CurrentDownloadConfiguration.MaximumBytesPerSecond *= 2; + if (CurrentDownloadConfiguration != null) + CurrentDownloadConfiguration.MaximumBytesPerSecond *= 2; break; case ConsoleKey.DownArrow: - CurrentDownloadConfiguration.MaximumBytesPerSecond /= 2; + if (CurrentDownloadConfiguration != null) + CurrentDownloadConfiguration.MaximumBytesPerSecond /= 2; break; } } } } - private static void CancelAll(object sender, ConsoleCancelEventArgs e) + + private static void CancelAll() { CancelAllTokenSource.Cancel(); CurrentDownloadService?.CancelAsync(); @@ -132,10 +141,12 @@ private static async Task DownloadAll(IEnumerable downloadList, Ca // begin download from url await DownloadFile(downloadItem).ConfigureAwait(false); + + await Task.Yield(); } } - private static async Task DownloadFile(DownloadItem downloadItem) + private static async Task DownloadFile(DownloadItem downloadItem) { CurrentDownloadConfiguration = GetDownloadConfiguration(); CurrentDownloadService = CreateDownloadService(CurrentDownloadConfiguration); @@ -143,18 +154,23 @@ private static async Task DownloadFile(DownloadItem downloadIt { Logger = FileLogger.Factory(downloadItem.FolderPath); CurrentDownloadService.AddLogger(Logger); - await CurrentDownloadService.DownloadFileTaskAsync(downloadItem.Url, new DirectoryInfo(downloadItem.FolderPath)).ConfigureAwait(false); + await CurrentDownloadService + .DownloadFileTaskAsync(downloadItem.Url, new DirectoryInfo(downloadItem.FolderPath)) + .ConfigureAwait(false); } else { Logger = FileLogger.Factory(downloadItem.FolderPath, Path.GetFileName(downloadItem.FileName)); CurrentDownloadService.AddLogger(Logger); - await CurrentDownloadService.DownloadFileTaskAsync(downloadItem.Url, downloadItem.FileName).ConfigureAwait(false); + await CurrentDownloadService.DownloadFileTaskAsync(downloadItem.Url, downloadItem.FileName) + .ConfigureAwait(false); } if (downloadItem.ValidateData) { - var isValid = await ValidateDataAsync(CurrentDownloadService.Package.FileName, CurrentDownloadService.Package.TotalFileSize).ConfigureAwait(false); + var isValid = + await ValidateDataAsync(CurrentDownloadService.Package.FileName, + CurrentDownloadService.Package.TotalFileSize).ConfigureAwait(false); if (!isValid) { var message = "Downloaded data is invalid: " + CurrentDownloadService.Package.FileName; @@ -162,8 +178,6 @@ private static async Task DownloadFile(DownloadItem downloadIt throw new InvalidDataException(message); } } - - return CurrentDownloadService; } private static async Task ValidateDataAsync(string filename, long size) @@ -174,7 +188,8 @@ private static async Task ValidateDataAsync(string filename, long size) var next = stream.ReadByte(); if (next != i % 256) { - Logger?.LogWarning($"Sample.Program.ValidateDataAsync(): Data at index [{i}] of `{filename}` is `{next}`, expectation is `{i % 256}`"); + Logger?.LogWarning( + $"Sample.Program.ValidateDataAsync(): Data at index [{i}] of `{filename}` is `{next}`, expectation is `{i % 256}`"); return false; } } @@ -182,15 +197,19 @@ private static async Task ValidateDataAsync(string filename, long size) return true; } - private static void WriteKeyboardGuidLines() + private static async Task WriteKeyboardGuidLines() { Console.Clear(); - Console.WriteLine("Press Esc to Stop current file download"); - Console.WriteLine("Press P to Pause and R to Resume downloading"); - Console.WriteLine("Press Up Arrow to Increase download speed 2X"); - Console.WriteLine("Press Down Arrow to Decrease download speed 2X"); - Console.WriteLine(); + Console.Beep(); + Console.CursorVisible = false; + await Console.Out.WriteLineAsync("Press Esc to Stop current file download"); + await Console.Out.WriteLineAsync("Press P to Pause and R to Resume downloading"); + await Console.Out.WriteLineAsync("Press Up Arrow to Increase download speed 2X"); + await Console.Out.WriteLineAsync("Press Down Arrow to Decrease download speed 2X \n"); + await Console.Out.FlushAsync(); + await Task.Yield(); } + private static DownloadService CreateDownloadService(DownloadConfiguration config) { var downloadService = new DownloadService(config); @@ -198,15 +217,15 @@ private static DownloadService CreateDownloadService(DownloadConfiguration confi // Provide `FileName` and `TotalBytesToReceive` at the start of each downloads downloadService.DownloadStarted += OnDownloadStarted; - // Provide any information about chunker downloads, + // Provide any information about chunk downloads, // like progress percentage per chunk, speed, - // total received bytes and received bytes array to live streaming. + // total received bytes and received bytes array to live-streaming. downloadService.ChunkDownloadProgressChanged += OnChunkDownloadProgressChanged; // Provide any information about download progress, // like progress percentage of sum of chunks, total speed, // average speed, total received bytes and received bytes array - // to live streaming. + // to live-streaming. downloadService.DownloadProgressChanged += OnDownloadProgressChanged; // Download completed event that can include occurred errors or @@ -216,30 +235,32 @@ private static DownloadService CreateDownloadService(DownloadConfiguration confi return downloadService; } - private static void OnDownloadStarted(object sender, DownloadStartedEventArgs e) + private static async void OnDownloadStarted(object sender, DownloadStartedEventArgs e) { - WriteKeyboardGuidLines(); - ConsoleProgress = new ProgressBar(10000, $"Downloading {Path.GetFileName(e.FileName)} ", ProcessBarOption); + await WriteKeyboardGuidLines(); + var progressMsg = $"Downloading {Path.GetFileName(e.FileName)} "; + await Console.Out.WriteLineAsync(progressMsg); + ConsoleProgress = new ProgressBar(10000, progressMsg, ProcessBarOption); } private static void OnDownloadFileCompleted(object sender, AsyncCompletedEventArgs e) { ConsoleProgress?.Tick(10000); + var lastState = " DONE"; if (e.Cancelled) { - ConsoleProgress.Message += " CANCELED"; + lastState = " CANCELED"; } else if (e.Error != null) { + lastState = " ERROR"; Console.Error.WriteLine(e.Error); Debugger.Break(); } - else - { - ConsoleProgress.Message += " DONE"; - Console.Title = "100%"; - } + + if (ConsoleProgress != null) + ConsoleProgress.Message += lastState; foreach (var child in ChildConsoleProgresses.Values) child.Dispose(); @@ -253,13 +274,17 @@ private static void OnChunkDownloadProgressChanged(object sender, DownloadProgre ChildProgressBar progress = ChildConsoleProgresses.GetOrAdd(e.ProgressId, id => ConsoleProgress?.Spawn(10000, $"chunk {id}", ChildOption)); progress.Tick((int)(e.ProgressPercentage * 100)); - var activeChunksCount = e.ActiveChunks; // Running chunks count + // var activeChunksCount = e.ActiveChunks; // Running chunks count } private static void OnDownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e) { - ConsoleProgress.Tick((int)(e.ProgressPercentage * 100)); + var isPaused = false; if (sender is DownloadService ds) - e.UpdateTitleInfo(ds.IsPaused); + { + isPaused = ds.IsPaused; + } + var title = e.UpdateTitleInfo(isPaused); + ConsoleProgress.Tick((int)(e.ProgressPercentage * 100), title); } } \ No newline at end of file diff --git a/src/Samples/Downloader.Sample/download.json b/src/Samples/Downloader.Sample/download.json index fe9d7185..766e01d3 100644 --- a/src/Samples/Downloader.Sample/download.json +++ b/src/Samples/Downloader.Sample/download.json @@ -1,21 +1,21 @@ [ { - "FileName": "D:\\TestDownload\\LocalFile10GB_Raw.dat", + "FileName": "./downloads/LocalFile10GB_Raw.dat", "Url": "http://localhost:3333/dummyfile/file/size/10737418240", "ValidateData": true }, { - "FolderPath": "D:\\TestDownload", + "FolderPath": "./downloads/", "Url": "http://localhost:3333/dummyfile/file/LocalFile100MB_WithContentDisposition.dat/size/104857600", "ValidateData": true }, { - "FolderPath": "D:\\TestDownload", + "FolderPath": "./downloads/", "Url": "http://localhost:3333/dummyfile/noheader/file/LocalFile100MB_WithoutHeader.dat?size=104857600", "ValidateData": true }, { - "FolderPath": "D:\\TestDownload", + "FolderPath": "./downloads/", "Url": "http://localhost:3333/dummyfile/file/LocalFile100MB.dat?size=104857600", "ValidateData": true }