forked from JohnCardenas/FileSyncScript
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathFileSync.ps1
343 lines (278 loc) · 10.9 KB
/
FileSync.ps1
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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
##################################################################################################################
# FileSync.ps1
##################################################################################################################
# Script used to perform a file sync operation.
#
# !!!WARNING!!!
# Before deploying this script, you should sign it with an Authenticode signature.
# This will prevent tampering and allow you to use the more secure RemoteSigned execution policy on clients.
#
# You can sign this script with the following PowerShell commands. Please note that you need to have a code
# signing cert from your enterprise CA installed in your Personal cert store.
#
# $cert = (dir cert:currentuser\my\ -CodeSigningCert)
# Set-AuthenticodeSignature $script $cert -TimeStampServer "http://timestamp.verisign.com/scripts/timstamp.dll"
##################################################################################################################
# For more information on this script, refer to the GitHub repository at
# https://github.com/JohnCardenas/FileSyncScript
##################################################################################################################
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true,Position=1)] [string] $configFilePath
)
# XML result document that gets saved to a server
[xml]$jobResultsXml = New-Object System.Xml.XmlDocument
$jobResultsXml.LoadXml("<?xml version=`"1.0`" encoding=`"utf-8`"?><jobs></jobs>")
# Associative array of job results
$jobResultsArray = @{}
# Detect if this instance is interactive or not
$interactive = !((gwmi win32_process -Filter "ProcessID=$PID" | ? { $_.ProcessName -eq "powershell.exe" }).commandline -match "-NonInteractive")
# This function pauses script execution until a key is pressed
Function Pause
{
Param (
[Parameter(Mandatory=$true)] [string] $prompt
)
if ($interactive)
{
Write-Host -NoNewline $prompt
[void][System.Console]::ReadKey($TRUE)
Write-Host ""
}
}
# Reads the robocopy log and parses the last relevant line for progress information
Function Parse-RoboCopyLog
{
Param (
[Parameter(Mandatory=$true)] [string] $logFile
)
try
{
$logLine = (Get-Content $logFile -ErrorAction SilentlyContinue | Select-Object -last 1)
}
catch
{
return
}
# Directory scan match
if ($logLine -Match '[\s]*([\d]+)[\s]*\\\\([\w\W]+)')
{
$script:lastActivity = "Scanning directory..."
$script:lastStatus = "\\" + $matches[2]
Write-Progress -Id 2 -ParentId 1 -Activity $script:lastActivity -Status $script:lastStatus
return
}
# New file match
if (($logLine -Match 'New File[\s]+([\d.\w ]+)[\s]+([\w\W]+)') -or
($logLine -Match 'Newer[\s]+([\d.\w ]+)[\s]+([\w\W]+)'))
{
$script:lastActivity = "Copying file..."
$script:lastStatus = $matches[2]
Write-Progress -Id 2 -ParentId 1 -Activity $script:lastActivity -Status $script:lastStatus -PercentComplete 0
return
}
# Percent completed match
if ($logLine -Match '([\d]{1,3})%')
{
if ($matches.Count -gt 1)
{
[int] $progress = [int] $matches[1]
Write-Progress -Id 2 -ParentId 1 -Activity $script:lastActivity -Status $script:lastStatus -PercentComplete $progress
}
return
}
}
# This function executes a robocopy job and saves the job result as XML
Function Execute-Job
{
Param (
[Parameter(Mandatory=$true)] [string] $jobName,
[Parameter(Mandatory=$true)] [string] $jobArguments
)
$stopwatch = [Diagnostics.Stopwatch]::StartNew()
$processInfo = New-Object System.Diagnostics.ProcessStartInfo
$processInfo.FileName = "robocopy.exe"
$processInfo.RedirectStandardError = $true
$processInfo.RedirectStandardOutput = $true
$processInfo.UseShellExecute = $false
$processInfo.Arguments = "$jobArguments /log:`"$logFolder\$jobName.txt`""
$process = New-Object System.Diagnostics.Process
$process.StartInfo = $processInfo
$process.Start() | Out-Null
do
{
if ($interactive)
{
Parse-RoboCopyLog "$logFolder\$jobName.txt"
}
Start-Sleep -m 1
}
while (Get-Process -Id $process.Id -ErrorAction SilentlyContinue | select -Property Responding)
Write-Progress -Id 2 -ParentId 1 -Activity "Done" -Status "Done" -Completed
$stopwatch.Stop()
$elapsed = $stopwatch.Elapsed
$return = Parse-RoboCopyReturnCode $process.ExitCode
# Add the job result to the XML log file
Add-JobResultXml $jobName $return $jobArguments $elapsed
# Add the job result to the array of completed jobs
$jobResultsArray[$jobName] = $return
}
# Adds a new row to the XML log file
Function Add-JobResultXml
{
Param (
[Parameter(Mandatory=$true)] [string] $jobName,
[Parameter(Mandatory=$true)] [string] $jobReturn,
[Parameter(Mandatory=$true)] [string] $jobArguments,
[Parameter(Mandatory=$true)] [string] $jobTime
)
$resultXml = $jobResultsXml.CreateElement("result")
$resultXmlName = $jobResultsXml.CreateAttribute("name")
$resultXmlName.Value = $jobName
$resultXmlCmd = $jobResultsXml.CreateAttribute("cmd")
$resultXmlCmd.Value = $jobArguments
$resultXmlTime = $jobResultsXml.CreateAttribute("time")
$resultXmlTime.Value = $jobTime
$resultXmlText = $jobResultsXml.CreateTextNode($jobReturn)
$resultXml.Attributes.Append($resultXmlCmd) | Out-Null # pipe to Out-Null to suppress messages to the console
$resultXml.Attributes.Append($resultXmlTime) | Out-Null
$resultXml.Attributes.Append($resultXmlName) | Out-Null
$resultXml.AppendChild($resultXmlText) | Out-Null
$jobResultsXml.LastChild.AppendChild($resultXml) | Out-Null
}
# Saves the job output to an XML file on a remote server
Function Save-JobResultXml
{
Param (
[Parameter(Mandatory=$true)] [string] $remoteLogFolder,
[Parameter(Mandatory=$true)] [string] $scriptRunTime,
[Parameter(Mandatory=$true)] [string] $scriptStartTime,
[Parameter(Mandatory=$true)] [string] $scriptFinishTime
)
$totalRunXml = $jobResultsXml.CreateAttribute("duration")
$totalRunXml.Value = $scriptRunTime
$jobResultsXml.SelectSingleNode("/jobs").Attributes.Append($totalRunXml) | Out-Null
$startedXml = $jobResultsXml.CreateAttribute("started")
$startedXml.Value = $scriptStartTime
$jobResultsXml.SelectSingleNode("/jobs").Attributes.Append($startedXml) | Out-Null
$finishedXml = $jobResultsXml.CreateAttribute("finished")
$finishedXml.Value = $scriptFinishTime
$jobResultsXml.SelectSingleNode("/jobs").Attributes.Append($finishedXml) | Out-Null
# Write output to remote location
$hostName = $env:COMPUTERNAME
$jobResultsXml.Save("$remoteLogFolder\$hostName.xml")
}
# Parses the RoboCopy return code to determine status. Returns a string message.
# RoboCopy returns its status as a bit flag
Function Parse-RoboCopyReturnCode
{
Param (
[Parameter(Mandatory=$true)] [int] $returnCode
)
if ($returnCode -eq 0)
{
return "OK (NoChange)"
}
$retStr = ""
if ($returnCode -band 1)
{
$retStr += "OK (CopyChanges) "
}
if ($returnCode -band 2)
{
$retStr += "Xtra "
}
if ($returnCode -band 4)
{
$retStr += "MISMATCHES "
}
if ($returnCode -band 8)
{
$retStr += "FAIL "
}
if ($returnCode -band 16)
{
$retStr += "FATALERROR "
}
return $retStr
}
# Starts running sync jobs specified in the $configFileLocation parameter
Function Start-SyncJobs
{
Param (
[Parameter(Mandatory=$true)] [string] $configFileLocation
)
$config = [xml](Get-Content $configFileLocation)
$rootNode = $config.SelectSingleNode("/syncConfig")
$logFolder = $config.SelectSingleNode("/syncConfig").getAttribute("localLogFolder")
$retries = "/r:" + $rootNode.getAttribute("retries")
$retryWaitTime = "/w:" + $rootNode.getAttribute("retryWaitTime")
$global:syncBatchName = $rootNode.getAttribute("syncBatchName")
# Create the path to the sync log folder and hide the log folder
$logFolderObj = New-Item -ItemType Directory -Force -Path $logFolder
Set-ItemProperty $logFolderObj -Name Attributes -Value "Hidden"
# Get all the fileSync jobs
$syncJobs = $config.SelectNodes("/syncConfig/fileSync")
$scriptStarted = (Get-Date -Format "G")
$scriptTimer = [Diagnostics.Stopwatch]::StartNew()
if ($interactive)
{
Clear-Host
Write-Host "Beginning file sync jobs. This might take a while, so please be patient."
}
$currentJobIndex = 0
# Run each job!
foreach ($job in $syncJobs)
{
# Job parameters
$jobName = $job.getAttribute("jobName")
$remoteRoot = $job.getAttribute("remoteRoot")
$localRoot = $job.getAttribute("localRoot")
$folderMode = ""
$exclusions = ""
# Detect if we want to include empty folders (/e) or exclude them (/s)
if ($job.getAttribute("includeEmptyFolders") -eq "true")
{
$folderMode = "/e"
}
else
{
$folderMode = "/s"
}
# Check for exclusion nodes
if ($job.HasChildNodes)
{
$exclusions = "/xd "
foreach ($exclusion in $job.ChildNodes)
{
$exclusions += $exclusion.InnerXML + " "
}
}
# Build the argument list and kick it off
$arguments = "`"$remoteRoot`" `"$localRoot`" /purge $folderMode $exclusions $retries $retryWaitTime"
if ($interactive)
{
Write-Progress -Id 1 -Activity ("Synchronizing " + $global:syncBatchName + "...") -Status "Executing job `"$jobName`"" -PercentComplete ((++$currentJobIndex / $syncJobs.Count) * 100)
$script:lastActivity = "Initializing..."
$script:lastStatus = "Starting up"
}
Execute-Job $jobName $arguments
}
Write-Progress -Id 1 -Activity "Done" -Status "Done" -Complete
# Performance timing
$scriptTimer.Stop()
$scriptElapsed = $scriptTimer.Elapsed
if ($interactive)
{
Write-Host ""
Write-Host "Finished file sync jobs ($scriptElapsed)"
Write-Host ""
Write-Host "Job Summary:"
$jobResultsArray.GetEnumerator() | Sort-Object Name | Format-Table -AutoSize # Print job results as a formatted table
Pause "Press any key to exit."
}
# Save the XML report to a remote server location
Save-JobResultXml $config.SelectSingleNode("/syncConfig").getAttribute("remoteLogFolder") $scriptElapsed $scriptStarted (Get-Date -Format "G")
}
# Begin!
Start-SyncJobs $configFilePath