using AcDream.Content.Pak; namespace AcDream.Bake; /// /// 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. /// public static class BakeOutputTransaction { public static TResult WriteValidateAndPublish( string destinationPath, Func writeTemporary, Action 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. } } } }