// 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);
}
}