136 lines
5.2 KiB
C#
136 lines
5.2 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace StellaOps.Concelier.Storage.Mongo.Exporting;
|
|
|
|
/// <summary>
|
|
/// Helper for exporters to read and persist their export metadata in Mongo-backed storage.
|
|
/// </summary>
|
|
public sealed class ExportStateManager
|
|
{
|
|
private readonly IExportStateStore _store;
|
|
private readonly TimeProvider _timeProvider;
|
|
|
|
public ExportStateManager(IExportStateStore store, TimeProvider? timeProvider = null)
|
|
{
|
|
_store = store ?? throw new ArgumentNullException(nameof(store));
|
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
|
}
|
|
|
|
public Task<ExportStateRecord?> GetAsync(string exporterId, CancellationToken cancellationToken)
|
|
{
|
|
ArgumentException.ThrowIfNullOrEmpty(exporterId);
|
|
return _store.FindAsync(exporterId, cancellationToken);
|
|
}
|
|
|
|
public async Task<ExportStateRecord> StoreFullExportAsync(
|
|
string exporterId,
|
|
string exportId,
|
|
string exportDigest,
|
|
string? cursor,
|
|
string? targetRepository,
|
|
string exporterVersion,
|
|
bool resetBaseline,
|
|
IReadOnlyList<ExportFileRecord> manifest,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
ArgumentException.ThrowIfNullOrEmpty(exporterId);
|
|
ArgumentException.ThrowIfNullOrEmpty(exportId);
|
|
ArgumentException.ThrowIfNullOrEmpty(exportDigest);
|
|
ArgumentException.ThrowIfNullOrEmpty(exporterVersion);
|
|
manifest ??= Array.Empty<ExportFileRecord>();
|
|
|
|
var existing = await _store.FindAsync(exporterId, cancellationToken).ConfigureAwait(false);
|
|
var now = _timeProvider.GetUtcNow();
|
|
|
|
if (existing is null)
|
|
{
|
|
var resolvedRepository = string.IsNullOrWhiteSpace(targetRepository) ? null : targetRepository;
|
|
return await _store.UpsertAsync(
|
|
new ExportStateRecord(
|
|
exporterId,
|
|
BaseExportId: exportId,
|
|
BaseDigest: exportDigest,
|
|
LastFullDigest: exportDigest,
|
|
LastDeltaDigest: null,
|
|
ExportCursor: cursor ?? exportDigest,
|
|
TargetRepository: resolvedRepository,
|
|
ExporterVersion: exporterVersion,
|
|
UpdatedAt: now,
|
|
Files: manifest),
|
|
cancellationToken).ConfigureAwait(false);
|
|
}
|
|
|
|
var repositorySpecified = !string.IsNullOrWhiteSpace(targetRepository);
|
|
var resolvedRepo = repositorySpecified ? targetRepository : existing.TargetRepository;
|
|
var repositoryChanged = repositorySpecified
|
|
&& !string.Equals(existing.TargetRepository, targetRepository, StringComparison.Ordinal);
|
|
|
|
var shouldResetBaseline =
|
|
resetBaseline
|
|
|| string.IsNullOrWhiteSpace(existing.BaseExportId)
|
|
|| string.IsNullOrWhiteSpace(existing.BaseDigest)
|
|
|| repositoryChanged;
|
|
|
|
var updatedRecord = shouldResetBaseline
|
|
? existing with
|
|
{
|
|
BaseExportId = exportId,
|
|
BaseDigest = exportDigest,
|
|
LastFullDigest = exportDigest,
|
|
LastDeltaDigest = null,
|
|
ExportCursor = cursor ?? exportDigest,
|
|
TargetRepository = resolvedRepo,
|
|
ExporterVersion = exporterVersion,
|
|
UpdatedAt = now,
|
|
Files = manifest,
|
|
}
|
|
: existing with
|
|
{
|
|
LastFullDigest = exportDigest,
|
|
LastDeltaDigest = null,
|
|
ExportCursor = cursor ?? existing.ExportCursor,
|
|
TargetRepository = resolvedRepo,
|
|
ExporterVersion = exporterVersion,
|
|
UpdatedAt = now,
|
|
Files = manifest,
|
|
};
|
|
|
|
return await _store.UpsertAsync(updatedRecord, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
|
|
public async Task<ExportStateRecord> StoreDeltaExportAsync(
|
|
string exporterId,
|
|
string deltaDigest,
|
|
string? cursor,
|
|
string exporterVersion,
|
|
IReadOnlyList<ExportFileRecord> manifest,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
ArgumentException.ThrowIfNullOrEmpty(exporterId);
|
|
ArgumentException.ThrowIfNullOrEmpty(deltaDigest);
|
|
ArgumentException.ThrowIfNullOrEmpty(exporterVersion);
|
|
manifest ??= Array.Empty<ExportFileRecord>();
|
|
|
|
var existing = await _store.FindAsync(exporterId, cancellationToken).ConfigureAwait(false);
|
|
if (existing is null)
|
|
{
|
|
throw new InvalidOperationException($"Full export state missing for '{exporterId}'.");
|
|
}
|
|
|
|
var now = _timeProvider.GetUtcNow();
|
|
var record = existing with
|
|
{
|
|
LastDeltaDigest = deltaDigest,
|
|
ExportCursor = cursor ?? existing.ExportCursor,
|
|
ExporterVersion = exporterVersion,
|
|
UpdatedAt = now,
|
|
Files = manifest,
|
|
};
|
|
|
|
return await _store.UpsertAsync(record, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
}
|