11using CollapseLauncher . FileDialogCOM ;
22using CollapseLauncher . Helper ;
3+ using CollapseLauncher . Helper . StreamUtility ;
34using Hi3Helper ;
45using Hi3Helper . Shared . Region ;
6+ using Hi3Helper . Win32 . ManagedTools ;
7+ using Hi3Helper . Win32 . Native . ManagedTools ;
58using Microsoft . UI . Dispatching ;
69using System ;
10+ using System . Collections . Generic ;
711using System . Diagnostics ;
812using System . IO ;
13+ using System . Linq ;
914using System . Threading ;
1015using 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}
0 commit comments