PSFileTransfer.psm1

$chunkSize = 1MB

#region Internals
#region Get-Type (helper function for creating generic types)
function Get-Type
{
    param (
        [Parameter(Position = 0, Mandatory = $true)]
        [string] $GenericType,
        
        [Parameter(Position = 1, Mandatory = $true)]
        [string[]] $T
    )
    
    $T = $T -as [type[]]
    
    try
    {
        $generic = [type]($GenericType + '`' + $T.Count)
        $generic.MakeGenericType($T)
    }
    catch [Exception]
    {
        throw New-Object -TypeName System.Exception -ArgumentList ('Cannot create generic type', $_.Exception)
    }
}
#endregion
#region Invoke-Ternary
function Invoke-Ternary ([scriptblock]$decider, [scriptblock]$ifTrue, [scriptblock]$ifFalse)
{
    if (&$decider)
    {
        &$ifTrue
    }
    else
    {
        &$ifFalse
    }
}
Set-Alias -Name ?? -Value Invoke-Ternary -Option AllScope -Description "Ternary Operator like '?' in C#"
#endregion
#endregion

#region File Transfer Functions
#region Send-File
function Send-File
{
  <#
    .SYNOPSIS
 
    Sends a file to a remote session.
 
    .EXAMPLE
 
    PS >$session = New-PsSession leeholmes1c23
    PS >Send-File c:\temp\test.exe c:\temp\test.exe $session
  #>

    
    param (
        [Parameter(Mandatory = $true)]
        [string]$Source,
        
        [Parameter(Mandatory = $true)]
        [string]$Destination,
        
        [Parameter(Mandatory = $true)]
        [System.Management.Automation.Runspaces.PSSession[]]$Session
    )
    
    #Set-StrictMode -Version Latest
    $firstChunk = $true
    
    Write-Verbose "PSFileTransfer: Sending file $Source to $Destination on $($Session.ComputerName) ($([Math]::Round($chunkSize / 1MB, 2)) MB chunks)"
    
    $sourcePath = (Resolve-Path $Source -ErrorAction SilentlyContinue).Path
    if (-not $sourcePath)
    {
        Write-Error 'Source file could not be found'
        return
    }
    
    $sourceFileStream = [IO.File]::OpenRead($sourcePath)
    
    for ($position = 0; $position -lt $sourceFileStream.Length; $position += $chunkSize)
    {
        <#
        Write-Progress -Activity "Send file $Source to $Destination on $($Session.ComputerName)" `
                       -Status 'Transmitting file' `
                       -PercentComplete ($position / $sourceFileStream.Length * 100)
        #>

        
        $remaining = $sourceFileStream.Length - $position
        $remaining = [Math]::Min($remaining, $chunkSize)
        
        $chunk = New-Object -TypeName byte[] -ArgumentList $remaining
        [void]$sourceFileStream.Read($chunk, 0, $remaining)
        
        try
        {
            #Write-File -DestinationFile $Destination -Bytes $chunk -Erase $firstChunk
            Invoke-Command -Session $Session -ScriptBlock (Get-Command Write-File).ScriptBlock `
                           -ArgumentList $Destination, $chunk, $firstChunk -ErrorAction Stop
        }
        catch [System.Exception]
        {
            Write-Error -Message 'Could not write destination file' -Exception $_.Exception
            return
        }
        
        $firstChunk = $false
    }
    
    $sourceFileStream.Close()
    
    Write-Verbose "PSFileTransfer: Finished sending file $Source"
}
#endregion Send-File

#region Receive-File
function Receive-File
{
  <#
    .SYNOPSIS
 
    Receives a file from a remote session.
 
    .EXAMPLE
 
    PS >$session = New-PsSession leeholmes1c23
    PS >Receive-File c:\temp\test.exe c:\temp\test.exe $session
  #>

    
    param (
        [Parameter(Mandatory = $true)]
        [string]$Source,
        
        [Parameter(Mandatory = $true)]
        [string]$Destination,
        
        [Parameter(Mandatory = $true)]
        [System.Management.Automation.Runspaces.PSSession] $Session
    )
    
    Set-StrictMode -Version Latest
    $firstChunk = $true
    
    Write-Verbose "PSFileTransfer: Receiving file $Source to $Destination from $($Session.ComputerName) ($([Math]::Round($chunkSize / 1MB, 2)) MB chunks)"
    
    $sourceLength = Invoke-Command -Session $Session -ScriptBlock (Get-Command Get-FileLength).ScriptBlock `
                                   -ArgumentList $Source -ErrorAction Stop
    #$sourceLength = Invoke-Command -Session $Session -ScriptBlock (Get-Command Read-File).ScriptBlock `
    # -ArgumentList $Source, 0 -ErrorAction Stop
    
    for ($position = 0; $position -lt $sourceLength; $position += $chunkSize)
    {
        <#
        Write-Progress -Activity "Receive file $Source to $Destination from $($Session.ComputerName)" `
                       -Status 'Transmitting file' `
                       -PercentComplete ($position / $sourceLength * 100)
        #>

        
        $remaining = $sourceLength - $position
        $remaining = [Math]::Min($remaining, $chunkSize)
        
        try
        {
            #$chunk = Read-File -SourceFile $Source -Offset $position -Length $remaining
            $chunk = Invoke-Command -Session $Session -ScriptBlock (Get-Command Read-File).ScriptBlock `
                                    -ArgumentList $Source, $position, $chunkSize -ErrorAction Stop
        }
        catch [System.Exception]
        {
            Write-Error -Message 'Could not read destination file' -Exception $_.Exception
            return
        }
        
        Write-File -DestinationFile $Destination -Bytes $chunk.Bytes -Erase $firstChunk
        
        $firstChunk = $false
    }
    
    Write-Verbose "PSFileTransfer: Finished receiving file $Source"
}
#endregion Receive-File

#region Receive-Directory
function Receive-Directory
{
    param (
        ## The target path on the remote computer
        [Parameter(Mandatory = $true)]
        $Source,
        
        ## The path on the local computer
        [Parameter(Mandatory = $true)]
        $Destination,
        
        ## The session that represents the remote computer
        [Parameter(Mandatory = $true)]
        [System.Management.Automation.Runspaces.PSSession] $Session
    )
    Write-Verbose "Receive-Directory $($env:COMPUTERNAME): remote source $Source, local destination $Destination, session $($Session.ComputerName)"
    
    <#
    Write-Progress -Activity "Receive directory $Destination from $Source on $($Session.ComputerName)" `
                   -Status 'Checking destination'
    #>

    
    $remoteDir = Invoke-Command -Session $Session -ScriptBlock {
        param ($Source)
        
        Get-Item $Source
    } -ArgumentList $Source -ErrorAction Stop
    
    if (-not $remoteDir.PSIsContainer)
    {
        Receive-File $Source $Destination $Session
    }
    
    if (-not (Test-Path $Destination))
    {
        New-Item $Destination -ItemType Container -ErrorAction Stop
    }
    elseif (-not (Test-Path $Destination -PathType Container))
    {
        throw "$Destination exists and is not a directory"
    }
    
    <#
    Write-Progress -Activity "Receive directory $Destination from $Source on $($Session.ComputerName)" `
                   -Status 'Reading remote content list'
    #>

    
    $remoteItems = Invoke-Command -Session $Session -ScriptBlock {
        param ($remoteDir)
        
        Get-ChildItem $remoteDir
    } -ArgumentList $remoteDir -ErrorAction Stop
    $position = 0
    
    foreach ($remoteItem in $remoteItems)
    {
        $itemSource = Join-Path -Path $Source -ChildPath $remoteItem.Name
        <#
        Write-Progress -Activity "Receive directory $Destination from $Source on $($Session.ComputerName)" `
                       -Status "Copying $itemSource" `
                       -PercentComplete ($position * 100 / @($remoteItems).Count)
        #>

        
        $itemDestination = Join-Path -Path $Destination -ChildPath $remoteItem.Name
        if ($remoteItem.PSIsContainer)
        {
            $null = Receive-Directory -Source $itemSource -Destination $itemDestination -Session $Session
        }
        else
        {
            $null = Receive-File -Source $itemSource -Destination $itemDestination -Session $Session
        }
        $position++
    }
    <#
    Write-Progress -Activity "Receive directory $Destination from $Source on $($Session.ComputerName)" `
                   -Status 'Completed' -Completed
    #>

}
#endregion Receive-Directory

#region Send-Directory
function Send-Directory
{
    param (
        ## The path on the local computer
        [Parameter(Mandatory = $true)]
        $Source,
        
        ## The target path on the remote computer
        [Parameter(Mandatory = $true)]
        $Destination,
        
        ## The session that represents the remote computer
        [Parameter(Mandatory = $true)]
        [System.Management.Automation.Runspaces.PSSession[]]$Session
    )
    
    Write-Verbose "Send-Directory $($env:COMPUTERNAME): local source $Source, remote destination $Destination, session $($Session.ComputerName)"
    
    #Write-Progress -Activity "Send directory $Source to $Destination on $($Session.ComputerName)" -Status 'Checking source'
    
    $localDir = Get-Item $Source -ErrorAction Stop
    if (-not $localDir.PSIsContainer)
    {
        Send-File -Source $Source -Destination $Destination -Session $Session
        return
    }
    
    Invoke-Command -Session $Session -ScriptBlock {
        param ($Destination)
        
        if (-not (Test-Path $Destination))
        {
            $null = New-Item $Destination -ItemType Container -ErrorAction Stop
        }
        elseif (-not (Test-Path $Destination -PathType Container))
        {
            throw "$Destination exists and is not a directory"
        }
    } -ArgumentList $Destination -ErrorAction Stop
    
    <#
    Write-Progress -Activity "Send directory $Source to $Destination on $($Session.ComputerName)" `
                   -Status 'Reading local content list'
    #>

    
    $localItems = Get-ChildItem $localDir -ErrorAction Stop
    $position = 0
    
    foreach ($localItem in $localItems)
    {
        $itemSource = Join-Path -Path $Source -ChildPath $localItem.Name
        <#
        Write-Progress -Activity "Send directory $Source to $Destination on $($Session.ComputerName)" `
                       -Status "Copying $itemSource" `
                       -PercentComplete ($position * 100 / @($localItems).Count)
        #>

        
        $itemDestination = Join-Path -Path $Destination -ChildPath $localItem.Name
        if ($localItem.PSIsContainer)
        {
            $null = Send-Directory -Source $itemSource -Destination $itemDestination -Session $Session
        }
        else
        {
            $null = Send-File -Source $itemSource -Destination $itemDestination -Session $Session
        }
        $position++
    }
    <#
    Write-Progress -Activity "Send directory $Source from $Destination on $($Session.ComputerName)" `
                   -Status 'Completed' -Completed
    #>

}
#endregion Send-Directory
#endregion File Transfer Functions

function Write-File
{
    param (
        [Parameter(Mandatory = $true)]
        [string]$DestinationFile,
        
        [Parameter(Mandatory = $true)]
        [byte[]]$Bytes,
        
        [bool]$Erase
    )
    
    Write-Debug "Send-File $($env:COMPUTERNAME): writing $DestinationFile length $($Bytes.Length)"
    
    #Convert the destination path to a full filesytem path (to support relative paths)
    try
    {
        $DestinationFile = $executionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($DestinationFile)
    }
    catch [System.Exception]
    {
        throw New-Object -TypeName System.IO.FileNotFoundException -ArgumentList ('Could not set destination path', $_)
    }
    
    if ($Erase)
    {
        Remove-Item $DestinationFile -Force -ErrorAction SilentlyContinue
    }
    
    $destFileStream = [IO.File]::OpenWrite($DestinationFile)
    $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 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 [System.Exception]
    {
        throw New-Object -TypeName System.IO.FileNotFoundException
    }
    
    if (-not (Test-Path -Path $SourceFile))
    {
        throw 'Source file could not be found'
    }
    
    $sourceFileStream = [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 Get-FileLength
{
    [OutputType([int])]
    param (
        [Parameter(Mandatory = $true)]
        [string]$File
    )
    
    try
    {
        $File = $executionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($File)
    }
    catch [System.Exception]
    {
        throw $_
    }
    
    (Get-Item -Path $File).Length
}

function Copy-LabFileItem
{
    param (
        [Parameter(Mandatory)]
        [string[]]$Path,
        
        [Parameter(Mandatory)]
        [string[]]$ComputerName,

        [string]$DestinationFolder,
        
        [switch]$Recurse,
        
        [switch]$FallbackToPSSession = $true
    )
    
    Write-LogFunctionEntry
    
    $machines = Get-LabMachine -ComputerName $ComputerName -ErrorAction Stop
    $connectedMachines = @{ }
    
    foreach ($machine in $machines)
    {
        $cred = $machine.GetCredential((Get-Lab))
        
        try
        {
            $drive = New-PSDrive -Name "C_on_$machine" -PSProvider FileSystem -Root "\\$machine\c$" -Credential $cred -ErrorAction Stop
            Write-Debug "Drive '$($drive.Name)' created"
            $connectedMachines.Add($machine.Name, $drive)
        }
        catch
        {
            if (-not $FallbackToPSSession)
            {
            Microsoft.PowerShell.Utility\Write-Error -Message "Could not create a SMB connection to '$machine' ('\\$machine\c$'). Files could not be copied." -TargetObject $machine -Exception $_.Exception
            continue
            }
            
            foreach ($p in $Path)
            {
                $session = New-LabPSSession -ComputerName $machine
                $destination = if (-not $DestinationFolder)
                {
                    Join-Path -Path C:\ -ChildPath (Split-Path -Path $p -Leaf)
                }
                else
                {
                    Join-Path -Path $DestinationFolder -ChildPath (Split-Path -Path $p -Leaf)
                }
                Send-Directory -Source $p -Session $session -Destination $destination
            }
        }
    }
    
    Write-Verbose -Message "Copying the items '$($Path -join ', ')' to machines '$($connectedMachines.Keys -join ', ')'"
    
    foreach ($machine in $connectedMachines.GetEnumerator())
    {
        Write-Debug "Starting copy job for machine '$($machine.Name)'..."
        
        if ($DestinationFolder)
        {
            $drive = "$($machine.Value):"
            $DestinationFolder = Split-Path -Path $DestinationFolder -NoQualifier
            $DestinationFolder = Join-Path -Path $drive -ChildPath $DestinationFolder

            if (-not (Test-Path -Path $DestinationFolder))
            {
                mkdir -Path $DestinationFolder | Out-Null
            }
        }
        else
        {
            $DestinationFolder = "$($machine.Value):\"
        }

        Copy-Item -Path $Path -Destination $DestinationFolder -Recurse -Force
        Write-Debug '...finished'
        
        $machine.Value | Remove-PSDrive
        Write-Debug "Drive '$($drive.Name)' removed"
        Write-Verbose "Files copied on to machine '$($machine.Name)'"
    }
    
    Write-LogFunctionExit
}