Skip to content

Commit 91a97c6

Browse files
committed
Fix FileMigrationProcess
+ Fix where the migration might remove unwanted non-game files. + Fix where some old empty folders aren't getting removed. + Fix #727 where user can't move the game files into a subfolder inside of the current folder.
1 parent 541ece3 commit 91a97c6

File tree

4 files changed

+150
-70
lines changed

4 files changed

+150
-70
lines changed

CollapseLauncher/Classes/FileMigrationProcess/FileMigrationProcess.cs

Lines changed: 112 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
using CollapseLauncher.FileDialogCOM;
22
using CollapseLauncher.Helper;
3+
using CollapseLauncher.Helper.StreamUtility;
34
using Hi3Helper;
45
using Hi3Helper.Shared.Region;
6+
using Hi3Helper.Win32.ManagedTools;
7+
using Hi3Helper.Win32.Native.ManagedTools;
58
using Microsoft.UI.Dispatching;
69
using System;
10+
using System.Collections.Generic;
711
using System.Diagnostics;
812
using System.IO;
13+
using System.Linq;
914
using System.Threading;
1015
using System.Threading.Tasks;
1116

@@ -49,7 +54,7 @@ internal async Task<string> StartRoutine()
4954
_currentFileCountMoved = 0;
5055
_totalFileSize = 0;
5156
_totalFileCount = 0;
52-
FileMigrationProcessUIRef? uiRef = null;
57+
FileMigrationProcessUIRef uiRef = null;
5358

5459
try
5560
{
@@ -62,20 +67,20 @@ internal async Task<string> StartRoutine()
6267
}
6368

6469
uiRef = BuildMainMigrationUI();
65-
string outputPath = await StartRoutineInner(uiRef.Value);
66-
uiRef.Value.MainDialogWindow!.Hide();
70+
string outputPath = await StartRoutineInner(uiRef);
71+
uiRef.MainDialogWindow!.Hide();
6772
isSuccess = true;
6873

6974
return outputPath;
7075
}
7176
catch when (!isSuccess) // Throw if the isSuccess is not set to true
7277
{
73-
if (!uiRef.HasValue || uiRef.Value.MainDialogWindow == null)
78+
if (uiRef?.MainDialogWindow == null)
7479
{
7580
throw;
7681
}
7782

78-
uiRef.Value.MainDialogWindow.Hide();
83+
uiRef.MainDialogWindow.Hide();
7984
await Task.Delay(500); // Give artificial delay to give main dialog window thread to close first
8085
throw;
8186
}
@@ -122,7 +127,7 @@ private async Task<string> MoveFile(FileMigrationProcessUIRef uiRef)
122127
else
123128
{
124129
Logger.LogWriteLine($"[FileMigrationProcess::MoveFile()] Moving file across different drives from: {inputPathInfo.FullName} to {outputPathInfo.FullName}", LogType.Default, true);
125-
await MoveWriteFile(uiRef, inputPathInfo, outputPathInfo, TokenSource?.Token ?? default);
130+
await MoveWriteFile(uiRef, inputPathInfo, outputPathInfo, TokenSource?.Token ?? CancellationToken.None);
126131
}
127132

128133
return outputPathInfo.FullName;
@@ -134,51 +139,115 @@ private async Task<string> MoveDirectory(FileMigrationProcessUIRef uiRef)
134139
DirectoryInfo outputPathInfo = new DirectoryInfo(OutputPath);
135140
outputPathInfo.Create();
136141

137-
int parentInputPathLength = inputPathInfo.Parent!.FullName.Length + 1;
138-
string outputDirBaseNamePath = inputPathInfo.FullName.Substring(parentInputPathLength);
139-
string outputDirPath = Path.Combine(OutputPath, outputDirBaseNamePath);
142+
bool isMoveBackward = inputPathInfo.FullName.StartsWith(outputPathInfo.FullName, StringComparison.OrdinalIgnoreCase) &&
143+
!inputPathInfo.FullName.Equals(outputPathInfo.FullName, StringComparison.OrdinalIgnoreCase);
144+
145+
// Listing all the existed files first
146+
List <FileInfo> inputFileList = [];
147+
inputFileList.AddRange(inputPathInfo
148+
.EnumerateFiles("*", SearchOption.AllDirectories)
149+
.EnumerateNoReadOnly()
150+
.Where(x => isMoveBackward || IsNotInOutputDir(x)));
151+
152+
// Check if both destination and source are SSDs. If true, enable multi-threading.
153+
// Disabling multi-threading while either destination or source are HDDs could help
154+
// reduce massive seeking, hence improving speed.
155+
bool isBothSsd = DriveTypeChecker.IsDriveSsd(InputPath) &&
156+
DriveTypeChecker.IsDriveSsd(OutputPath);
157+
ParallelOptions parallelOptions = new ParallelOptions
158+
{
159+
CancellationToken = TokenSource?.Token ?? CancellationToken.None,
160+
MaxDegreeOfParallelism = isBothSsd ? LauncherConfig.AppCurrentThread : 1
161+
};
162+
163+
// Get old list of empty directories so it can be removed later.
164+
StringComparer comparer = StringComparer.OrdinalIgnoreCase;
165+
HashSet<string> oldDirectoryList = new(inputFileList
166+
.Select(x => x.DirectoryName)
167+
.Where(x => !string.IsNullOrEmpty(x) && !x.Equals(inputPathInfo.FullName, StringComparison.OrdinalIgnoreCase))
168+
.Distinct(comparer)
169+
.OrderDescending(), comparer);
170+
171+
// Perform file migration task
172+
await Parallel.ForEachAsync(inputFileList, parallelOptions, Impl);
173+
foreach (string dir in oldDirectoryList)
174+
{
175+
RemoveEmptyDirectory(dir);
176+
}
140177

141-
await Parallel.ForEachAsync(
142-
inputPathInfo.EnumerateFiles("*", SearchOption.AllDirectories),
143-
new ParallelOptions
144-
{
145-
CancellationToken = TokenSource?.Token ?? default,
146-
MaxDegreeOfParallelism = LauncherConfig.AppCurrentThread
147-
},
148-
async (inputFileInfo, cancellationToken) =>
149-
{
150-
int parentInputPathLengthLocal = inputPathInfo.Parent!.FullName.Length + 1;
151-
string inputFileBasePath = inputFileInfo!.FullName[parentInputPathLengthLocal..];
178+
return OutputPath;
152179

153-
// Update path display
154-
UpdateCountProcessed(uiRef, inputFileBasePath);
180+
bool IsNotInOutputDir(FileInfo fileInfo)
181+
{
182+
bool isEmpty = string.IsNullOrEmpty(fileInfo.DirectoryName);
183+
if (isEmpty)
184+
{
185+
return false;
186+
}
155187

156-
string outputTargetPath = Path.Combine(outputPathInfo.FullName, inputFileBasePath);
157-
string outputTargetDirPath = Path.GetDirectoryName(outputTargetPath) ?? Path.GetPathRoot(outputTargetPath);
188+
bool isStartsWith = fileInfo.DirectoryName.StartsWith(outputPathInfo.FullName);
189+
return !isStartsWith;
190+
}
158191

159-
if (string.IsNullOrEmpty(outputTargetDirPath))
160-
throw new InvalidOperationException(string.Format(Locale.Lang._Dialogs.InvalidGameDirNewTitleFormat,
161-
InputPath));
162-
163-
DirectoryInfo outputTargetDirInfo = new DirectoryInfo(outputTargetDirPath);
164-
outputTargetDirInfo.Create();
192+
void RemoveEmptyDirectory(string dir)
193+
{
194+
foreach (string innerDir in Directory.EnumerateDirectories(dir))
195+
{
196+
RemoveEmptyDirectory(innerDir);
197+
}
165198

166-
if (IsSameOutputDrive)
167-
{
168-
Logger.LogWriteLine($"[FileMigrationProcess::MoveDirectory()] Moving directory content in the same drive from: {inputFileInfo.FullName} to {outputTargetPath}", LogType.Default, true);
169-
inputFileInfo.MoveTo(outputTargetPath, true);
170-
UpdateSizeProcessed(uiRef, inputFileInfo.Length);
171-
}
172-
else
199+
try
200+
{
201+
_ = FindFiles.TryIsDirectoryEmpty(dir, out bool isEmpty);
202+
if (!isEmpty)
173203
{
174-
Logger.LogWriteLine($"[FileMigrationProcess::MoveDirectory()] Moving directory content across different drives from: {inputFileInfo.FullName} to {outputTargetPath}", LogType.Default, true);
175-
FileInfo outputFileInfo = new FileInfo(outputTargetPath);
176-
await MoveWriteFile(uiRef, inputFileInfo, outputFileInfo, cancellationToken);
204+
string parentDir = Path.GetDirectoryName(dir);
205+
if (!string.IsNullOrEmpty(parentDir))
206+
{
207+
RemoveEmptyDirectory(parentDir);
208+
}
177209
}
178-
});
179210

180-
inputPathInfo.Delete(true);
181-
return outputDirPath;
211+
Directory.Delete(dir);
212+
Logger.LogWriteLine($"[FileMigrationProcess::MoveDirectory()] Empty directory: {dir} has been deleted!", LogType.Default, true);
213+
}
214+
catch (IOException)
215+
{
216+
// ignored
217+
}
218+
}
219+
220+
async ValueTask Impl(FileInfo inputFileInfo, CancellationToken cancellationToken)
221+
{
222+
string inputFileRelativePath = inputFileInfo.FullName
223+
.AsSpan(InputPath.Length)
224+
.TrimStart("\\/")
225+
.ToString();
226+
227+
string outputNewFilePath = Path.Combine(OutputPath, inputFileRelativePath);
228+
string outputNewFileDir = Path.GetDirectoryName(outputNewFilePath) ?? Path.GetPathRoot(outputNewFilePath);
229+
if (!string.IsNullOrEmpty(outputNewFileDir))
230+
Directory.CreateDirectory(outputNewFileDir);
231+
232+
// Update path display
233+
UpdateCountProcessed(uiRef, inputFileRelativePath);
234+
if (string.IsNullOrEmpty(outputNewFileDir))
235+
throw new InvalidOperationException(string.Format(Locale.Lang._Dialogs.InvalidGameDirNewTitleFormat,
236+
InputPath));
237+
238+
if (IsSameOutputDrive)
239+
{
240+
Logger.LogWriteLine($"[FileMigrationProcess::MoveDirectory()] Moving directory content in the same drive from: {inputFileInfo.FullName} to {outputNewFilePath}", LogType.Default, true);
241+
inputFileInfo.MoveTo(outputNewFilePath, true);
242+
UpdateSizeProcessed(uiRef, inputFileInfo.Length);
243+
}
244+
else
245+
{
246+
Logger.LogWriteLine($"[FileMigrationProcess::MoveDirectory()] Moving directory content across different drives from: {inputFileInfo.FullName} to {outputNewFilePath}", LogType.Default, true);
247+
FileInfo outputFileInfo = new FileInfo(outputNewFilePath);
248+
await MoveWriteFile(uiRef, inputFileInfo, outputFileInfo, cancellationToken);
249+
}
250+
}
182251
}
183252
}
184253
}

CollapseLauncher/Classes/FileMigrationProcess/FileMigrationProcessRef.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@
44

55
namespace CollapseLauncher
66
{
7-
internal struct FileMigrationProcessUIRef
7+
internal class FileMigrationProcessUIRef
88
{
9-
internal ContentDialogCollapse MainDialogWindow;
10-
internal TextBlock PathActivitySubtitle;
11-
internal Run SpeedIndicatorSubtitle;
12-
internal Run FileCountIndicatorSubtitle;
13-
internal Run FileSizeIndicatorSubtitle;
14-
internal ProgressBar ProgressBarIndicator;
9+
internal ContentDialogCollapse MainDialogWindow { get; set; }
10+
internal TextBlock PathActivitySubtitle { get; set; }
11+
internal Run SpeedIndicatorSubtitle { get; set; }
12+
internal Run FileCountIndicatorSubtitle { get; set; }
13+
internal Run FileSizeIndicatorSubtitle { get; set; }
14+
internal ProgressBar ProgressBarIndicator { get; set; }
1515
}
1616
}

CollapseLauncher/Classes/FileMigrationProcess/Statics.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ private static bool IsOutputPathSameAsInput(string inputPath, string outputPath,
5656
bool isStringEmpty = string.IsNullOrEmpty(outputPath);
5757

5858
if (!isFilePath) inputPath = Path.GetDirectoryName(inputPath);
59-
bool isPathEqual = inputPath.AsSpan().TrimEnd('\\').SequenceEqual(outputPath.AsSpan().TrimEnd('\\'));
59+
bool isPathEqual = inputPath.AsSpan().TrimEnd("\\/").SequenceEqual(outputPath.AsSpan().TrimEnd("\\/"));
6060

6161
return isStringEmpty || isPathEqual;
6262
}

CollapseLauncher/Classes/FileMigrationProcess/UIBuilder.cs

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using CollapseLauncher.Dialogs;
33
using CollapseLauncher.Extension;
44
using CollapseLauncher.FileDialogCOM;
5+
using CollapseLauncher.Helper.StreamUtility;
56
using Hi3Helper;
67
using Hi3Helper.Data;
78
using Hi3Helper.Win32.FileDialogCOM;
@@ -20,19 +21,20 @@ internal partial class FileMigrationProcess
2021
{
2122
private static async ValueTask<string> BuildCheckOutputPathUI(string dialogTitle, string inputPath, string outputPath, bool isFileTransfer)
2223
{
24+
Grid mainGrid = UIElementExtensions.CreateGrid()
25+
.WithRows(new GridLength(1.0, GridUnitType.Star), new GridLength(1.0, GridUnitType.Star), new GridLength(1.0, GridUnitType.Star))
26+
.WithColumns(new GridLength(1.0, GridUnitType.Star), GridLength.Auto);
27+
2328
ContentDialogCollapse mainDialogWindow = new ContentDialogCollapse(ContentDialogTheme.Informational)
2429
{
25-
Title = dialogTitle,
26-
CloseButtonText = Locale.Lang!._Misc!.Cancel,
27-
PrimaryButtonText = null,
30+
Title = dialogTitle,
31+
CloseButtonText = Locale.Lang!._Misc!.Cancel,
32+
PrimaryButtonText = null,
2833
SecondaryButtonText = null,
29-
DefaultButton = ContentDialogButton.Primary
34+
DefaultButton = ContentDialogButton.Primary,
35+
Content = mainGrid
3036
};
3137

32-
Grid mainGrid = UIElementExtensions.CreateGrid()
33-
.WithRows(new GridLength(1.0, GridUnitType.Star), new GridLength(1.0, GridUnitType.Star), new GridLength(1.0, GridUnitType.Star))
34-
.WithColumns(new GridLength(1.0, GridUnitType.Star), GridLength.Auto);
35-
3638
// ReSharper disable once UnusedVariable
3739
TextBlock locateFolderSubtitle = mainGrid.AddElementToGridColumn(new TextBlock
3840
{
@@ -63,8 +65,6 @@ private static async ValueTask<string> BuildCheckOutputPathUI(string dialogTitle
6365
IsOpen = false
6466
}, 2, 0, 0, 2).WithMargin(0, 16, 0, 0);
6567

66-
mainDialogWindow.Content = mainGrid;
67-
6868
if (!string.IsNullOrEmpty(outputPath))
6969
ToggleOrCheckPathWarning(outputPath);
7070

@@ -80,38 +80,49 @@ private static async ValueTask<string> BuildCheckOutputPathUI(string dialogTitle
8080
ContentDialogResult mainDialogWindowResult = await mainDialogWindow.QueueAndSpawnDialog();
8181
return mainDialogWindowResult == ContentDialogResult.Primary ? choosePathTextBox.Text : null;
8282

83-
void ToggleWarningText(string text = null)
83+
void ToggleWarningText(string text = null, bool isError = true)
8484
{
85-
bool canContinue = string.IsNullOrEmpty(text);
85+
bool canContinue = string.IsNullOrEmpty(text) || !isError;
8686
mainDialogWindow.PrimaryButtonText = canContinue ? Locale.Lang!._Misc!.Next : null;
8787

8888
warningTextInfoBar.Title = Locale.Lang!._FileMigrationProcess!.ChoosePathErrorTitle;
89-
warningTextInfoBar.Severity = canContinue ? InfoBarSeverity.Success : InfoBarSeverity.Error;
89+
warningTextInfoBar.Severity = canContinue ? !isError ? InfoBarSeverity.Warning : InfoBarSeverity.Success : InfoBarSeverity.Error;
9090
warningTextInfoBar.IsOpen = !canContinue;
9191
warningTextInfoBar.Message = text;
9292
}
9393

9494
void ToggleOrCheckPathWarning(string path)
9595
{
96-
string parentPath = path;
96+
string parentPath = path.NormalizePath();
9797
if (isFileTransfer) parentPath = Path.GetDirectoryName(path);
9898

99-
if (string.IsNullOrEmpty(parentPath))
99+
ReadOnlySpan<char> parentPathTrimmed = parentPath.TrimEnd("\\/");
100+
ReadOnlySpan<char> inputPathTrimmed = inputPath.TrimEnd("\\/");
101+
102+
if (parentPathTrimmed.IsEmpty)
100103
{
101104
ToggleWarningText(Locale.Lang!._FileMigrationProcess!.ChoosePathErrorPathUnselected);
102105
return;
103106
}
104-
if (parentPath.StartsWith(inputPath, StringComparison.OrdinalIgnoreCase) || IsOutputPathSameAsInput(inputPath, path, isFileTransfer))
107+
108+
if (inputPathTrimmed.Equals(parentPathTrimmed, StringComparison.OrdinalIgnoreCase))
105109
{
106110
ToggleWarningText(Locale.Lang!._FileMigrationProcess!.ChoosePathErrorPathIdentical);
107111
return;
108112
}
109-
if (!(File.Exists(parentPath) || Directory.Exists(parentPath)))
113+
114+
string pathRoot = Path.GetPathRoot(path);
115+
116+
if ((isFileTransfer && !File.Exists(parentPath)) ||
117+
string.IsNullOrEmpty(pathRoot) ||
118+
!Directory.Exists(pathRoot))
110119
{
111120
ToggleWarningText(Locale.Lang!._FileMigrationProcess!.ChoosePathErrorPathNotExist);
112121
return;
113122
}
114-
if (!ConverterTool.IsUserHasPermission(parentPath))
123+
124+
if (Directory.Exists(parentPath) &&
125+
!ConverterTool.IsUserHasPermission(parentPath))
115126
{
116127
ToggleWarningText(Locale.Lang!._FileMigrationProcess!.ChoosePathErrorPathNoPermission);
117128
return;
@@ -132,7 +143,7 @@ private FileMigrationProcessUIRef BuildMainMigrationUI()
132143
};
133144

134145
Grid mainGrid = UIElementExtensions.CreateGrid()
135-
.WithWidth(500d)
146+
.WithWidth(600d)
136147
.WithColumns(new GridLength(1.0d, GridUnitType.Star), new GridLength(1.0d, GridUnitType.Star))
137148
.WithRows(new GridLength(1.0d, GridUnitType.Auto), new GridLength(20d, GridUnitType.Pixel), new GridLength(20d, GridUnitType.Pixel), new GridLength(20d, GridUnitType.Pixel));
138149

0 commit comments

Comments
 (0)