feat: add owned package staging cleanup
This commit is contained in:
@@ -21,6 +21,8 @@ public sealed record PackageActivationReceipt(string OperationId, string Package
|
|||||||
public sealed record PackageActivationResult(string Outcome, PackageActivationReceipt? Receipt);
|
public sealed record PackageActivationResult(string Outcome, PackageActivationReceipt? Receipt);
|
||||||
public sealed record PackageRollbackResult(string Outcome, string? CurrentVersion);
|
public sealed record PackageRollbackResult(string Outcome, string? CurrentVersion);
|
||||||
public sealed record ImmutableLocalPackage(string PackageId, string Version, string LocalPackageRoot);
|
public sealed record ImmutableLocalPackage(string PackageId, string Version, string LocalPackageRoot);
|
||||||
|
public sealed record PackageStagingOwnership(string OperationId, string PackageId, string Version, string StagingDirectory, DateTimeOffset CreatedUtc, string MarkerIdentity);
|
||||||
|
public sealed record PackageStagingCleanupResult(string Outcome, string OperationId);
|
||||||
public interface IPackageMaterializer { Task<ImmutableLocalPackage> MaterializeAsync(PackageManifest manifest, string sourceRoot, CancellationToken cancellationToken, IProgress<PackageProgress>? progress = null); }
|
public interface IPackageMaterializer { Task<ImmutableLocalPackage> MaterializeAsync(PackageManifest manifest, string sourceRoot, CancellationToken cancellationToken, IProgress<PackageProgress>? progress = null); }
|
||||||
public sealed record PackageRequirement(string PackageId, string Product, int ProductYear, string Architecture, string Channel, string RequiredVersion, string RosterIdentity, bool AllowOfflineFallback);
|
public sealed record PackageRequirement(string PackageId, string Product, int ProductYear, string Architecture, string Channel, string RequiredVersion, string RosterIdentity, bool AllowOfflineFallback);
|
||||||
public sealed record CatalogPackage(PackageManifest Manifest, string SourceRoot);
|
public sealed record CatalogPackage(PackageManifest Manifest, string SourceRoot);
|
||||||
|
|||||||
@@ -71,11 +71,20 @@ public sealed class LocalPackageStore(string root, IFileHasher hasher) : ILocalP
|
|||||||
{
|
{
|
||||||
if (!new PackageManifestValidator().Validate(manifest, manifest.ProductYear, manifest.Architecture).IsValid) throw new InvalidOperationException("MANIFEST_INVALID");
|
if (!new PackageManifestValidator().Validate(manifest, manifest.ProductYear, manifest.Architecture).IsValid) throw new InvalidOperationException("MANIFEST_INVALID");
|
||||||
if (manifest.Files.Any(x => string.Equals(x.Path, StoredManifestFileName, StringComparison.OrdinalIgnoreCase))) throw new InvalidOperationException("MANIFEST_RESERVED_FILE");
|
if (manifest.Files.Any(x => string.Equals(x.Path, StoredManifestFileName, StringComparison.OrdinalIgnoreCase))) throw new InvalidOperationException("MANIFEST_RESERVED_FILE");
|
||||||
var staging = Path.Combine(root, "Launcher", "staging", manifest.PackageId + "-" + manifest.Version + "-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(staging);
|
var operationId = Guid.NewGuid().ToString("N"); var staging = Path.Combine(root, "Launcher", "staging", operationId); Directory.CreateDirectory(staging); await File.WriteAllTextAsync(Path.Combine(staging, ".package-staging-owner.json"), JsonSerializer.Serialize(new PackageStagingOwnership(operationId, manifest.PackageId, manifest.Version, staging, DateTimeOffset.UtcNow, operationId)), ct);
|
||||||
try { var i = 0; foreach (var f in manifest.Files) { ct.ThrowIfCancellationRequested(); var source = Path.Combine(sourceRoot, f.Path); var dest = PackageFilePath(staging, f.Path); Directory.CreateDirectory(Path.GetDirectoryName(dest)!); File.Copy(source, dest, true); if (!string.Equals(await hasher.Sha256Async(dest, ct), f.Sha256, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("HASH_MISMATCH"); progress?.Report(new("copy", ++i, manifest.Files.Count)); }
|
try { var i = 0; foreach (var f in manifest.Files) { ct.ThrowIfCancellationRequested(); var source = Path.Combine(sourceRoot, f.Path); var dest = PackageFilePath(staging, f.Path); Directory.CreateDirectory(Path.GetDirectoryName(dest)!); File.Copy(source, dest, true); if (!string.Equals(await hasher.Sha256Async(dest, ct), f.Sha256, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("HASH_MISMATCH"); progress?.Report(new("copy", ++i, manifest.Files.Count)); }
|
||||||
await File.WriteAllTextAsync(Path.Combine(staging, StoredManifestFileName), JsonSerializer.Serialize(manifest), ct);
|
await File.WriteAllTextAsync(Path.Combine(staging, StoredManifestFileName), JsonSerializer.Serialize(manifest), ct);
|
||||||
var versionDir = VersionDirectory(manifest.PackageId, manifest.Version); Directory.CreateDirectory(Path.GetDirectoryName(versionDir)!); if (Directory.Exists(versionDir)) Directory.Delete(staging, true); else Directory.Move(staging, versionDir); return new(manifest.PackageId, manifest.Version, versionDir); }
|
File.Delete(marker); var versionDir = VersionDirectory(manifest.PackageId, manifest.Version); Directory.CreateDirectory(Path.GetDirectoryName(versionDir)!); if (Directory.Exists(versionDir)) Directory.Delete(staging, true); else Directory.Move(staging, versionDir); return new(manifest.PackageId, manifest.Version, versionDir); }
|
||||||
catch { if (Directory.Exists(staging)) Directory.Delete(staging, true); throw; }
|
catch { await CleanupOwnedStagingAsync(new PackageStagingOwnership(operationId, manifest.PackageId, manifest.Version, staging, DateTimeOffset.UtcNow, operationId), CancellationToken.None); throw; }
|
||||||
|
}
|
||||||
|
public async Task<PackageStagingCleanupResult> CleanupOwnedStagingAsync(PackageStagingOwnership ownership, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var stagingRoot=Path.GetFullPath(Path.Combine(root,"Launcher","staging")); var directory=Path.GetFullPath(ownership.StagingDirectory); var prefix=stagingRoot.TrimEnd(Path.DirectorySeparatorChar)+Path.DirectorySeparatorChar;
|
||||||
|
if (!directory.StartsWith(prefix,StringComparison.OrdinalIgnoreCase) || directory.Equals(stagingRoot,StringComparison.OrdinalIgnoreCase)) return new("OwnershipUnknown",ownership.OperationId);
|
||||||
|
if (!Directory.Exists(directory)) return new("AlreadyAbsent",ownership.OperationId);
|
||||||
|
var marker=Path.Combine(directory,".package-staging-owner.json"); if (!File.Exists(marker)) return new("OwnershipUnknown",ownership.OperationId);
|
||||||
|
var stored=JsonSerializer.Deserialize<PackageStagingOwnership>(await File.ReadAllTextAsync(marker,ct)); if (stored is null || stored.OperationId!=ownership.OperationId || stored.PackageId!=ownership.PackageId || stored.Version!=ownership.Version) return new("OwnershipMismatch",ownership.OperationId);
|
||||||
|
Directory.Delete(directory,true); return new("Cleaned",ownership.OperationId);
|
||||||
}
|
}
|
||||||
private async Task<string?> VerifyVersionAsync(PackageRequirement requirement, string version, CancellationToken ct)
|
private async Task<string?> VerifyVersionAsync(PackageRequirement requirement, string version, CancellationToken ct)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user