63 lines
2.4 KiB
C#
63 lines
2.4 KiB
C#
using AcDream.Content.Pak;
|
|
|
|
namespace AcDream.Bake;
|
|
|
|
/// <summary>
|
|
/// Publishes a bake as one same-volume filesystem transaction. Work and
|
|
/// validation happen against a unique adjacent temporary file; failure or
|
|
/// cancellation leaves an existing destination untouched.
|
|
/// </summary>
|
|
public static class BakeOutputTransaction
|
|
{
|
|
public static TResult WriteValidateAndPublish<TResult>(
|
|
string destinationPath,
|
|
Func<string, TResult> writeTemporary,
|
|
Action<string, TResult> validateTemporary,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(destinationPath);
|
|
ArgumentNullException.ThrowIfNull(writeTemporary);
|
|
ArgumentNullException.ThrowIfNull(validateTemporary);
|
|
|
|
string fullDestination = Path.GetFullPath(destinationPath);
|
|
string? directory = Path.GetDirectoryName(fullDestination);
|
|
if (string.IsNullOrEmpty(directory))
|
|
throw new InvalidOperationException("destination has no parent directory");
|
|
Directory.CreateDirectory(directory);
|
|
|
|
string temporaryPath = Path.Combine(
|
|
directory,
|
|
$".{Path.GetFileName(fullDestination)}.{Guid.NewGuid():N}.tmp");
|
|
|
|
try
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
TResult result = writeTemporary(temporaryPath);
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
validateTemporary(temporaryPath, result);
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
// Same-volume MoveFileEx/rename is the publication primitive.
|
|
// File.Replace additionally performs destination metadata/backup
|
|
// semantics and proved brittle for a validated 28 GiB artifact on
|
|
// Windows. Move(overwrite:true) changes only the directory entry:
|
|
// readers observe the complete old file or the complete new file.
|
|
File.Move(temporaryPath, fullDestination, overwrite: true);
|
|
|
|
return result;
|
|
}
|
|
finally
|
|
{
|
|
try
|
|
{
|
|
if (File.Exists(temporaryPath))
|
|
File.Delete(temporaryPath);
|
|
}
|
|
catch
|
|
{
|
|
// Preserve the original exception. An adjacent .tmp is
|
|
// recognizable and never mistaken for a published pak.
|
|
}
|
|
}
|
|
}
|
|
}
|