PSFileTransfer.psm1
function Get-FileLength { [OutputType([int])] param ( [Parameter(Mandatory = $true)] [string]$FilePath ) try { $FilePath = $executionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($FilePath) } catch { throw $_ } (Get-Item -Path $FilePath -Force).Length } function Read-File { [OutputType([Byte[]])] param ( [Parameter(Mandatory = $true)] [string]$SourceFile, [Parameter(Mandatory = $true)] [int]$Offset, [int]$Length ) #Convert the destination path to a full filesytem path (to support relative paths) try { $sourcePath = $executionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($SourceFile) } catch { throw New-Object -TypeName System.IO.FileNotFoundException } if (-not (Test-Path -Path $SourceFile)) { throw 'Source file could not be found' } $sourceFileStream = [System.IO.File]::OpenRead($sourcePath) $chunk = New-Object -TypeName byte[] -ArgumentList $Length [void]$sourceFileStream.Seek($Offset, 'Begin') [void]$sourceFileStream.Read($chunk, 0, $Length) $sourceFileStream.Close() return @{ Bytes = $chunk } } function Write-File { param ( [Parameter(Mandatory = $true)] [string]$DestinationFullName, [Parameter(Mandatory = $true)] [byte[]]$Bytes, [bool]$Erase, [bool]$Force ) Write-Debug -Message "Send-File $($env:COMPUTERNAME): writing $DestinationFullName length $($Bytes.Length)" #Convert the destination path to a full filesytem path (to support relative paths) try { $DestinationFullName = $executionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($DestinationFullName) } catch { throw New-Object -TypeName System.IO.FileNotFoundException -ArgumentList ('Could not set destination path', $_) } if ((Test-Path -Path $DestinationFullName -PathType Container)) { Write-Error "Please define the target file's full name. '$DestinationFullName' points to a folder." return } if ($Erase) { Remove-Item $DestinationFullName -Force -ErrorAction SilentlyContinue } if ($Force) { $parentPath = Split-Path -Path $DestinationFullName -Parent if (-not (Test-Path -Path $parentPath)) { Write-Verbose -Message "Force is set and destination folder '$parentPath' does not exist, creating it." New-Item -ItemType Directory -Path $parentPath -Force | Out-Null } } $destFileStream = [System.IO.File]::OpenWrite($DestinationFullName) $destBinaryWriter = New-Object -TypeName System.IO.BinaryWriter -ArgumentList ($destFileStream) [void]$destBinaryWriter.Seek(0, 'End') $destBinaryWriter.Write($Bytes) $destBinaryWriter.Close() $destFileStream.Close() $Bytes = $null [GC]::Collect() } function Receive-Directory { param ( ## The target path on the remote computer [Parameter(Mandatory = $true)] $SourceFolderPath, ## The path on the local computer [Parameter(Mandatory = $true)] $DestinationFolderPath, ## The session that represents the remote computer [Parameter(Mandatory = $true)] [System.Management.Automation.Runspaces.PSSession] $Session ) Write-Verbose -Message "Receive-Directory $($env:COMPUTERNAME): remote source $SourceFolderPath, local destination $DestinationFolderPath, session $($Session.ComputerName)" $remoteDir = Invoke-Command -Session $Session -ScriptBlock { param ($Source) Get-Item $Source -Force } -ArgumentList $SourceFolderPath -ErrorAction Stop if (-not $remoteDir.PSIsContainer) { Receive-File -SourceFilePath $SourceFolderPath -DestinationFilePath $DestinationFolderPath -Session $Session } if (-not (Test-Path -Path $DestinationFolderPath)) { New-Item -Path $DestinationFolderPath -ItemType Container -ErrorAction Stop | Out-Null } elseif (-not (Test-Path -Path $DestinationFolderPath -PathType Container)) { throw "$DestinationFolderPath exists and is not a directory" } $remoteItems = Invoke-Command -Session $Session -ScriptBlock { param ($remoteDir) Get-ChildItem $remoteDir -Force } -ArgumentList $remoteDir -ErrorAction Stop $position = 0 foreach ($remoteItem in $remoteItems) { $itemSource = Join-Path -Path $SourceFolderPath -ChildPath $remoteItem.Name $itemDestination = Join-Path -Path $DestinationFolderPath -ChildPath $remoteItem.Name if ($remoteItem.PSIsContainer) { $null = Receive-Directory -SourceFolderPath $itemSource -DestinationFolderPath $itemDestination -Session $Session } else { $null = Receive-File -SourceFilePath $itemSource -DestinationFilePath $itemDestination -Session $Session } $position++ } } function Receive-File { param ( [Parameter(Mandatory = $true)] [string]$SourceFilePath, [Parameter(Mandatory = $true)] [string]$DestinationFilePath, [Parameter(Mandatory = $true)] [System.Management.Automation.Runspaces.PSSession] $Session ) $firstChunk = $true Write-Verbose -Message "PSFileTransfer: Receiving file $SourceFilePath to $DestinationFilePath from $($Session.ComputerName) ($([Math]::Round($chunkSize / 1MB, 2)) MB chunks)" $sourceLength = Invoke-Command -Session $Session -ScriptBlock (Get-Command Get-FileLength).ScriptBlock ` -ArgumentList $SourceFilePath -ErrorAction Stop $chunkSize = [Math]::Min($sourceLength, $chunkSize) for ($position = 0; $position -lt $sourceLength; $position += $chunkSize) { $remaining = $sourceLength - $position $remaining = [Math]::Min($remaining, $chunkSize) try { $chunk = Invoke-Command -Session $Session -ScriptBlock (Get-Command Read-File).ScriptBlock ` -ArgumentList $SourceFilePath, $position, $remaining -ErrorAction Stop } catch { Write-Error -Message 'Could not read destination file' -Exception $_.Exception return } Write-File -DestinationFullName $DestinationFilePath -Bytes $chunk.Bytes -Erase $firstChunk $firstChunk = $false } Write-Verbose -Message "PSFileTransfer: Finished receiving file $SourceFilePath" } function Send-Directory { param ( ## The path on the local computer [Parameter(Mandatory = $true)] $SourceFolderPath, ## The target path on the remote computer [Parameter(Mandatory = $true)] $DestinationFolderPath, ## The session that represents the remote computer [Parameter(Mandatory = $true)] [System.Management.Automation.Runspaces.PSSession[]]$Session ) $isCalledRecursivly = (Get-PSCallStack | Where-Object -Property Command -EQ -Value $MyInvocation.InvocationName | Measure-Object | Select-Object -ExpandProperty Count) -gt 1 if ($DestinationFolderPath -ne '/' -and -not $DestinationFolderPath.EndsWith('\')) { $DestinationFolderPath = $DestinationFolderPath + '\' } if (-not $isCalledRecursivly) { $initialDestinationFolderPath = $DestinationFolderPath $initialSource = $SourceFolderPath $initialSourceParent = Split-Path -Path $initialSource -Parent } Write-Verbose -Message "Send-Directory $($env:COMPUTERNAME): local source $SourceFolderPath, remote destination $DestinationFolderPath, session $($Session.ComputerName)" $localDir = Get-Item $SourceFolderPath -ErrorAction Stop -Force if (-not $localDir.PSIsContainer) { Send-File -SourceFilePath $SourceFolderPath -DestinationFolderPath $DestinationFolderPath -Session $Session -Force return } Invoke-Command -Session $Session -ScriptBlock { param ($DestinationPath) if (-not (Test-Path $DestinationPath)) { $null = New-Item -ItemType Directory -Path $DestinationPath -ErrorAction Stop } elseif (-not (Test-Path $DestinationPath -PathType Container)) { throw "$DestinationPath exists and is not a directory" } } -ArgumentList $DestinationFolderPath -ErrorAction Stop $localItems = Get-ChildItem -Path $localDir -ErrorAction Stop -Force $position = 0 foreach ($localItem in $localItems) { $itemSource = Join-Path -Path $SourceFolderPath -ChildPath $localItem.Name $newDestinationFolder = $itemSource.Replace($initialSourceParent, $initialDestinationFolderPath).Replace('\\', '\') if ($localItem.PSIsContainer) { $null = Send-Directory -SourceFolderPath $itemSource -DestinationFolderPath $newDestinationFolder -Session $Session } else { $newDestinationFolder = Split-Path -Path $newDestinationFolder -Parent $null = Send-File -SourceFilePath $itemSource -DestinationFolderPath $newDestinationFolder -Session $Session -Force } $position++ } } function Send-File { param ( [Parameter(Mandatory = $true)] [string]$SourceFilePath, [Parameter(Mandatory = $true)] [string]$DestinationFolderPath, [Parameter(Mandatory = $true)] [System.Management.Automation.Runspaces.PSSession[]]$Session, [switch]$Force ) $firstChunk = $true Write-Verbose -Message "PSFileTransfer: Sending file $SourceFilePath to $DestinationFolderPath on $($Session.ComputerName) ($([Math]::Round($chunkSize / 1MB, 2)) MB chunks)" $sourcePath = (Resolve-Path $SourceFilePath -ErrorAction SilentlyContinue).Path $sourcePath = Convert-Path $sourcePath if (-not $sourcePath) { Write-Error -Message 'Source file could not be found.' return } if (-not (Test-Path -Path $SourceFilePath -PathType Leaf)) { Write-Error -Message 'Source path points to a directory and not a file.' return } $sourceFileStream = [System.IO.File]::OpenRead($sourcePath) for ($position = 0; $position -lt $sourceFileStream.Length; $position += $chunkSize) { $remaining = $sourceFileStream.Length - $position $remaining = [Math]::Min($remaining, $chunkSize) $chunk = New-Object -TypeName byte[] -ArgumentList $remaining [void]$sourceFileStream.Read($chunk, 0, $remaining) $destinationFullName = Join-Path -Path $DestinationFolderPath -ChildPath (Split-Path -Path $SourceFilePath -Leaf) try { Invoke-Command -Session $Session -ScriptBlock (Get-Command Write-File).ScriptBlock ` -ArgumentList $destinationFullName, $chunk, $firstChunk, $Force -ErrorAction Stop } catch { Write-Error -Message "Could not write destination file. The error was '$($_.Exception.Message)'. Please use the Force switch if the destination folder does not exist" -Exception $_.Exception return } $firstChunk = $false } $sourceFileStream.Close() Write-Verbose -Message "PSFileTransfer: Finished sending file $SourceFilePath" } $chunkSize = 1MB |