| File: RemotePackageArchiveDownloader.cs | Web Access |
| Project: src\nuget-client\src\NuGet.Core\NuGet.Protocol\NuGet.Protocol.csproj (NuGet.Protocol) |
// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. #nullable disable using System; using System.IO; using System.Threading; using System.Threading.Tasks; using NuGet.Common; using NuGet.Packaging; using NuGet.Packaging.Core; using NuGet.Packaging.Signing; using NuGet.Protocol.Core.Types; namespace NuGet.Protocol { /// <summary> /// A remote package downloader. /// </summary> public sealed class RemotePackageArchiveDownloader : IPackageDownloader { private readonly SourceCacheContext _cacheContext; private string _destinationFilePath; private Func<Exception, Task<bool>> _handleExceptionAsync; private bool _isDisposed; private readonly ILogger _logger; private readonly PackageIdentity _packageIdentity; private Lazy<PackageArchiveReader> _packageReader; private readonly FindPackageByIdResource _resource; private SemaphoreSlim _throttle; /// <summary> /// Gets an asynchronous package content reader. /// </summary> /// <exception cref="ObjectDisposedException">Thrown if this object is disposed.</exception> public IAsyncPackageContentReader ContentReader { get { ThrowIfDisposed(); return _packageReader.Value; } } /// <summary> /// Gets an asynchronous package core reader. /// </summary> /// <exception cref="ObjectDisposedException">Thrown if this object is disposed.</exception> public IAsyncPackageCoreReader CoreReader { get { ThrowIfDisposed(); return _packageReader.Value; } } public ISignedPackageReader SignedPackageReader { get { ThrowIfDisposed(); return _packageReader.Value; } } public string Source { get; } /// <summary> /// Initializes a new <see cref="RemotePackageArchiveDownloader" /> class. /// </summary> /// <param name="source">A package source.</param> /// <param name="resource">A <see cref="FindPackageByIdResource" /> resource.</param> /// <param name="packageIdentity">A package identity.</param> /// <param name="cacheContext">A source cache context.</param> /// <param name="logger">A logger.</param> /// <exception cref="ArgumentNullException">Thrown if <paramref name="resource" /> is <see langword="null" />.</exception> /// <exception cref="ArgumentNullException">Thrown if <paramref name="packageIdentity" /> /// is <see langword="null" />.</exception> /// <exception cref="ArgumentNullException">Thrown if <paramref name="cacheContext" /> /// is <see langword="null" />.</exception> /// <exception cref="ArgumentNullException">Thrown if <paramref name="logger" /> is <see langword="null" />.</exception> public RemotePackageArchiveDownloader( string source, FindPackageByIdResource resource, PackageIdentity packageIdentity, SourceCacheContext cacheContext, ILogger logger) { if (resource == null) { throw new ArgumentNullException(nameof(resource)); } if (packageIdentity == null) { throw new ArgumentNullException(nameof(packageIdentity)); } if (cacheContext == null) { throw new ArgumentNullException(nameof(cacheContext)); } if (logger == null) { throw new ArgumentNullException(nameof(logger)); } _resource = resource; _packageIdentity = packageIdentity; _cacheContext = cacheContext; _logger = logger; _packageReader = new Lazy<PackageArchiveReader>(GetPackageReader); _handleExceptionAsync = exception => TaskResult.False; Source = source; } /// <summary> /// Disposes this instance. /// </summary> public void Dispose() { if (!_isDisposed) { if (_packageReader.IsValueCreated) { _packageReader.Value.Dispose(); } GC.SuppressFinalize(this); _isDisposed = true; } } /// <summary> /// Asynchronously copies a .nupkg to a target file path. /// </summary> /// <param name="destinationFilePath">The destination file path.</param> /// <param name="cancellationToken">A cancellation token.</param> /// <returns>A task that represents the asynchronous operation. /// The task result (<see cref="Task{TResult}.Result" />) returns a <see cref="bool" /> /// indicating whether or not the copy was successful.</returns> /// <exception cref="ObjectDisposedException">Thrown if this object is disposed.</exception> /// <exception cref="ArgumentException">Thrown if <paramref name="destinationFilePath" /> /// is either <see langword="null" /> or empty.</exception> /// <exception cref="OperationCanceledException">Thrown if <paramref name="cancellationToken" /> /// is cancelled.</exception> public async Task<bool> CopyNupkgFileToAsync(string destinationFilePath, CancellationToken cancellationToken) { ThrowIfDisposed(); if (string.IsNullOrEmpty(destinationFilePath)) { throw new ArgumentException(Strings.ArgumentCannotBeNullOrEmpty, nameof(destinationFilePath)); } try { if (_throttle != null) { await _throttle.WaitAsync(cancellationToken); } cancellationToken.ThrowIfCancellationRequested(); using (var destination = new FileStream( destinationFilePath, FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite | FileShare.Delete, bufferSize: 4096)) { var result = await _resource.CopyNupkgToStreamAsync( _packageIdentity.Id, _packageIdentity.Version, destination, _cacheContext, _logger, cancellationToken); _destinationFilePath = destinationFilePath; return result; } } catch (Exception ex) { if (!await _handleExceptionAsync(ex)) { throw; } } finally { _throttle?.Release(); } return false; } /// <summary> /// Asynchronously gets a package hash. /// </summary> /// <param name="hashAlgorithm">The hash algorithm.</param> /// <param name="cancellationToken">A cancellation token.</param> /// <returns>A task that represents the asynchronous operation. /// The task result (<see cref="Task{TResult}.Result" />) returns a <see cref="string" /> /// representing the package hash.</returns> /// <exception cref="ObjectDisposedException">Thrown if this object is disposed.</exception> /// <exception cref="ArgumentException">Thrown if <paramref name="hashAlgorithm" /> /// is either <see langword="null" /> or empty.</exception> /// <exception cref="OperationCanceledException">Thrown if <paramref name="cancellationToken" /> /// is cancelled.</exception> public Task<string> GetPackageHashAsync(string hashAlgorithm, CancellationToken cancellationToken) { ThrowIfDisposed(); if (string.IsNullOrEmpty(hashAlgorithm)) { throw new ArgumentException(Strings.ArgumentCannotBeNullOrEmpty, nameof(hashAlgorithm)); } cancellationToken.ThrowIfCancellationRequested(); using (var stream = GetDestinationStream()) { var bytes = new CryptoHashProvider(hashAlgorithm).CalculateHash(stream); var packageHash = Convert.ToBase64String(bytes); return Task.FromResult(packageHash); } } /// <summary> /// Sets an exception handler for package downloads. /// </summary> /// <remarks>The exception handler returns a task that represents the asynchronous operation. /// The task result (<see cref="Task{TResult}.Result" />) returns a <see cref="bool" /> /// indicating whether or not the exception was handled. To handle an exception and stop its /// propagation, the task should return <see langword="true" />. Otherwise, the exception will be rethrown.</remarks> /// <param name="handleExceptionAsync">An exception handler.</param> /// <exception cref="ArgumentNullException">Thrown if <paramref name="handleExceptionAsync" /> /// is <see langword="null" />.</exception> public void SetExceptionHandler(Func<Exception, Task<bool>> handleExceptionAsync) { if (handleExceptionAsync == null) { throw new ArgumentNullException(nameof(handleExceptionAsync)); } _handleExceptionAsync = handleExceptionAsync; } /// <summary> /// Sets a throttle for package downloads. /// </summary> /// <param name="throttle">A throttle. Can be <see langword="null" />.</param> public void SetThrottle(SemaphoreSlim throttle) { _throttle = throttle; } private PackageArchiveReader GetPackageReader() { var stream = GetDestinationStream(); return new PackageArchiveReader(stream); } private FileStream GetDestinationStream() { ThrowIfDisposed(); if (string.IsNullOrEmpty(_destinationFilePath)) { throw new InvalidOperationException(); } return new FileStream( _destinationFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 4096); } private void ThrowIfDisposed() { if (_isDisposed) { throw new ObjectDisposedException(nameof(RemotePackageArchiveDownloader)); } } } }