Files
Libation/Source/FileManager/MoveWithProgress.cs
Michael Bucari-Tovo 647eb8b9d9 Use MemoryPool
2026-01-13 11:35:59 -07:00

184 lines
5.9 KiB
C#
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Buffers;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Win32.SafeHandles;
using Serilog;
#nullable enable
namespace FileManager;
public class MoveFileProgressEventArgs : EventArgs
{
public long TotalFileSize { get; }
public long TotalBytesTransferred { get; }
public long BytesMoved { get; }
public bool Continue { get; set; } = true;
internal MoveFileProgressEventArgs(long bytesMoved, long totalBytesTransferred, long totalFileSize)
{
BytesMoved = bytesMoved;
TotalBytesTransferred = totalBytesTransferred;
TotalFileSize = totalFileSize;
}
}
public class MoveWithProgress
{
public event EventHandler<MoveFileProgressEventArgs>? MoveProgress;
public async Task<bool> MoveAsync(LongPath source, LongPath destination, bool overwrite = false, CancellationToken cancellation = default)
{
ArgumentException.ThrowIfNullOrEmpty(source, nameof(source));
ArgumentException.ThrowIfNullOrEmpty(destination, nameof(destination));
var sourceFileInfo = new FileInfo(source);
if (!sourceFileInfo.Exists)
throw new FileNotFoundException($"Source file '{source}' does not exist.", source);
var destinationFile = new FileInfo(destination);
var sourceDevice = GetDeviceId(sourceFileInfo);
var destinationDevice = GetDeviceId(destinationFile.Directory);
if (sourceDevice == destinationDevice)
{
File.Move(sourceFileInfo.FullName, destinationFile.FullName, overwrite);
MoveProgress?.Invoke(this, new MoveFileProgressEventArgs(destinationFile.Length, destinationFile.Length, sourceFileInfo.Length));
return true;
}
if (destinationFile.Exists && !overwrite)
throw new IOException("The file exists.");
bool success = false;
try
{
success = await CopyWithProgressAsync(sourceFileInfo, destinationFile, cancellation);
}
finally
{
if (success)
FileUtility.SaferDelete(sourceFileInfo.FullName);
else
FileUtility.SaferDelete(destinationFile.FullName);
}
return success;
}
private static string? GetDeviceId(FileSystemInfo? fsEntry)
=> fsEntry?.FullName is not string path ? null
: LongPath.IsWindows ? GetDriveSerialNumber(path)
: LongPath.IsOSX ? RunShellCommand("stat -L -f %d \"" + path + "\"")
: RunShellCommand("stat -L -f -c %d \"" + path + "\"");
private async Task<bool> CopyWithProgressAsync(FileInfo sourceFileInfo, FileInfo destinationFile, CancellationToken cancellation)
{
const int BlockSizeMb = 8;
const int BlockSizeBytes = BlockSizeMb * (1 << 20);
using FileStream sourceStream = sourceFileInfo.Open(FileMode.Open, FileAccess.Read, FileShare.Read);
using FileStream destinationStream = destinationFile.Open(FileMode.OpenOrCreate, FileAccess.Write, FileShare.Read);
using IMemoryOwner<byte> pool = MemoryPool<byte>.Shared.Rent(2 * BlockSizeBytes);
Memory<byte> readBuff = pool.Memory.Slice(0, BlockSizeBytes);
Memory<byte> writeBuff = pool.Memory.Slice(BlockSizeBytes, BlockSizeBytes);
long totalCopied = 0, bytesMovedSinceLastReport = 0;
DateTime nextReport = default;
int bytesRead = await sourceStream.ReadAsync(writeBuff, cancellation);
while (bytesRead > 0)
{
totalCopied += bytesRead;
bytesMovedSinceLastReport += bytesRead;
var readTask = sourceStream.ReadAsync(readBuff, cancellation);
await destinationStream.WriteAsync(writeBuff[..bytesRead], cancellation);
if (DateTime.UtcNow >= nextReport)
{
var args = new MoveFileProgressEventArgs(bytesMovedSinceLastReport, totalCopied, sourceFileInfo.Length);
bytesMovedSinceLastReport = 0;
MoveProgress?.Invoke(this, args);
if (!args.Continue)
break;
nextReport = DateTime.UtcNow.AddMilliseconds(200.0);
}
bytesRead = await readTask;
(readBuff, writeBuff) = (writeBuff, readBuff);
}
destinationStream.SetLength(totalCopied);
MoveProgress?.Invoke(this, new MoveFileProgressEventArgs(bytesMovedSinceLastReport, totalCopied, sourceFileInfo.Length));
return totalCopied == sourceFileInfo.Length;
}
private static string? RunShellCommand(string command)
{
var psi = new ProcessStartInfo
{
FileName = "/bin/sh",
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true,
ArgumentList = { "-c", command }
};
try
{
var proc = Process.Start(psi);
proc?.WaitForExit();
return proc?.StandardOutput?.ReadToEnd()?.Trim();
}
catch (Exception e)
{
Log.Logger.Error(e, "Failed to run shell command. {@Arguments}", psi.ArgumentList);
return null;
}
}
private static string? GetDriveSerialNumber(string path)
{
const uint FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;
const uint OPEN_EXISTING = 3;
var handle = CreateFile(path, FileAccess.Read, FileShare.Read, 0, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0);
if (handle.IsInvalid)
return null;
try
{
BY_HANDLE_FILE_INFORMATION info = default;
if (!GetFileInformationByHandle(handle, ref info))
{
return null;
}
return info.dwVolumeSerialNumber.ToString("x8");
}
finally
{
handle.Close();
}
}
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GetFileInformationByHandle(SafeFileHandle hFile, ref BY_HANDLE_FILE_INFORMATION lpFileInformation);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern SafeFileHandle CreateFile(string fileName, FileAccess fileAccess, FileShare fileShare, nint lpSecurityAttributes, uint dwCreationDisposition, uint dwFlagsAndAttributes, nint hTemplateFile);
[StructLayout(LayoutKind.Sequential, Pack = 4)]
private struct BY_HANDLE_FILE_INFORMATION
{
private uint dwFileAttributes;
private long ftCreationTime;
private long ftLastAccessTime;
private long ftLastWriteTime;
public uint dwVolumeSerialNumber;
private uint nFileSizeHigh;
private uint nFileSizeLow;
private uint nNumberOfLinks;
private uint nFileIndexHigh;
private uint nFileIndexLow;
}
}