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? MoveProgress; public async Task 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 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 pool = MemoryPool.Shared.Rent(2 * BlockSizeBytes); Memory readBuff = pool.Memory.Slice(0, BlockSizeBytes); Memory 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; } }