| | 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; |
| | 22 | |
|
| 0 | 23 | | public string SnapConfigurationLocation => SnapConfigPath; |
| | 24 | | public string DotnetInstallLocation => |
| 0 | 25 | | Environment.GetEnvironmentVariable("DOTNET_INSTALL_DIR") |
| 0 | 26 | | ?? throw new ApplicationException("DOTNET_INSTALL_DIR is not set."); |
| | 27 | |
|
| | 28 | | /// <summary> |
| | 29 | | /// The local manifest, which includes currently installed components. |
| | 30 | | /// </summary> |
| | 31 | | public IEnumerable<Component> Local |
| | 32 | | { |
| 0 | 33 | | get => _local; |
| 0 | 34 | | private set => _local = value.ToList(); |
| | 35 | | } |
| | 36 | |
|
| | 37 | | /// <summary> |
| | 38 | | /// The remote manifest, which includes available components to be downloaded. |
| | 39 | | /// </summary> |
| | 40 | | public IEnumerable<Component> Remote |
| | 41 | | { |
| 0 | 42 | | get => _remote; |
| 0 | 43 | | private set => _remote = value.ToList(); |
| | 44 | | } |
| | 45 | |
|
| | 46 | | /// <summary> |
| | 47 | | /// The merged manifest, which is the local and remote manifests merged into one list. |
| | 48 | | /// Installed components can be told apart by verifying whether <c>Installation != null</c>. |
| | 49 | | /// </summary> |
| | 50 | | public IEnumerable<Component> Merged |
| | 51 | | { |
| 0 | 52 | | get => _merged; |
| 0 | 53 | | private set => _merged = value.ToList(); |
| | 54 | | } |
| | 55 | |
|
| | 56 | | public Task Initialize(bool includeUnsupported = false, CancellationToken cancellationToken = default) |
| 0 | 57 | | { |
| 0 | 58 | | _includeUnsupported = includeUnsupported; |
| 0 | 59 | | return Refresh(cancellationToken); |
| 0 | 60 | | } |
| | 61 | |
|
| | 62 | | public async Task Add(Component component, CancellationToken cancellationToken = default) |
| 0 | 63 | | { |
| 0 | 64 | | component.Installation = new Installation |
| 0 | 65 | | { |
| 0 | 66 | | InstalledAt = DateTimeOffset.UtcNow |
| 0 | 67 | | }; |
| 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 | | } |