using StellaOps.PacksRegistry.Core.Contracts; using StellaOps.PacksRegistry.Core.Models; using System.Text.Json; namespace StellaOps.PacksRegistry.Infrastructure.FileSystem; public sealed class FileAuditRepository : IAuditRepository { private readonly string _path; private readonly JsonSerializerOptions _jsonOptions; private readonly SemaphoreSlim _mutex = new(1, 1); public FileAuditRepository(string rootPath) { var root = string.IsNullOrWhiteSpace(rootPath) ? Path.GetFullPath("data/packs") : Path.GetFullPath(rootPath); Directory.CreateDirectory(root); _path = Path.Combine(root, "audit.ndjson"); _jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; } public async Task AppendAsync(AuditRecord record, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(record); await _mutex.WaitAsync(cancellationToken).ConfigureAwait(false); try { await using var stream = new FileStream(_path, FileMode.Append, FileAccess.Write, FileShare.Read); await using var writer = new StreamWriter(stream); var json = JsonSerializer.Serialize(record, _jsonOptions); await writer.WriteLineAsync(json.AsMemory(), cancellationToken).ConfigureAwait(false); } finally { _mutex.Release(); } } public async Task> ListAsync(string? tenantId = null, CancellationToken cancellationToken = default) { if (!File.Exists(_path)) { return Array.Empty(); } await _mutex.WaitAsync(cancellationToken).ConfigureAwait(false); try { var lines = await File.ReadAllLinesAsync(_path, cancellationToken).ConfigureAwait(false); IEnumerable records = lines .Where(l => !string.IsNullOrWhiteSpace(l)) .Select(l => JsonSerializer.Deserialize(l, _jsonOptions)) .Where(r => r is not null)! .Cast(); if (!string.IsNullOrWhiteSpace(tenantId)) { records = records.Where(r => string.Equals(r.TenantId, tenantId, StringComparison.OrdinalIgnoreCase)); } return records .OrderBy(r => r.OccurredAtUtc) .ThenBy(r => r.PackId, StringComparer.OrdinalIgnoreCase) .ToList(); } finally { _mutex.Release(); } } }