@@ -40,6 +40,11 @@ function Sync-Directory
4040 For rsync: array of rsync flags (e.g., @('--compress', '--links'))
4141 For robocopy: array of robocopy switches (e.g., @('/MT:8', '/R:3'))
4242
43+ . PARAMETER ThreadCount
44+ Number of threads to use for robocopy on Windows (`/MT:n`).
45+ Ignored on macOS/Linux. Valid range is 1-128.
46+ If `-ExtraOptions` already includes `/MT` or `/MT:n`, that value is used instead.
47+
4348 . EXAMPLE
4449 PS > Sync-Directory -Source '.\MyProject' -Destination 'D:\Backup\MyProject'
4550
@@ -151,30 +156,41 @@ function Sync-Directory
151156 [String []]$Exclude = @ (),
152157
153158 [Parameter ()]
154- [String []]$ExtraOptions = @ ()
159+ [String []]$ExtraOptions = @ (),
160+
161+ [Parameter ()]
162+ [ValidateRange (1 , 128 )]
163+ [Int32 ]$ThreadCount = ([Math ]::Min(32 , [Math ]::Max(4 , [Environment ]::ProcessorCount)))
155164 )
156165
157166 begin
158167 {
159168 Write-Verbose ' Starting Sync-Directory'
160169
161- # Detect platform
162- if ($PSVersionTable.PSVersion.Major -lt 6 )
163- {
164- # PowerShell 5.1 - Windows only
165- $IsWindowsPlatform = $true
166- }
167- else
168- {
169- # PowerShell Core - use built-in variables
170- $IsWindowsPlatform = $IsWindows
171- }
170+ # Detect platform once for path comparison and tool selection.
171+ $IsWindowsPlatform = $IsWindows -or $env: OS -eq ' Windows_NT'
172+ $pathComparison = if ($IsWindowsPlatform ) { [System.StringComparison ]::OrdinalIgnoreCase } else { [System.StringComparison ]::Ordinal }
173+ $separatorChars = @ ([System.IO.Path ]::DirectorySeparatorChar, [System.IO.Path ]::AltDirectorySeparatorChar)
172174
173175 Write-Verbose " Platform: $ ( if ($IsWindowsPlatform ) { ' Windows' } else { ' macOS/Linux' }) "
174176
175177 # Resolve paths to absolute paths (cross-platform compatible)
176178 $Source = $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath ($Source )
177179 $Destination = $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath ($Destination )
180+ $Source = [System.IO.Path ]::GetFullPath($Source )
181+ $Destination = [System.IO.Path ]::GetFullPath($Destination )
182+
183+ $sourceComparable = $Source.TrimEnd ($separatorChars )
184+ if ([String ]::IsNullOrEmpty($sourceComparable ))
185+ {
186+ $sourceComparable = [System.IO.Path ]::DirectorySeparatorChar.ToString()
187+ }
188+
189+ $destinationComparable = $Destination.TrimEnd ($separatorChars )
190+ if ([String ]::IsNullOrEmpty($destinationComparable ))
191+ {
192+ $destinationComparable = [System.IO.Path ]::DirectorySeparatorChar.ToString()
193+ }
178194
179195 Write-Verbose " Resolved source path: $Source "
180196 Write-Verbose " Resolved destination path: $Destination "
@@ -184,6 +200,45 @@ function Sync-Directory
184200 {
185201 throw " Source directory does not exist: $Source "
186202 }
203+
204+ if (Test-Path - Path $Destination - PathType Leaf)
205+ {
206+ throw " Destination path exists as a file. Specify a directory path instead: $Destination "
207+ }
208+
209+ # Prevent recursive self-sync scenarios.
210+ if ([String ]::Equals($sourceComparable , $destinationComparable , $pathComparison ))
211+ {
212+ throw " Source and destination cannot be the same directory: $Source "
213+ }
214+
215+ $sourcePrefix = if ($sourceComparable.EndsWith ([System.IO.Path ]::DirectorySeparatorChar.ToString()))
216+ {
217+ $sourceComparable
218+ }
219+ else
220+ {
221+ $sourceComparable + [System.IO.Path ]::DirectorySeparatorChar
222+ }
223+
224+ $destinationPrefix = if ($destinationComparable.EndsWith ([System.IO.Path ]::DirectorySeparatorChar.ToString()))
225+ {
226+ $destinationComparable
227+ }
228+ else
229+ {
230+ $destinationComparable + [System.IO.Path ]::DirectorySeparatorChar
231+ }
232+
233+ if ($destinationComparable.StartsWith ($sourcePrefix , $pathComparison ))
234+ {
235+ throw " Destination cannot be inside source: $Destination "
236+ }
237+
238+ if ($Delete -and $sourceComparable.StartsWith ($destinationPrefix , $pathComparison ))
239+ {
240+ throw " Source cannot be inside destination when -Delete is used: $Source "
241+ }
187242 }
188243
189244 process
@@ -240,6 +295,13 @@ function Sync-Directory
240295 $robocopyArgs += ' /R:1'
241296 $robocopyArgs += ' /W:1'
242297
298+ # Enable multi-threaded copy by default for better performance
299+ $hasThreadingOption = $ExtraOptions | Where-Object { $_ -match ' ^/MT(?::\d+)?$' }
300+ if (-not $hasThreadingOption )
301+ {
302+ $robocopyArgs += " /MT:$ThreadCount "
303+ }
304+
243305 # Handle exclusions
244306 foreach ($Pattern in $Exclude )
245307 {
0 commit comments