40

I'm interested in a PowerShell script that copies a large amount of files from a server daily and I'm interested in implementing a in-console progress bar like

File copy status - XX% complete.

where XX% updates on the same line instead of newline after newline. I've decided to go with RoboCopy for now. I've currently got

ROBOCOPY 'C:\Users\JMondy\Desktop\Sample1' 'C:\Users\JMondy\Desktop\Sample2' . /E /IS /NFL /NJH

What is the next step?

codo-sapien
  • 833
  • 3
  • 14
  • 24
  • I hope someone can explain this too. Personally I don't think it's possible. To use a progressbar you need value for percentcomplete. Robocopy already has a progressbar(at least percent complete) so it's hard if not impossible to read it. A workaround might be to split a transfer job in multiple robocopy commands/runs and make a progressbar that updates on "1 of x jobs completed". – Frode F. Dec 14 '12 at 19:40
  • 1
    Great question. I've added a new answer, that should hopefully get you going. –  Jan 18 '14 at 21:04

8 Answers8

113

I wrote a PowerShell function called Copy-WithProgress that will achieve what you are after. Since you specifically stated that you were using robocopy, I built a PowerShell function that encapsulates the robocopy functionality (at least, parts of it).

Allow me to show you how it works. I've also recorded and posted a YouTube video demonstrating how the function is designed to work, and invoking a test run.

The function is divided into regions:

  • Common robocopy parameters
  • Staging (where the robocopy job size is calculated)
  • Copy (where the robocopy job is kicked off)
  • Progress bar (where the robocopy progress is monitored)
  • Function output (where some useful statistics are outputted, for use in the rest of your script)

There are several parameters on the function.

  • Source: The source directory
  • Destination: The destination directory
  • Gap: The "inter-packet gap" in milliseconds supported by robocopy, which artificially slows down the copy, for testing)
  • ReportGap: The interval (in milliseconds) to check on robocopy progress

At the bottom of the script (after the function definition), is a complete example of how to call it. It should work on your computer, since everything is variable-ized. There are five steps:

  1. Generate a random source directory
  2. Generate a destination directory
  3. Call the Copy-WithProgress function
  4. Create some additional source files (to emulate changes over time)
  5. Call the Copy-WithProgress function again, and validate only changes are replicated

Here is a screenshot of what the function's output looks like. You can leave off the -Verbose parameter, if you do not want all of the debugging information. A PSCustomObject is returned, by the function, which tells you:

  1. How many bytes were copied
  2. How many files were copied

Copy-WithProgress PowerShell Function

Here is a screenshot of the PowerShell Progress Bar in the PowerShell ISE, and the PowerShell Console Host.

PowerShell Progress Bar (ISE)

PowerShell Progress Bar (Console Host)

Here is the code:

function Copy-WithProgress {
    [CmdletBinding()]
    param (
            [Parameter(Mandatory = $true)]
            [string] $Source
        , [Parameter(Mandatory = $true)]
            [string] $Destination
        , [int] $Gap = 200
        , [int] $ReportGap = 2000
    )
    # Define regular expression that will gather number of bytes copied
    $RegexBytes = '(?<=\s+)\d+(?=\s+)';

    #region Robocopy params
    # MIR = Mirror mode
    # NP  = Don't show progress percentage in log
    # NC  = Don't log file classes (existing, new file, etc.)
    # BYTES = Show file sizes in bytes
    # NJH = Do not display robocopy job header (JH)
    # NJS = Do not display robocopy job summary (JS)
    # TEE = Display log in stdout AND in target log file
    $CommonRobocopyParams = '/MIR /NP /NDL /NC /BYTES /NJH /NJS';
    #endregion Robocopy params

    #region Robocopy Staging
    Write-Verbose -Message 'Analyzing robocopy job ...';
    $StagingLogPath = '{0}\temp\{1} robocopy staging.log' -f $env:windir, (Get-Date -Format 'yyyy-MM-dd HH-mm-ss');

    $StagingArgumentList = '"{0}" "{1}" /LOG:"{2}" /L {3}' -f $Source, $Destination, $StagingLogPath, $CommonRobocopyParams;
    Write-Verbose -Message ('Staging arguments: {0}' -f $StagingArgumentList);
    Start-Process -Wait -FilePath robocopy.exe -ArgumentList $StagingArgumentList -NoNewWindow;
    # Get the total number of files that will be copied
    $StagingContent = Get-Content -Path $StagingLogPath;
    $TotalFileCount = $StagingContent.Count - 1;

    # Get the total number of bytes to be copied
    [RegEx]::Matches(($StagingContent -join "`n"), $RegexBytes) | % { $BytesTotal = 0; } { $BytesTotal += $_.Value; };
    Write-Verbose -Message ('Total bytes to be copied: {0}' -f $BytesTotal);
    #endregion Robocopy Staging

    #region Start Robocopy
    # Begin the robocopy process
    $RobocopyLogPath = '{0}\temp\{1} robocopy.log' -f $env:windir, (Get-Date -Format 'yyyy-MM-dd HH-mm-ss');
    $ArgumentList = '"{0}" "{1}" /LOG:"{2}" /ipg:{3} {4}' -f $Source, $Destination, $RobocopyLogPath, $Gap, $CommonRobocopyParams;
    Write-Verbose -Message ('Beginning the robocopy process with arguments: {0}' -f $ArgumentList);
    $Robocopy = Start-Process -FilePath robocopy.exe -ArgumentList $ArgumentList -Verbose -PassThru -NoNewWindow;
    Start-Sleep -Milliseconds 100;
    #endregion Start Robocopy

    #region Progress bar loop
    while (!$Robocopy.HasExited) {
        Start-Sleep -Milliseconds $ReportGap;
        $BytesCopied = 0;
        $LogContent = Get-Content -Path $RobocopyLogPath;
        $BytesCopied = [Regex]::Matches($LogContent, $RegexBytes) | ForEach-Object -Process { $BytesCopied += $_.Value; } -End { $BytesCopied; };
        $CopiedFileCount = $LogContent.Count - 1;
        Write-Verbose -Message ('Bytes copied: {0}' -f $BytesCopied);
        Write-Verbose -Message ('Files copied: {0}' -f $LogContent.Count);
        $Percentage = 0;
        if ($BytesCopied -gt 0) {
           $Percentage = (($BytesCopied/$BytesTotal)*100)
        }
        Write-Progress -Activity Robocopy -Status ("Copied {0} of {1} files; Copied {2} of {3} bytes" -f $CopiedFileCount, $TotalFileCount, $BytesCopied, $BytesTotal) -PercentComplete $Percentage
    }
    #endregion Progress loop

    #region Function output
    [PSCustomObject]@{
        BytesCopied = $BytesCopied;
        FilesCopied = $CopiedFileCount;
    };
    #endregion Function output
}

# 1. TESTING: Generate a random, unique source directory, with some test files in it
$TestSource = '{0}\{1}' -f $env:temp, [Guid]::NewGuid().ToString();
$null = mkdir -Path $TestSource;
# 1a. TESTING: Create some test source files
1..20 | % -Process { Set-Content -Path $TestSource\$_.txt -Value ('A'*(Get-Random -Minimum 10 -Maximum 2100)); };

# 2. TESTING: Create a random, unique target directory
$TestTarget = '{0}\{1}' -f $env:temp, [Guid]::NewGuid().ToString();
$null = mkdir -Path $TestTarget;

# 3. Call the Copy-WithProgress function
Copy-WithProgress -Source $TestSource -Destination $TestTarget -Verbose;

# 4. Add some new files to the source directory
21..40 | % -Process { Set-Content -Path $TestSource\$_.txt -Value ('A'*(Get-Random -Minimum 950 -Maximum 1400)); };

# 5. Call the Copy-WithProgress function (again)
Copy-WithProgress -Source $TestSource -Destination $TestTarget -Verbose;
  • 11
    I have to say, this is a really well thought out solution and post! I dunno about the OP but I'm blown away and will likely use this myself :-) – Graham Gold Jan 18 '14 at 21:59
  • 3
    @GrahamGold Thank you, kind sir :) I appreciate the compliment. Make sure you check out the YouTube video, if necessary: http://www.youtube.com/watch?v=z9KeYa842rc –  Jan 18 '14 at 22:02
  • 2
    I also learned about region/endregion folding which was new to me and very useful, and you use `-f` a lot in places where I don't and it would make life easier. Validates why I use SO, to get help but also to learn :-) – Graham Gold Jan 18 '14 at 23:06
  • Yeah, I really love the .NET String formatting / substitution stuff. I generally avoid double-quotes if at all possible, because people sometimes don't know to expect the variable and subexpression evaluation happening :) –  Jan 18 '14 at 23:07
  • 6
    @GrahamGold No, don't worry. I'm blown away. This is fantastic. – codo-sapien Jan 23 '14 at 17:58
  • I'm glad that you came back and found it :) –  Jan 23 '14 at 18:01
  • 3
    I love this code. I've incorporated it into some of my own code. The only issue I have found so far is with "$FileCount = $StagingContent.Count;". If the $StagingLogContent is empty (because no files are being copied), the .Count throws an exception so I've added an if condition on mine to check for .Length -gt 0 before getting .Count. I've defaulted $FileCount to 0 before the if. – Dave May 08 '14 at 15:50
  • This Regex worked better for me: `$RegexBytes = '(?<=\s+New File\s+)\d+(?=\s+)'` This way you don't count any dir stats and you won't catch any bogus numbers in filenames. – MKesper Dec 01 '15 at 15:48
  • 1
    Hello, this is absolutely awesome. I have come to minor bug with percentage overflow over 100% ... [`Write-Progress : Cannot validate argument on parameter 'PercentComplete'. The 103 argument is greater than the maximum allowed range of 100. Supply an argument that is less than or equal to 100 and then try the command again.`] – Morignus Jul 17 '17 at 08:08
  • Just a minor glitch the log file date time stamp uses the lower case `hh` for 12h format instead of the 24h upper case `HH` format (changed above). Otherwise +1 nice work. –  Sep 28 '18 at 14:44
  • 1
    Love this solution! I'll definitely use this in the future. The only caveat is that it is not optimum when you are copying millions of small files. The `Start-Sleep` can really slow one down. Still keeping it in my "toolbox" though. – Charles Caldwell Nov 14 '18 at 23:01
  • This does not seem to work with the /MT switch, right? – Alek Davis Jan 28 '19 at 21:23
  • Deos not work when the source is a file. The error with redacted file name: `ERROR 267 (0x0000010B) Accessing Source Directory \\myserver.com\network\path\myexecutable.exe\ The directory name is invalid.` – ultracrepidarian Feb 21 '20 at 02:54
  • Your solution is truly elegant. I wrote something myself in native PowerShell, just a POC, not complete yet. If you want to take a look. https://github.com/FranciscoNabas/PowerShellPublic/blob/main/Copy-File.ps1 – FranciscoNabas Apr 18 '23 at 23:23
6

These solutions are great but a quick and easy way to get a floating progress for all the files easily is as follows:

robocopy <source> <destination> /MIR /NDL /NJH /NJS | %{$data = $_.Split([char]9); if("$($data[4])" -ne "") { $file = "$($data[4])"} ;Write-Progress "Percentage $($data[0])" -Activity "Robocopy" -CurrentOperation "$($file)"  -ErrorAction SilentlyContinue; }
MichaelS
  • 5,941
  • 6
  • 31
  • 46
Amrinder
  • 69
  • 1
  • 1
2

Do you absolutely have to use robocopy?

If not you could call the code in this thread for each file: Progress during large file copy (Copy-Item & Write-Progress?)

Alternatively use the /L switch of robocopy as called from powershell to get list of files robocopy would have copied and use a for-each loop to run each file through that copy function.

You could even nest write-progress commands so you can report "file x of y - XX% complete"

Something like this should work, needs a little work for subdirectories (I suspect more than just adding -recurse to the gci command) but will put you inthe right direction.

NOTE: I'm writing this on a phone, the code is untested as yet...

function Copy-File {
param( [string]$from, [string]$to)
$ffile = [io.file]::OpenRead($from)
$tofile = [io.file]::OpenWrite($to)
Write-Progress `
    -Activity ("Copying file " + $filecount + " of " + $files.count) `
    -status ($from.Split("\")|select -last 1) `
    -PercentComplete 0
try {
    $sw = [System.Diagnostics.Stopwatch]::StartNew();
    [byte[]]$buff = new-object byte[] 65536
    [long]$total = [long]$count = 0
    do {
        $count = $ffile.Read($buff, 0, $buff.Length)
        $tofile.Write($buff, 0, $count)
        $total += $count
        if ($total % 1mb -eq 0) {
            if([int]($total/$ffile.Length* 100) -gt 0)`
                {[int]$secsleft = ([int]$sw.Elapsed.Seconds/([int]($total/$ffile.Length* 100))*100)
                } else {
                [int]$secsleft = 0};
            Write-Progress `
                -Activity ([string]([int]($total/$ffile.Length* 100)) + "% Copying file")`
                -status ($from.Split("\")|select -last 1) `
                -PercentComplete ([int]($total/$ffile.Length* 100))`
                -SecondsRemaining $secsleft;
        }
    } while ($count -gt 0)
$sw.Stop();
$sw.Reset();
}
finally {
    $ffile.Close()
    $tofile.Close()
    }
}

$srcdir = "C:\Source;
$destdir = "C:\Dest";
[int]$filecount = 0;
$files = (Get-ChildItem $SrcDir | where-object {-not ($_.PSIsContainer)});
$files|foreach($_){
$filecount++
if ([system.io.file]::Exists($destdir+$_.name)){
                [system.io.file]::Delete($destdir+$_.name)}
                Copy-File -from $_.fullname -to ($destdir+$_.name)
};

Personally I use this code for small copies to a USB stick but I use robocopy in a powershell script for PC backups.

Community
  • 1
  • 1
Graham Gold
  • 2,435
  • 2
  • 25
  • 34
2

Here is native PowerShell GUI version of RoboCopy. (NO EXE file)

I hope it helps some one.

enter image description here

https://gallery.technet.microsoft.com/PowerShell-Robocopy-GUI-08c9cacb

FYI : Are there any one who can combine PowerCopy GUI tool with Copy-WithProgress bar?

Deniz Porsuk
  • 492
  • 1
  • 6
  • 20
2

Progress bars are nice and all but when copying hundreds of files, showing progress slows down the operation, in some cases quite a bit. It's one reason that the robocopy help says for the /MT flag to redirect output to log for better performance.

Nelis
  • 21
  • 1
2

This is the code-snippet I finally used for such task:

$fileName = 'test.txt'
$fromDir  = 'c:\'
$toDir    = 'd:\'

$title = $null
&robocopy "$fromDir" "$toDir" "$fileName" /z /mt /move /w:3 /r:10 /xo | %{
    $data = $_.Split("`t")
    if ($title -and $data[0] -match '\d+(?=%)') {
        Write-Progress $title -Status $data -PercentComplete $matches[0]
    }
    if($data[4]) {$title = $data[4]}
}
Write-Progress $title -complete
Carsten
  • 1,612
  • 14
  • 21
1

I ended up using this based on Amrinder's suggested answer:

robocopy.exe $Source $Destination $PatternArg $MirrorArg /NDL /NJH /NJS | ForEach-Object -Process {
    $data = $_.Split([char]9);
    if (($data.Count -gt 4) -and ("$($data[4])" -ne ""))
    {
        $file = "$($data[4])"
        Write-Progress "Percentage $($data[0])" -Activity "Robocopy" -CurrentOperation "$($file)" -ErrorAction SilentlyContinue; 
    }
    else
    {
        Write-Progress "Percentage $($data[0])" -Activity "Robocopy" -CurrentOperation "$($file)"
    }
}
# Robocopy has a bitmask set of exit codes, so only complain about failures:
[int] $exitCode = $global:LastExitCode;
[int] $someCopyErrors = $exitCode -band 8;
[int] $seriousError = $exitCode -band 16;
if (($someCopyErrors -ne 0) -or ($seriousError -ne 0))
{
    Write-Error "ERROR: robocopy failed with a non-successful exit code: $exitCode"
    exit 1
}

Fyi, Bill

Bill Tutt
  • 414
  • 4
  • 6
1

I created a simple solution just showing how many files and MBs are already copied, without a percentage. In our case, a percentage is not needed, users just want to see a friendly progress message.

In our case, Copy-WithProgress took a while just to create a logfile, before it actually starts copying thousands of tiny files.

This is how it looks during the copy-progress: robocopy with progress bar

And when completed, the default summary is shown: Robocopy

Here's the code:

$copiedFilesCount = 0
$copiedBytes = 0

robocopy $source $destination /ndl /bytes | ForEach-Object {
    $data = $_.Split([char]9)

    if ($data.Length -ge 4) { # check if message is from a file copy-process
        $copiedBytes += $data[3]
        $copiedFilesCount++
        
        Write-Progress -Activity "Robocopy" -Status "Copied $($copiedFilesCount.ToString("N0")) files ($(($copiedBytes / 1MB).ToString("N2")) MB)" -CurrentOperation $data[4]
    }
    elseif ($data -notmatch "\d+%") {
        Write-Output $_
    }
}

Write-Progress -Activity "Robocopy" -Completed