using System.Text; using SQLInsight.Licensing; using SQLInsight.Rules.Lifecycle; namespace SQLInsight.Updates; public sealed record CheckedManifest(UpdateManifest Manifest, string Token); /// /// The unprivileged half of updating, run by the agent: reads the signed manifest, installs newer rule packs, and /// downloads and stages application packages. It never replaces program files; does that. /// public sealed class UpdateChecker(UpdateFeed feed, TrustedKeys keys, UpdateDirectories directories, string channel, TimeProvider time) { public string Channel { get; } = channel; public async Task FetchAsync(CancellationToken cancellationToken) { var token = await feed.ReadManifestAsync(Channel, cancellationToken).ConfigureAwait(false); var manifest = ManifestVerifier.Verify(token, keys, Channel, time.GetUtcNow(), directories.LastManifest(Channel)); directories.RememberManifest(Channel, manifest.PublishedAt); return new CheckedManifest(manifest, token); } /// Downloads the manifest's rule pack if it is newer than . Returns the new pack or null. public async Task UpdateRulePackAsync(CheckedManifest source, RulePack current, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(source); ArgumentNullException.ThrowIfNull(current); if (source.Manifest.RulePack is not { } reference || !Version.TryParse(reference.Version, out var offered) || offered <= current.Version) { return null; } var bytes = await feed.DownloadAsync(reference.File, cancellationToken).ConfigureAwait(false); RulePack pack; try { pack = RulePack.Parse(bytes, $"regelpakke fra {source.Manifest.Channel}-kanalen, {source.Manifest.PublishedAt:yyyy-MM-dd}"); } catch (InvalidDataException ex) { throw new UpdateException(ex.Message, ex); } if (pack.Version != offered) { throw new UpdateException($"Regelpakkens version ({pack.Version}) svarer ikke til manifestet ({offered})."); } new RulePackStore(directories).Save(bytes, source.Token); return pack; } /// The release in the manifest if it is newer than the running version, otherwise null. public static AppRelease? NewerRelease(UpdateManifest manifest, AppVersion installed) { ArgumentNullException.ThrowIfNull(manifest); return manifest.Release is { } release && AppVersion.Parse(release.Version) > installed ? release : null; } /// /// Downloads the package for this platform into the pending folder, where the privileged updater picks it up. /// Returns false if there is nothing newer than . /// public async Task StageAsync(CheckedManifest source, AppVersion installed, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(source); if (NewerRelease(source.Manifest, installed) is not { } release) { return false; } var asset = release.Assets.FirstOrDefault(a => string.Equals(a.Platform, Platform.Current, StringComparison.OrdinalIgnoreCase)) ?? throw new UpdateException($"Version {release.Version} har ingen pakke til {Platform.Current}."); var package = Path.Combine(directories.Pending, "package.zip"); var manifestFile = Path.Combine(directories.Pending, UpdateFeed.ManifestFile); if (File.Exists(package) && File.Exists(manifestFile) && await File.ReadAllTextAsync(manifestFile, cancellationToken).ConfigureAwait(false) == source.Token) { return true; } Directory.CreateDirectory(directories.Pending); File.Delete(manifestFile); var download = package + ".download"; await feed.DownloadToFileAsync(asset.File, download, cancellationToken).ConfigureAwait(false); File.Move(download, package, overwrite: true); UpdateDirectories.WriteAtomic(manifestFile, Encoding.UTF8.GetBytes(source.Token)); return true; } }