|
|
|
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();
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Determines suitable buffer size based on stream length.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="totalBytes"></param>
|
|
|
|
/// <returns></returns>
|
|
|
|
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
|
|
|
|
};
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Copy all files and subfolders using a dictionary of source and destination file paths.
|
|
|
|
/// Non-existing directories within the paths will be created.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="files">Dictionary of source and destination file paths</param>
|
|
|
|
/// <param name="fileProgress">
|
|
|
|
/// Optional (per file) progress
|
|
|
|
/// <list type="bullet">
|
|
|
|
/// <item>Current - Bytes read for file.</item>
|
|
|
|
/// <item>Total - Size of file in bytes.</item>
|
|
|
|
/// <item>Title - </item>
|
|
|
|
/// </list>
|
|
|
|
/// </param>
|
|
|
|
/// <param name="totalProgress">
|
|
|
|
/// Optional (total) progress.
|
|
|
|
/// </param>
|
|
|
|
public static async Task CopyFiles(Dictionary<string, string> files, IProgress<ProgressReport>? fileProgress = default, IProgress<ProgressReport>? 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<long> progress)
|
|
|
|
{
|
|
|
|
var shared = ArrayPool<byte>.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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// 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.
|
|
|
|
/// </summary>
|
|
|
|
/// <exception cref="FileTransferExistsException">
|
|
|
|
/// If moving files results in name collision with different hashes.
|
|
|
|
/// </exception>
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// 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.
|
|
|
|
/// </summary>
|
|
|
|
/// <exception cref="FileTransferExistsException">
|
|
|
|
/// If moving files results in name collision with different hashes.
|
|
|
|
/// </exception>
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|