RoboCopy class

‘Sup PSHomies,

It all started a year ago… Always wanting to learn anything PowerShell related, classes caught my eye ever since it was introduced in v5.  I wanted to try my hand at classes with a real life application… So I got on twitter for some tips…

powershell-class-tweet-2
powershell-class-tweet-3

Doug was kind enough to reach out and point me in the right direction, for which I owe him a great debt! Appreciate it Doug!!!

Like I said, I wanted to try my hand at classes with a real life application… If you’ve read my blogs then you’ll know that I’m a fan of robocopy, seriously, huge fan! . Did I mention how awesome robocopy is? 😛 I think I found my real life application 😉

When I started out with my Robocopy class, it was just about logging initially, but it could be so much more! Classes are native to v5. Now that v5 is mainstream I decided to finish the class. Richard Siddaway’s article  was just the spark I needed to get me going (again)!

Here’s what the Robocopy class looks like:

robocopy-class

Here a quick rundown on the properties:

The source/destination properties of the class are self explanatory (if you’re familiar with robocopy). The property logdir and JobID will be used to define a valid logfile name (with extension .log). Robocopy has quite a bit of options. I wanted to keep it as generic as possible. The property $this.Options is still a work in progress. The property $this.WildCards  is where you’ll define what will be filtered.  I’ll get back to rcCMDs and rcExitCodes later on…

These are the methods I came up with (so far, still a work in progress)

  • Mirror(). Mirrors $this.Source to $this.Destination with some default options
  • Move(). Moves this.Source to $this.Destination with some default options
  • RollBack(). Rollback $this.Destination to $this.Source with some default options
  • Sync(). Sync will synchronize the delta’s from $this.Source to $this.Destination using any additional $this.Options defined (at least that’s the idea). I’ve added a few options by default, mostly to exclude files and folders, think recycle.bin “System Volume Information” and the likes.
  • VerifyPaths(). This let’s you know if the $this.Source, $this.Destination and $this.LogDir are valid.
  • GetlistSource(). This will list the content of the $this.Source
  • GetListDestionation(). This will list the content of $this.Destination
  • GetLogSummary. This will return a summary of the log file (Hehe). The method is static so that you don’t have to instantiate the class in order to use it. (Thanks again Doug!)

The two methods: StartRCProcess and ExecuteRCCMD are actually helper methods. I just haven’t figured out how that works in classes. Ideally I’d like to have them hidden or as a function if that even makes sense. So here’s where they come in. At first I just executed robocopy with the necessary arguments. If you’re not interested in the exitcode then using ExecuteRCCMD is what you need. I wrote a blog about enumerating RoboCopy Exitcodes. Using $LastExitCode isn’t going to cut it if you decide to run robocopy jobs parallel. That’s where StartRCProcess comes in.Using Start-Process comes with an overhead of say 20 MB, which could add up in the long run. You do need to wait until the process has finished to retrieve the exitcode. If you really need the exitcode then StartRCProcess is what you need. The property $this.rcExitCodes will only be populated if StartRCProcess is used. Both will populate the $this.rcCMDs property.

Ok I think I’ve covered the basics, time to show some code! 😉

[Flags()] Enum RoboCopyExitCodes{
NoChange = 0
OKCopy = 1
ExtraFiles = 2
MismatchedFilesFolders = 4
FailedCopyAttempts = 8
FatalError = 16
}
Class RoboCopy{
[String]$Source
[String]$Destination
[String]$LogDir
[String]$JobID
[String]$Options
[String[]]$WildCards
[PSObject[]]$rcCMDs
[PSObject[]]$rcExitCodes
#default constructor
RoboCopy(){}
#constructor
RoboCopy([String]$src,[String]$des,[String]$jid,[String]$ld,[string[]]$wc){
$this.Source = $src
$this.Destination = $des
$this.JobID = $jid
$this.LogDir = $ld
$this.WildCards = $wc
}
[PSCustomObject]VerifyPaths(){
return [PSCustomObject]@{
Source = $(Test-Path -Path $this.Source)
Destination = $(Test-Path -Path $this.Destination)
LogDirectory = $(Test-Path -Path $this.LogDir)
}
}
StartRCProcess($source,$target,$params,$logFile,$tag){
#Save RoboCopy Command
$MessageData = $('robocopy "{0}" "{1}" {2} {3}' -f $source,$target,$($params -join ' '),$logFile)
$this.rcCMDs += Write-Information -MessageData $MessageData 6>&1 -Tags $tag | Select-Object *
#Execute Process
$rcArgs = "`"$($source)`" `"$($target)`" $params $logFile"
$rcResult = Start-Process robocopy -ArgumentList $rcArgs -WindowStyle Hidden -PassThru -Wait
#Save ExitCode
$this.rcExitCodes += [PSCustomObject]@{
ExitCode = [RoboCopyExitCodes]$rcResult.ExitCode
StartTime = $rcResult.StartTime
ExitTime = $rcResult.ExitTime
rcCMD = $MessageData
}
}
ExecuteRCCMD($source,$target,$params,$logFile,$tag){
#Save RoboCopy Command
$MessageData = $('robocopy "{0}" "{1}" {2} {3}' -f $source,$target,$($params -join ' '),$logFile)
$this.rcCMDs += Write-Information -MessageData $MessageData 6>&1 -Tags $tag | Select-Object *
#Execute Robocopy CMD
$rcArgs = @("$($source)","$($target)",$params,$logFile)
robocopy @rcArgs
}
Mirror(){
$tag = @('Mirror',$this.JobID)
$params = New-Object System.Collections.Arraylist
$params.AddRange(@('/MIR','/E','/BYTES','/NP','/NDL','/R:1','/W:1'))
$logFile = '/LOG:{0}\mirror-{1}.log' -f $this.LogDir,$this.JobID
$this.StartRCProcess($this.Source,$this.Destination,$params,$logFile,$tag)
#$this.ExecuteRCCMD($this.Source,$this.Destination,$params,$logFile,$tag)
}
Move(){
$tag = @('Move',$this.JobID)
$params = New-Object System.Collections.Arraylist
$params.AddRange(@('/COPY:DATS','/DCOPY:T','/SECFIX','/BYTES','/S','/E','/NP','/NDL','/R:1','/W:1','/MOVE'))
$logFile = '/LOG:{0}\move-{1}.log' -f $this.LogDir,$this.JobID
#$this.StartRCProcess($this.Source,$this.Destination,$params,$logFile,$tag)
$this.ExecuteRCCMD($this.Source,$this.Destination,$params,$logFile,$tag)
}
RollBack(){
$tag = @('RollBack',$this.JobID)
$params = New-Object System.Collections.Arraylist
$params.AddRange(@('/COPY:DATS','/DCOPY:T','/SECFIX','/BYTES','/S','/E','/NP','/NDL','/R:1','/W:1','/MOVE'))
$logFile = '/LOG:{0}\rollback-{1}.log' -f $this.LogDir,$this.JobID
#$this.StartRCProcess($$this.Source,$this.Destination,$params,$logFile,$tag)
$this.ExecuteRCCMD($this.Destination,$this.Source,$params,$logFile,$tag)
}
Sync(){
$tag = @('Sync',$this.JobID)
#Excluded Files & Directories
$XF = @(
'/XF'
'thumbs.db'
'~*.*'
'*.pst'
'desktop.ini'
'*.lnk'
)
$XD = @(
'/XD'
"`$Recycle.bin"
'Boot Recycler'
'IECompatCache'
'IEDownloadHistory'
'Cookies'
'WINDOWS'
'PrivacIE'
"`"System Volume Information`""
)
$Excluded = "$($XF -join ' ') $($XD -join ' ')"
$params = New-Object System.Collections.Arraylist
$params.AddRange(@($($this.WildCards),'/COPY:DATS','/SECFIX','/BYTES','/S','/PURGE','/E','/NP','/NDL','/R:1','/W:1'))
$logFile = '/LOG:{0}\sync-{1}.log' -f $this.LogDir,$this.JobID
#Save RoboCopy Command
$arrMessageDataInput = @(
$this.Source
$this.Destination
$($params -join ' ')
$($this.Options -join ' ')
$Excluded
$logFile
)
$MessageData = $('robocopy "{0}" "{1}" {2} {3} {4} {5}' -f $arrMessageDataInput )
$this.rcCMDs += Write-Information -MessageData $MessageData 6>&1 -Tags $tag | Select-Object *
#Execute Robocopy CMD
$rcArgs = @("$($this.Source)","$($this.Destination)",$params,$XF,$XD,$logFile)
robocopy @rcArgs
}
GetListSource(){
$tag = @('ListSRC',$this.JobID)
$params = New-Object System.Collections.Arraylist
$params.AddRange(@($($this.WildCards),'/L','/S','/E', '/BYTES','/FP','/NC','/NDL','/TS','/R:0','/W:0'))
$logFile = '/LOG:{0}\listSRC-{1}.log' -f $this.LogDir,$this.JobID
$this.StartRCProcess($this.Source,'NULL',$params,$logFile,$tag)
#$this.ExecuteRCCMD($this.Source,$this.Destination,$params,$logFile,$tag)
}
GetListDestination(){
$tag = 'ListDES'
$params = New-Object System.Collections.Arraylist
$params.AddRange(@($($this.WildCards),'/L','/S','/E','/BYTES','/FP','/NC','/NDL','/TS','/R:0','/W:0'))
$logFile = '/LOG:{0}\listDES-{1}.log' -f $this.LogDir,$this.JobID
#$this.StartRCProcess($this.Source,$this.Destination,$params,$logFile,$tag)
$this.ExecuteRCCMD($this.Destination,'NULL',$params,$logFile,$tag)
}
static [PSCustomObject]GetLogSummary([String]$rcLogFile){
filter Get-CapacitySize {
'{0:N2} {1}' -f $(
if ($_ -lt 1kb) { $_, 'Bytes' }
elseif ($_ -lt 1mb) { ($_/1kb), 'KB' }
elseif ($_ -lt 1gb) { ($_/1mb), 'MB' }
elseif ($_ -lt 1tb) { ($_/1gb), 'GB' }
elseif ($_ -lt 1pb) { ($_/1tb), 'TB' }
else { ($_/1pb), 'PB' }
)
}
$rcLogSummary = [PSCustomObject]@{
Start = $null
End = $null
LogFile = $null
Source = $null
Destination = $null
TotalDirs = $null
CopiedDirs = $null
FailedDirs = $null
TotalFiles = $null
CopiedFiles = $null
FailedFiles = $null
TotalBytes = $null
CopiedBytes = $null
FailedBytes = $null
TotalTimes = $null
Speed = $null
}
$rcLogSummary.LogFile = $rcLogFile.Split('\')[-1].ToLower()
$logFileContent = Get-Content $rcLogFile -Raw
[regex]$regex_Start = 'Started\s:\s+(?<StartTime>.+[^\n\r])'
if ($logFileContent -match $regex_Start){
$rcLogSummary.Start = $Matches['StartTime']
}
[regex]$regex_End = 'Ended\s:\s+(?<EndTime>.+[^\n\r])'
if ($logFileContent -match $regex_End){
$rcLogSummary.End = $Matches['EndTime']
}
[regex]$regex_Source = 'Source\s:\s+(?<Source>.+[^\n\r])'
if($logFileContent -match $regex_Source){
$rcLogSummary.Source = $Matches['Source'].Tolower()
}
[regex]$regex_Target = 'Dest\s:\s+(?<Target>.+[^\n\r])'
if($logFileContent -match $regex_Target){
$rcLogSummary.Destination = $Matches['Target'].ToLower()
}
[regex]$regex_Dirs = 'Dirs\s:\s+(?<TotalDirs>\d+)\s+(?<CopiedDirs>\d+)(?:\s+\d+){2}\s+(?<FailedDirs>\d+)\s+\d+'
if ($logFileContent -match $regex_Dirs){
$rcLogSummary.TotalDirs = [int]$Matches['TotalDirs']
$rcLogSummary.CopiedDirs = [int]$Matches['CopiedDirs']
$rcLogSummary.FailedDirs = [int]$Matches['FailedDirs']
}
[regex]$regex_Files = 'Files\s:\s+(?<TotalFiles>\d+)\s+(?<CopiedFiles>\d+)(?:\s+\d+){2}\s+(?<FailedFiles>\d+)\s+\d+'
if ($logFileContent -match $regex_Files){
$rcLogSummary.TotalFiles = [int]$Matches['TotalFiles']
$rcLogSummary.CopiedFiles = [int]$Matches['CopiedFiles']
$rcLogSummary.FailedFiles = [int]$Matches['FailedFiles']
}
[regex]$regex_Speed = 'Speed\s:\s+(?<Speed>.+\/min)'
if ($logFileContent -match $regex_Speed){
$rcLogSummary.Speed = $Matches['Speed']
}
$arrBytes = @(
'Bytes\s:\s+(?<TotalBytes>(\d+\.\d+\s)[bmg]|\d+)\s+' #TotalBytes
'(?<CopiedBytes>\d+.\d+\s[bmg]|\d+)\s+' #CopiedBytes
'(?:(\d+.\d+\s[bmg]|\d+)\s+){2}' #Skip two
'(?<FailedBytes>\d+.\d+\s[bmg]|\d+)' #FailedBytes
)
[regex]$regex_Bytes = -join $arrBytes
if ($logFileContent -match $regex_Bytes){
$rcLogSummary.TotalBytes = [int64]$Matches['TotalBytes'] | Get-CapacitySize
$rcLogSummary.CopiedBytes = [int64]$Matches['CopiedBytes'] | Get-CapacitySize
$rcLogSummary.FailedBytes = [int64]$Matches['FailedBytes'] | Get-CapacitySize
}
[regex]$regex_Times = 'Times\s:\s+(?<TotalTimes>\d+:\d+:\d+)'
if ($logFileContent -match $regex_Times){
$rcLogSummary.TotalTimes = $Matches['TotalTimes']
}
return $rcLogSummary
}
[String]GetRoboCopyCMD(){
$paramRoboCopy = @(
$this.Source
$this.Destination
$($this.WildCards -join ' ')
$this.Options
$('/LOG:{0}\{1}.log' -f $this.LogDir,$this.JobID)
)
return $('robocopy "{0}" "{1}" {2} {3} {4}' -f $paramRoboCopy)
}
}

Here what’s happening in the List methods:

methods-lists

GetListSource() is using $this.StartRCProcess to generate a list of $this.Source using some default option. While writing I noticed that I forgot to add the wildcards to the parameter. All I had to do was add it!. I added it at the beginning so it lines up accordingly… Robocopy is fickle like that…  GetListDestination does the same only it uses ExecuteRCCMD instead.

Here’s what’s going on in StartRCProcess and ExcuteRCCMD

startexecuterc

Both StartRCProcess and ExcuteRCCMD will save the robocopy command using Write-Information. I’m loving Write-Information more and more! StartRCProcess saves the exitcode with some extra information. Here’s where the robocopy exitcode enumeration came in handy! ExecuteRCCMD will run robocopy with the specified arguments. Truth be told I’m more partial to the ExecuteRCCMD method. I added the StartRCProcess more for demo purposes and finally getting to use my Robocopy exitcode enumeration!

For Mirror(),Move() and RollBack(), I omitted the Wildcards. These methods all or nothing in my opinion. If omitted, . will be the default.

Sync() had me going for a while. I still have some issues with Options. For now Sync() uses some default switches. Like I said work in progress…

Quite a bit of code, so does it work? Here’s some code to play with. be sure to edit the source,destination and logdir to your liking. Just remember that robocopy is unforgiving so make sure not to use it of production folders!

#region Main
$rc = [RoboCopy]::New('C:\scripts\move','C:\temp\move','rc-0001','c:\scripts\log',@('*.*'))

#Run RoboCopy methods
$rc.Sync()
$rc.GetListSource()
$rc.GetListDestination()

#Get RoboCopy LogFile Summary
[RoboCopy]::GetLogSummary("$env:HOMEDRIVE\scripts\log\listSRC-rc-0001.log")
[RoboCopy]::GetLogSummary("$env:HOMEDRIVE\scripts\log\listDES-rc-0001.log")
[RoboCopy]::GetLogSummary("$env:HOMEDRIVE\scripts\log\sync-rc-0001.log")

#Get RoboCopy executed CMDs
$rc.rcCMDs
$rc.rcExitCodes
#endregion

First I instantiate the class with arguments. I then run the methods Sync(),GetListSource() and GetListDestination(). Up next is retrieve the LogSummaries from the methods. Here’s a screenshot of the Sync LogSummary

synclogfile

I did a select of $rc.rcCMDs to give you an idea what is being stored

rc-rccmds

Only want ListDES?

$rc.rcCMDs |
Where-Object{$_.Tags -contains 'ListDes'} |
Select-Object -Property Time*,Tag*,Mess*
rc-rccmdswhereobject

The information stream is quite handy! The tags will definitely come in handy when you need to filter on action verb or job ID.

The methods GetListSource() & Mirror() both make use of StartRCProcess(), so let’s see what $rc.rcExitcode has shall we?

rcexitcodes

Nice!

This is by far my longest blog, if you made this far then… Congratulations! There’s still so much to discover when it comes to classes.

Classes definitely  makes your code look and feel more like a developer 😉 . I feel more comfortable giving a colleague this class than a bunch of scripts. In Richard’s article he’s using both classes and modules. There are sure to be some gotcha’s… Do you go all in with classes or only partial?

I’m hoping that the community can shed some light on the subject. I’d love to hear from you guys on how to improve on this… Let’s make this year, a year of PowerShell Classes! 😛

Hope it’s worth something to you…

Ttyl,

Urv

1 thought on “RoboCopy class

  1. Pingback: PSConfAsia 2017 | pshirwin

Leave a comment