Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Version 9.0 Impl #134

Merged
merged 22 commits into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file removed AnakinOSS.snk
Binary file not shown.
6 changes: 6 additions & 0 deletions CommonUtilities.sln
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommonUtilities.SimplePipel
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommonUtilities.SimplePipeline.Test", "src\CommonUtilities.SimplePipeline\test\CommonUtilities.SimplePipeline.Test.csproj", "{C8BF3F01-B1D5-4C29-9164-6DC7B9744589}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommonUtilities.TestingUtilities", "src\CommonUtilities.TestingUtilities\CommonUtilities.TestingUtilities.csproj", "{99A3B9B4-6482-410A-A001-9D62F4B259CC}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -115,6 +117,10 @@ Global
{C8BF3F01-B1D5-4C29-9164-6DC7B9744589}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C8BF3F01-B1D5-4C29-9164-6DC7B9744589}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C8BF3F01-B1D5-4C29-9164-6DC7B9744589}.Release|Any CPU.Build.0 = Release|Any CPU
{99A3B9B4-6482-410A-A001-9D62F4B259CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{99A3B9B4-6482-410A-A001-9D62F4B259CC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{99A3B9B4-6482-410A-A001-9D62F4B259CC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{99A3B9B4-6482-410A-A001-9D62F4B259CC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
16 changes: 6 additions & 10 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,23 @@
</PropertyGroup>
<PropertyGroup>
<Product>.NET Common Utilities</Product>
<Copyright>Copyright © AnakinRaW 2023</Copyright>
<Copyright>Copyright © AnakinRaW 2024</Copyright>
<Authors>AnakinRaW</Authors>
<Owners>AnakinRaW</Owners>
<PackageProjectUrl>https://github.com/AnakinRaW/CommonUtilities</PackageProjectUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>
<PropertyGroup>
<SignAssembly Condition="'$(Configuration)' == 'Release'">True</SignAssembly>
<AssemblyOriginatorKeyFile>$(MSBuildThisFileDirectory)AnakinOSS.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Nerdbank.GitVersioning" Condition="!Exists('packages.config')">
<PrivateAssets>all</PrivateAssets>
<Version>3.6.133</Version>
</PackageReference>
<PackageReference Include="SauceControl.InheritDoc" Version="2.0.0" PrivateAssets="all" />
<PackageReference Include="SauceControl.InheritDoc" Version="2.0.1" PrivateAssets="all" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Nerdbank.GitVersioning" Condition="!Exists('packages.config')">
<PrivateAssets>all</PrivateAssets>
<Version>3.6.133</Version>
</PackageReference>
<None Include="$(MSBuildThisFileDirectory)README.md" Pack="true" PackagePath="" />
</ItemGroup>
</Project>
7 changes: 0 additions & 7 deletions src/CommonUtilities.DownloadManager/src/AssemblyInfo.cs

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,10 @@
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
<PackageReference Include="System.Net.Http" Version="4.3.4" />
<PackageReference Include="Validation" Version="2.5.51" PrivateAssets="compile" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\CommonUtilities\src\CommonUtilities.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public record DownloadManagerConfiguration : IDownloadManagerConfiguration
public bool AllowEmptyFileDownload { get; init; }

/// <inheritdoc/>
public VerificationPolicy VerificationPolicy { get; init; }
public ValidationPolicy ValidationPolicy { get; init; }

/// <inheritdoc/>
public InternetClient InternetClient { get; init; }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using System.Threading;
using Validation;
using System;
using System.Threading;

namespace AnakinRaW.CommonUtilities.DownloadManager.Configuration;

Expand All @@ -14,7 +14,8 @@ public abstract class DownloadManagerConfigurationProviderBase : IDownloadManage
public IDownloadManagerConfiguration GetConfiguration()
{
var configuration = LazyInitializer.EnsureInitialized(ref _configuration, CreateConfiguration);
Assumes.NotNull(configuration);
if (configuration is null)
throw new InvalidOperationException();
return configuration;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public interface IDownloadManagerConfiguration
/// <summary>
/// Specifies how verification after the download shall be handled.
/// </summary>
VerificationPolicy VerificationPolicy { get; }
ValidationPolicy ValidationPolicy { get; }

/// <summary>
/// The <see cref="Configuration.InternetClient"/> implementation which shall get used.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace AnakinRaW.CommonUtilities.DownloadManager.Configuration;

/// <summary>
/// Options how validation at the end of a download shall be handled.
/// </summary>
public enum ValidationPolicy
{
/// <summary>
/// Validation will always be skipped.
/// </summary>
NoValidation,
/// <summary>
/// Validation is optional.
/// </summary>
Optional,
/// <summary>
/// Validation is required.
/// </summary>
Required,
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace AnakinRaW.CommonUtilities.DownloadManager;
/// <summary>
/// Aggregated exception which holds all <see cref="DownloadFailureInformation"/> of a file download operation.
/// </summary>
public class DownloadFailedException : Exception
public sealed class DownloadFailedException : Exception
{
/// <summary>
/// All failures during a file download operation.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace AnakinRaW.CommonUtilities.DownloadManager;
/// <summary>
/// Contains information about a failed download
/// </summary>
public class DownloadFailureInformation
public sealed class DownloadFailureInformation
{
/// <summary>
/// The exception of the download failure.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace AnakinRaW.CommonUtilities.DownloadManager;
/// <summary>
/// The supported source file location of an <see cref="IDownloadProvider"/>.
/// </summary>
public enum DownloadSource
public enum DownloadKind
{
/// <summary>
/// The provider supports downloading files from the local file system or local network.
Expand Down
85 changes: 52 additions & 33 deletions src/CommonUtilities.DownloadManager/src/DownloadManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,23 @@
using System.Threading.Tasks;
using AnakinRaW.CommonUtilities.DownloadManager.Configuration;
using AnakinRaW.CommonUtilities.DownloadManager.Providers;
using AnakinRaW.CommonUtilities.Verification;
using AnakinRaW.CommonUtilities.DownloadManager.Validation;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Validation;

namespace AnakinRaW.CommonUtilities.DownloadManager;

/// <summary>
/// Download manager which supports local file system and HTTP downloads by default.
/// </summary>
public class DownloadManager : IDownloadManager {
public sealed class DownloadManager : IDownloadManager
{

private readonly ILogger? _logger;
private readonly IDownloadManagerConfiguration _configuration;

private readonly List<IDownloadProvider> _allProviders = new();
private readonly PreferredDownloadProviders _preferredDownloadProviders = new();
private readonly IVerificationManager _verifier;

/// <inheritdoc/>
public IEnumerable<string> Providers => _allProviders.Select(e => e.Name);
Expand All @@ -34,11 +33,11 @@ public class DownloadManager : IDownloadManager {
/// <param name="serviceProvider">The service provider of this instance.</param>
public DownloadManager(IServiceProvider serviceProvider)
{
Requires.NotNull(serviceProvider, nameof(serviceProvider));
if (serviceProvider == null)
throw new ArgumentNullException(nameof(serviceProvider));
_logger = serviceProvider.GetService<ILoggerFactory>()?.CreateLogger(GetType());
_configuration = serviceProvider.GetService<IDownloadManagerConfigurationProvider>()?.GetConfiguration() ??
DownloadManagerConfiguration.Default;
_verifier = serviceProvider.GetRequiredService<IVerificationManager>();
switch (_configuration.InternetClient)
{
case InternetClient.HttpClient:
Expand All @@ -58,21 +57,24 @@ public DownloadManager(IServiceProvider serviceProvider)
/// <inheritdoc/>
public void AddDownloadProvider(IDownloadProvider provider)
{
Requires.NotNull(provider, nameof(provider));
if (provider == null)
throw new ArgumentNullException(nameof(provider));
if (_allProviders.Any(e => string.Equals(e.Name, provider.Name, StringComparison.OrdinalIgnoreCase)))
throw new InvalidOperationException("Provider " + provider.Name + " already exists.");
_allProviders.Add(provider);
}

/// <inheritdoc/>
public Task<DownloadSummary> DownloadAsync(Uri uri, Stream outputStream, ProgressUpdateCallback? progress,
IVerificationContext? verificationContext = null, CancellationToken cancellationToken = default)
public Task<DownloadResult> DownloadAsync(Uri uri, Stream outputStream, ProgressUpdateCallback? progress,
IDownloadValidator? validator = null, CancellationToken cancellationToken = default)
{
_logger?.LogTrace($"Download requested: {uri.AbsoluteUri}");
if (outputStream == null)
throw new ArgumentNullException(nameof(outputStream));
if (!outputStream.CanWrite)
throw new InvalidOperationException("Input stream must be writable.");
throw new NotSupportedException("Input stream must be writable.");

_logger?.LogTrace($"Download requested: {uri.AbsoluteUri}");

if (!uri.IsFile && !uri.IsUnc)
{
if (!string.Equals(uri.Scheme, "http", StringComparison.OrdinalIgnoreCase) &&
Expand All @@ -95,7 +97,7 @@ public Task<DownloadSummary> DownloadAsync(Uri uri, Stream outputStream, Progres
{
var providers = GetSuitableProvider(uri);
return Task.Run(async () =>
await DownloadWithRetry(providers, uri, outputStream, progress, verificationContext, cancellationToken)
await DownloadWithRetry(providers, uri, outputStream, progress, validator, cancellationToken)
.ConfigureAwait(false), cancellationToken);
}
catch (Exception ex)
Expand All @@ -110,12 +112,12 @@ internal void RemoveAllEngines()
_allProviders.Clear();
}

private async Task<DownloadSummary> DownloadWithRetry(IList<IDownloadProvider> providers, Uri uri, Stream outputStream,
ProgressUpdateCallback? progress, IVerificationContext? verificationContext, CancellationToken cancellationToken)
private async Task<DownloadResult> DownloadWithRetry(IList<IDownloadProvider> providers, Uri uri, Stream outputStream,
ProgressUpdateCallback? progress, IDownloadValidator? validator, CancellationToken cancellationToken)
{
if (_configuration.VerificationPolicy == VerificationPolicy.Enforce && verificationContext is null)
if (_configuration.ValidationPolicy == ValidationPolicy.Required && validator is null)
{
var exception = new VerificationFailedException("No verification context available to verify the download.");
var exception = new NotSupportedException("A validation callback is required for this download.");
_logger?.LogError(exception, exception.Message);
throw exception;
}
Expand All @@ -133,40 +135,56 @@ private async Task<DownloadSummary> DownloadWithRetry(IList<IDownloadProvider> p
{
progress?.Invoke(new ProgressUpdateStatus(provider.Name, status.BytesRead, status.TotalBytes, status.BitRate));
}, cancellationToken).ConfigureAwait(false);

if (outputStream.Length == 0 && !_configuration.AllowEmptyFileDownload)
{
var exception = new Exception($"Empty file downloaded on '{uri}'.");
var exception = new InvalidOperationException($"Empty file downloaded on '{uri}'.");
_logger?.LogError(exception, exception.Message);
throw exception;
}

if (_configuration.VerificationPolicy != VerificationPolicy.Skip && verificationContext is not null)

if (_configuration.ValidationPolicy == ValidationPolicy.NoValidation)
{
_logger?.LogTrace("Skipping validation because verification context of is not valid.");
}
else
{
var valid = verificationContext.Verify();
if (valid)
if (validator is null)
{
_logger?.LogTrace("Skipping validation because verification context of is not valid.");
}
else
{
var verificationResult = _verifier.Verify(outputStream, verificationContext);
summary.ValidationResult = verificationResult;
if (verificationResult.Status != VerificationResultStatus.Success)
bool validationSuccess;
try
{
validationSuccess = await validator.Validate(outputStream, summary.DownloadedSize, cancellationToken)
.ConfigureAwait(false);
}
catch (Exception e)
{
var exception = new VerificationFailedException(
$"Verification on downloaded file '{uri.AbsoluteUri}' was not successful: {verificationResult.Status}");
var exception = new DownloadValidationFailedException(
$"Validation of '{uri.AbsoluteUri}' failed with exception: {e.Message}", e);
_logger?.LogError(exception, exception.Message);
throw exception;
}

if (!validationSuccess)
{
var exception = new DownloadValidationFailedException(
$"Downloaded file '{uri.AbsoluteUri}' is not valid.");
_logger?.LogError(exception, exception.Message);
throw exception;
}
}
else
{
if (_configuration.VerificationPolicy is VerificationPolicy.Optional or VerificationPolicy.Enforce)
throw new VerificationFailedException("Download is missing or has an invalid VerificationContext");
_logger?.LogTrace("Skipping validation because verification context of is not valid.");
}
}

_logger?.LogInformation($"Download of '{uri.AbsoluteUri}' succeeded using provider '{provider.Name}'");
_preferredDownloadProviders.LastSuccessfulProviderName = provider.Name;

summary.DownloadProvider = provider.Name;

return summary;
}
catch (OperationCanceledException)
Expand All @@ -192,7 +210,8 @@ private async Task<DownloadSummary> DownloadWithRetry(IList<IDownloadProvider> p
continue;

_logger?.LogTrace($"Sleeping {millisecondsTimeout} before retrying download.");
Thread.Sleep(millisecondsTimeout);

await Task.Delay(TimeSpan.FromMilliseconds(millisecondsTimeout), cancellationToken);
}
}

Expand All @@ -201,7 +220,7 @@ private async Task<DownloadSummary> DownloadWithRetry(IList<IDownloadProvider> p

private IList<IDownloadProvider> GetSuitableProvider(Uri uri)
{
var source = uri.IsFile || uri.IsUnc ? DownloadSource.File : DownloadSource.Internet;
var source = uri.IsFile || uri.IsUnc ? DownloadKind.File : DownloadKind.Internet;
var supportedProviders = _allProviders.Where(e => e.IsSupported(source)).ToList();
if (!supportedProviders.Any())
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
namespace AnakinRaW.CommonUtilities.DownloadManager;

/// <summary>
/// Get's thrown if there could be no <see cref="IDownloadProvider"/> found for a download operation.
/// Thrown if there could be no <see cref="IDownloadProvider"/> found for a download operation.
/// </summary>
public class DownloadProviderNotFoundException : InvalidOperationException
public sealed class DownloadProviderNotFoundException : InvalidOperationException
{
/// <summary>
/// Creates the exception
/// Initializes a new instance of the DownloadProviderNotFoundException class.
/// </summary>
/// <param name="message">The message of the exception</param>
public DownloadProviderNotFoundException(string message)
Expand Down
Loading
Loading