Created
September 28, 2018 13:06
-
-
Save NegativeZero000/76dde93966055bed68793c10ea54dda7 to your computer and use it in GitHub Desktop.
Copy-WithRoboCopyProgress
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function Copy-WithRobocopyProgress{ | |
# Copy-WithRobocopyProgress -Source C:\Temp\test\from -Destination C:\Temp\test\to -Arguments "/e" | |
[CmdletBinding()] | |
param ( | |
[Parameter(Mandatory = $true)] | |
[string] $Source, | |
[Parameter(Mandatory = $true)] | |
[string] $Destination, | |
[Parameter(Mandatory = $true)] | |
[string[]]$Arguments | |
) | |
<# Query arguments. These are used in addition to those passed via parameters to determine the cost of the | |
robocopy operation. | |
/l - Specifies that files are to be listed only (and not copied, deleted, or time stamped). | |
/bytes - Prints sizes, as bytes. | |
/ndl - Specifies that directory names are not to be logged. | |
/njh - Specifies that there is no job header. | |
/njs - Specifies that there is no job summary. | |
/nfl - Specifies that file names are not to be logged. | |
#> | |
$executionArguments = "/ndl","/bytes","/njh" | |
$costArguments = $executionArguments + "/l", "/nfl" | |
function Parse-RobocopySummary{ | |
# This function will parse the robocopy footer as custom object | |
param( | |
[string[]]$Footer | |
) | |
$Footer | ForEach-Object { | |
if ($_ -like "*Dirs*"){ | |
$lineAsArray = (($_.Split(':')[1]).trim()) -split '\s+' | |
$Dirs = [pscustomobject][ordered]@{ | |
Total = $lineAsArray[0] | |
Copied = $lineAsArray[1] | |
Skipped = $lineAsArray[2] | |
Mismatch = $lineAsArray[3] | |
FAILED = $lineAsArray[4] | |
Extras = $lineAsArray[5] | |
} | |
} | |
if ($_ -like "*Files*"){ | |
$lineAsArray = ($_.Split(':')[1]).trim() -split '\s+' | |
$Files = [pscustomobject][ordered]@{ | |
Total = $lineAsArray[0] | |
Copied = $lineAsArray[1] | |
Skipped = $lineAsArray[2] | |
Mismatch = $lineAsArray[3] | |
FAILED = $lineAsArray[4] | |
Extras = $lineAsArray[5] | |
} | |
} | |
if ($_ -like "*Bytes*"){ | |
$lineAsArray = ($_.Split(':')[1]).trim() -split '\s+' | |
$Bytes = [pscustomobject][ordered]@{ | |
Total = $lineAsArray[0] | |
Copied = $lineAsArray[1] | |
Skipped = $lineAsArray[2] | |
Mismatch = $lineAsArray[3] | |
FAILED = $lineAsArray[4] | |
Extras = $lineAsArray[5] | |
} | |
} | |
if ($_ -like "*Times*"){ | |
$lineAsArray = ($_.Split(':',2)[1]).trim() -split '\s+' | |
$Times = [pscustomobject][ordered]@{ | |
Total = $lineAsArray[0] | |
Copied = $lineAsArray[1] | |
FAILED = $lineAsArray[2] | |
Extras = $lineAsArray[3] | |
} | |
} | |
if ($_ -like "*Error*"){ | |
# Something wrong has happened. | |
Write-Error ($Footer -join "`n") | |
} | |
} | |
[PSCustomObject]@{ | |
'Dirs' = $Dirs | |
'Files' = $Files | |
'Bytes' = $Bytes | |
'Times' = $Times | |
} | |
} | |
# Check if this is a folder or single file request. Arguments need to change if that is the case. | |
if(-not (Get-Item $Source).PSisContainer){ | |
Write-Verbose "Source is a file." | |
Write-Verbose "Supplied source: $Source" | |
Write-Verbose "Supplied arguments: $Arguments" | |
$fileName = Split-Path $Source -Leaf | |
# Change the source so that the file name is not present. | |
$Source = Split-Path $Source | |
# Remove the '/e' if present in the arguments and add the file name | |
$Arguments = @($fileName) + ($Arguments | Where-Object{$_ -ne "/e"}) | |
Write-Verbose "Updated source: $Source" | |
Write-Verbose "Updated arguments: $Arguments" | |
} | |
# Use robocopy to get a list only view of the proposed copy operation. | |
# Determine overall cost from the job summary | |
Write-Verbose "robocopy ""$Source"" ""$Destination"" $($Arguments + $costArguments)" | |
$reportSummary = Parse-RobocopySummary -Footer (robocopy "$Source" "$Destination" ($Arguments + $costArguments)) | |
Write-Verbose ($reportSummary | Format-List * | Out-String).TrimEnd() | |
# Baseline statistics | |
$totalFilesCount = $reportSummary.Files.Copied | |
$totalByteCost = $reportSummary.Bytes.Copied | |
$bytesTotalDenomination = if($totalByteCost -gt 2GB){"1GB"}else{"1MB"} | |
# Start the copy as a job and monitor progress. Ensure bytes switch is used. No issues if duplicated | |
$robocopyJob = Start-Job -Name "RoboCopy" -ScriptBlock {robocopy.exe $args} -ArgumentList @("$Source","$Destination" + $Arguments + $executionArguments) | |
# Progress variable initialization | |
$bytesCopied = New-Object 'System.Collections.Generic.List[Int64]' | |
$totalFilesCopied = 0 | |
while ($robocopyjob.State -eq 'running'){ | |
# Get jobs current data output | |
$currentProgress = Receive-Job -Job $robocopyJob -ErrorAction SilentlyContinue | |
# Assuming we have output to process | |
if($currentProgress -and $reportSummary.Files.Copied -gt 0){ | |
# Are we in the middle of copying a file right now? | |
# If the last line in the output contains a percentage value record or assume 0 | |
$currentFilePercentage = if($currentProgress[-1] -match "^\s+([\d|\.]+)%"){$Matches[1]}else{0} | |
# Determine number of files copied so far. | |
# Find the number of lines that contain New File or Newer designations as they need to be counted. | |
$robocopyNewFileData = ($currentProgress | Select-String -Pattern "^\s+New") | |
# Check if new files are being copied that were not previously | |
if($robocopyNewFileData){ | |
# Update the files counts | |
$totalFilesCopied += $robocopyNewFileData.Count | |
# Update the byte counts | |
# Get the first number in the line which would be after the newer/new file text | |
$robocopyNewFileData -replace "^.*?(\d+).*$",'$1' | ForEach-Object{ | |
$bytesCopied.Add($_) | |
} | |
# Update progress indicators. | |
$currentFile = ($robocopyNewFileData[-1] -replace "^.*?(\d+)").Trim() | |
$bytesFileDenomination = if($bytesCopied[-1] -gt 1GB){"1GB"}elseif($bytesCopied[-1] -gt 1MB){"1MB"}else{"1KB"} | |
} | |
# Byte count of files that have already been copied. | |
[int64]$bytesCopiedInProgressTotal = ($bytesCopied | Select-Object -SkipLast 1 | Measure-Object -Sum).Sum | |
$parentProgressArgs = @{ | |
ID = 1 | |
Activity = "Copying files from '$Source' to '$Destination', $($totalFilesCopied - 1) of $totalFilesCount files copied" | |
Status = "{0:N2} of {1:N2} {2} copied" -f ($bytesCopiedInProgressTotal / $bytesTotalDenomination),($totalByteCost / $bytesTotalDenomination),$bytesTotalDenomination.TrimStart("1") | |
Percent = $bytesCopiedInProgressTotal / $totalByteCost * 100 | |
} | |
$childProgressArgs = @{ | |
ID = 2 | |
# To prevent nulls here make activity at least an empty string. | |
Activity = "" + $currentFile | |
Status = "{0:N2} / {1:N2} {2}" -f (($bytesCopied[-1] * ($currentFilePercentage / 100)) / $bytesFileDenomination),($bytesCopied[-1] / $bytesFileDenomination),$bytesFileDenomination.TrimStart("1") | |
Percent = $currentFilePercentage | |
ParentID = 1 | |
} | |
Write-Progress @parentProgressArgs | |
Write-Progress @childProgressArgs | |
} | |
# Limit update frequency | |
sleep -Milliseconds 500 | |
} | |
# Mark the progress as completed assuming it was started. | |
if($parentProgressArgs){ | |
$parentProgressArgs.Percent = 100 | |
$parentProgressArgs.Status = "{0:N2} of {0:N2} {1} copied" -f ($totalByteCost / $bytesTotalDenomination),$bytesTotalDenomination.TrimStart("1") | |
$parentProgressArgs.Activity = "Done" | |
$parentProgressArgs.Completed = $true | |
Write-Progress @parentProgressArgs | |
} | |
# Remove the completed job | |
$robocopyJob | Remove-Job | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment