136 lines
5.0 KiB
C#
136 lines
5.0 KiB
C#
|
|
using StellaOps.PacksRegistry.Core.Contracts;
|
|
using StellaOps.PacksRegistry.Core.Models;
|
|
using System.Text.Json;
|
|
|
|
namespace StellaOps.PacksRegistry.Infrastructure.FileSystem;
|
|
|
|
public sealed class FilePackRepository : IPackRepository
|
|
{
|
|
private readonly string _root;
|
|
private readonly string _indexPath;
|
|
private readonly JsonSerializerOptions _jsonOptions;
|
|
private readonly SemaphoreSlim _mutex = new(1, 1);
|
|
|
|
public FilePackRepository(string root)
|
|
{
|
|
_root = string.IsNullOrWhiteSpace(root) ? Path.GetFullPath("data/packs") : Path.GetFullPath(root);
|
|
_indexPath = Path.Combine(_root, "index.ndjson");
|
|
_jsonOptions = new JsonSerializerOptions
|
|
{
|
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
|
WriteIndented = false
|
|
};
|
|
|
|
Directory.CreateDirectory(_root);
|
|
Directory.CreateDirectory(Path.Combine(_root, "blobs"));
|
|
}
|
|
|
|
public async Task UpsertAsync(PackRecord record, byte[] content, byte[]? provenance, CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(record);
|
|
ArgumentNullException.ThrowIfNull(content);
|
|
|
|
await _mutex.WaitAsync(cancellationToken).ConfigureAwait(false);
|
|
try
|
|
{
|
|
var blobPath = Path.Combine(_root, "blobs", record.Digest.Replace(':', '_'));
|
|
await File.WriteAllBytesAsync(blobPath, content, cancellationToken).ConfigureAwait(false);
|
|
|
|
if (provenance is { Length: > 0 } && record.ProvenanceDigest is not null)
|
|
{
|
|
var provPath = Path.Combine(_root, "provenance", record.ProvenanceDigest.Replace(':', '_'));
|
|
Directory.CreateDirectory(Path.GetDirectoryName(provPath)!);
|
|
await File.WriteAllBytesAsync(provPath, provenance, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
|
|
await using var stream = new FileStream(_indexPath, 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<PackRecord?> GetAsync(string packId, CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(packId);
|
|
var records = await ReadAllAsync(cancellationToken).ConfigureAwait(false);
|
|
return records.LastOrDefault(r => string.Equals(r.PackId, packId, StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
public async Task<IReadOnlyList<PackRecord>> ListAsync(string? tenantId = null, CancellationToken cancellationToken = default)
|
|
{
|
|
var records = await ReadAllAsync(cancellationToken).ConfigureAwait(false);
|
|
if (!string.IsNullOrWhiteSpace(tenantId))
|
|
{
|
|
records = records.Where(r => string.Equals(r.TenantId, tenantId, StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
return records
|
|
.OrderBy(r => r.TenantId, StringComparer.OrdinalIgnoreCase)
|
|
.ThenBy(r => r.Name, StringComparer.OrdinalIgnoreCase)
|
|
.ThenBy(r => r.Version, StringComparer.OrdinalIgnoreCase)
|
|
.ToArray();
|
|
}
|
|
|
|
private async Task<IEnumerable<PackRecord>> ReadAllAsync(CancellationToken cancellationToken)
|
|
{
|
|
if (!File.Exists(_indexPath))
|
|
{
|
|
return Array.Empty<PackRecord>();
|
|
}
|
|
|
|
await _mutex.WaitAsync(cancellationToken).ConfigureAwait(false);
|
|
try
|
|
{
|
|
var lines = await File.ReadAllLinesAsync(_indexPath, cancellationToken).ConfigureAwait(false);
|
|
return lines
|
|
.Where(line => !string.IsNullOrWhiteSpace(line))
|
|
.Select(line => JsonSerializer.Deserialize<PackRecord>(line, _jsonOptions))
|
|
.Where(r => r is not null)!;
|
|
}
|
|
finally
|
|
{
|
|
_mutex.Release();
|
|
}
|
|
}
|
|
|
|
public async Task<byte[]?> GetContentAsync(string packId, CancellationToken cancellationToken = default)
|
|
{
|
|
var record = await GetAsync(packId, cancellationToken).ConfigureAwait(false);
|
|
if (record is null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var blobPath = Path.Combine(_root, "blobs", record.Digest.Replace(':', '_'));
|
|
if (!File.Exists(blobPath))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return await File.ReadAllBytesAsync(blobPath, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
|
|
public async Task<byte[]?> GetProvenanceAsync(string packId, CancellationToken cancellationToken = default)
|
|
{
|
|
var record = await GetAsync(packId, cancellationToken).ConfigureAwait(false);
|
|
if (record?.ProvenanceDigest is null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var provPath = Path.Combine(_root, "provenance", record.ProvenanceDigest.Replace(':', '_'));
|
|
if (!File.Exists(provPath))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return await File.ReadAllBytesAsync(provPath, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
}
|