File: NuGet\TemporaryNuGetConfigFile.cs
Web Access
Project: src\src\Aspire.Cli\Aspire.Cli.csproj (aspire)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.IO.Hashing;
using System.Text;
using System.Xml.Linq;
 
namespace Aspire.Cli.NuGet;
 
/// <summary>
/// Materializes a supplied NuGet configuration and owns its temporary file lifetime.
/// </summary>
internal sealed class TemporaryNuGetConfigFile : IDisposable
{
    private readonly FileInfo _configFile;
    private readonly bool _ownsDirectory;
    private bool _disposed;
 
    private TemporaryNuGetConfigFile(FileInfo configFile, string cacheIdentity, bool ownsDirectory = true)
    {
        _configFile = configFile;
        CacheIdentity = cacheIdentity;
        _ownsDirectory = ownsDirectory;
    }
 
    public FileInfo ConfigFile => _configFile;
 
    public string CacheIdentity { get; private set; }
 
    public static Task<TemporaryNuGetConfigFile> CreateAsync(Action<string> writeConfig)
    {
        ArgumentNullException.ThrowIfNull(writeConfig);
        return CreateInDirectoryAsync(Directory.CreateTempSubdirectory("aspire-nuget-config"), writeConfig);
    }
 
    public static Task<TemporaryNuGetConfigFile> CreateAsync(DirectoryInfo parentDirectory, Action<string> writeConfig)
    {
        ArgumentNullException.ThrowIfNull(parentDirectory);
        ArgumentNullException.ThrowIfNull(writeConfig);
        parentDirectory.Create();
        DirectoryInfo directory;
        do
        {
            directory = new DirectoryInfo(Path.Combine(
                parentDirectory.FullName,
                $".aspire-nuget-config-{Path.GetRandomFileName()}"));
        }
        while (directory.Exists);
 
        directory.Create();
        return CreateInDirectoryAsync(directory, writeConfig);
    }
 
    internal static async Task<TemporaryNuGetConfigFile> CreatePreviewAsync(
        FileInfo targetFile, ReadOnlyMemory<byte> content, CancellationToken cancellationToken)
    {
        var file = new FileInfo(Path.Combine(
            targetFile.DirectoryName!, $".aspire-nuget-candidate-{Path.GetRandomFileName()}.config"));
        var created = false;
        try
        {
            var options = new FileStreamOptions
            {
                Mode = FileMode.CreateNew,
                Access = FileAccess.Write,
                Share = FileShare.None,
                Options = FileOptions.Asynchronous
            };
            if (!OperatingSystem.IsWindows())
            {
                options.UnixCreateMode = UnixFileMode.UserRead | UnixFileMode.UserWrite;
            }
            await using (var stream = new FileStream(file.FullName, options))
            {
                created = true;
                await stream.WriteAsync(content, cancellationToken);
            }
            return new TemporaryNuGetConfigFile(file, await ComputeCacheIdentityAsync(file), ownsDirectory: false);
        }
        catch
        {
            if (created)
            {
                file.Delete();
            }
            throw;
        }
    }
 
    private static async Task<TemporaryNuGetConfigFile> CreateInDirectoryAsync(DirectoryInfo directory, Action<string> writeConfig)
    {
        try
        {
            var configFile = new FileInfo(Path.Combine(directory.FullName, "NuGet.Config"));
            writeConfig(configFile.FullName);
            return new TemporaryNuGetConfigFile(
                configFile, await ComputeCacheIdentityAsync(configFile).ConfigureAwait(false));
        }
        catch
        {
            TryDeleteDirectory(directory.FullName);
            throw;
        }
    }
 
    public async Task RegenerateAsync(Action<string> writeConfig)
    {
        ArgumentNullException.ThrowIfNull(writeConfig);
        writeConfig(_configFile.FullName);
        CacheIdentity = await ComputeCacheIdentityAsync(_configFile).ConfigureAwait(false);
    }
 
    private static async Task<string> ComputeCacheIdentityAsync(FileInfo configFile)
    {
        await using var stream = configFile.OpenRead();
        var document = await XDocument.LoadAsync(stream, LoadOptions.None, CancellationToken.None).ConfigureAwait(false);
        document.Descendants("config").Elements("add")
            .Where(static element => string.Equals(
                (string?)element.Attribute("key"), "globalPackagesFolder", StringComparison.OrdinalIgnoreCase))
            .Remove();
        document.Descendants("config").Where(static section => !section.Elements().Any()).Remove();
        var bytes = Encoding.UTF8.GetBytes(document.ToString(SaveOptions.DisableFormatting));
        return Convert.ToHexString(XxHash3.Hash(bytes));
    }
 
    public void Dispose()
    {
        if (_disposed)
        {
            return;
        }
 
        if (!_ownsDirectory)
        {
            _configFile.Delete();
        }
        else if (_configFile.DirectoryName is { } directory)
        {
            TryDeleteDirectory(directory);
        }
 
        _disposed = true;
    }
 
    private static void TryDeleteDirectory(string directory)
    {
        try
        {
            Directory.Delete(directory, recursive: true);
        }
        catch (IOException)
        {
            // A temporary file can remain locked by NuGet after a failed operation.
        }
        catch (UnauthorizedAccessException)
        {
            // Cleanup must not replace the original operation failure.
        }
    }
}