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;
}
}