< Summary

Line coverage
0%
Covered lines: 0
Uncovered lines: 106
Coverable lines: 106
Total lines: 201
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 24
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/dotnet-snap/dotnet-snap/src/Dotnet.Installer.Core/Services/Implementations/ManifestService.cs

#LineLine coverage
 1using System.Text.Json;
 2using Dotnet.Installer.Core.Models;
 3using Dotnet.Installer.Core.Services.Contracts;
 4
 5namespace Dotnet.Installer.Core.Services.Implementations;
 6
 7public partial class ManifestService : IManifestService
 8{
 09    private static readonly JsonSerializerOptions JsonSerializerOptions = new()
 010    {
 011        PropertyNameCaseInsensitive = true
 012    };
 13
 014    private List<Component> _local = [];
 015    private List<Component> _remote = [];
 016    private List<Component> _merged = [];
 17
 018    public string SnapConfigurationLocation => SnapConfigPath;
 19    public string DotnetInstallLocation =>
 020        Environment.GetEnvironmentVariable("DOTNET_INSTALL_DIR")
 021            ?? throw new ApplicationException("DOTNET_INSTALL_DIR is not set.");
 22
 23    /// <summary>
 24    /// The local manifest, which includes currently installed components.
 25    /// </summary>
 26    public IEnumerable<Component> Local
 27    {
 028        get => _local;
 029        private set => _local = value.ToList();
 30    }
 31
 32    /// <summary>
 33    /// The remote manifest, which includes available components to be downloaded.
 34    /// </summary>
 35    public IEnumerable<Component> Remote
 36    {
 037        get => _remote;
 038        private set => _remote = value.ToList();
 39    }
 40
 41    /// <summary>
 42    /// The merged manifest, which is the local and remote manifests merged into one list.
 43    /// Installed components can be told apart by verifying whether <c>Installation != null</c>.
 44    /// </summary>
 45    public IEnumerable<Component> Merged
 46    {
 047        get => _merged;
 048        private set => _merged = value.ToList();
 49    }
 50
 51    public async Task Initialize(bool includeArchive = false, CancellationToken cancellationToken = default)
 052    {
 053        _local = await LoadLocal(cancellationToken);
 054        _remote = await LoadRemote(includeArchive, cancellationToken);
 055        _merged = Merge(_remote, _local);
 056    }
 57
 58    public async Task Add(Component component, CancellationToken cancellationToken = default)
 059    {
 060        component.Installation = new Installation
 061        {
 062            InstalledAt = DateTimeOffset.UtcNow
 063        };
 064        _local.Add(component);
 065        await Save(cancellationToken);
 066    }
 67
 68    public async Task Remove(Component component, CancellationToken cancellationToken = default)
 069    {
 070        var componentToRemove = _local.FirstOrDefault(c => c.Key == component.Key);
 071        if (componentToRemove is not null)
 072        {
 073            _local.Remove(componentToRemove);
 074        }
 075        await Save(cancellationToken);
 076    }
 77}

/home/runner/work/dotnet-snap/dotnet-snap/src/Dotnet.Installer.Core/Services/Implementations/ManifestService.Private.cs

#LineLine coverage
 1using System.Net.Http.Json;
 2using System.Text;
 3using System.Text.Json;
 4using CliWrap;
 5using Dotnet.Installer.Core.Models;
 6
 7namespace Dotnet.Installer.Core.Services.Implementations;
 8
 9public partial class ManifestService
 10{
 011    private static readonly string SnapConfigPath = Path.Join(
 012        Environment.GetEnvironmentVariable("DOTNET_INSTALL_DIR"), "..", "snap");
 13
 014    private static readonly string LocalManifestPath = Path.Join(SnapConfigPath, "manifest.json");
 15
 016    private static readonly string[] SupportedVersions = ["6.0", "7.0", "8.0"];
 17
 018    private static readonly HttpClient HttpClient = new()
 019    {
 020        #if !DEBUG
 021        BaseAddress = new Uri(Environment.GetEnvironmentVariable("SERVER_URL")
 022                              ?? throw new ApplicationException(
 023                                  "SERVER_URL environment variable is not defined."))
 024        #endif
 025    };
 26
 27    private static async Task<List<Component>> LoadLocal(CancellationToken cancellationToken = default)
 028    {
 029        if (!File.Exists(LocalManifestPath)) return [];
 30
 031        await using var fs = File.OpenRead(LocalManifestPath);
 032        var result = await JsonSerializer.DeserializeAsync<List<Component>>(
 033            fs, JsonSerializerOptions, cancellationToken
 034        );
 35
 036        return result ?? [];
 037    }
 38
 39    private static async Task<List<Component>> LoadRemote(bool includeArchive = false, CancellationToken cancellationTok
 040    {
 041        var content = new List<Component>();
 42
 43        #if DEBUG
 044        await Cli.Wrap("git")
 045            .WithArguments(["rev-parse", "--show-toplevel"])
 046            .WithStandardOutputPipe(PipeTarget.ToDelegate(path =>
 047            {
 048                var manifestLocation = Path.Join(path, "manifest", "latest.json");
 049                if (!File.Exists(manifestLocation)) return;
 050                var latest = JsonSerializer.Deserialize<List<Component>>(
 051                    File.ReadAllText(manifestLocation), JsonSerializerOptions);
 052                content.AddRange(latest ?? []);
 053
 054                if (includeArchive)
 055                {
 056                    foreach (var version in SupportedVersions)
 057                    {
 058                        var versionArchiveLocation = Path.Join(path, "manifest", version, "archive.json");
 059                        if (!File.Exists(versionArchiveLocation)) return;
 060                        var versionArchive = JsonSerializer.Deserialize<List<Component>>(
 061                            File.ReadAllText(versionArchiveLocation), JsonSerializerOptions);
 062                        content.AddRange(versionArchive ?? []);
 063                    }
 064                }
 065            }))
 066            .ExecuteAsync(cancellationToken);
 67        #else
 68        var response = await HttpClient.GetAsync("latest.json", cancellationToken);
 69
 70        if (response.IsSuccessStatusCode)
 71        {
 72            var latest = await response.Content.ReadFromJsonAsync<List<Component>>(
 73                cancellationToken: cancellationToken);
 74            if (latest is not null) content.AddRange(latest);
 75        }
 76
 77        if (includeArchive)
 78        {
 79            foreach (var version in SupportedVersions)
 80            {
 81                response = await HttpClient.GetAsync($"{version}/archive.json", cancellationToken);
 82
 83                if (!response.IsSuccessStatusCode) continue;
 84
 85                var archive =
 86                    await response.Content.ReadFromJsonAsync<List<Component>>(cancellationToken: cancellationToken);
 87
 88                if (archive is not null) content.AddRange(archive);
 89            }
 90        }
 91        #endif
 92
 093        return content;
 094    }
 95
 96    private static List<Component> Merge(List<Component> remoteComponents, List<Component> localComponents)
 097    {
 098        var result = new List<Component>();
 099        result.AddRange(remoteComponents);
 100
 0101        foreach (var localComponent in localComponents)
 0102        {
 0103            if (result.All(c => c.Key != localComponent.Key))
 0104            {
 0105                result.Add(localComponent);
 0106            }
 107            else
 0108            {
 0109                var remote = result.First(c => c.Key == localComponent.Key);
 0110                remote.Installation = localComponent.Installation;
 0111            }
 0112        }
 113
 0114        return result;
 0115    }
 116
 117    private async Task Save(CancellationToken cancellationToken = default)
 0118    {
 0119        await using var sw = new StreamWriter(LocalManifestPath, append: false, Encoding.UTF8);
 0120        var content = JsonSerializer.Serialize(_local, JsonSerializerOptions);
 0121        var stringBuilder = new StringBuilder(content);
 0122        await sw.WriteLineAsync(stringBuilder, cancellationToken);
 0123    }
 124}