PowerShell: Copy Directory With Progress Meter
Posted on 22 June 2010 by Jason Grimme
Earlier today I thought it would be nice to display a progress meter while copying a directory. After thinking about it for a while I realized that I would need to do something asynchronous, like creating a new thread. Much to my dismay I found no such thing which was quite a shock considering how impressed I have been with PowerShell.
A few hours later it was still bothering me so I did some more research and found the Start-Job cmdlet which seemed to do what I wanted. Two hours later and I had the function below.
I won’t say that it is the best function in the world, or even that it is that great because it isn’t. But it does seem to work. I’m still quite new to PowerShell, so forgive any foolish mistakes.
If you set -DisplayMeter $True, the built in progress meter (Write-Progress) meter will be displayed.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | function Copy-Progress { # Jason Grimme - StudioShorts.com # Copies a Source directory to Destination, while displaying progress. param( [string] $Source = $(throw "Source parameter required."), [string] $destination = $(throw "Destination parameter required."), [boolean] $Verbose = $true, [boolean] $Overwrite = $true, [boolean] $DisplayMeter = $false ) # Setup temporary environment variables that will passed between the jobs #[boolean] $ENV:Copy_Progress_Finished = $false; [string] $ENV:Copy_Progress_Source = $Source; [string] $ENV:Copy_Progress_Desttination = $destination; # Unless we aren't overwriting, remove the future directory if( ($Overwrite) -and (Test-path $dest) ) { remove-item $dest -Recurse -Force; } # Create our job or 'thread' $job = Start-Job { copy-item $ENV:Copy_Progress_Source $ENV:Copy_Progress_Desttination -recurse -force }; #[string] $eventName = "Copy-Progress_JobStateChanged1"; #Register-ObjectEvent $job -EventName StateChanged -SourceIdentifier $eventName -Action { $ENV:Copy_Progress_Finished = $true; if( [string]$($Sender.JobStateInfo) -ne "Completed"){ Write-Host "Did not complete: Job $($Sender.Id) $($Sender.JobStateInfo)"; } } | out-null # Get the size of the source directory $SourceSize = (Get-ChildItem $source | Measure-Object -property length -sum).sum; [double] $DestinationSize = 0; # Do/While the size of the destination directory is less than the source do { # Once the destination directory has been created by Copy-Item if(Test-path $dest) { # Calculate the current size and the percent complete [double] $DestinationSize = (Get-ChildItem $dest | Measure-Object -property length -sum).sum [int] $percentComplete = (($DestinationSize / $SourceSize)* 100); if($Verbose) { Write-Output "Current Size: $DestinationSize bytes. Percent Complete: $percentComplete%"; } if($DisplayMeter) { Write-Progress -Activity "Performing Copy..." -PercentComplete $percentComplete -CurrentOperation "$percentComplete% complete" -Status "Please wait." } } start-sleep -Milliseconds 500; } while($SourceSize -gt $DestinationSize); #while($ENV:Copy_Progress_Finished -ne $true); if($Verbose) { write-Host "= Copy-Progress $percentComplete% Complete" Write-Host "= Source: '$Source', Destination: '$destination'"; Write-Host "= Source Size: $SourceSize bytes. Destination Size: $DestinationSize bytes"; } # Unset the variables that we don't want to hang around $ENV:Copy_Progress_Finished = $null; $ENV:Copy_Progress_Source = $null; $ENV:Copy_Progress_Desttination = $null; #Unregister-Event $eventName; } #Example Usage Copy-Progress -Source "C:\scripts\msc\" -Destination "C:\scripts\temp\" -Verbose $true -Overwrite $true -DisplayMeter $True; |
Because of how PowerShell reads file sizes, this function does not work with copying individual files. It must be a directory. If anybody can find a way to read a file while it is being copied to and grab the true file size at that moment in time, I would be very interested. I am also looking for a better way to pass variables from the main scope into the Start-Job scriptblock, using $env: variables isn’t very professional.
An example of the output with Verbose on and the Display meter off would be something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | PS C:\scripts> Copy-Progress -Source "C:\scripts\Dino\" -Destination "C:\scripts\Saur\" -Verbose $true-Overwrite $true -DisplayMeter $false; Current Size: 3639974 bytes. Percent Complete: 1% Current Size: 27120693 bytes. Percent Complete: 5% Current Size: 50755136 bytes. Percent Complete: 9% Current Size: 67796400 bytes. Percent Complete: 11% Current Size: 84230737 bytes. Percent Complete: 14% Current Size: 106248435 bytes. Percent Complete: 18% Current Size: 217981590 bytes. Percent Complete: 37% Current Size: 318419162 bytes. Percent Complete: 54% Current Size: 397551370 bytes. Percent Complete: 67% Current Size: 413262946 bytes. Percent Complete: 70% Current Size: 431720922 bytes. Percent Complete: 73% Current Size: 446876222 bytes. Percent Complete: 75% Current Size: 466876615 bytes. Percent Complete: 79% Current Size: 483184623 bytes. Percent Complete: 81% Current Size: 505980571 bytes. Percent Complete: 85% Current Size: 521798269 bytes. Percent Complete: 88% Current Size: 536622872 bytes. Percent Complete: 90% Current Size: 552066196 bytes. Percent Complete: 93% Current Size: 574065976 bytes. Percent Complete: 97% Current Size: 593613675 bytes. Percent Complete: 100% = Copy-Progress 100% Complete = Source: 'C:\scripts\Dino\', Destination: 'C:\scripts\Saur\' = Source Size: 593613675 bytes. Destination Size: 593613675 bytes |
Tags | .NET, Powershell

Nice but refuses to work.
here is the error output.
Test-Path : Cannot bind argument to parameter ‘Path’ because it is null.
At C:\Repository\Upgrade Copy PowerShell.ps1:18 char:37
+ if( ($Overwrite) -and (Test-path <<<< $dest) )
+ CategoryInfo : InvalidData: (:) [Test-Path], ParameterBindingValidationException
+ FullyQualifiedErrorId : ParameterArgumentValidationErrorNullNotAllowed,Microsoft.PowerShell.Commands.TestPathCommand
Measure-Object : Property "length" cannot be found in any object(s) input.
At C:\Repository\Upgrade Copy PowerShell.ps1:29 char:58
+ $SourceSize = (Get-ChildItem $source | Measure-Object <<<< -property length -sum).sum;
+ CategoryInfo : InvalidArgument: (:) [Measure-Object], PSArgumentException
+ FullyQualifiedErrorId : GenericMeasurePropertyNotFound,Microsoft.PowerShell.Commands.MeasureObjectCommand
Test-Path : Cannot bind argument to parameter 'Path' because it is null.
At C:\Repository\Upgrade Copy PowerShell.ps1:36 char:21
+ if(Test-path <<<< $dest)
+ CategoryInfo : InvalidData: (:) [Test-Path], ParameterBindingValidationException
+ FullyQualifiedErrorId : ParameterArgumentValidationErrorNullNotAllowed,Microsoft.PowerShell.Commands.TestPathCommand
Rename $dest to $destination and it should work.
I also noticed that it doesn’t work if directory has sub directory because it doesn’t have length property.
I got this error :
The term \’Start-Job\’ is not recognized as a cmdlet, function, operable program, or script file. Verify the term and try
again.
At C:\\SCRIPTS\\copy-progress.ps1:24 char:21
+ $job = Start-Job <<<< { copy-item $ENV:Copy_Progress_Source $ENV:Copy_Progress_Desttination -recurse -force };