namespace marlin.agents.shell; using System.Security.Cryptography; using System.Text.Json; using System.Text.Json.Serialization; using core.util; using Microsoft.Extensions.FileSystemGlobbing; using Microsoft.Extensions.FileSystemGlobbing.Abstractions; public class BlobArtifactStore : IArtifactStore { private static readonly string _storeDir = Path.Combine(CommonPaths.AgentRoot, "store"); private static readonly string _blobDir = Path.Combine(_storeDir, "blob"); private static readonly string _manifestFile = Path.Combine(_storeDir, "manifest.json"); private Dictionary _manifest { get => field != null ? field : throw new InvalidOperationException("Init not run, cannot access manifest"); set; } public void Init() { Directory.CreateDirectory(_storeDir); Directory.CreateDirectory(_blobDir); if (File.Exists(_manifestFile)) { _manifest = JsonSerializer.Deserialize>( File.ReadAllText(_manifestFile)) ?? throw new InvalidOperationException("Could not read manifest, possibly corrupted"); } else _manifest = new(); } public async Task Stow(Guid jobId, string stepId, string artifactId, string basePath, string glob) { var key = new ArtifactManifestKey { JobId = jobId, StepId = stepId, ArtifactId = artifactId }; var entries = new List(); Matcher matcher = new(); matcher.AddInclude(glob); var globMatches = matcher.Execute(new DirectoryInfoWrapper(new DirectoryInfo(basePath))); foreach (var globMatch in globMatches.Files) { var fullPath = Path.Join(basePath, globMatch.Path); var entry = new ArtifactManifestEntry { RelativePath = globMatch.Path, Hash = ComputeFileHash(fullPath) }; var targetPath = Path.Join(_blobDir, entry.Hash); if (!File.Exists(targetPath)) File.Move(fullPath, targetPath); else File.Delete(fullPath); entries.Add(entry); } _manifest[key] = entries.ToArray(); await File.WriteAllTextAsync(_manifestFile, JsonSerializer.Serialize(this._manifest)); } public async Task Access(Guid jobId, string stepId, string artifactId) { throw new NotImplementedException(); } private static string ComputeFileHash(string filePath) { using var stream = File.OpenRead(filePath); byte[] hash = SHA256.HashData(stream); return Convert.ToHexStringLower(hash); } } public class BlobArtifactAccessHandle : IArtifactAccessHandle { public string Path { get; } public void Dispose() => Directory.Delete(Path, true); } public class ArtifactManifestKey { public required Guid JobId { get; init; } public required string StepId { get; init; } public required string ArtifactId { get; init; } public override int GetHashCode() => HashCode.Combine(JobId.GetHashCode(), StepId.GetHashCode(), ArtifactId.GetHashCode()); } public class ArtifactManifestEntry { public required string RelativePath { get; init; } public required string Hash { get; init; } }