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 } } }