This is a tiny class that creates backup copies of a file so these can be diff'ed to spot the changes compared to last run; used when generating code and so far has proved to be very helpful.
Basically, I reuse the async disposable logic because it doesn't really make sense to write it twice; the documentation simply doesn't bother about that so it's more or less an improvisation.
Example:
await using (var history = await FileHistory.CreateAsync("test.cs"))
{
if (history != null)
{
var path = history.Info.FullName;
var text = DateTime.Now.ToString(CultureInfo.InvariantCulture);
await File.WriteAllTextAsync(path, text);
}
}
The first time it is run:
- user writes
test.cs
- nothing else happens as there's no history yet
The second time it is run:
- user writes
test.cs
- writes previous version of
test.cs
totest-backup.cs
The third time it is run:
- user writes
test.cs
- writes previous version of
test-backup.cs
totest-backup-2022-12-31T22-29-28-8881210Z.cs
- writes previous version of
test.cs
totest-backup.cs
And so on, it creates backup copies of the file which can then be compared for changes.
Code:
using JetBrains.Annotations;
namespace abcd;
public sealed class FileHistory : IDisposable, IAsyncDisposable
{
private FileHistory(FileInfo info, byte[] data)
{
Info = info;
Data = data;
}
[PublicAPI]
public FileInfo Info { get; }
private byte[] Data { get; }
private bool IsDisposed { get; set; }
public async ValueTask DisposeAsync()
{
await DisposeAsyncCore(false).ConfigureAwait(false);
GC.SuppressFinalize(this);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
~FileHistory()
{
Dispose(false);
}
private async ValueTask DisposeAsyncCore(bool disposing)
{
if (IsDisposed)
return;
if (disposing)
{
// NOP
}
if (Data.Length is not 0)
{
Info.Refresh();
if (Info.Exists)
{
var name = Info.FullName;
var data = await File.ReadAllBytesAsync(name);
if (data.AsSpan().SequenceEqual(Data) is false)
{
var type = Path.GetExtension(name);
name = name[..^type.Length];
var path = $"{name}-backup{type}";
if (File.Exists(path))
{
var time = File.GetLastWriteTimeUtc(path);
var dest = $"{name}-backup-{time.ToString("O").Replace(':', '-').Replace('.', '-')}{type}";
File.Move(path, dest);
}
await File.WriteAllBytesAsync(path, Data);
}
}
}
IsDisposed = true;
}
private void Dispose(bool disposing)
{
DisposeAsyncCore(disposing).AsTask().Wait();
}
public static async Task<FileHistory?> CreateAsync(string path, CancellationToken cancellationToken = default)
{
var info = new FileInfo(path);
byte[] data;
if (info.Exists)
{
await using var stream = info.OpenRead();
data = new byte[stream.Length];
// ReSharper disable once UnusedVariable
var read = await stream.ReadAsync(data, cancellationToken);
}
else
{
data = Array.Empty<byte>();
}
return cancellationToken.IsCancellationRequested ? null : new FileHistory(info, data);
}
}