<# .SYNOPSIS Invokes multiple runspaces in powershell to execute file copies in paralel, using more resources for a better transfer speed. This will effectively max out the netork throughput, making your network the bottleneck rather than single-thread transfer rate. Useful for transferring files between computers with high-bandwidth NICs. .DESCRIPTION 1. Enumerates the folder structure, breaking the files into separate groups. The first layer of folders and files contained in $SourceFolder are split up among the total number of threads allowed. 2. The groups are assigned to instances of PowerShell RunSpaces, executing a copy command .PARAMETER SourceFolder The path of the folder you want to copy (Ex. "\\cameron\d$" ) .PARAMETER DestinationFolder Where you'd like the contents of the folder to end up (Ex. "\\cameronR\d$" ) .PARAMETER MaxThreads The maximum number of threads you'd like to run as instances of Copy-NetworkFiles .PARAMETER Max A Switch that sets the MaxThreads to the actuall maximum number of threads on your computer .EXAMPLE PS> Copy-FilesFast -SourceFolder "\\wds2019\REMINST" -DestinationFolder "\\itnas\backup$\WDS Images" -MaxThreads 30 .NOTES Author: Cameron Ratchford Date: March 17, 2020 #> function Copy-FilesFast { [CmdletBinding()] param( [Parameter(Mandatory=$true)] [string]$SourceFolder, [Parameter(Mandatory=$true)] [string]$DestinationFolder, [Parameter(Mandatory=$false)] [int16]$MaxThreads=4 ) # Get the theoritical maximum threads $Proc = Get-CimInstance -ClassName Win32_Processor $ProcCores = $Proc.ThreadCount # If this PC has multiple sockets, add the sockets together to make the final count if ($ProcCores.Length -gt 1) { $ThreadCount = 0 foreach ($CPU in $ProcCores) { $ThreadCount += $CPU } } # If the computer only has one socket, the $ProcCores number is final else { $ThreadCount = $ProcCores } if ($Max) { $MaxThreads = $ThreadCount } else { # Warn user if the number of threads they chose is lower than the actual number that the computer is capable of running if ($MaxThreads -gt $ThreadCount) { Write-Warning "Your machine only has $ThreadCount threads available, using $ThreadCount instead of $MaxThreads threads" $MaxThreads = $ThreadCount } } # Enumerate the folder structure $FolderStructure = Get-ChildItem -Path $SourceFolder -Force # Divide the number of files by the number of threads chosen, rounding down $GroupSize = [math]::Floor(($FolderStructure.Length / $MaxThreads)) # The table that we store the group objects in $GroupTable = @() # If there is a remainder after dividing the groups up by the calculated group size $GroupDivRemainder = ($FolderStructure.Length) % $GroupSize if ($GroupDivRemainder -ne 0) { # Assign group names and sizes to all but the last group $i = 1 write-host -ForegroundColor Magenta $GroupDivRemainder for ($i; $i -le ($MaxThreads - 1); $i++) { $GroupObj = [PSCustomObject]@{ Group = $i Size = $GroupSize Files = @() } $GroupTable += $GroupObj } # Assign the group name and the group size (Standard group size + remainder) $GroupObj = [PSCustomObject]@{ Group = $i Size = $GroupSize + $GroupDivRemainder Files = @() } $GroupTable += $GroupObj } # If there is no remainder after dividing the groups up by the calculated group size else { # Assign group names and sizes to all but the last group for ($i = 1; $i -le $MaxThreads; $i++) { $GroupObj = [PSCustomObject]@{ Group = $i Size = $GroupSize Files = @() } $GroupTable += $GroupObj } } # Assign group names and sizes to all but the last group $IndexOn = 0 foreach ($Group in $GroupTable) { $IndexRange = ($IndexOn .. ($IndexOn + $Group.Size)) $IndexOn += $Group.Size foreach ($i in $IndexRange) { $Group.Files += $FolderStructure[$i].FullName } } # The copy function, in scriptblock form $ScriptBlock = { param($SourceFileList, $DestinationFolder) foreach ($File in $SourceFileList) { Copy-Item -Path $File -Destination $DestinationFolder -Force -Recurse } } # Create a PS Runpool $RunspacePool = [runspacefactory]::CreateRunspacePool(1, $MaxThreads) $RunspacePool.Open() $Jobs = @() # Loop through all the groups, invoking a different job (thread) for each group foreach ($Group in $GroupTable) { $PowerShell = [powershell]::Create() $PowerShell.RunspacePool = $RunspacePool $PowerShell.AddScript($ScriptBlock).addArgument($Group.Files).addArgument($DestinationFolder) $Jobs += $PowerShell.BeginInvoke() } # If there are jobs still running, don't stop the processes while ($Jobs.IsCompleted -contains $false) { Start-Sleep 1 } }