File: Templating\TemplateSourceStore.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.Text.Json;
using System.Text.Json.Nodes;
using Aspire.Cli.Configuration;
using Aspire.Cli.Resources;
using Aspire.Cli.Utils;
using Microsoft.Extensions.Logging;
 
namespace Aspire.Cli.Templating;
 
internal sealed class TemplateSourceStore(
    IConfigurationService configurationService,
    CliExecutionContext executionContext,
    ILogger<TemplateSourceStore> logger)
{
    public async Task<JsonObject> ListAsync(CancellationToken cancellationToken)
    {
        var settings = await ReadAsync(GetSettingsFilePath(), cancellationToken);
        // Listing is configuration management, not discovery. Keep incomplete sources visible
        // so users can inspect and finish objects created through stepwise config edits.
        return GetSources(settings);
    }
 
    public async Task AddAsync(string name, JsonObject source, CancellationToken cancellationToken)
    {
        TemplateSourceConfiguration.ValidateName(name);
        using var document = JsonDocument.Parse(source.ToJsonString());
        TemplateSourceConfiguration.Parse(document.RootElement, $"templateSources.{name}", requireComplete: true);
 
        var path = GetSettingsFilePath();
        using var configLock = await ConfigurationHelper.TryAcquireWorkspaceConfigLockAsync(
            executionContext, ConfigurationHelper.GetCanonicalGlobalSettingsFile(new FileInfo(path)), logger, cancellationToken)
            ?? throw new IOException("Could not acquire the global template source configuration lock.");
        path = GetSettingsFilePath();
        var settings = await ReadAsync(path, cancellationToken);
        var sources = GetSources(settings);
        // IConfiguration keys are case-insensitive even though JSON properties are not.
        // Do not create a global file that the configuration provider cannot load.
        if (sources.Any(entry => entry.Key.Equals(name, StringComparisons.TemplateSourceName)))
        {
            throw new TemplateSourceConfigurationException($"templateSources.{name}", NewCommandStrings.TemplateSourceAlreadyExists);
        }
        sources[name] = source.DeepClone();
        await ConfigurationHelper.WriteSettingsFileAtomicallyAsync(path, settings, cancellationToken, preserveOwnershipAndAcl: true);
    }
 
    public async Task<bool> RemoveAsync(string name, CancellationToken cancellationToken)
    {
        TemplateSourceConfiguration.ValidateName(name);
        var path = GetSettingsFilePath();
        using var configLock = await ConfigurationHelper.TryAcquireWorkspaceConfigLockAsync(
            executionContext, ConfigurationHelper.GetCanonicalGlobalSettingsFile(new FileInfo(path)), logger, cancellationToken)
            ?? throw new IOException("Could not acquire the global template source configuration lock.");
        path = GetSettingsFilePath();
        var settings = await ReadAsync(path, cancellationToken);
        var sources = GetSources(settings);
        var existing = sources.Select(entry => entry.Key).FirstOrDefault(key => key.Equals(name, StringComparisons.TemplateSourceName));
        if (existing is null)
        {
            return false;
        }
        sources.Remove(existing);
        if (sources.Count == 0)
        {
            settings.Remove(TemplateSourceConfiguration.GetPropertyName(settings, "templateSources", StringComparison.OrdinalIgnoreCase));
        }
        await ConfigurationHelper.WriteSettingsFileAtomicallyAsync(path, settings, cancellationToken, preserveOwnershipAndAcl: true);
        return true;
    }
 
    private string GetSettingsFilePath()
    {
        var path = configurationService.GetSettingsFilePath(isGlobal: true);
        if (Path.GetFileName(path).Equals("globalsettings.json", StringComparison.OrdinalIgnoreCase))
        {
            throw new TemplateSourceConfigurationException("templateSources", NewCommandStrings.TemplateSourceLegacyConfiguration);
        }
        return path;
    }
 
    private static JsonObject GetSources(JsonObject settings)
    {
        var key = TemplateSourceConfiguration.GetPropertyName(settings, "templateSources", StringComparison.OrdinalIgnoreCase);
        if (settings.TryGetPropertyValue(key, out var value))
        {
            return value as JsonObject
                ?? throw new TemplateSourceConfigurationException("templateSources", NewCommandStrings.TemplateSourcesMustBeObject);
        }
        var sources = new JsonObject();
        settings["templateSources"] = sources;
        return sources;
    }
 
    private static async Task<JsonObject> ReadAsync(string path, CancellationToken cancellationToken)
    {
        try
        {
            var content = await File.ReadAllTextAsync(path, cancellationToken);
            var settings = JsonNode.Parse(content, nodeOptions: null, ConfigurationHelper.ParseOptions) as JsonObject
                ?? throw new TemplateSourceConfigurationException("templateSources", NewCommandStrings.TemplateSourcesMustBeObject);
            ConfigurationHelper.NormalizeSettings(settings);
            return settings;
        }
        catch (Exception ex) when (ex is FileNotFoundException or DirectoryNotFoundException)
        {
            return new JsonObject();
        }
    }
}