using System; using System.Globalization; using System.IO; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using StellaOps.Signals.Options; using StellaOps.Signals.Storage.Models; namespace StellaOps.Signals.Storage; /// /// Stores callgraph artifacts on the local filesystem. /// internal sealed class FileSystemCallgraphArtifactStore : ICallgraphArtifactStore { private readonly SignalsArtifactStorageOptions storageOptions; private readonly ILogger logger; public FileSystemCallgraphArtifactStore(IOptions options, ILogger logger) { ArgumentNullException.ThrowIfNull(options); storageOptions = options.Value.Storage; this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); } public async Task SaveAsync(CallgraphArtifactSaveRequest request, Stream content, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(request); ArgumentNullException.ThrowIfNull(content); var root = storageOptions.RootPath; var directory = Path.Combine(root, Sanitize(request.Language), Sanitize(request.Component), Sanitize(request.Version)); Directory.CreateDirectory(directory); var fileName = string.IsNullOrWhiteSpace(request.FileName) ? FormattableString.Invariant($"artifact-{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}.bin") : request.FileName.Trim(); var destinationPath = Path.Combine(directory, fileName); await using (var fileStream = File.Create(destinationPath)) { await content.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false); } var fileInfo = new FileInfo(destinationPath); logger.LogInformation("Stored callgraph artifact at {Path} (length={Length}).", destinationPath, fileInfo.Length); return new StoredCallgraphArtifact( Path.GetRelativePath(root, destinationPath), fileInfo.Length, request.Hash, request.ContentType); } private static string Sanitize(string value) => string.Join('_', value.Split(Path.GetInvalidFileNameChars(), StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)).ToLowerInvariant(); }