278 lines
14 KiB
PowerShell
278 lines
14 KiB
PowerShell
param(
|
|
[string]$SourcePath,
|
|
[string]$TargetPath
|
|
)
|
|
|
|
Add-Type -AssemblyName System.Windows.Forms
|
|
Add-Type -AssemblyName System.Drawing
|
|
|
|
function Show-GUI {
|
|
$form = New-Object System.Windows.Forms.Form
|
|
$form.Text = "Folder Mirror Tool"
|
|
$form.Size = New-Object System.Drawing.Size(600,450)
|
|
$form.StartPosition = "CenterScreen"
|
|
$form.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedDialog
|
|
|
|
# Source Controls
|
|
$lblSource = New-Object Windows.Forms.Label
|
|
$lblSource.Location = New-Object Drawing.Point(10,10)
|
|
$lblSource.Text = "Source Path:"
|
|
$txtSource = New-Object Windows.Forms.TextBox
|
|
$txtSource.Location = New-Object Drawing.Point(120,10)
|
|
$txtSource.Size = New-Object Drawing.Size(350,20)
|
|
$btnSource = New-Object Windows.Forms.Button
|
|
$btnSource.Location = New-Object Drawing.Point(480,10)
|
|
$btnSource.Text = "Browse"
|
|
$btnSource.Add_Click({
|
|
$folder = New-Object Windows.Forms.FolderBrowserDialog
|
|
if($folder.ShowDialog() -eq "OK") { $txtSource.Text = $folder.SelectedPath }
|
|
})
|
|
|
|
# Target Controls
|
|
$lblTarget = New-Object Windows.Forms.Label
|
|
$lblTarget.Location = New-Object Drawing.Point(10,40)
|
|
$lblTarget.Text = "Target Path:"
|
|
$txtTarget = New-Object Windows.Forms.TextBox
|
|
$txtTarget.Location = New-Object Drawing.Point(120,40)
|
|
$txtTarget.Size = New-Object Drawing.Size(350,20)
|
|
$btnTarget = New-Object Windows.Forms.Button
|
|
$btnTarget.Location = New-Object Drawing.Point(480,40)
|
|
$btnTarget.Text = "Browse"
|
|
$btnTarget.Add_Click({
|
|
$folder = New-Object Windows.Forms.FolderBrowserDialog
|
|
if($folder.ShowDialog() -eq "OK") { $txtTarget.Text = $folder.SelectedPath }
|
|
})
|
|
|
|
# Log Box
|
|
$script:txtLogGui = New-Object Windows.Forms.RichTextBox
|
|
$script:txtLogGui.Location = New-Object Drawing.Point(10,70)
|
|
$script:txtLogGui.Size = New-Object Drawing.Size(560,300)
|
|
$script:txtLogGui.ReadOnly = $true
|
|
$script:txtLogGui.Font = New-Object System.Drawing.Font("Consolas", 8)
|
|
$script:txtLogGui.WordWrap = $false
|
|
$script:txtLogGui.ScrollBars = [System.Windows.Forms.RichTextBoxScrollBars]::Both
|
|
|
|
# Start Button
|
|
$script:btnStartGui = New-Object Windows.Forms.Button
|
|
$script:btnStartGui.Location = New-Object Drawing.Point(480,380)
|
|
$script:btnStartGui.Size = New-Object Drawing.Size(90,30)
|
|
$script:btnStartGui.Text = "Start Mirror"
|
|
$script:btnStartGui.Add_Click({
|
|
$currentSourcePath = $txtSource.Text
|
|
$currentTargetPath = $txtTarget.Text
|
|
|
|
try {
|
|
if (-not (Test-Path $currentSourcePath -PathType Container)) { throw "Source path invalid or not a folder." }
|
|
if (-not (Test-Path $currentTargetPath -PathType Container)) {
|
|
try {
|
|
$script:txtLogGui.AppendText("Target path does not exist. Attempting to create: $currentTargetPath`n")
|
|
$null = New-Item -ItemType Directory -Path $currentTargetPath -Force -ErrorAction Stop
|
|
$script:txtLogGui.AppendText("Created target directory: $currentTargetPath`n")
|
|
} catch {
|
|
throw "Target path invalid or not a folder, and could not be created: $($_.Exception.Message)"
|
|
}
|
|
}
|
|
|
|
$script:txtLogGui.Clear()
|
|
$script:txtLogGui.AppendText("Starting mirror operation...`n")
|
|
$script:txtLogGui.AppendText("Source: $currentSourcePath`n")
|
|
$script:txtLogGui.AppendText("Target: $currentTargetPath`n")
|
|
|
|
$global:logFile = "$PSScriptRoot\MirrorLog_$(Get-Date -Format 'yyyyMMdd_HHmmss').log"
|
|
$script:txtLogGui.AppendText("Detailed log will also be saved to: $global:logFile`n`n")
|
|
|
|
# Robocopy parameters for GUI (Verbose, but no per-file % progress to keep GUI clean)
|
|
$robocopyArgs = @(
|
|
"`"$currentSourcePath`"",
|
|
"`"$currentTargetPath`"",
|
|
"/MIR", # Mirror mode
|
|
"/V", # Verbose output (shows skipped files)
|
|
"/FP", # Include Full Pathname of files in output
|
|
"/BYTES", # Print sizes as bytes
|
|
"/NP", # No Progress percentage (to keep GUI log cleaner)
|
|
# Removed /NDL (No Directory List) -> will show directory list
|
|
# Removed /NJH (No Job Header) -> will show job header
|
|
# Removed /NJS (No Job Summary) -> will show job summary
|
|
"/LOG+:`"$global:logFile`"",
|
|
"/R:5", # 5 retries on failed files
|
|
"/W:5" # 5 second wait between retries
|
|
)
|
|
|
|
$script:btnStartGui.Enabled = $false
|
|
|
|
$process = New-Object System.Diagnostics.Process
|
|
$process.StartInfo.FileName = "robocopy.exe"
|
|
$process.StartInfo.Arguments = $robocopyArgs -join " "
|
|
$process.StartInfo.UseShellExecute = $false
|
|
$process.StartInfo.RedirectStandardOutput = $true
|
|
$process.StartInfo.RedirectStandardError = $true
|
|
$process.StartInfo.CreateNoWindow = $true
|
|
$process.EnableRaisingEvents = $true
|
|
|
|
$process.add_OutputDataReceived({
|
|
if (-not [string]::IsNullOrEmpty($_.Data)) {
|
|
$script:txtLogGui.BeginInvoke([Action[string]]{
|
|
param($line)
|
|
$script:txtLogGui.AppendText("$line`r`n")
|
|
$script:txtLogGui.ScrollToCaret()
|
|
}, $_.Data)
|
|
}
|
|
})
|
|
|
|
$process.add_ErrorDataReceived({
|
|
if (-not [string]::IsNullOrEmpty($_.Data)) {
|
|
$script:txtLogGui.BeginInvoke([Action[string]]{
|
|
param($line)
|
|
# Robocopy often uses stderr for summary or non-critical info too,
|
|
# so not always coloring it as a hard error.
|
|
# Let Robocopy's output speak for itself mostly.
|
|
$script:txtLogGui.AppendText("[STDERR] $line`r`n")
|
|
$script:txtLogGui.ScrollToCaret()
|
|
}, $_.Data)
|
|
}
|
|
})
|
|
|
|
$process.add_Exited({
|
|
$exitCode = $process.ExitCode
|
|
$script:txtLogGui.BeginInvoke([Action]{
|
|
$script:txtLogGui.AppendText("`n------------------------------------`n")
|
|
$script:txtLogGui.AppendText("Robocopy process finished.`n")
|
|
$script:txtLogGui.AppendText("Robocopy Exit Code: $exitCode`n")
|
|
|
|
if ($exitCode -ge 8) {
|
|
$script:txtLogGui.SelectionStart = $script:txtLogGui.TextLength
|
|
$script:txtLogGui.SelectionLength = 0
|
|
$script:txtLogGui.SelectionColor = [System.Drawing.Color]::Red
|
|
$script:txtLogGui.AppendText("Robocopy reported errors or critical failures (Code >= 8). Check log for details.`n")
|
|
$script:txtLogGui.SelectionColor = $script:txtLogGui.ForeColor
|
|
[Windows.Forms.MessageBox]::Show("Robocopy completed with errors or critical failures (Exit Code: $exitCode). Check Robocopy output and the log file for details.", "Robocopy Error", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error)
|
|
} elseif ($exitCode -eq 0) {
|
|
$script:txtLogGui.AppendText("Robocopy: No files were copied. No failure was encountered. No files were mismatched. The files already exist in the destination directory; therefore, the copy operation was skipped.`n")
|
|
} elseif ($exitCode -gt 0 -and $exitCode -lt 8) {
|
|
$script:txtLogGui.SelectionStart = $script:txtLogGui.TextLength
|
|
$script:txtLogGui.SelectionLength = 0
|
|
$script:txtLogGui.SelectionColor = [System.Drawing.Color]::DarkGreen
|
|
$script:txtLogGui.AppendText("Robocopy completed. Some files may have been copied, or extra/mismatched files detected (Code $exitCode). This is generally considered successful or informational. Review output for details.`n")
|
|
$script:txtLogGui.SelectionColor = $script:txtLogGui.ForeColor
|
|
}
|
|
$script:txtLogGui.AppendText("Log file: $global:logFile`n")
|
|
$script:txtLogGui.ScrollToCaret()
|
|
$script:btnStartGui.Enabled = $true
|
|
})
|
|
$process.Close()
|
|
$process.Dispose()
|
|
})
|
|
|
|
$process.Start() | Out-Null
|
|
$process.BeginOutputReadLine()
|
|
$process.BeginErrorReadLine()
|
|
}
|
|
catch {
|
|
$errorMessage = $_.Exception.Message
|
|
if ($_.Exception.InnerException) { $errorMessage += " Inner: " + $_.Exception.InnerException.Message }
|
|
$script:txtLogGui.AppendText("[SCRIPT ERROR] $errorMessage`n")
|
|
[Windows.Forms.MessageBox]::Show($errorMessage, "Script Error", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error)
|
|
if ($script:btnStartGui) {$script:btnStartGui.Enabled = $true}
|
|
}
|
|
})
|
|
|
|
$form.Controls.AddRange(@(
|
|
$lblSource, $txtSource, $btnSource,
|
|
$lblTarget, $txtTarget, $btnTarget,
|
|
$script:txtLogGui,
|
|
$script:btnStartGui
|
|
))
|
|
|
|
if ($SourcePath) { $txtSource.Text = $SourcePath }
|
|
if ($TargetPath) { $txtTarget.Text = $TargetPath }
|
|
|
|
$form.Add_Shown({
|
|
# If parameters were passed and GUI is shown, optionally auto-start
|
|
# For now, we require manual click even if params are pre-filled.
|
|
# if ($txtSource.Text -and $txtTarget.Text) { $script:btnStartGui.PerformClick() }
|
|
})
|
|
|
|
[void]$form.ShowDialog()
|
|
}
|
|
|
|
# --- Main script execution logic ---
|
|
|
|
if (-not $PSBoundParameters.ContainsKey('SourcePath') -or -not $PSBoundParameters.ContainsKey('TargetPath')) {
|
|
Show-GUI # Pass potentially existing parameters for pre-filling
|
|
}
|
|
else {
|
|
$cliErrorOccurred = $false
|
|
try {
|
|
if (-not (Test-Path $SourcePath -PathType Container)) { throw "Source path '$SourcePath' invalid or not a folder." }
|
|
if (-not (Test-Path $TargetPath -PathType Container)) {
|
|
Write-Host "Target path '$TargetPath' does not exist. Attempting to create..." -ForegroundColor Yellow
|
|
try {
|
|
$null = New-Item -ItemType Directory -Path $TargetPath -Force -ErrorAction Stop
|
|
Write-Host "Created target directory: $TargetPath" -ForegroundColor Green
|
|
} catch {
|
|
throw "Target path '$TargetPath' invalid or not a folder, and could not be created: $($_.Exception.Message)"
|
|
}
|
|
}
|
|
|
|
$logFile = "$PSScriptRoot\MirrorLog_$(Get-Date -Format 'yyyyMMdd_HHmmss').log"
|
|
Write-Host "[$(Get-Date)] Starting mirror operation..."
|
|
|
|
Write-Host "Source: $SourcePath"
|
|
Write-Host "Target: $TargetPath"
|
|
Write-Host "Log file: $logFile"
|
|
|
|
# Robocopy parameters for CLI (Fully verbose, including progress)
|
|
$robocopyArgs = @(
|
|
"`"$SourcePath`"",
|
|
"`"$TargetPath`"",
|
|
"/MIR", # Mirror mode
|
|
"/V", # Verbose output (shows skipped files)
|
|
"/FP", # Include Full Pathname of files in output
|
|
"/BYTES", # Print sizes as bytes
|
|
# Removed /NP -> will show progress percentage
|
|
# Removed /NDL -> will show directory list
|
|
# Removed /NJH -> will show job header
|
|
# Removed /NJS -> will show job summary
|
|
"/TEE", # Output to console AND log file
|
|
"/LOG+:`"$logFile`"",
|
|
"/R:5",
|
|
"/W:5"
|
|
)
|
|
|
|
Write-Host "Executing: robocopy $($robocopyArgs -join " ")"
|
|
Write-Host "------------------------------------ ROBOCOPY OUTPUT START ------------------------------------"
|
|
$process = Start-Process robocopy -ArgumentList $robocopyArgs -NoNewWindow -PassThru -Wait
|
|
Write-Host "------------------------------------ ROBOCOPY OUTPUT END --------------------------------------"
|
|
Write-Host "Robocopy Exit Code: $($process.ExitCode)"
|
|
|
|
|
|
if ($process.ExitCode -ge 8) {
|
|
throw "Robocopy reported errors or critical failures with exit code $($process.ExitCode). Check output above and log: $logFile"
|
|
} elseif ($process.ExitCode -eq 0) {
|
|
Write-Host "[$(Get-Date)] Robocopy: No files were copied. No failure was encountered. No files were mismatched. (Exit Code: 0)" -ForegroundColor Cyan
|
|
} else { # 1 to 7
|
|
Write-Host "[$(Get-Date)] Robocopy completed. Some files may have been copied, or extra/mismatched files detected (Exit Code: $($process.ExitCode)). This is generally considered successful or informational. Review output for details." -ForegroundColor Cyan
|
|
}
|
|
|
|
Write-Host "[$(Get-Date)] Mirror operation main phase completed." -ForegroundColor Green
|
|
if ($process.ExitTime -and $process.StartTime) {
|
|
Write-Host "Total Robocopy processing time: $($process.ExitTime - $process.StartTime)"
|
|
}
|
|
}
|
|
catch {
|
|
Write-Host "[SCRIPT ERROR] $($_.Exception.Message)" -ForegroundColor Red
|
|
if ($_.Exception.InnerException) { Write-Host "[INNER SCRIPT ERROR] $($_.Exception.InnerException.Message)" -ForegroundColor Red }
|
|
$cliErrorOccurred = $true
|
|
}
|
|
finally {
|
|
if ($cliErrorOccurred) {
|
|
Write-Host "Operation finished with errors." -ForegroundColor Red
|
|
} else {
|
|
Write-Host "Operation finished." -ForegroundColor Green
|
|
}
|
|
Read-Host "Press Enter to exit..."
|
|
if ($cliErrorOccurred) {
|
|
if ($Host.Name -eq "ConsoleHost") { exit 1 } # Exit with error code for automation
|
|
}
|
|
}
|
|
} |