feat(applications): preserve submitted packages

This commit is contained in:
cesnimda
2026-08-31 16:54:41 +02:00
parent 47e8e3e50e
commit 5337ba3a5e
22 changed files with 903 additions and 11 deletions
@@ -4,14 +4,17 @@ using Microsoft.EntityFrameworkCore;
namespace JobTrackerApi.Services;
public sealed record AttachmentReconciliationResult(int Promoted, int Restored, int Purged, int Missing, int UnknownOrphans, int UnsafePaths, int Failures);
public sealed record StoredSnapshotFile(string Path, long Length, string Sha256);
public interface IAttachmentStorage
{
string CreateFinalPath(int jobId, string storedFileName);
string StagePath(string finalPath);
string DeletePath(string finalPath);
string CreatePackagePath(int jobId, int packageVersion, string storedFileName);
bool IsManagedPath(string path);
Task StageAsync(IFormFile file, string stagePath, CancellationToken cancellationToken);
Task<StoredSnapshotFile> SnapshotAsync(string sourcePath, string destinationPath, CancellationToken cancellationToken);
void Promote(string stagePath, string finalPath);
void Quarantine(string finalPath, string deletePath);
void Restore(string deletePath, string finalPath);
@@ -43,6 +46,14 @@ public sealed class AttachmentStorage : IAttachmentStorage
public string StagePath(string finalPath) => EnsureManagedPath(finalPath, true) + UploadSuffix;
public string DeletePath(string finalPath) => EnsureManagedPath(finalPath, true) + DeleteSuffix;
public string CreatePackagePath(int jobId, int packageVersion, string storedFileName)
{
var folder = Path.Combine(_root, jobId.ToString(System.Globalization.CultureInfo.InvariantCulture), "submitted", packageVersion.ToString(System.Globalization.CultureInfo.InvariantCulture));
Directory.CreateDirectory(folder);
EnsureManagedPath(folder, allowMissingLeaf: false);
return EnsureManagedPath(Path.Combine(folder, Path.GetFileName(storedFileName)), allowMissingLeaf: true);
}
public bool IsManagedPath(string path)
{
try
@@ -75,6 +86,36 @@ public sealed class AttachmentStorage : IAttachmentStorage
}
}
public async Task<StoredSnapshotFile> SnapshotAsync(string sourcePath, string destinationPath, CancellationToken cancellationToken)
{
var source = EnsureManagedPath(sourcePath, allowMissingLeaf: false);
var destination = EnsureManagedPath(destinationPath, allowMissingLeaf: true);
if (File.Exists(destination)) throw new IOException("A submitted package file already exists at the destination.");
try
{
await using var input = new FileStream(source, FileMode.Open, FileAccess.Read, FileShare.Read);
await using var output = new FileStream(destination, FileMode.CreateNew, FileAccess.Write, FileShare.None);
using var hash = System.Security.Cryptography.IncrementalHash.CreateHash(System.Security.Cryptography.HashAlgorithmName.SHA256);
var buffer = new byte[81920];
int read;
long length = 0;
while ((read = await input.ReadAsync(buffer, cancellationToken)) > 0)
{
await output.WriteAsync(buffer.AsMemory(0, read), cancellationToken);
hash.AppendData(buffer, 0, read);
length += read;
}
await output.FlushAsync(cancellationToken);
return new StoredSnapshotFile(destination, length, Convert.ToHexString(hash.GetHashAndReset()).ToLowerInvariant());
}
catch
{
if (File.Exists(destination)) File.Delete(destination);
throw;
}
}
public void Promote(string stagePath, string finalPath) =>
File.Move(EnsureManagedPath(stagePath, false), EnsureManagedPath(finalPath, true), overwrite: false);
@@ -95,6 +136,9 @@ public sealed class AttachmentStorage : IAttachmentStorage
var storedPaths = await db.Attachments.IgnoreQueryFilters().AsNoTracking()
.Select(x => x.FilePath)
.ToListAsync(cancellationToken);
storedPaths.AddRange(await db.SubmittedPackageAttachments.IgnoreQueryFilters().AsNoTracking()
.Select(x => x.FilePath)
.ToListAsync(cancellationToken));
var known = new HashSet<string>(storedPaths.Where(IsManagedPath).Select(Path.GetFullPath), _comparer);
var unsafePaths = storedPaths.Count(path => !IsManagedPath(path));
var promoted = 0;