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