File: Utility\GetDownloadResultUtility.cs
Web Access
Project: src\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.


using System;
using System.Globalization;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using NuGet.Common;
using NuGet.Packaging.Core;
using NuGet.Packaging.Signing;
using NuGet.Protocol.Core.Types;

namespace NuGet.Protocol
{
    public static class GetDownloadResultUtility
    {
        private const int BufferSize = 8192;
        private const string DirectDownloadExtension = ".nugetdirectdownload";
        private const string DirectDownloadPattern = "*" + DirectDownloadExtension;

        public static async Task<DownloadResourceResult> GetDownloadResultAsync(
           HttpSource client,
           PackageIdentity identity,
           Uri uri,
           PackageDownloadContext downloadContext,
           string globalPackagesFolder,
           ILogger logger,
           CancellationToken token)
        {
            // Observe the NoCache argument.
            var directDownload = downloadContext.DirectDownload;
            DownloadResourceResult? packageFromGlobalPackages = null;
            try
            {
                packageFromGlobalPackages = GlobalPackagesFolderUtility.GetPackage(
                    identity,
                    globalPackagesFolder);

                if (packageFromGlobalPackages != null)
                {
                    if (!downloadContext.SourceCacheContext.NoCache)
                    {
                        return packageFromGlobalPackages;
                    }
                    else
                    {
                        // The package already exists in the global packages folder but the caller has requested NoCache,
                        // which means the package in the global packages folder should not be used. In this particular
                        // case, NoCache needs to imply DirectDownload.
                        directDownload = true;
                        packageFromGlobalPackages.Dispose();
                    }
                }
            }
            catch
            {
                packageFromGlobalPackages?.Dispose();
            }

            // Get the package from the source.
            for (var retry = 0; retry < 3; retry++)
            {
                try
                {
                    return await client.ProcessStreamAsync(
                        new HttpSourceRequest(uri, logger)
                        {
                            IgnoreNotFounds = true,
                            MaxTries = 1
                        },
                        async packageStream =>
                        {
                            if (packageStream == null)
                            {
                                return new DownloadResourceResult(DownloadResourceResultStatus.NotFound);
                            }

                            if (directDownload)
                            {
                                return await DirectDownloadAsync(
                                    client.PackageSource,
                                    identity,
                                    packageStream,
                                    downloadContext,
                                    token);
                            }
                            else
                            {
                                return await GlobalPackagesFolderUtility.AddPackageAsync(
                                    client.PackageSource,
                                    identity,
                                    packageStream,
                                    globalPackagesFolder,
                                    downloadContext.ParentId,
                                    downloadContext.ClientPolicyContext,
                                    logger,
                                    token);
                            }
                        },
                        downloadContext.SourceCacheContext,
                        logger,
                        token);
                }
                catch (OperationCanceledException)
                {
                    return new DownloadResourceResult(DownloadResourceResultStatus.Cancelled);
                }
                catch (SignatureException)
                {
                    throw;
                }
                catch (Exception ex) when (retry < 2)
                {
                    var message = string.Format(CultureInfo.CurrentCulture, Strings.Log_ErrorDownloading, identity, uri)
                        + Environment.NewLine
                        + ExceptionUtilities.DisplayMessage(ex);
                    logger.LogWarning(message);
                }
                catch (Exception ex)
                {
                    var message = string.Format(CultureInfo.CurrentCulture, Strings.Log_ErrorDownloading, identity, uri);

                    throw new FatalProtocolException(message, ex);
                }
            }

            throw new InvalidOperationException("Reached an unexpected point in the code");
        }

        /// <summary>
        /// Allow explicit clean-up of direct download files. This is important because although direct downloads are
        /// opened with the <see cref="FileOptions.DeleteOnClose"/> option, some systems (e.g. Linux) do not perform
        /// the delete if the process dies. Additionally, if the system dies before the process dies (e.g. loss of
        /// power), the direct download files will be left over.
        /// </summary>
        /// <param name="downloadContext">The download context.</param>
        public static void CleanUpDirectDownloads(PackageDownloadContext downloadContext)
        {
            foreach (var file in Directory.EnumerateFiles(
                downloadContext.DirectDownloadDirectory!,
                DirectDownloadPattern,
                SearchOption.TopDirectoryOnly))
            {
                try
                {
                    File.Delete(file);
                }
                catch (Exception e) when (e is UnauthorizedAccessException ||
                                          e is IOException)
                {
                    // Ignore exceptions indicating the file has permissions protecting it or if the file is in use.
                }
            }
        }

        private static async Task<DownloadResourceResult> DirectDownloadAsync(
            string source,
            PackageIdentity packageIdentity,
            Stream packageStream,
            PackageDownloadContext downloadContext,
            CancellationToken token)
        {
            if (packageIdentity == null)
            {
                throw new ArgumentNullException(nameof(packageIdentity));
            }

            if (packageStream == null)
            {
                throw new ArgumentNullException(nameof(packageStream));
            }

            if (downloadContext == null)
            {
                throw new ArgumentNullException(nameof(downloadContext));
            }

            // Build a file name for the package that is being downloaded. The caller provided the directory that
            // should be written to, but a random file name is used to avoid the necessity of locking. The caller
            // provides a directory so that a high performance or local drive can be used (instead of the %TEMP%
            // directory which can be different from the extraction location). The random file name is not just a
            // performance optimization. This also means that future versions of NuGet can co-exist with this
            // extraction code since the random component is specifically designed to avoid collisions.
            var randomComponent = Path.GetRandomFileName();
            var fileName = $"{randomComponent}{DirectDownloadExtension}";
            var directDownloadPath = Path.Combine(downloadContext.DirectDownloadDirectory!, fileName);

            FileStream? fileStream = null;

            try
            {
                Directory.CreateDirectory(downloadContext.DirectDownloadDirectory!);

                // Use DeleteOnClose when opening this stream since this file is just used for for the package
                // extraction. This file is meant to be ephemeral because package extraction does not always result
                // in a .nupkg on disk. Even if a .nupkg is in the extraction result (via PackageSaveMode.Nupkg), it
                // is not written to disk first, as is happening here.
                fileStream = new FileStream(
                   directDownloadPath,
                   FileMode.Create,
                   FileAccess.ReadWrite,
                   FileShare.Read,
                   BufferSize,
                   FileOptions.DeleteOnClose);

                await packageStream.CopyToAsync(fileStream, BufferSize, token);

                HttpStreamValidation.ValidatePackageIdentity(directDownloadPath, fileStream, packageIdentity);

                fileStream.Seek(0, SeekOrigin.Begin);

                return new DownloadResourceResult(fileStream, source);
            }
            catch
            {
                fileStream?.Dispose();

                throw;
            }
        }
    }
}