feat(launcher): implement verified atomic updates

This commit is contained in:
Erik 2026-08-14 22:09:34 +02:00
parent 2198a0cc8e
commit 2d2a5b5046
34 changed files with 6755 additions and 61 deletions

View file

@ -0,0 +1,84 @@
using System.Text.Json;
namespace AcDream.Launcher.Core.Updates;
internal static class AtomicJsonFile
{
internal static async Task WriteAsync<T>(
string path,
T value,
JsonSerializerOptions options,
CancellationToken cancellationToken = default)
{
string fullPath = Path.GetFullPath(path);
string directory = Path.GetDirectoryName(fullPath)
?? throw new InvalidOperationException("The JSON path has no parent directory.");
Directory.CreateDirectory(directory);
string temporaryPath = Path.Combine(
directory,
$".{Path.GetFileName(fullPath)}.{Guid.NewGuid():N}.tmp");
try
{
await using (var stream = new FileStream(
temporaryPath,
FileMode.CreateNew,
FileAccess.Write,
FileShare.None,
16 * 1024,
FileOptions.Asynchronous | FileOptions.WriteThrough))
{
await JsonSerializer.SerializeAsync(
stream,
value,
options,
cancellationToken)
.ConfigureAwait(false);
await stream.FlushAsync(cancellationToken).ConfigureAwait(false);
stream.Flush(flushToDisk: true);
}
cancellationToken.ThrowIfCancellationRequested();
File.Move(temporaryPath, fullPath, overwrite: true);
}
finally
{
VerifiedArtifactDownloader.TryDelete(temporaryPath);
}
}
internal static async Task WriteBytesAsync(
string path,
ReadOnlyMemory<byte> bytes,
CancellationToken cancellationToken = default)
{
string fullPath = Path.GetFullPath(path);
string directory = Path.GetDirectoryName(fullPath)
?? throw new InvalidOperationException("The file path has no parent directory.");
Directory.CreateDirectory(directory);
string temporaryPath = Path.Combine(
directory,
$".{Path.GetFileName(fullPath)}.{Guid.NewGuid():N}.tmp");
try
{
await using (var stream = new FileStream(
temporaryPath,
FileMode.CreateNew,
FileAccess.Write,
FileShare.None,
16 * 1024,
FileOptions.Asynchronous | FileOptions.WriteThrough))
{
await stream.WriteAsync(bytes, cancellationToken).ConfigureAwait(false);
await stream.FlushAsync(cancellationToken).ConfigureAwait(false);
stream.Flush(flushToDisk: true);
}
cancellationToken.ThrowIfCancellationRequested();
File.Move(temporaryPath, fullPath, overwrite: true);
}
finally
{
VerifiedArtifactDownloader.TryDelete(temporaryPath);
}
}
}