Copy-ItemWithProgress.ps1


<#PSScriptInfo
 
.VERSION 1.0
 
.GUID c000f0c4-f6d4-407c-81e5-7e824d339f4a
 
.AUTHOR sosbu
 
.COMPANYNAME
 
.COPYRIGHT
 
.TAGS
 
.LICENSEURI
 
.PROJECTURI
 
.ICONURI
 
.EXTERNALMODULEDEPENDENCIES
 
.REQUIREDSCRIPTS
 
.EXTERNALSCRIPTDEPENDENCIES
 
.RELEASENOTES
 
 
.PRIVATEDATA
 
#>


<#
 
.DESCRIPTION
 Copy Files with RoboCopy, showing progress with Standard PowerShell Write-Progress
 
#>
 

Param()


Function Copy-ItemWithProgress{
    <#
    .SYNOPSIS
    RoboCopy with PowerShell progress.
 
    .DESCRIPTION
    Copies Files with RoboCopy while processing the RoboCopy output to display
    Powershell native status and progress.
 
    .PARAMETER Path
    Directory to copy files from, this should not contain trailing slashes
 
    .PARAMETER Destination
    Directory to copy files to, this should not contain trailing slahes
 
    .PARAMETER Filter
    (Optional)
    A wildcard expresion of which files to copy, defaults to *.*
 
    .PARAMETER Recurse
    (Optional)
    Recursive Copy through the Path sub-directories
 
    .PARAMETER ProgressParentID
    (Optional) [Int]
    Use this as the ParentID for the write-progress bar. This allows nesting
    in complex scripts
 
    .PARAMETER ProgressID
    (Optional) [Int]
    Use this as the ID for the write-progress bar
     
    .PARAMETER LogFile
    (Optional)
    Path to copy the Robocopy Log to
     
    .OUTPUTS
    Returns an object with the status of final copy.
    REMINDER: Any error level below 8 can be considered a success by RoboCopy.
 
    .EXAMPLE
    C:\PS> .\Copy-ItemWithProgress c:\Src d:\Dest
 
    Copy the contents of the c:\Src directory to a directory d:\Dest
    Without -Recurse, only files from the root of c:\src are copied.
 
    .EXAMPLE
    C:\PS> .\Copy-ItemWithProgress "c:\Source Files" d:\Dest -Recurse -Verbose
 
    Copy the contents of the 'c:\Source Files' directory and sub-directories to
    d:\Dest, and script is run verbose
 
    .LINK
     
 
    .NOTES
    By sosburne@gmail.com
    By Keith S. Garner (KeithGa@KeithGa.com) - 6/23/2014
    With inspiration by Keith S. Garner (KeithGa@KeithGa.com) - 6/23/2014, Trevor Sullivan @pcgeek86, Justin Marshall - 02/20/2020
    #>


    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]$Path,
        [Parameter(Mandatory=$true)]
        [string]$Destination,
        [Parameter(Mandatory=$false)]
        [string]$Filter="*.*",
        [Parameter(Mandatory=$false)]
        [bool]$Recurse=$False,
        [Parameter(Mandatory=$false)]
        [int]$ParentProgressID=0,
        [Parameter(Mandatory=$false)]
        [int]$ProgressID=-1,
        [Parameter(Mandatory=$false)] 
        [string[]] $Log=$false
    )

    #handle spaces and trailing slashes
    $SourceDir = '"{0}"' -f ($Path -replace "\\+$","")
    $TargetDir = '"{0}"' -f ($Destination -replace "\\+$","")

    $ScanLog  = [IO.Path]::GetTempFileName()
    $RoboLog  = [IO.Path]::GetTempFileName()
    $ScanArgs = "$SourceDir $TargetDir $FilesToCopy /E /ZB /MT /R:5 /W:3 /ndl /bytes /NP /Log:$ScanLog /nfl /L"
    $RoboArgs = "$SourceDir $TargetDir $FilesToCopy /E /ZB /COPY:DATO /DCOPY:DAT /MT /R:5 /W:3 /J /ndl /bytes /Log:$RoboLog /NC"

    $ScanRun = start-process robocopy -PassThru -WindowStyle Hidden -ArgumentList $ScanArgs
    try
    {
        $RoboRun = start-process robocopy -PassThru -WindowStyle Hidden -ArgumentList $RoboArgs
        #determine progress parameters
<#
        $ProgressParms=@{}
        if ($ParentProgressID -ge 0) {
            $ProgressParms['ParentID']=$ParentProgressID
        }
        if ($ProgressID -gt 0) {
            $ProgressParms['ID']=$ProgressID
        } else {
            $ProgressParms['ID']=$($RoboRun.Id)
        }
/#>

        if ($ProgressID -lt 0) { $ProgressID = $($RoboRun.Id) }
        Write-Verbose -Message "ParentProgressID $ParentProgressID"
        Write-Verbose -Message "ProgressID $ProgressID"
        try
        {
            $CurrentFile = "Skipped"
            $CurrentFilePercent = $null
            If ($VerbosePreference -eq "Continue") {
                Write-Verbose "Waiting on ScanRun PID $($ScanRun.Id)"
                While (-not $ScanRun.HasExited) {
                    Start-Sleep -Seconds 1
                    $Seconds++
                    If ($PsISE) {
                        Write-Host "." -ForegroundColor Cyan -NoNewline
                    } Else {
                        Write-Host "`rScanRun PID $($ScanRun.Id) - (Running for $Seconds seconds)" -NoNewline -ForegroundColor Cyan
                    }
                }
                Write-Host `r`n -NoNewline
                Write-Verbose "ScanRun PID $($ScanRun.Id) Completed"
            }
            $ScanRun.WaitForExit()

            # Parse Robocopy "Scan" pass
            $LogData = get-content $ScanLog
            if ($ScanRun.ExitCode -ge 8)
            {
                $LogData|out-string|Write-Error
                #throw "Robocopy $($ScanRun.ExitCode)"
                Write-Warning "ScanRun ExitCode: $($ScanRun.ExitCode)"
            }
            $FilesLengthSum = [regex]::Match($LogData[-4],".+:\s+(\d+)\s+(\d+)").Groups[2].Value
            #write-verbose ($LogData -join "`n")

            # Monitor Full RoboCopy
            #Write-Verbose "RoboRun has exited = $($RoboRun.HasExited)"
            write-progress -Activity "ROBOCOPY $Source to $Destination" -PercentComplete 0 -Id $ProgressID -ParentId $ParentProgressID
            while (-not $($RoboRun.HasExited))
            {
                #Write-Verbose "RoboRun PID $($RoboRun.Id) running"
                Start-Sleep -Milliseconds 100
                $LogData = get-content $RoboLog
                $Files = $LogData -match "^\s*(\d+)\s+(\S+)"
                #$Files[-1]
                if (!([string]::IsNullOrEmpty($Files)))
                {
                    $CopiedLength = ($Files[0..($Files.Length-2)] | ForEach-Object {$_.Split("`t")[-2]} | Measure-Object -sum).Sum
                    $File = $Files[-1].Split("`t")[-1]
                    $FileLength = $Files[-1].Split("`t")[-2]
                    write-progress -Activity "ROBOCOPY $Source to $Destination" -Status "$([math]::Round($($CopiedLength / 1MB),2)) MB of $([math]::Round($($FilesLengthSum / 1MB), 2)) MB" -PercentComplete $($CopiedLength / $FilesLengthSum * 100) -Id $ProgressID -ParentId $ParentProgressID
                    #Write-Verbose "$($CopiedLength / 1MB) of $($FilesLengthSum / 1MB)"
                    if ($LogData[-1] -match "(100|\d?\d)\%")
                    {
                        $FilePercent = $LogData[-1].Trim("% `t")
                        If (($File -eq $CurrentFile) -and ($FilePercent -eq $CurrentFilePercent)) { Continue }
                        $CurrentFile = $File
                        $CurrentFilePercent = $FilePercent
                        write-progress -Activity "$file" -Status "$([math]::Round($($FileLength / 100 * $FilePercent / 1MB),2)) MB of $([math]::Round($($FileLength / 1MB), 2)) MB" -PercentComplete $FilePercent -ParentID $ProgressID -Id $($ProgressID + 100)
                    }
                    else
                    {
                        If ($File -eq $CurrentFile) { Continue }
                        write-progress -Activity "$file" -Status "$([math]::Round($($FileLength / 1MB),2)) MB of $([math]::Round($($FileLength / 1MB), 2)) MB" -PercentComplete "100" -ParentID $ProgressID -Id $($ProgressID + 100)
                    }
    
                }
            }
        } finally {
            #if (!$RoboRun.HasExited) {Write-Warning "Terminating copy process with ID $($RoboRun.Id)..."; $RoboRun.Kill() ; }
            $RoboRun.WaitForExit()
            write-progress -Activity "$CurrentFile" -ParentID $ProgressID -Id $($ProgressID + 100) -Completed
            Write-Progress -Activity "ROBOCOPY $Source to Destination" -Id $ProgressID -ParentId $ParentProgressID -Completed
            # Parse full RoboCopy pass results, and cleanup
            (get-content $RoboLog)[-11..-2] | out-string | Write-Verbose
            If ($Log) {Copy-Item -Path $RoboLog -Destination "$Log"}
            remove-item $RoboLog
        }
    } finally {
        if (!$ScanRun.HasExited) {Write-Warning "Terminating scan process with ID $($ScanRun.Id)..."; $ScanRun.Kill() }
        $ScanRun.WaitForExit()

        remove-item $ScanLog
    }
}