// ----------------------------------------------------------------------------- // ServiceCollectionExtensions.cs // Sprint: SPRINT_20260118_017_Evidence_artifact_store_unification // Tasks: AS-002, AS-003 - Service registration // Description: DI registration for artifact store services // ----------------------------------------------------------------------------- using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using StellaOps.Artifact.Core; using StellaOps.Infrastructure.Postgres.Options; namespace StellaOps.Artifact.Infrastructure; /// /// Extension methods for registering artifact store services. /// public static class ServiceCollectionExtensions { /// /// Adds unified artifact store services with S3 backend. /// /// Service collection. /// Configuration root. /// Configuration section for options. /// Service collection for chaining. public static IServiceCollection AddUnifiedArtifactStore( this IServiceCollection services, IConfiguration configuration, string sectionName = "ArtifactStore") { // Configure S3 store options services.Configure(configuration.GetSection($"{sectionName}:S3")); // Configure PostgreSQL options for index services.Configure("Artifact", configuration.GetSection($"{sectionName}:Postgres")); // Register data source services.AddSingleton(sp => { var options = sp.GetRequiredService>().Get("Artifact"); var logger = sp.GetRequiredService>(); return new ArtifactDataSource(Options.Create(options), logger); }); // Register core services services.AddSingleton(); // Register index repository services.AddScoped(sp => { var dataSource = sp.GetRequiredService(); var logger = sp.GetRequiredService>(); // TODO: Get tenant ID from context return new PostgresArtifactIndexRepository(dataSource, logger, "default"); }); // Register S3 artifact store services.AddScoped(); return services; } /// /// Adds unified artifact store with in-memory backend (for testing). /// /// Service collection. /// Service collection for chaining. public static IServiceCollection AddInMemoryArtifactStore(this IServiceCollection services) { services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); return services; } /// /// Adds artifact migration services. /// /// Service collection. /// Options configuration. /// Service collection for chaining. public static IServiceCollection AddArtifactMigration( this IServiceCollection services, Action? configure = null) { var options = new ArtifactMigrationOptions(); configure?.Invoke(options); services.AddSingleton(options); services.AddScoped(); return services; } } /// /// In-memory artifact store for testing. /// public sealed class InMemoryArtifactStore : IArtifactStore { private readonly Dictionary _artifacts = new(); private readonly object _lock = new(); public Task StoreAsync(ArtifactStoreRequest request, CancellationToken ct = default) { var key = $"{request.BomRef}/{request.SerialNumber}/{request.ArtifactId}"; using var ms = new MemoryStream(); request.Content.CopyTo(ms); var content = ms.ToArray(); using var sha = System.Security.Cryptography.SHA256.Create(); var hash = sha.ComputeHash(content); var sha256 = Convert.ToHexStringLower(hash); var metadata = new ArtifactMetadata { StorageKey = key, BomRef = request.BomRef, SerialNumber = request.SerialNumber, ArtifactId = request.ArtifactId, ContentType = request.ContentType, SizeBytes = content.Length, Sha256 = sha256, CreatedAt = DateTimeOffset.UtcNow, Type = request.Type, TenantId = request.TenantId }; lock (_lock) { var wasCreated = !_artifacts.ContainsKey(key); _artifacts[key] = (content, metadata); return Task.FromResult(ArtifactStoreResult.Succeeded(key, sha256, content.Length, wasCreated)); } } public Task ReadAsync(string bomRef, string? serialNumber, string? artifactId, CancellationToken ct = default) { lock (_lock) { var matching = _artifacts .Where(kvp => kvp.Value.Metadata.BomRef == bomRef) .Where(kvp => serialNumber == null || kvp.Value.Metadata.SerialNumber == serialNumber) .Where(kvp => artifactId == null || kvp.Value.Metadata.ArtifactId == artifactId) .FirstOrDefault(); if (matching.Value.Content == null) { return Task.FromResult(ArtifactReadResult.NotFound()); } return Task.FromResult(ArtifactReadResult.Succeeded( new MemoryStream(matching.Value.Content), matching.Value.Metadata)); } } public Task> ListAsync(string bomRef, string? serialNumber = null, CancellationToken ct = default) { lock (_lock) { var result = _artifacts.Values .Where(x => x.Metadata.BomRef == bomRef) .Where(x => serialNumber == null || x.Metadata.SerialNumber == serialNumber) .Select(x => x.Metadata) .ToList(); return Task.FromResult>(result); } } public Task ExistsAsync(string bomRef, string serialNumber, string artifactId, CancellationToken ct = default) { var key = $"{bomRef}/{serialNumber}/{artifactId}"; lock (_lock) { return Task.FromResult(_artifacts.ContainsKey(key)); } } public Task GetMetadataAsync(string bomRef, string serialNumber, string artifactId, CancellationToken ct = default) { var key = $"{bomRef}/{serialNumber}/{artifactId}"; lock (_lock) { return Task.FromResult(_artifacts.TryGetValue(key, out var entry) ? entry.Metadata : null); } } public Task DeleteAsync(string bomRef, string serialNumber, string artifactId, CancellationToken ct = default) { var key = $"{bomRef}/{serialNumber}/{artifactId}"; lock (_lock) { return Task.FromResult(_artifacts.Remove(key)); } } }