Skip to content

Commit

Permalink
Merge branch 'main' into shortcuts
Browse files Browse the repository at this point in the history
  • Loading branch information
gablm committed Jan 17, 2024
2 parents 719e541 + d5ad751 commit 9e21a70
Show file tree
Hide file tree
Showing 18 changed files with 993 additions and 9 deletions.
67 changes: 67 additions & 0 deletions CollapseLauncher/Classes/FileMigrationProcess/Events.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using Hi3Helper;
using Hi3Helper.Data;
using System;
using System.Threading.Tasks;

namespace CollapseLauncher
{
internal partial class FileMigrationProcess
{
private void UpdateCountProcessed(FileMigrationProcessUIRef uiRef, string currentPathProcessed)
{
lock (this) { this.CurrentFileCountMoved++; }

string fileCountProcessedString = string.Format(Locale.Lang._Misc.PerFromTo,
this.CurrentFileCountMoved,
this.TotalFileCount);

lock (uiRef.fileCountIndicatorSubtitle)
{
this.parentUI.DispatcherQueue.TryEnqueue(() =>
{
uiRef.fileCountIndicatorSubtitle.Text = fileCountProcessedString;
uiRef.pathActivitySubtitle.Text = currentPathProcessed;
});
}
}

private async void UpdateSizeProcessed(FileMigrationProcessUIRef uiRef, long currentRead)
{
lock (this) { this.CurrentSizeMoved += currentRead; }

if (await CheckIfNeedRefreshStopwatch())
{
double percentage = Math.Round((double)this.CurrentSizeMoved / this.TotalFileSize * 100d, 2);
double speed = this.CurrentSizeMoved / this.ProcessStopwatch.Elapsed.TotalSeconds;

lock (uiRef.progressBarIndicator)
{
this.parentUI.DispatcherQueue.TryEnqueue(() =>
{
string speedString = string.Format(Locale.Lang._Misc.SpeedPerSec, ConverterTool.SummarizeSizeSimple(speed));
string sizeProgressString = string.Format(Locale.Lang._Misc.PerFromTo,
ConverterTool.SummarizeSizeSimple(this.CurrentSizeMoved),
ConverterTool.SummarizeSizeSimple(this.TotalFileSize));

uiRef.speedIndicatorSubtitle.Text = speedString;
uiRef.fileSizeIndicatorSubtitle.Text = sizeProgressString;
uiRef.progressBarIndicator.Value = percentage;
uiRef.progressBarIndicator.IsIndeterminate = false;
});
}
}
}

private async ValueTask<bool> CheckIfNeedRefreshStopwatch()
{
if (this.EventsStopwatch.ElapsedMilliseconds > _refreshInterval)
{
this.EventsStopwatch.Restart();
return true;
}

await Task.Delay(_refreshInterval);
return false;
}
}
}
160 changes: 160 additions & 0 deletions CollapseLauncher/Classes/FileMigrationProcess/FileMigrationProcess.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
using Microsoft.UI.Xaml;
using System;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace CollapseLauncher
{
internal partial class FileMigrationProcess
{
private const int _refreshInterval = 100; // 100ms UI refresh interval

private string dialogTitle { get; set; }
private string inputPath { get; set; }
private string outputPath { get; set; }
private bool isFileTransfer { get; set; }
private UIElement parentUI { get; set; }
private CancellationTokenSource tokenSource { get; set; }

private long CurrentSizeMoved;
private long CurrentFileCountMoved;
private long TotalFileSize;
private long TotalFileCount;
private bool IsSameOutputDrive;
private Stopwatch ProcessStopwatch;
private Stopwatch EventsStopwatch;

private FileMigrationProcess(UIElement parentUI, string dialogTitle, string inputPath, string outputPath, bool isFileTransfer, CancellationTokenSource tokenSource)
{
this.dialogTitle = dialogTitle;
this.inputPath = inputPath;
this.outputPath = outputPath;
this.isFileTransfer = isFileTransfer;
this.parentUI = parentUI;
this.tokenSource = tokenSource;
}

internal async Task<string> StartRoutine()
{
bool isSuccess = false;
this.CurrentSizeMoved = 0;
this.CurrentFileCountMoved = 0;
this.TotalFileSize = 0;
this.TotalFileCount = 0;
FileMigrationProcessUIRef? uiRef = null;

try
{
if (!await IsOutputPathSpaceSufficient(this.inputPath, this.outputPath))
throw new OperationCanceledException($"Disk space is not sufficient. Cancelling!");

uiRef = BuildMainMigrationUI();
string outputPath = await StartRoutineInner(uiRef.Value);
uiRef.Value.mainDialogWindow.Hide();
isSuccess = true;

return outputPath;
}
catch when (!isSuccess) // Throw if the isSuccess is not set to true
{
if (uiRef.HasValue && uiRef.Value.mainDialogWindow != null)
{
uiRef.Value.mainDialogWindow.Hide();
await Task.Delay(500); // Give artificial delay to give main dialog window thread to close first
}
throw;
}
finally
{
if (ProcessStopwatch != null) ProcessStopwatch.Stop();
if (EventsStopwatch != null) EventsStopwatch.Stop();
}
}

private async Task<string> StartRoutineInner(FileMigrationProcessUIRef uiRef)
{
this.ProcessStopwatch = Stopwatch.StartNew();
this.EventsStopwatch = Stopwatch.StartNew();
return this.isFileTransfer ? await MoveFile(uiRef) : await MoveDirectory(uiRef);
}

private async Task<string> MoveFile(FileMigrationProcessUIRef uiRef)
{
FileInfo inputPathInfo = new FileInfo(this.inputPath);
FileInfo outputPathInfo = new FileInfo(this.outputPath);

string inputPathDir = Path.GetDirectoryName(inputPathInfo.FullName);
string outputPathDir = Path.GetDirectoryName(outputPathInfo.FullName);

if (!Directory.Exists(outputPathDir))
Directory.CreateDirectory(outputPathDir);

// Update path display
string inputFileBasePath = inputPathInfo.FullName.Substring(inputPathDir.Length + 1);
UpdateCountProcessed(uiRef, inputFileBasePath);

if (this.IsSameOutputDrive)
{
// Logger.LogWriteLine($"[FileMigrationProcess::MoveFile()] Moving file in the same drive from: {inputPathInfo.FullName} to {outputPathInfo.FullName}", LogType.Default, true);
inputPathInfo.MoveTo(outputPathInfo.FullName);
UpdateSizeProcessed(uiRef, inputPathInfo.Length);
}
else
{
// Logger.LogWriteLine($"[FileMigrationProcess::MoveFile()] Moving file across different drives from: {inputPathInfo.FullName} to {outputPathInfo.FullName}", LogType.Default, true);
await MoveWriteFile(uiRef, inputPathInfo, outputPathInfo, this.tokenSource == null ? default : this.tokenSource.Token);
}

return outputPathInfo.FullName;
}

private async Task<string> MoveDirectory(FileMigrationProcessUIRef uiRef)
{
DirectoryInfo inputPathInfo = new DirectoryInfo(this.inputPath);
if (!Directory.Exists(this.outputPath))
Directory.CreateDirectory(this.outputPath);

DirectoryInfo outputPathInfo = new DirectoryInfo(this.outputPath);

int parentInputPathLength = inputPathInfo.Parent.FullName.Length + 1;
string outputDirBaseNamePath = inputPathInfo.FullName.Substring(parentInputPathLength);
string outputDirPath = Path.Combine(this.outputPath, outputDirBaseNamePath);

await Parallel.ForEachAsync(
inputPathInfo.EnumerateFiles("*", SearchOption.AllDirectories),
this.tokenSource?.Token ?? default,
async (inputFileInfo, cancellationToken) =>
{
int parentInputPathLength = inputPathInfo.Parent.FullName.Length + 1;
string inputFileBasePath = inputFileInfo.FullName.Substring(parentInputPathLength);

// Update path display
UpdateCountProcessed(uiRef, inputFileBasePath);

string outputTargetPath = Path.Combine(outputPathInfo.FullName, inputFileBasePath);
string outputTargetDirPath = Path.GetDirectoryName(outputTargetPath);

if (!Directory.Exists(outputTargetDirPath))
Directory.CreateDirectory(outputTargetDirPath);

if (this.IsSameOutputDrive)
{
// Logger.LogWriteLine($"[FileMigrationProcess::MoveDirectory()] Moving directory content in the same drive from: {inputFileInfo.FullName} to {outputTargetPath}", LogType.Default, true);
inputFileInfo.MoveTo(outputTargetPath);
UpdateSizeProcessed(uiRef, inputFileInfo.Length);
}
else
{
// Logger.LogWriteLine($"[FileMigrationProcess::MoveDirectory()] Moving directory content across different drives from: {inputFileInfo.FullName} to {outputTargetPath}", LogType.Default, true);
FileInfo outputFileInfo = new FileInfo(outputTargetPath);
await MoveWriteFile(uiRef, inputFileInfo, outputFileInfo, cancellationToken);
}
});

inputPathInfo.Delete(true);
return outputDirPath;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using CollapseLauncher.CustomControls;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Documents;

namespace CollapseLauncher
{
internal struct FileMigrationProcessUIRef
{
internal ContentDialogCollapse mainDialogWindow;
internal TextBlock pathActivitySubtitle;
internal Run speedIndicatorSubtitle;
internal Run fileCountIndicatorSubtitle;
internal Run fileSizeIndicatorSubtitle;
internal ProgressBar progressBarIndicator;
}
}
140 changes: 140 additions & 0 deletions CollapseLauncher/Classes/FileMigrationProcess/IO.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
using CollapseLauncher.CustomControls;
using CollapseLauncher.Dialogs;
using Hi3Helper;
using Hi3Helper.Data;
using Microsoft.UI.Xaml.Controls;
using System;
using System.Buffers;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace CollapseLauncher
{
internal partial class FileMigrationProcess
{
private async Task MoveWriteFile(FileMigrationProcessUIRef uiRef, FileInfo inputFile, FileInfo outputFile, CancellationToken token)
{
int bufferSize = 1 << 18; // 256 kB Buffer

if (inputFile.Length < bufferSize)
bufferSize = (int)inputFile.Length;

bool isUseArrayPool = Environment.ProcessorCount * bufferSize > 2 << 20;
byte[] buffer = isUseArrayPool ? ArrayPool<byte>.Shared.Rent(bufferSize) : new byte[bufferSize];

try
{
await using (FileStream inputStream = inputFile.OpenRead())
await using (FileStream outputStream = outputFile.Exists && outputFile.Length <= inputFile.Length ? outputFile.Open(FileMode.Open) : outputFile.Create())
{
// Just in-case if the previous move is incomplete, then update and seek to the last position.
if (outputFile.Length <= inputStream.Length && outputFile.Length >= bufferSize)
{
// Do check by comparing the first and last 128K data of the file
Memory<byte> firstCompareInputBytes = new byte[bufferSize];
Memory<byte> firstCompareOutputBytes = new byte[bufferSize];
Memory<byte> lastCompareInputBytes = new byte[bufferSize];
Memory<byte> lastCompareOutputBytes = new byte[bufferSize];

// Seek to the first data
inputStream.Position = 0;
await inputStream.ReadExactlyAsync(firstCompareInputBytes);
outputStream.Position = 0;
await outputStream.ReadExactlyAsync(firstCompareOutputBytes);

// Seek to the last data
long lastPos = outputStream.Length - bufferSize;
inputStream.Position = lastPos;
await inputStream.ReadExactlyAsync(lastCompareInputBytes);
outputStream.Position = lastPos;
await outputStream.ReadExactlyAsync(lastCompareOutputBytes);

bool isMatch = firstCompareInputBytes.Span.SequenceEqual(firstCompareOutputBytes.Span)
&& lastCompareInputBytes.Span.SequenceEqual(lastCompareOutputBytes.Span);

// If the buffers don't match, then start the copy from the beginning
if (!isMatch)
{
inputStream.Position = 0;
outputStream.Position = 0;
}
else
{
UpdateSizeProcessed(uiRef, outputStream.Length);
}
}

await MoveWriteFileInner(uiRef, inputStream, outputStream, buffer, token);
}

inputFile.IsReadOnly = false;
inputFile.Delete();
}
catch { throw; } // Re-throw to
finally
{
if (isUseArrayPool) ArrayPool<byte>.Shared.Return(buffer);
}
}

private async Task MoveWriteFileInner(FileMigrationProcessUIRef uiRef, FileStream inputStream, FileStream outputStream, byte[] buffer, CancellationToken token)
{
int read;
while ((read = await inputStream.ReadAsync(buffer, 0, buffer.Length, token)) > 0)
{
await outputStream.WriteAsync(buffer, 0, read, token);
UpdateSizeProcessed(uiRef, read);
}
}

private async ValueTask<bool> IsOutputPathSpaceSufficient(string inputPath, string outputPath)
{
DriveInfo inputDriveInfo = new DriveInfo(Path.GetPathRoot(inputPath));
DriveInfo outputDriveInfo = new DriveInfo(Path.GetPathRoot(outputPath));

this.TotalFileSize = await Task.Run(() =>
{
if (this.isFileTransfer)
{
FileInfo fileInfo = new FileInfo(inputPath);
return fileInfo.Length;
}

DirectoryInfo directoryInfo = new DirectoryInfo(inputPath);
long returnSize = directoryInfo.EnumerateFiles("*", SearchOption.AllDirectories).Sum(x =>
{
this.TotalFileCount++;
return x.Length;
});
return returnSize;
});

if (IsSameOutputDrive = inputDriveInfo.Name == outputDriveInfo.Name)
return true;

bool isSpaceSufficient = outputDriveInfo.TotalFreeSpace < this.TotalFileSize;
if (!isSpaceSufficient)
{
string errStr = $"Free Space on {outputDriveInfo.Name} is not sufficient! (Free space: {outputDriveInfo.TotalFreeSpace}, Req. Space: {this.TotalFileSize}, Drive: {outputDriveInfo.Name})";
Logger.LogWriteLine(errStr, LogType.Error, true);
await SimpleDialogs.SpawnDialog(
string.Format(Locale.Lang._Dialogs.OperationErrorDiskSpaceInsufficientTitle, outputDriveInfo.Name),
string.Format(Locale.Lang._Dialogs.OperationErrorDiskSpaceInsufficientMsg,
ConverterTool.SummarizeSizeSimple(outputDriveInfo.TotalFreeSpace),
ConverterTool.SummarizeSizeSimple(this.TotalFileSize),
outputDriveInfo.Name),
parentUI,
null,
Locale.Lang._Misc.Okay,
null,
ContentDialogButton.Primary,
ContentDialogTheme.Error
);
}

return isSpaceSufficient;
}
}
}
Loading

0 comments on commit 9e21a70

Please sign in to comment.