// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Buffers; using System.IO; using System.Threading; using System.Threading.Tasks; namespace EpicGames.Core { /// /// Methods for implementing transactions that replace a files contents, while supporting a fallback path if the operation is interrupted. /// public static class FileTransaction { /// /// Opens a file for reading, recovering from a partial transaction if necessary. /// /// File to read from /// Stream to the file, or null if it does not exist public static FileStream? OpenRead(FileReference file) { if (!FileReference.Exists(file)) { FileReference incomingFile = GetIncomingFile(file); if (!FileReference.Exists(incomingFile)) { return null; } try { FileReference.Move(incomingFile, file, false); } catch (IOException) when (FileReference.Exists(file)) { } catch { return null; } } try { return FileReference.Open(file, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete); } catch (FileNotFoundException) { return null; } } /// /// Reads all data from a file into a byte array /// /// File to read from /// The data that was read public static byte[]? ReadAllBytes(FileReference file) { using (Stream? stream = OpenRead(file)) { if (stream != null) { byte[] data = new byte[stream.Length]; stream.ReadFixedLengthBytes(data); return data; } } return null; } /// /// Reads all data from a file into a byte array /// /// File to read from /// The data that was read public static async Task ReadAllBytesAsync(FileReference file) { using (Stream? stream = OpenRead(file)) { if (stream != null) { byte[] data = new byte[stream.Length]; await stream.ReadAsync(data); return data; } } return null; } /// /// Opens a file for writing. Call on the returned stream to flush its state. /// /// File to write to /// Stream to the file, or null if it does not exist public static FileTransactionStream OpenWrite(FileReference file) { FileReference incomingFile = GetIncomingFile(file); Stream stream = FileReference.Open(incomingFile, FileMode.Create, FileAccess.ReadWrite, FileShare.Read); return new FileTransactionStream(stream, incomingFile, file); } /// /// Writes data from a byte array into a file, as a transaction /// /// File to write to /// Data to be written /// Cancellation token for the operation public static Task WriteAllBytesAsync(FileReference file, ReadOnlyMemory data, CancellationToken cancellationToken = default) => WriteAllBytesAsync(file, new ReadOnlySequence(data), cancellationToken); /// /// Writes data from a byte sequence into a file, as a transaction /// /// File to write to /// Data to be written /// Cancellation token for the operation public static async Task WriteAllBytesAsync(FileReference file, ReadOnlySequence data, CancellationToken cancellationToken = default) { using (FileTransactionStream stream = OpenWrite(file)) { foreach (ReadOnlyMemory memory in data) { await stream.WriteAsync(memory, cancellationToken); } stream.CompleteTransaction(); } } /// /// Gets the name of the temp file to use for writing /// /// File /// static FileReference GetIncomingFile(FileReference file) { return FileReference.Combine(file.Directory, file.GetFileName() + ".incoming"); } } /// /// Stream used to write to a new copy of a file in an atomic transaction /// public class FileTransactionStream : Stream { readonly Stream _inner; readonly FileReference _file; FileReference? _finalFile; internal FileTransactionStream(Stream inner, FileReference outputFile, FileReference finalFile) { _inner = inner; _file = outputFile; _finalFile = finalFile; } /// /// Marks the transaction as complete, and moves the file to its final location /// public void CompleteTransaction() { if (_finalFile == null) { throw new InvalidOperationException("Stream cannot be written to"); } FileReference finalFile = _finalFile; Close(); FileReference.Delete(finalFile); FileReference.Move(_file, finalFile); _finalFile = null; } /// protected override void Dispose(bool disposing) { base.Dispose(disposing); if (disposing) { _inner.Dispose(); } } /// public override async ValueTask DisposeAsync() { GC.SuppressFinalize(this); await base.DisposeAsync(); await _inner.DisposeAsync(); } /// public override bool CanRead => _inner.CanRead; /// public override bool CanSeek => _inner.CanSeek; /// public override bool CanTimeout => _inner.CanTimeout; /// public override bool CanWrite => _inner.CanWrite; /// public override long Length => _inner.Length; /// public override long Position { get => _inner.Position; set => _inner.Position = value; } /// public override int ReadTimeout { get => _inner.ReadTimeout; set => _inner.ReadTimeout = value; } /// public override int WriteTimeout { get => _inner.WriteTimeout; set => _inner.WriteTimeout = value; } /// public override void Close() { _inner.Close(); _finalFile = null; } /// public override void CopyTo(Stream destination, int bufferSize) => _inner.CopyTo(destination, bufferSize); /// public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) => _inner.CopyToAsync(destination, bufferSize, cancellationToken); /// public override void Flush() => _inner.Flush(); /// public override int Read(byte[] buffer, int offset, int count) => _inner.Read(buffer, offset, count); /// public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => _inner.ReadAsync(buffer, offset, count, cancellationToken); /// public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) => _inner.ReadAsync(buffer, cancellationToken); /// public override int ReadByte() => _inner.ReadByte(); /// public override long Seek(long offset, SeekOrigin origin) => _inner.Seek(offset, origin); /// public override void SetLength(long value) => _inner.SetLength(value); /// public override void Write(byte[] buffer, int offset, int count) => _inner.Write(buffer, offset, count); /// public override void Write(ReadOnlySpan buffer) => _inner.Write(buffer); /// public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => _inner.WriteAsync(buffer, offset, count, cancellationToken); /// public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) => _inner.WriteAsync(buffer, cancellationToken); /// public override void WriteByte(byte value) => _inner.WriteByte(value); } }