mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-01-21 04:09:27 -05:00
184 lines
5.9 KiB
C#
184 lines
5.9 KiB
C#
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;
|
||
}
|
||
} |