| | | 1 | | using System.Text.Json; |
| | | 2 | | using System.Text.RegularExpressions; |
| | | 3 | | using Dotnet.Installer.Core.Models; |
| | | 4 | | using Dotnet.Installer.Core.Services.Contracts; |
| | | 5 | | |
| | | 6 | | namespace Dotnet.Installer.Core.Services.Implementations; |
| | | 7 | | |
| | | 8 | | public partial class ManifestService : IManifestService |
| | | 9 | | { |
| | 0 | 10 | | private static readonly Regex DotnetVersionPattern = new ( |
| | 0 | 11 | | pattern: @"\A(?'major'\d+)(?:\.(?'minor'\d+))?\z"); |
| | | 12 | | |
| | 0 | 13 | | private static readonly JsonSerializerOptions JsonSerializerOptions = new() |
| | 0 | 14 | | { |
| | 0 | 15 | | PropertyNameCaseInsensitive = true |
| | 0 | 16 | | }; |
| | | 17 | | |
| | 0 | 18 | | private List<Component> _local = []; |
| | 0 | 19 | | private List<Component> _remote = []; |
| | 0 | 20 | | private List<Component> _merged = []; |
| | 0 | 21 | | private bool _includeUnsupported = false; |
| | 0 | 22 | | private bool _includePrerelease = false; |
| | | 23 | | |
| | 0 | 24 | | public string SnapConfigurationLocation => SnapConfigPath; |
| | | 25 | | public string DotnetInstallLocation => |
| | 0 | 26 | | Environment.GetEnvironmentVariable("DOTNET_INSTALL_DIR") |
| | 0 | 27 | | ?? throw new ApplicationException("DOTNET_INSTALL_DIR is not set."); |
| | | 28 | | |
| | | 29 | | /// <summary> |
| | | 30 | | /// The local manifest, which includes currently installed components. |
| | | 31 | | /// </summary> |
| | | 32 | | public IEnumerable<Component> Local |
| | | 33 | | { |
| | 0 | 34 | | get => _local; |
| | 0 | 35 | | private set => _local = value.ToList(); |
| | | 36 | | } |
| | | 37 | | |
| | | 38 | | /// <summary> |
| | | 39 | | /// The remote manifest, which includes available components to be downloaded. |
| | | 40 | | /// </summary> |
| | | 41 | | public IEnumerable<Component> Remote |
| | | 42 | | { |
| | 0 | 43 | | get => _remote; |
| | 0 | 44 | | private set => _remote = value.ToList(); |
| | | 45 | | } |
| | | 46 | | |
| | | 47 | | /// <summary> |
| | | 48 | | /// The merged manifest, which is the local and remote manifests merged into one list. |
| | | 49 | | /// Installed components can be told apart by verifying whether <c>Installation != null</c>. |
| | | 50 | | /// </summary> |
| | | 51 | | public IEnumerable<Component> Merged |
| | | 52 | | { |
| | 0 | 53 | | get => _merged; |
| | 0 | 54 | | private set => _merged = value.ToList(); |
| | | 55 | | } |
| | | 56 | | |
| | | 57 | | public Task Initialize(bool includeUnsupported = false, bool includePrerelease = false, |
| | | 58 | | CancellationToken cancellationToken = default) |
| | 0 | 59 | | { |
| | 0 | 60 | | _includeUnsupported = includeUnsupported; |
| | 0 | 61 | | _includePrerelease = includePrerelease; |
| | 0 | 62 | | return Refresh(cancellationToken); |
| | 0 | 63 | | } |
| | | 64 | | |
| | | 65 | | public async Task Add(Component component, CancellationToken cancellationToken = default) |
| | 0 | 66 | | { |
| | 0 | 67 | | component.Installation = new Installation(DateTimeOffset.UtcNow); |
| | 0 | 68 | | _local.Add(component); |
| | 0 | 69 | | await Save(cancellationToken); |
| | 0 | 70 | | await Refresh(cancellationToken); |
| | 0 | 71 | | } |
| | | 72 | | |
| | | 73 | | public async Task Remove(Component component, CancellationToken cancellationToken = default) |
| | 0 | 74 | | { |
| | 0 | 75 | | var componentToRemove = _local.FirstOrDefault(c => c.Key == component.Key); |
| | 0 | 76 | | if (componentToRemove is not null) |
| | 0 | 77 | | { |
| | 0 | 78 | | _local.Remove(componentToRemove); |
| | 0 | 79 | | } |
| | 0 | 80 | | await Save(cancellationToken); |
| | 0 | 81 | | await Refresh(cancellationToken); |
| | 0 | 82 | | } |
| | | 83 | | |
| | | 84 | | public Component? MatchRemoteComponent(string component, string version) |
| | 0 | 85 | | { |
| | 0 | 86 | | return MatchComponent(component, version, remote: true); |
| | 0 | 87 | | } |
| | | 88 | | |
| | | 89 | | public Component? MatchLocalComponent(string component, string version) |
| | 0 | 90 | | { |
| | 0 | 91 | | return MatchComponent(component, version, remote: false); |
| | 0 | 92 | | } |
| | | 93 | | |
| | | 94 | | private Component? MatchComponent(string component, string version, bool remote = true) |
| | 0 | 95 | | { |
| | 0 | 96 | | if (string.IsNullOrWhiteSpace(component)) return null; |
| | 0 | 97 | | if (string.IsNullOrWhiteSpace(version)) return null; |
| | | 98 | | |
| | 0 | 99 | | var components = remote ? _remote : _local; |
| | | 100 | | |
| | 0 | 101 | | if (version.Equals("lts", StringComparison.CurrentCultureIgnoreCase)) |
| | 0 | 102 | | { |
| | 0 | 103 | | return components |
| | 0 | 104 | | .Where(c => c.IsLts && c.Name.Equals(component, StringComparison.CurrentCultureIgnoreCase)) |
| | 0 | 105 | | .MaxBy(c => c.MajorVersion); |
| | | 106 | | } |
| | | 107 | | |
| | 0 | 108 | | if (version.Equals("latest", StringComparison.CurrentCultureIgnoreCase)) |
| | 0 | 109 | | { |
| | 0 | 110 | | return components |
| | 0 | 111 | | .Where(c => c.Name.Equals(component, StringComparison.CurrentCultureIgnoreCase)) |
| | 0 | 112 | | .MaxBy(c => c.MajorVersion); |
| | | 113 | | } |
| | | 114 | | |
| | 0 | 115 | | var parsedVersion = DotnetVersionPattern.Match(version); |
| | | 116 | | |
| | 0 | 117 | | if (!parsedVersion.Success) return null; |
| | 0 | 118 | | if (parsedVersion.Groups["minor"].Success |
| | 0 | 119 | | && int.Parse(parsedVersion.Groups["minor"].Value) != 0) |
| | 0 | 120 | | { |
| | 0 | 121 | | return null; |
| | | 122 | | } |
| | | 123 | | |
| | 0 | 124 | | var majorVersion = int.Parse(parsedVersion.Groups["major"].Value); |
| | | 125 | | |
| | 0 | 126 | | return components.Where(c => |
| | 0 | 127 | | c.MajorVersion == majorVersion && |
| | 0 | 128 | | c.Name.Equals(component, StringComparison.CurrentCultureIgnoreCase)) |
| | 0 | 129 | | .MaxBy(c => c.MajorVersion); |
| | 0 | 130 | | } |
| | | 131 | | } |