using System.Buffers; using System.Diagnostics.CodeAnalysis; using NLog; using StabilityMatrix.Core.Exceptions; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Progress; namespace StabilityMatrix.Core.Helper; [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] public static class FileTransfers { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); /// /// Determines suitable buffer size based on stream length. /// /// /// public static ulong GetBufferSize(ulong totalBytes) => totalBytes switch { < Size.MiB => 8 * Size.KiB, < 100 * Size.MiB => 16 * Size.KiB, < 500 * Size.MiB => Size.MiB, < Size.GiB => 16 * Size.MiB, _ => 32 * Size.MiB }; /// /// Copy all files and subfolders using a dictionary of source and destination file paths. /// Non-existing directories within the paths will be created. /// /// Dictionary of source and destination file paths /// /// Optional (per file) progress /// /// Current - Bytes read for file. /// Total - Size of file in bytes. /// Title - /// /// /// /// Optional (total) progress. /// public static async Task CopyFiles(Dictionary files, IProgress? fileProgress = default, IProgress? totalProgress = default) { var totalFiles = files.Count; var completedFiles = 0; var totalSize = Convert.ToUInt64(files.Keys.Select(x => new FileInfo(x).Length).Sum()); var totalRead = 0ul; foreach(var (sourcePath, destPath) in files) { var totalReadForFile = 0ul; await using var outStream = new FileStream(destPath, FileMode.Create, FileAccess.Write, FileShare.Read); await using var inStream = new FileStream(sourcePath, FileMode.Open, FileAccess.Read, FileShare.Read); var fileSize = (ulong) inStream.Length; var fileName = Path.GetFileName(sourcePath); completedFiles++; await CopyStream(inStream , outStream, fileReadBytes => { var lastRead = totalReadForFile; totalReadForFile = Convert.ToUInt64(fileReadBytes); totalRead += totalReadForFile - lastRead; fileProgress?.Report(new ProgressReport(totalReadForFile, fileSize, fileName, $"{completedFiles}/{totalFiles}")); totalProgress?.Report(new ProgressReport(totalRead, totalSize, fileName, $"{completedFiles}/{totalFiles}")); } ); } } private static async Task CopyStream(Stream from, Stream to, Action progress) { var shared = ArrayPool.Shared; var bufferSize = (int) GetBufferSize((ulong) from.Length); var buffer = shared.Rent(bufferSize); var totalRead = 0L; try { while (totalRead < from.Length) { var read = await from.ReadAsync(buffer.AsMemory(0, bufferSize)); await to.WriteAsync(buffer.AsMemory(0, read)); totalRead += read; progress(totalRead); } } finally { shared.Return(buffer); } } /// /// Move all files and sub-directories within the source directory to the destination directory. /// If the destination contains a file with the same name, we'll check if the hashes match. /// On matching hashes we skip the file, otherwise we throw an exception. /// /// /// If moving files results in name collision with different hashes. /// public static async Task MoveAllFilesAndDirectories( DirectoryPath sourceDir, DirectoryPath destinationDir, bool overwrite = false, bool overwriteIfHashMatches = false) { // Create the destination directory if it doesn't exist if (!destinationDir.Exists) { destinationDir.Create(); } // First move files await MoveAllFiles(sourceDir, destinationDir, overwrite, overwriteIfHashMatches); // Then move directories foreach (var subDir in sourceDir.Info.EnumerateDirectories()) { var destinationSubDir = destinationDir.JoinDir(subDir.Name); // Recursively move sub directories await MoveAllFilesAndDirectories(subDir, destinationSubDir, overwrite, overwriteIfHashMatches); } } /// /// Move all files within the source directory to the destination directory. /// If the destination contains a file with the same name, we'll check if the hashes match. /// On matching hashes we skip the file, otherwise we throw an exception. /// /// /// If moving files results in name collision with different hashes. /// public static async Task MoveAllFiles( DirectoryPath sourceDir, DirectoryPath destinationDir, bool overwrite = false, bool overwriteIfHashMatches = false) { foreach (var file in sourceDir.Info.EnumerateFiles()) { var sourceFile = sourceDir.JoinFile(file.Name); var destinationFile = destinationDir.JoinFile(file.Name); if (destinationFile.Exists) { if (overwrite) { destinationFile.Delete(); } if (overwriteIfHashMatches) { // Check if files hashes are the same var sourceHash = await FileHash.GetBlake3Async(sourceFile); var destinationHash = await FileHash.GetBlake3Async(destinationFile); // For same hash, just delete original file if (sourceHash == destinationHash) { Logger.Info( $"Deleted source file {file.Name} as it already exists in {destinationDir}." + $" Matching Blake3 hash: {sourceHash}"); sourceFile.Delete(); continue; } } throw new FileTransferExistsException(sourceFile, destinationFile); } // Move the file await sourceFile.MoveToAsync(destinationFile); } } }