Files
git.stella-ops.org/src/__Libraries/StellaOps.Artifact.Infrastructure/ServiceCollectionExtensions.cs
2026-02-01 21:37:40 +02:00

203 lines
7.6 KiB
C#

// -----------------------------------------------------------------------------
// 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;
/// <summary>
/// Extension methods for registering artifact store services.
/// </summary>
public static class ServiceCollectionExtensions
{
/// <summary>
/// Adds unified artifact store services with S3 backend.
/// </summary>
/// <param name="services">Service collection.</param>
/// <param name="configuration">Configuration root.</param>
/// <param name="sectionName">Configuration section for options.</param>
/// <returns>Service collection for chaining.</returns>
public static IServiceCollection AddUnifiedArtifactStore(
this IServiceCollection services,
IConfiguration configuration,
string sectionName = "ArtifactStore")
{
// Configure S3 store options
services.Configure<S3UnifiedArtifactStoreOptions>(configuration.GetSection($"{sectionName}:S3"));
// Configure PostgreSQL options for index
services.Configure<PostgresOptions>("Artifact", configuration.GetSection($"{sectionName}:Postgres"));
// Register data source
services.AddSingleton<ArtifactDataSource>(sp =>
{
var options = sp.GetRequiredService<IOptionsSnapshot<PostgresOptions>>().Get("Artifact");
var logger = sp.GetRequiredService<Microsoft.Extensions.Logging.ILogger<ArtifactDataSource>>();
return new ArtifactDataSource(Options.Create(options), logger);
});
// Register core services
services.AddSingleton<ICycloneDxExtractor, CycloneDxExtractor>();
// Register index repository
services.AddScoped<IArtifactIndexRepository>(sp =>
{
var dataSource = sp.GetRequiredService<ArtifactDataSource>();
var logger = sp.GetRequiredService<Microsoft.Extensions.Logging.ILogger<PostgresArtifactIndexRepository>>();
// TODO: Get tenant ID from context
return new PostgresArtifactIndexRepository(dataSource, logger, "default");
});
// Register S3 artifact store
services.AddScoped<IArtifactStore, S3UnifiedArtifactStore>();
return services;
}
/// <summary>
/// Adds unified artifact store with in-memory backend (for testing).
/// </summary>
/// <param name="services">Service collection.</param>
/// <returns>Service collection for chaining.</returns>
public static IServiceCollection AddInMemoryArtifactStore(this IServiceCollection services)
{
services.AddSingleton<ICycloneDxExtractor, CycloneDxExtractor>();
services.AddSingleton<IArtifactIndexRepository, InMemoryArtifactIndexRepository>();
services.AddSingleton<IArtifactStore, InMemoryArtifactStore>();
return services;
}
/// <summary>
/// Adds artifact migration services.
/// </summary>
/// <param name="services">Service collection.</param>
/// <param name="configure">Options configuration.</param>
/// <returns>Service collection for chaining.</returns>
public static IServiceCollection AddArtifactMigration(
this IServiceCollection services,
Action<ArtifactMigrationOptions>? configure = null)
{
var options = new ArtifactMigrationOptions();
configure?.Invoke(options);
services.AddSingleton(options);
services.AddScoped<ArtifactMigrationService>();
return services;
}
}
/// <summary>
/// In-memory artifact store for testing.
/// </summary>
public sealed class InMemoryArtifactStore : IArtifactStore
{
private readonly Dictionary<string, (byte[] Content, ArtifactMetadata Metadata)> _artifacts = new();
private readonly object _lock = new();
public Task<ArtifactStoreResult> 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<ArtifactReadResult> 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<IReadOnlyList<ArtifactMetadata>> 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<IReadOnlyList<ArtifactMetadata>>(result);
}
}
public Task<bool> 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<ArtifactMetadata?> 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<bool> DeleteAsync(string bomRef, string serialNumber, string artifactId, CancellationToken ct = default)
{
var key = $"{bomRef}/{serialNumber}/{artifactId}";
lock (_lock)
{
return Task.FromResult(_artifacts.Remove(key));
}
}
}