-
-
Notifications
You must be signed in to change notification settings - Fork 67
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
18 changed files
with
993 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
160
CollapseLauncher/Classes/FileMigrationProcess/FileMigrationProcess.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
16 changes: 16 additions & 0 deletions
16
CollapseLauncher/Classes/FileMigrationProcess/FileMigrationProcessRef.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
Oops, something went wrong.