feat: restore legacy named playlist workflows
This commit is contained in:
@@ -0,0 +1,571 @@
|
||||
using System.Data;
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
using MBN_STOCK_WEBVIEW.Core.Playout.Scenes;
|
||||
using MBN_STOCK_WEBVIEW.Infrastructure;
|
||||
using MMoneyCoderSharp.Data;
|
||||
|
||||
public sealed record NamedManualRestoreReadOnlyAuditSummary(
|
||||
int CandidateCount,
|
||||
int FreshMaterializedCount,
|
||||
int BlockedCount,
|
||||
LegacyManualOperatorDataRuntimeState RuntimeState,
|
||||
bool OperatorFilesStable,
|
||||
IReadOnlyDictionary<LegacyNamedManualRestoreKind, int> KindCounts,
|
||||
IReadOnlyDictionary<string, int> BlockReasons);
|
||||
|
||||
/// <summary>
|
||||
/// Production-data audit for the manual subset of PLAY_LIST. Every database
|
||||
/// command is a SELECT through an IDataQueryExecutor. Operator files are opened
|
||||
/// only through their production read paths; the before/after snapshot proves
|
||||
/// that no file, marker, or intent entry was created or changed.
|
||||
/// </summary>
|
||||
public static class NamedManualRestoreReadOnlyAudit
|
||||
{
|
||||
private const string QueryName = "AUDIT_NAMED_MANUAL_RESTORE_ROWS";
|
||||
private const int MaximumRows = 20_000;
|
||||
private const int BrowserStockSearchLimit = 200;
|
||||
|
||||
private const string Sql = """
|
||||
SELECT DC_CODE, DC_TITLE, LIST_TEXT, ITEM_INDEX
|
||||
FROM (
|
||||
SELECT TRIM(d.DC_CODE) DC_CODE,
|
||||
d.DC_TITLE DC_TITLE,
|
||||
p.LIST_TEXT LIST_TEXT,
|
||||
TO_CHAR(TO_NUMBER(p.ITEM_INDEX)) ITEM_INDEX
|
||||
FROM DC_LIST d
|
||||
JOIN PLAY_LIST p ON p.PG_CODE = d.DC_CODE
|
||||
ORDER BY TRIM(d.DC_CODE), TO_NUMBER(p.ITEM_INDEX)
|
||||
)
|
||||
WHERE ROWNUM <= :row_limit
|
||||
""";
|
||||
|
||||
public static async Task<NamedManualRestoreReadOnlyAuditSummary> RunAsync(
|
||||
IDataQueryExecutor executor,
|
||||
string trustedDirectory,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(executor);
|
||||
if (string.IsNullOrWhiteSpace(trustedDirectory) ||
|
||||
!Path.IsPathFullyQualified(trustedDirectory))
|
||||
{
|
||||
throw InvalidBoundary("trusted directory composition");
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var candidates = await LoadCandidatesAsync(executor, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
var runtime = LegacyManualOperatorDataRuntimeGuard.Inspect(trustedDirectory);
|
||||
if (!runtime.IsReady)
|
||||
{
|
||||
throw InvalidBoundary("trusted runtime state");
|
||||
}
|
||||
|
||||
var before = SnapshotDirectory(trustedDirectory);
|
||||
var results = new List<RestoreResult>(candidates.Count);
|
||||
var financialService = new LegacyManualFinancialScreenService(executor);
|
||||
var stockSearchService = new LegacyStockSearchService(executor);
|
||||
var netSellSource = new S5025TrustedManualFileDataSource(trustedDirectory);
|
||||
TrustedSourceEvidence trustedSources;
|
||||
using (var viSource = new ViTrustedManualFileStore(trustedDirectory))
|
||||
{
|
||||
trustedSources = await ReadTrustedSourcesAsync(
|
||||
netSellSource,
|
||||
viSource,
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
foreach (var candidate in candidates)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
results.Add(candidate.Kind switch
|
||||
{
|
||||
LegacyNamedManualRestoreKind.Financial =>
|
||||
await ResolveFinancialAsync(
|
||||
candidate,
|
||||
financialService,
|
||||
stockSearchService,
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false),
|
||||
LegacyNamedManualRestoreKind.NetSell =>
|
||||
await ResolveNetSellAsync(candidate, netSellSource, cancellationToken)
|
||||
.ConfigureAwait(false),
|
||||
LegacyNamedManualRestoreKind.Vi =>
|
||||
await ResolveViAsync(candidate, viSource, cancellationToken)
|
||||
.ConfigureAwait(false),
|
||||
_ => throw InvalidBoundary("manual candidate kind")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var after = SnapshotDirectory(trustedDirectory);
|
||||
var filesStable = before.Matches(after);
|
||||
if (!filesStable)
|
||||
{
|
||||
throw InvalidBoundary("operator file immutability");
|
||||
}
|
||||
|
||||
var kindCounts = candidates
|
||||
.GroupBy(static candidate => candidate.Kind)
|
||||
.ToDictionary(static group => group.Key, static group => group.Count());
|
||||
var screenCounts = candidates
|
||||
.Where(static candidate =>
|
||||
candidate.Kind == LegacyNamedManualRestoreKind.Financial)
|
||||
.GroupBy(static candidate => candidate.FinancialScreen!.Value)
|
||||
.OrderBy(static group => group.Key)
|
||||
.Select(group =>
|
||||
$"{group.Key}={group.Count().ToString(CultureInfo.InvariantCulture)}");
|
||||
var audienceCounts = candidates
|
||||
.Where(static candidate => candidate.Kind == LegacyNamedManualRestoreKind.NetSell)
|
||||
.GroupBy(static candidate => candidate.Audience!.Value)
|
||||
.OrderBy(static group => group.Key)
|
||||
.Select(group =>
|
||||
$"{group.Key}={group.Count().ToString(CultureInfo.InvariantCulture)}");
|
||||
var viRows = candidates
|
||||
.Where(static candidate => candidate.Kind == LegacyNamedManualRestoreKind.Vi)
|
||||
.ToArray();
|
||||
var financialOutcomes = candidates
|
||||
.Select((candidate, index) => (Candidate: candidate, Result: results[index]))
|
||||
.Where(static pair =>
|
||||
pair.Candidate.Kind == LegacyNamedManualRestoreKind.Financial)
|
||||
.GroupBy(static pair => pair.Candidate.FinancialScreen!.Value)
|
||||
.OrderBy(static group => group.Key)
|
||||
.Select(group =>
|
||||
{
|
||||
var freshCount = group.Count(static pair => pair.Result.IsFresh);
|
||||
var blocked = group
|
||||
.Where(static pair => !pair.Result.IsFresh)
|
||||
.GroupBy(static pair => pair.Result.Reason, StringComparer.Ordinal)
|
||||
.OrderBy(static reason => reason.Key, StringComparer.Ordinal)
|
||||
.Select(reason =>
|
||||
$"{reason.Key}:{reason.Count().ToString(CultureInfo.InvariantCulture)}");
|
||||
return $"{group.Key}=fresh:{freshCount.ToString(CultureInfo.InvariantCulture)}" +
|
||||
(freshCount == group.Count()
|
||||
? string.Empty
|
||||
: $"/blocked:{string.Join('+', blocked)}");
|
||||
});
|
||||
var blockReasons = results
|
||||
.Where(static result => !result.IsFresh)
|
||||
.GroupBy(static result => result.Reason, StringComparer.Ordinal)
|
||||
.ToDictionary(static group => group.Key, static group => group.Count(), StringComparer.Ordinal);
|
||||
var fresh = results.Count(static result => result.IsFresh);
|
||||
|
||||
Console.WriteLine(
|
||||
"PLIST_MANUAL_GRAMMAR_AUDIT: " +
|
||||
$"candidates={candidates.Count.ToString(CultureInfo.InvariantCulture)} " +
|
||||
$"financial={kindCounts.GetValueOrDefault(LegacyNamedManualRestoreKind.Financial).ToString(CultureInfo.InvariantCulture)} " +
|
||||
$"netSell={kindCounts.GetValueOrDefault(LegacyNamedManualRestoreKind.NetSell).ToString(CultureInfo.InvariantCulture)} " +
|
||||
$"vi={kindCounts.GetValueOrDefault(LegacyNamedManualRestoreKind.Vi).ToString(CultureInfo.InvariantCulture)} " +
|
||||
$"historical={candidates.Count(static candidate => candidate.IsHistorical).ToString(CultureInfo.InvariantCulture)} " +
|
||||
$"canonical={candidates.Count(static candidate => !candidate.IsHistorical).ToString(CultureInfo.InvariantCulture)}");
|
||||
Console.WriteLine("PLIST_MANUAL_FINANCIAL_SCREENS: " + string.Join(',', screenCounts));
|
||||
Console.WriteLine(
|
||||
"PLIST_MANUAL_FINANCIAL_OUTCOMES: " + string.Join(',', financialOutcomes));
|
||||
Console.WriteLine("PLIST_MANUAL_NET_SELL_AUDIENCES: " + string.Join(',', audienceCounts));
|
||||
Console.WriteLine(
|
||||
"PLIST_MANUAL_VI_GRAMMAR: " +
|
||||
$"historical={viRows.Count(static candidate => candidate.IsHistorical).ToString(CultureInfo.InvariantCulture)} " +
|
||||
$"versioned={viRows.Count(static candidate => !candidate.IsHistorical).ToString(CultureInfo.InvariantCulture)}");
|
||||
Console.WriteLine(
|
||||
"PLIST_MANUAL_TRUSTED_SOURCES: " +
|
||||
$"netSellAudiencesReady={trustedSources.NetSellAudiencesReady.ToString(CultureInfo.InvariantCulture)} " +
|
||||
$"viSnapshotReady={trustedSources.ViSnapshotReady.ToString().ToLowerInvariant()} " +
|
||||
$"viItems={trustedSources.ViItemCount.ToString(CultureInfo.InvariantCulture)} " +
|
||||
$"viPages={trustedSources.ViPageCount.ToString(CultureInfo.InvariantCulture)} " +
|
||||
"openedHandle=true");
|
||||
Console.WriteLine(
|
||||
"PLIST_MANUAL_FRESH_PROOF: " +
|
||||
$"freshMaterialized={fresh.ToString(CultureInfo.InvariantCulture)} " +
|
||||
$"restoreBlocked={(results.Count - fresh).ToString(CultureInfo.InvariantCulture)} " +
|
||||
"graphE=double-read-row-version+fresh-stock-identity " +
|
||||
"netSell=opened-handle-production-parser " +
|
||||
"vi=opened-handle-version+identity+page-check " +
|
||||
"sceneDataAndAssetsNotAsserted=true");
|
||||
Console.WriteLine(
|
||||
"PLIST_MANUAL_BLOCK_REASONS: " +
|
||||
(blockReasons.Count == 0
|
||||
? "none"
|
||||
: string.Join(
|
||||
',',
|
||||
blockReasons.OrderBy(static pair => pair.Key, StringComparer.Ordinal)
|
||||
.Select(pair =>
|
||||
$"{pair.Key}={pair.Value.ToString(CultureInfo.InvariantCulture)}"))));
|
||||
Console.WriteLine(
|
||||
"PLIST_MANUAL_LOCAL_READ_AUDIT: " +
|
||||
$"runtimeState={runtime.State} " +
|
||||
$"entries={before.Entries.Count.ToString(CultureInfo.InvariantCulture)} " +
|
||||
"filesStable=true writesAttempted=false markerOrIntentCreated=false");
|
||||
|
||||
return new NamedManualRestoreReadOnlyAuditSummary(
|
||||
candidates.Count,
|
||||
fresh,
|
||||
results.Count - fresh,
|
||||
runtime.State,
|
||||
filesStable,
|
||||
kindCounts,
|
||||
blockReasons);
|
||||
}
|
||||
|
||||
public static string DefaultTrustedDirectory()
|
||||
{
|
||||
var localAppData = Environment.GetFolderPath(
|
||||
Environment.SpecialFolder.LocalApplicationData,
|
||||
Environment.SpecialFolderOption.DoNotVerify);
|
||||
if (string.IsNullOrWhiteSpace(localAppData))
|
||||
{
|
||||
throw InvalidBoundary("local application-data root");
|
||||
}
|
||||
|
||||
return Path.GetFullPath(Path.Combine(
|
||||
localAppData,
|
||||
"MBN_STOCK_WEBVIEW",
|
||||
"OperatorData"));
|
||||
}
|
||||
|
||||
private static async Task<IReadOnlyList<LegacyNamedManualRestoreCandidate>> LoadCandidatesAsync(
|
||||
IDataQueryExecutor executor,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var spec = new DataQuerySpec(
|
||||
Sql,
|
||||
[new DataQueryParameter("row_limit", MaximumRows + 1, DbType.Int32)]);
|
||||
spec.ValidateFor(DataSourceKind.Oracle);
|
||||
var table = await executor.ExecuteAsync(
|
||||
DataSourceKind.Oracle,
|
||||
QueryName,
|
||||
spec,
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var columns = new[] { "DC_CODE", "DC_TITLE", "LIST_TEXT", "ITEM_INDEX" };
|
||||
if (table is null ||
|
||||
table.Columns.Count != columns.Length ||
|
||||
table.Rows.Count is < 1 or > MaximumRows)
|
||||
{
|
||||
throw InvalidBoundary("PList query schema or row bound");
|
||||
}
|
||||
|
||||
for (var index = 0; index < columns.Length; index++)
|
||||
{
|
||||
if (table.Columns[index].ColumnName != columns[index] ||
|
||||
table.Columns[index].DataType != typeof(string))
|
||||
{
|
||||
throw InvalidBoundary("PList query schema");
|
||||
}
|
||||
}
|
||||
|
||||
var candidates = new List<LegacyNamedManualRestoreCandidate>();
|
||||
var routedManualCount = 0;
|
||||
foreach (DataRow dataRow in table.Rows)
|
||||
{
|
||||
var parsed = LegacyPListReadAuditParser.Parse(
|
||||
dataRow[0] as string,
|
||||
dataRow[1] as string,
|
||||
dataRow[3] as string,
|
||||
dataRow[2] as string);
|
||||
if (!parsed.IsSuccess)
|
||||
{
|
||||
throw InvalidBoundary("PList row parse");
|
||||
}
|
||||
|
||||
var row = parsed.Row!;
|
||||
var route = LegacyPListReadAuditEvidence.ClassifyRoute(row);
|
||||
if (route == LegacyPListAuditRoute.ManualFreshMaterialization)
|
||||
{
|
||||
routedManualCount++;
|
||||
}
|
||||
|
||||
var candidate = LegacyNamedManualRestoreClassifier.Classify(row);
|
||||
if (candidate is not null)
|
||||
{
|
||||
candidates.Add(candidate);
|
||||
}
|
||||
else if (route == LegacyPListAuditRoute.ManualFreshMaterialization)
|
||||
{
|
||||
throw InvalidBoundary("manual route closed grammar");
|
||||
}
|
||||
}
|
||||
|
||||
if (candidates.Count != routedManualCount)
|
||||
{
|
||||
throw InvalidBoundary("manual route/classifier agreement");
|
||||
}
|
||||
|
||||
return candidates.AsReadOnly();
|
||||
}
|
||||
|
||||
private static async Task<RestoreResult> ResolveFinancialAsync(
|
||||
LegacyNamedManualRestoreCandidate candidate,
|
||||
LegacyManualFinancialScreenService financialService,
|
||||
LegacyStockSearchService stockSearchService,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var identity = new ManualFinancialIdentity(
|
||||
candidate.FinancialScreen!.Value,
|
||||
candidate.StockName!);
|
||||
var firstRead = await financialService.GetAsync(identity, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
var query = LegacyNamedManualFreshProof.FinancialStockSearchQuery(candidate);
|
||||
var stockSearch = await stockSearchService.SearchAsync(
|
||||
query,
|
||||
BrowserStockSearchLimit,
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
var secondRead = await financialService.GetAsync(identity, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
var failure = LegacyNamedManualFreshProof.EvaluateFinancial(
|
||||
candidate,
|
||||
firstRead,
|
||||
secondRead,
|
||||
stockSearch);
|
||||
return RestoreResult.From(failure);
|
||||
}
|
||||
catch (ManualFinancialNotFoundException)
|
||||
{
|
||||
return RestoreResult.Blocked("GRAPH_E_NOT_FOUND");
|
||||
}
|
||||
catch (ManualFinancialAmbiguousIdentityException)
|
||||
{
|
||||
return RestoreResult.Blocked("GRAPH_E_AMBIGUOUS");
|
||||
}
|
||||
catch (ManualFinancialDataException)
|
||||
{
|
||||
return RestoreResult.Blocked("GRAPH_E_DATA_INVALID");
|
||||
}
|
||||
catch (StockSearchDataException)
|
||||
{
|
||||
return RestoreResult.Blocked("STOCK_MASTER_DATA_INVALID");
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
return RestoreResult.Blocked("GRAPH_E_IDENTITY_INVALID");
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<RestoreResult> ResolveNetSellAsync(
|
||||
LegacyNamedManualRestoreCandidate candidate,
|
||||
S5025TrustedManualFileDataSource source,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var rows = await source.ReadAsync(candidate.Audience!.Value, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
return RestoreResult.From(
|
||||
LegacyNamedManualFreshProof.EvaluateNetSell(candidate, rows));
|
||||
}
|
||||
catch (S5025TrustedManualDataException)
|
||||
{
|
||||
return RestoreResult.Blocked("NET_SELL_TRUSTED_READ_INVALID");
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<TrustedSourceEvidence> ReadTrustedSourcesAsync(
|
||||
S5025TrustedManualFileDataSource netSellSource,
|
||||
ViTrustedManualFileStore viSource,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var audiencesReady = 0;
|
||||
foreach (var audience in Enum.GetValues<S5025ManualAudience>())
|
||||
{
|
||||
var rows = await netSellSource.ReadAsync(audience, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (rows.Count != 5)
|
||||
{
|
||||
throw InvalidBoundary("net-sell production parser");
|
||||
}
|
||||
|
||||
audiencesReady++;
|
||||
}
|
||||
|
||||
var vi = await viSource.ReadStockNameSnapshotAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (!TrustedViManualReference.IsRowVersion(vi.RowVersion) ||
|
||||
vi.StockNames.Count is < 1 or > TrustedViManualReference.MaximumItemCount)
|
||||
{
|
||||
throw InvalidBoundary("VI production parser");
|
||||
}
|
||||
|
||||
var canonical = new LegacySceneSelection(
|
||||
"PAGED_VI",
|
||||
TrustedViManualReference.SubjectSentinel,
|
||||
"INPUT_ORDER",
|
||||
"CURRENT",
|
||||
vi.RowVersion);
|
||||
var secondRead = await TrustedViManualReference.ResolveAsync(
|
||||
canonical,
|
||||
pageIndexZeroBased: 0,
|
||||
viSource,
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (!secondRead.StockNames.SequenceEqual(vi.StockNames, StringComparer.Ordinal))
|
||||
{
|
||||
throw InvalidBoundary("VI stable second read");
|
||||
}
|
||||
|
||||
return new TrustedSourceEvidence(
|
||||
audiencesReady,
|
||||
ViSnapshotReady: true,
|
||||
vi.StockNames.Count,
|
||||
checked((vi.StockNames.Count + 4) / 5));
|
||||
}
|
||||
|
||||
private static async Task<RestoreResult> ResolveViAsync(
|
||||
LegacyNamedManualRestoreCandidate candidate,
|
||||
ViTrustedManualFileStore source,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var snapshot = await source.ReadStockNameSnapshotAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
var failure = LegacyNamedManualFreshProof.EvaluateVi(candidate, snapshot);
|
||||
if (failure != LegacyNamedManualFreshProofFailure.None)
|
||||
{
|
||||
return RestoreResult.From(failure);
|
||||
}
|
||||
|
||||
var canonical = new LegacySceneSelection(
|
||||
"PAGED_VI",
|
||||
TrustedViManualReference.SubjectSentinel,
|
||||
"INPUT_ORDER",
|
||||
"CURRENT",
|
||||
snapshot.RowVersion);
|
||||
var resolved = await TrustedViManualReference.ResolveAsync(
|
||||
canonical,
|
||||
pageIndexZeroBased: 0,
|
||||
source,
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
return resolved.PageNumberOneBased == 1 &&
|
||||
resolved.StockNames.SequenceEqual(
|
||||
snapshot.StockNames,
|
||||
StringComparer.Ordinal)
|
||||
? RestoreResult.Fresh()
|
||||
: RestoreResult.Blocked("VI_SECOND_READ_CHANGED");
|
||||
}
|
||||
catch (ViTrustedManualDataException)
|
||||
{
|
||||
return RestoreResult.Blocked("VI_TRUSTED_READ_INVALID");
|
||||
}
|
||||
catch (LegacySceneDataException)
|
||||
{
|
||||
return RestoreResult.Blocked("VI_SECOND_READ_CHANGED");
|
||||
}
|
||||
}
|
||||
|
||||
private static DirectorySnapshot SnapshotDirectory(string trustedDirectory)
|
||||
{
|
||||
try
|
||||
{
|
||||
var directory = new DirectoryInfo(trustedDirectory);
|
||||
if (!directory.Exists ||
|
||||
(directory.Attributes & (FileAttributes.ReparsePoint | FileAttributes.Directory)) !=
|
||||
FileAttributes.Directory)
|
||||
{
|
||||
throw InvalidBoundary("operator directory");
|
||||
}
|
||||
|
||||
var entries = new List<FileSnapshot>();
|
||||
foreach (var path in Directory.EnumerateFileSystemEntries(trustedDirectory)
|
||||
.OrderBy(static value => Path.GetFileName(value), StringComparer.Ordinal))
|
||||
{
|
||||
var attributes = File.GetAttributes(path);
|
||||
if ((attributes & (FileAttributes.Directory | FileAttributes.ReparsePoint)) != 0)
|
||||
{
|
||||
throw InvalidBoundary("operator directory entry");
|
||||
}
|
||||
|
||||
var before = new FileInfo(path);
|
||||
byte[] digest;
|
||||
long length;
|
||||
using (var stream = new FileStream(
|
||||
path,
|
||||
FileMode.Open,
|
||||
FileAccess.Read,
|
||||
FileShare.Read,
|
||||
bufferSize: 4_096,
|
||||
FileOptions.SequentialScan))
|
||||
{
|
||||
length = stream.Length;
|
||||
digest = SHA256.HashData(stream);
|
||||
if (stream.Length != length)
|
||||
{
|
||||
throw InvalidBoundary("operator file stable read");
|
||||
}
|
||||
}
|
||||
|
||||
var after = new FileInfo(path);
|
||||
if (after.Length != length ||
|
||||
before.LastWriteTimeUtc != after.LastWriteTimeUtc ||
|
||||
before.CreationTimeUtc != after.CreationTimeUtc)
|
||||
{
|
||||
throw InvalidBoundary("operator file stable read");
|
||||
}
|
||||
|
||||
entries.Add(new FileSnapshot(
|
||||
Path.GetFileName(path),
|
||||
length,
|
||||
after.CreationTimeUtc,
|
||||
after.LastWriteTimeUtc,
|
||||
Convert.ToHexString(digest)));
|
||||
}
|
||||
|
||||
directory.Refresh();
|
||||
return new DirectorySnapshot(directory.LastWriteTimeUtc, entries.AsReadOnly());
|
||||
}
|
||||
catch (InvalidDataException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception exception) when (exception is
|
||||
IOException or
|
||||
UnauthorizedAccessException or
|
||||
ArgumentException or
|
||||
NotSupportedException)
|
||||
{
|
||||
throw InvalidBoundary("operator file snapshot");
|
||||
}
|
||||
}
|
||||
|
||||
private static InvalidDataException InvalidBoundary(string detail) =>
|
||||
new($"The read-only named manual restore audit failed its {detail} boundary. " +
|
||||
"No source values were printed.");
|
||||
|
||||
private sealed record RestoreResult(bool IsFresh, string Reason)
|
||||
{
|
||||
public static RestoreResult Fresh() => new(true, "NONE");
|
||||
|
||||
public static RestoreResult Blocked(string reason) => new(false, reason);
|
||||
|
||||
public static RestoreResult From(LegacyNamedManualFreshProofFailure failure) =>
|
||||
failure == LegacyNamedManualFreshProofFailure.None
|
||||
? Fresh()
|
||||
: Blocked(failure.ToString().ToUpperInvariant());
|
||||
}
|
||||
|
||||
private sealed record TrustedSourceEvidence(
|
||||
int NetSellAudiencesReady,
|
||||
bool ViSnapshotReady,
|
||||
int ViItemCount,
|
||||
int ViPageCount);
|
||||
|
||||
private sealed record FileSnapshot(
|
||||
string Name,
|
||||
long Length,
|
||||
DateTime CreationTimeUtc,
|
||||
DateTime LastWriteTimeUtc,
|
||||
string Sha256);
|
||||
|
||||
private sealed record DirectorySnapshot(
|
||||
DateTime LastWriteTimeUtc,
|
||||
IReadOnlyList<FileSnapshot> Entries)
|
||||
{
|
||||
public bool Matches(DirectorySnapshot other) =>
|
||||
other is not null &&
|
||||
LastWriteTimeUtc == other.LastWriteTimeUtc &&
|
||||
Entries.SequenceEqual(other.Entries);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user