File: Installers\NuGet\NuGetInstaller.cs
Web Access
Project: src\src\sdk\src\TemplateEngine\Microsoft.TemplateEngine.Edge\Microsoft.TemplateEngine.Edge.csproj (Microsoft.TemplateEngine.Edge)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.Extensions.Logging;
using Microsoft.TemplateEngine.Abstractions;
using Microsoft.TemplateEngine.Abstractions.Installer;
using Microsoft.TemplateEngine.Abstractions.TemplatePackage;
using NuGet.Packaging;
using static Microsoft.TemplateEngine.Edge.Installers.NuGet.NuGetApiPackageManager;

namespace Microsoft.TemplateEngine.Edge.Installers.NuGet
{
    internal class NuGetInstaller : IInstaller, ISerializableInstaller
    {
        private readonly IEngineEnvironmentSettings _environmentSettings;
        private readonly ILogger _logger;
        private readonly string _installPath;
        private readonly IDownloader _packageDownloader;
        private readonly IUpdateChecker _updateChecker;

        public NuGetInstaller(IInstallerFactory factory, IEngineEnvironmentSettings settings, string installPath)
        {
            Factory = factory ?? throw new ArgumentNullException(nameof(factory));
            _environmentSettings = settings ?? throw new ArgumentNullException(nameof(settings));
            _logger = settings.Host.LoggerFactory.CreateLogger<NuGetInstaller>();

            if (string.IsNullOrWhiteSpace(installPath))
            {
                throw new ArgumentException($"{nameof(installPath)} should not be null or empty", nameof(installPath));
            }
            if (!_environmentSettings.Host.FileSystem.DirectoryExists(installPath))
            {
                _environmentSettings.Host.FileSystem.CreateDirectory(installPath);
            }
            _installPath = installPath;

            NuGetApiPackageManager packageManager = new NuGetApiPackageManager(settings);
            _packageDownloader = packageManager;
            _updateChecker = packageManager;
        }

        public NuGetInstaller(IInstallerFactory factory, IEngineEnvironmentSettings settings, string installPath, IDownloader packageDownloader, IUpdateChecker updateChecker)
        {
            Factory = factory ?? throw new ArgumentNullException(nameof(factory));
            _environmentSettings = settings ?? throw new ArgumentNullException(nameof(settings));
            _logger = settings.Host.LoggerFactory.CreateLogger<NuGetInstaller>();
            _packageDownloader = packageDownloader ?? throw new ArgumentNullException(nameof(packageDownloader));
            _updateChecker = updateChecker ?? throw new ArgumentNullException(nameof(updateChecker));

            if (string.IsNullOrWhiteSpace(installPath))
            {
                throw new ArgumentException($"{nameof(installPath)} should not be null or empty", nameof(installPath));
            }
            if (!_environmentSettings.Host.FileSystem.DirectoryExists(installPath))
            {
                _environmentSettings.Host.FileSystem.CreateDirectory(installPath);
            }
            _installPath = installPath;
        }

        public IInstallerFactory Factory { get; }

        public Task<bool> CanInstallAsync(InstallRequest installationRequest, CancellationToken cancellationToken)
        {
            try
            {
                ReadPackageInformation(installationRequest.PackageIdentifier);
            }
            catch (Exception)
            {
                _logger.LogDebug($"{installationRequest.PackageIdentifier} is not a local NuGet package.");

                //check if identifier is a valid package ID
                bool validPackageId = PackageIdValidator.IsValidPackageId(installationRequest.PackageIdentifier);
                //check if version is specified it is correct version
                bool hasValidVersion = NuGetVersionHelper.IsSupportedVersionString(installationRequest.Version);
                if (!validPackageId)
                {
                    _logger.LogDebug($"{installationRequest.PackageIdentifier} is not a valid NuGet package ID.");
                }
                if (!hasValidVersion)
                {
                    _logger.LogDebug($"{installationRequest.Version} is not a valid NuGet package version.");
                }
                if (validPackageId && hasValidVersion)
                {
                    _logger.LogDebug($"{installationRequest.DisplayName} is identified as the downloadable NuGet package.");
                }

                //not a local package file
                return Task.FromResult(validPackageId && hasValidVersion);
            }
            _logger.LogDebug($"{installationRequest.PackageIdentifier} is identified as the local NuGet package.");
            return Task.FromResult(true);
        }

        public IManagedTemplatePackage Deserialize(IManagedTemplatePackageProvider provider, TemplatePackageData data)
        {
            _ = provider ?? throw new ArgumentNullException(nameof(provider));
            if (data.InstallerId != Factory.Id)
            {
                throw new ArgumentException($"{nameof(NuGetInstaller)} can only deserialize packages with {nameof(data.InstallerId)} {Factory.Id}", nameof(data));
            }
            _ = data.Details ?? throw new ArgumentException($"{nameof(data)} should contain {nameof(data.Details)} with package identifier.", nameof(data));
            return NuGetManagedTemplatePackage.Deserialize(_environmentSettings, this, provider, data.MountPointUri, data.Details);
        }

        public async Task<IReadOnlyList<CheckUpdateResult>> GetLatestVersionAsync(
            IEnumerable<IManagedTemplatePackage> packages,
            IManagedTemplatePackageProvider provider,
            CancellationToken cancellationToken)
        {
            _ = packages ?? throw new ArgumentNullException(nameof(packages));
            return await Task.WhenAll(packages.Select(async package =>
                {
                    if (package is NuGetManagedTemplatePackage nugetPackage)
                    {
                        try
                        {
                            (string version, bool isLatestVersion, NugetPackageMetadata packageMetadata) = await _updateChecker.GetLatestVersionAsync(
                                nugetPackage.Identifier,
                                nugetPackage.Version,
                                nugetPackage.NuGetSource,
                                cancellationToken).ConfigureAwait(false);

                            if (packageMetadata.Vulnerabilities != null && packageMetadata.Vulnerabilities.Any())
                            {
                                throw new VulnerablePackageException(
                                    string.Format(LocalizableStrings.NuGetApiPackageManager_UpdateCheckError_VulnerablePackage, nugetPackage.Identifier),
                                    nugetPackage.Identifier,
                                    nugetPackage?.Version ?? string.Empty,
                                    packageMetadata.Vulnerabilities);
                            }

                            nugetPackage.Owners = packageMetadata.Owners;
                            nugetPackage.Reserved = packageMetadata.PrefixReserved.ToString();

                            return CheckUpdateResult.CreateSuccess(nugetPackage, version, isLatestVersion);
                        }
                        catch (PackageNotFoundException e)
                        {
                            return CheckUpdateResult.CreateFailure(
                                package,
                                InstallerErrorCode.PackageNotFound,
                                string.Format(LocalizableStrings.NuGetInstaller_Error_FailedToReadPackage, e.PackageIdentifier, string.Join(", ", e.SourcesList)),
                                []);
                        }
                        catch (InvalidNuGetSourceException e)
                        {
                            string message = e.SourcesList == null || !e.SourcesList.Any()
                                ? LocalizableStrings.NuGetInstaller_InstallResult_Error_InvalidSources_None
                                : string.Format(LocalizableStrings.NuGetInstaller_InstallResult_Error_InvalidSources, string.Join(", ", e.SourcesList));

                            return CheckUpdateResult.CreateFailure(
                                package,
                                InstallerErrorCode.InvalidSource,
                                message,
                                []);
                        }
                        catch (OperationCanceledException)
                        {
                            return CheckUpdateResult.CreateFailure(
                                package,
                                InstallerErrorCode.GenericError,
                                LocalizableStrings.NuGetInstaller_InstallResult_Error_OperationCancelled,
                                []);
                        }
                        catch (VulnerablePackageException e)
                        {
                            return CheckUpdateResult.CreateFailure(
                                package,
                                InstallerErrorCode.VulnerablePackage,
                                string.Format(LocalizableStrings.NuGetInstaller_UpdateCheck_Error_VulnerablePackage, nugetPackage.Identifier),
                                e.Vulnerabilities);
                        }
                        catch (Exception e)
                        {
                            _logger.LogDebug($"Retrieving latest version for package {package.DisplayName} failed. Details: {e}.");
                            return CheckUpdateResult.CreateFailure(
                                package,
                                InstallerErrorCode.GenericError,
                                string.Format(LocalizableStrings.NuGetInstaller_InstallResult_Error_UpdateCheckGeneric, package.DisplayName, e.Message),
                                []);
                        }
                    }
                    else
                    {
                        return CheckUpdateResult.CreateFailure(
                            package,
                            InstallerErrorCode.UnsupportedRequest,
                            string.Format(LocalizableStrings.NuGetInstaller_InstallResult_Error_PackageNotSupported, package.DisplayName, Factory.Name),
                            []);
                    }
                })).ConfigureAwait(false);
        }

        public async Task<InstallResult> InstallAsync(InstallRequest installRequest, IManagedTemplatePackageProvider provider, CancellationToken cancellationToken)
        {
            _ = installRequest ?? throw new ArgumentNullException(nameof(installRequest));
            _ = provider ?? throw new ArgumentNullException(nameof(provider));

            if (!await CanInstallAsync(installRequest, cancellationToken).ConfigureAwait(false))
            {
                return InstallResult.CreateFailure(
                    installRequest,
                    InstallerErrorCode.UnsupportedRequest,
                    string.Format(LocalizableStrings.NuGetInstaller_InstallResult_Error_PackageNotSupported, installRequest.DisplayName, Factory.Name),
                    []);
            }

            try
            {
                bool isLocalPackage = IsLocalPackage(installRequest);
                NuGetPackageInfo nuGetPackageInfo;
                if (isLocalPackage)
                {
                    nuGetPackageInfo = InstallLocalPackage(installRequest);
                }
                else
                {
                    string[] additionalNuGetSources = [];
                    if (installRequest.Details != null && installRequest.Details.TryGetValue(InstallerConstants.NuGetSourcesKey, out string nugetSources))
                    {
                        additionalNuGetSources = nugetSources.Split(InstallerConstants.NuGetSourcesSeparator);
                    }

                    nuGetPackageInfo = await _packageDownloader.DownloadPackageAsync(
                        _installPath,
                        installRequest.PackageIdentifier,
                        installRequest.Version,
                        additionalNuGetSources,
                        force: installRequest.Force,
                        cancellationToken)
                        .ConfigureAwait(false);
                }

                NuGetManagedTemplatePackage package = new NuGetManagedTemplatePackage(
                    _environmentSettings,
                    installer: this,
                    provider,
                    nuGetPackageInfo.FullPath,
                    nuGetPackageInfo.PackageIdentifier)
                {
                    Author = nuGetPackageInfo.Author,
                    Owners = nuGetPackageInfo.Owners,
                    Reserved = nuGetPackageInfo.Reserved.ToString(),
                    NuGetSource = nuGetPackageInfo.NuGetSource,
                    Version = nuGetPackageInfo.PackageVersion.ToString(),
                    IsLocalPackage = isLocalPackage
                };

                return InstallResult.CreateSuccess(installRequest, package, nuGetPackageInfo.PackageVulnerabilities);
            }
            catch (DownloadException e)
            {
                string? packageLocation = e.SourcesList == null
                    ? e.PackageLocation
                    : string.Join(", ", e.SourcesList);

                return InstallResult.CreateFailure(
                    installRequest,
                    InstallerErrorCode.DownloadFailed,
                    string.Format(LocalizableStrings.NuGetInstaller_InstallResult_Error_DownloadFailed, installRequest.DisplayName, packageLocation),
                    []);
            }
            catch (PackageNotFoundException e)
            {
                return InstallResult.CreateFailure(
                    installRequest,
                    InstallerErrorCode.PackageNotFound,
                    string.Format(LocalizableStrings.NuGetInstaller_Error_FailedToReadPackage, e.PackageIdentifier, string.Join(", ", e.SourcesList)),
                    []);
            }
            catch (InvalidNuGetSourceException e)
            {
                string message = e.SourcesList == null || !e.SourcesList.Any()
                    ? LocalizableStrings.NuGetInstaller_InstallResult_Error_InvalidSources_None
                    : string.Format(LocalizableStrings.NuGetInstaller_InstallResult_Error_InvalidSources, string.Join(", ", e.SourcesList));

                return InstallResult.CreateFailure(
                        installRequest,
                        InstallerErrorCode.InvalidSource,
                        message,
                        []);
            }
            catch (InvalidNuGetPackageException e)
            {
                return InstallResult.CreateFailure(
                    installRequest,
                    InstallerErrorCode.InvalidPackage,
                    string.Format(LocalizableStrings.NuGetInstaller_InstallResult_Error_InvalidPackage, e.PackageLocation),
                    []);
            }
            catch (VulnerablePackageException e)
            {
                return InstallResult.CreateFailure(
                    installRequest,
                    InstallerErrorCode.VulnerablePackage,
                    string.Format(LocalizableStrings.NuGetInstaller_InstallResult_Error_VulnerablePackage, e.PackageIdentifier),
                    e.Vulnerabilities);
            }
            catch (OperationCanceledException)
            {
                return InstallResult.CreateFailure(
                    installRequest,
                    InstallerErrorCode.GenericError,
                    LocalizableStrings.NuGetInstaller_InstallResult_Error_OperationCancelled,
                    []);
            }
            catch (Exception e)
            {
                _logger.LogDebug($"Installing {installRequest.DisplayName} failed. Details:{e}");
                return InstallResult.CreateFailure(
                    installRequest,
                    InstallerErrorCode.GenericError,
                    string.Format(LocalizableStrings.NuGetInstaller_InstallResult_Error_InstallGeneric, installRequest.DisplayName, e.Message),
                    []);
            }
        }

        public TemplatePackageData Serialize(IManagedTemplatePackage templatePackage)
        {
            _ = templatePackage ?? throw new ArgumentNullException(nameof(templatePackage));
            NuGetManagedTemplatePackage nuGetTemplatePackage = templatePackage as NuGetManagedTemplatePackage
                ?? throw new ArgumentException($"{nameof(templatePackage)} should be of type {nameof(NuGetManagedTemplatePackage)}", nameof(templatePackage));

            return new TemplatePackageData(
                Factory.Id,
                nuGetTemplatePackage.MountPointUri,
                nuGetTemplatePackage.LastChangeTime,
                nuGetTemplatePackage.Details);
        }

        public Task<UninstallResult> UninstallAsync(IManagedTemplatePackage templatePackage, IManagedTemplatePackageProvider provider, CancellationToken cancellationToken)
        {
            _ = templatePackage ?? throw new ArgumentNullException(nameof(templatePackage));
            if (templatePackage is not NuGetManagedTemplatePackage)
            {
                return Task.FromResult(UninstallResult.CreateFailure(
                    templatePackage,
                    InstallerErrorCode.UnsupportedRequest,
                    string.Format(LocalizableStrings.NuGetInstaller_InstallResult_Error_PackageNotSupported, templatePackage.DisplayName, Factory.Name)));
            }
            try
            {
                _environmentSettings.Host.FileSystem.FileDelete(templatePackage.MountPointUri);
                return Task.FromResult(UninstallResult.CreateSuccess(templatePackage));
            }
            catch (Exception e)
            {
                _logger.LogDebug("Uninstalling {0} failed. Details:{1}", templatePackage.DisplayName, e);
                return Task.FromResult(UninstallResult.CreateFailure(
                    templatePackage,
                    InstallerErrorCode.GenericError,
                    string.Format(LocalizableStrings.NuGetInstaller_InstallResult_Error_UninstallGeneric, templatePackage.DisplayName, e.Message)));
            }
        }

        public async Task<UpdateResult> UpdateAsync(UpdateRequest updateRequest, IManagedTemplatePackageProvider provider, CancellationToken cancellationToken)
        {
            _ = updateRequest ?? throw new ArgumentNullException(nameof(updateRequest));
            _ = provider ?? throw new ArgumentNullException(nameof(provider));

            if (string.IsNullOrWhiteSpace(updateRequest.Version))
            {
                throw new ArgumentException("Version cannot be null or empty", nameof(updateRequest.Version));
            }

            //ensure uninstall is performed
            UninstallResult uninstallResult = await UninstallAsync(updateRequest.TemplatePackage, provider, cancellationToken).ConfigureAwait(false);
            if (!uninstallResult.Success)
            {
                if (uninstallResult.ErrorMessage is null)
                {
                    throw new InvalidOperationException($"{nameof(uninstallResult.ErrorMessage)} cannot be null when {nameof(uninstallResult.Success)} is 'true'");
                }
                return UpdateResult.CreateFailure(updateRequest, uninstallResult.Error, uninstallResult.ErrorMessage, []);
            }

            Dictionary<string, string> installationDetails = new Dictionary<string, string>();
            if (updateRequest.TemplatePackage is NuGetManagedTemplatePackage nuGetManagedSource && !string.IsNullOrWhiteSpace(nuGetManagedSource.NuGetSource))
            {
                installationDetails.Add(InstallerConstants.NuGetSourcesKey, nuGetManagedSource.NuGetSource!);
            }
            InstallRequest installRequest = new InstallRequest(updateRequest.TemplatePackage.Identifier, updateRequest.Version, details: installationDetails);
            return UpdateResult.FromInstallResult(updateRequest, await InstallAsync(installRequest, provider, cancellationToken).ConfigureAwait(false));
        }

        private bool IsLocalPackage(InstallRequest installRequest)
        {
            return _environmentSettings.Host.FileSystem.FileExists(installRequest.PackageIdentifier);
        }

        private NuGetPackageInfo InstallLocalPackage(InstallRequest installRequest)
        {
            _ = installRequest ?? throw new ArgumentNullException(nameof(installRequest));

            NuGetPackageInfo packageInfo;
            try
            {
                packageInfo = ReadPackageInformation(installRequest.PackageIdentifier);
            }
            catch (Exception ex)
            {
                _logger.LogError(string.Format(LocalizableStrings.NuGetInstaller_Error_FailedToReadPackage, installRequest.PackageIdentifier));
                _logger.LogDebug($"Details: {ex}.");
                throw new InvalidNuGetPackageException(installRequest.PackageIdentifier, ex);
            }
            string targetPackageLocation = Path.Combine(_installPath, packageInfo.PackageIdentifier + "." + packageInfo.PackageVersion + ".nupkg");
            if (!installRequest.Force && _environmentSettings.Host.FileSystem.FileExists(targetPackageLocation))
            {
                _logger.LogError(string.Format(LocalizableStrings.NuGetInstaller_Error_CopyFailed, installRequest.PackageIdentifier, targetPackageLocation));
                _logger.LogError(string.Format(LocalizableStrings.NuGetInstaller_Error_FileAlreadyExists, targetPackageLocation));
                throw new DownloadException(packageInfo.PackageIdentifier, packageInfo.PackageVersion, installRequest.PackageIdentifier);
            }
            try
            {
                _environmentSettings.Host.FileSystem.FileCopy(installRequest.PackageIdentifier, targetPackageLocation, overwrite: installRequest.Force);
                packageInfo = packageInfo.WithFullPath(targetPackageLocation);
            }
            catch (Exception ex)
            {
                _logger.LogError(string.Format(LocalizableStrings.NuGetInstaller_Error_CopyFailed, installRequest.PackageIdentifier, targetPackageLocation), null, 0);
                _logger.LogDebug($"Details: {ex}.");
                throw new DownloadException(packageInfo.PackageIdentifier, packageInfo.PackageVersion, installRequest.PackageIdentifier);
            }
            return packageInfo;
        }

        private NuGetPackageInfo ReadPackageInformation(string packageLocation)
        {
            using Stream inputStream = _environmentSettings.Host.FileSystem.OpenRead(packageLocation);
            using PackageArchiveReader reader = new PackageArchiveReader(inputStream);

            NuspecReader nuspec = reader.NuspecReader;

            return new NuGetPackageInfo(
                nuspec.GetAuthors(),
                nuspec.GetOwners(),
                // The prefix reservation is not applicable to local packages.
                reserved: false,
                packageLocation,
                null,
                nuspec.GetId(),
                nuspec.GetVersion().ToNormalizedString(),
                []);
        }
    }
}