<# .SYNOPSIS Backup specified folders from $Source to $Destination with GUI progress. .DESCRIPTION Reads a master list of folders (FilesToBackup.txt) and copies each folder from $Source to $Destination if space permits. Skips folders already listed in SuccessfuleBackup.txt. Uses a WinForms progress bar to display total-file progress, and prompts for a new destination via a Yes/No message box if the drive runs out of space. Logs successes and failures. .PARAMETER Source Base source directory containing the folders to back up. .PARAMETER Destination Initial backup destination directory. Can be changed during runtime if the drive fills up. .NOTES Author: Brian Hinds Date: 2025-06-23 Requires PowerShell 7.0 or later (optimized for 7.4). See comments for usage details and inline explanations. #> #region Configuration and Log Setup # Manually set source/destination at top of script: $Source = 'C:\' # e.g. 'C:\Users\Me\Documents' $Destination = 'D:\' # e.g. 'D:\Backups' # Ensure script directory is in path for log files (PowerShell provides $PSScriptRoot:contentReference[oaicite:6]{index=6}): $ScriptDir = $PSScriptRoot # Define log file paths in the script directory: $successLog = Join-Path $ScriptDir 'SuccessfuleBackup.txt' $failLog = Join-Path $ScriptDir 'FailToBackup.txt' $backupList = Join-Path $ScriptDir 'FilesToBackup.txt' # Session log with timestamp for this run: $timestamp = Get-Date -Format 'yyyy-MM-dd_HH-mm-ss' $sessionLog = Join-Path $ScriptDir ("BackupSession_$timestamp.log") # Ensure log files exist or create them: if (-not (Test-Path $successLog)) { New-Item -Path $successLog -ItemType File -Force | Out-Null } if (-not (Test-Path $failLog)) { New-Item -Path $failLog -ItemType File -Force | Out-Null } if (-not (Test-Path $backupList)) { New-Item -Path $backupList -ItemType File -Force | Out-Null } # Start a new session log: "`n===== Backup Session: $timestamp =====`n" | Out-File -FilePath $sessionLog -Encoding utf8 #endregion # Get all child items (folders) in the target directory Get-ChildItem -Path $Source -Directory | # Select only the Name property (folder name) Select-Object -ExpandProperty Name | # Sort the folder names alphabetically Sort-Object | # Save the sorted names to a text file Out-File -FilePath (Join-Path $ScriptDir 'FilesToBackup.txt') #region Read and Filter Folder List # Read master list of folders to back up (each line is a folder name under $Source): $foldersToBackup = Get-Content -Path (Join-Path $ScriptDir 'FilesToBackup.txt') # If success log exists, read backed-up folder names: $completed = @(Get-Content -Path $successLog) # array of completed folder names (if log is empty, result is empty array) # Filter out folders already backed up (compare two lists): $remainingFolders = $foldersToBackup | Where-Object { $_ -notin $completed } # Log this filtering for diagnostics: "Remaining folders to backup: $($remainingFolders -join ', ')" | Tee-Object -FilePath $sessionLog -Append #endregion #region Initialize GUI Progress Bar # Load WinForms assembly to create GUI elements: Add-Type -AssemblyName System.Windows.Forms Add-Type -AssemblyName System.Drawing # Create a form to host the progress bar and status label: $form = New-Object System.Windows.Forms.Form $form.Text = "Backup Progress" $form.Size = New-Object System.Drawing.Size(400,120) $form.FormBorderStyle = 'FixedDialog' $form.MaximizeBox = $false $form.MinimizeBox = $false $form.StartPosition = 'CenterScreen' # Add a label to show percentage/status: $label = New-Object System.Windows.Forms.Label $label.AutoSize = $true $label.Location = New-Object System.Drawing.Point(10,20) $label.Font = New-Object System.Drawing.Font("Segoe UI",10) $form.Controls.Add($label) # Add a progress bar control: $progressBar = New-Object System.Windows.Forms.ProgressBar $progressBar.Location = New-Object System.Drawing.Point(10,50) $progressBar.Size = New-Object System.Drawing.Size(360, 20) $progressBar.Minimum = 0 # Maximum will be set after counting total files below. $form.Controls.Add($progressBar) #endregion #region Calculate Total Files and Setup Progress # Get total number of files across all remaining folders (for progress bar scaling): $totalFiles = 0 foreach ($folder in $remainingFolders) { $path = Join-Path $Source $folder if (Test-Path $path) { $count = (Get-ChildItem -Path $path -Recurse -File -ErrorAction SilentlyContinue).Count $totalFiles += $count } } if ($totalFiles -eq 0) { Write-Host "No files to copy. Exiting script." -ForegroundColor Yellow Add-Content -Path $sessionLog -Value "No files found in the specified folders. Exiting." exit } $progressBar.Maximum = $totalFiles $progressBar.Value = 0 $label.Text = "0% (0 / $totalFiles files)" # Show the form (modeless) to allow updating progress: $form.Show() #endregion #region Copy Loop with Space Check and Error Handling # Initialize count of copied files: $filesCopied = 0 # Determine the drive letter or root of $Destination to check free space: $destDriveRoot = (Split-Path $Destination -Qualifier).TrimEnd('\') # e.g. "D:" function Get-FreeSpace { param($path) $driveName = $path.TrimEnd('\').Split(':')[0] $psdrive = Get-PSDrive -Name $driveName return [int64]$psdrive.Free # Already in bytes } # Loop through each folder to copy: for ($i = 0; $i -lt $remainingFolders.Count; $i++) { $folder = $remainingFolders[$i] $sourceFolder = Join-Path $Source $folder if (-not (Test-Path $sourceFolder)) { "`nFolder not found: $sourceFolder" | Tee-Object -FilePath $sessionLog -Append Add-Content -Path $failLog -Value $folder continue } # Calculate total size of this folder (bytes): $folderSize = (Get-ChildItem -Path $sourceFolder -Recurse -File | Measure-Object -Property Length -Sum).Sum $freeSpace = Get-FreeSpace $Destination # If not enough space on destination for this folder, skip and prompt for new destination: if ($folderSize -gt $freeSpace) { "`nInsufficient space for folder '$folder' (needs $folderSize bytes, has $freeSpace bytes)." | Tee-Object -FilePath $sessionLog -Append # Prompt user: yes to choose new destination, no to stop $result = [System.Windows.Forms.MessageBox]::Show( "Destination is full. Provide a new destination?", "Out of Space", [System.Windows.Forms.MessageBoxButtons]::YesNo, [System.Windows.Forms.MessageBoxIcon]::Question) if ($result -eq [System.Windows.Forms.DialogResult]::Yes) { # Open a folder browser dialog for new destination $folderBrowser = New-Object System.Windows.Forms.FolderBrowserDialog $folderBrowser.Description = "Select new backup destination" if ($folderBrowser.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { $Destination = $folderBrowser.SelectedPath "`nUser selected new destination: $Destination" | Tee-Object -FilePath $sessionLog -Append # Update drive root for free space checks: $destDriveRoot = (Split-Path $Destination -Qualifier).TrimEnd('\') # Re-check free space after changing destination $freeSpace = Get-FreeSpace $Destination # Re-check if folder now fits: if ($folderSize -gt $freeSpace) { "`nStill not enough space on $Destination for folder '$folder'." | Tee-Object -FilePath $sessionLog -Append Add-Content -Path $failLog -Value $folder continue } # Else, proceed to copy on new drive } else { # User canceled folder selection: stop processing "`nUser canceled new destination selection. Exiting." | Tee-Object -FilePath $sessionLog -Append break } } else { # User chose No: stop processing further folders "`nUser chose not to continue. Exiting backup." | Tee-Object -FilePath $sessionLog -Append break } } # Proceed to copy files within this folder "`nCopying folder: $folder" | Tee-Object -FilePath $sessionLog -Append try { # Copy files one by one to implement skip logic on timestamps: Get-ChildItem -Path $sourceFolder -Recurse -File | ForEach-Object { $srcFile = $_.FullName # Determine target path $relativePath = $srcFile.Substring($sourceFolder.Length).TrimStart('\') $destFile = Join-Path $Destination (Join-Path $folder $relativePath) # Ensure the destination directory exists: $destDir = Split-Path $destFile -Parent if (-not (Test-Path $destDir)) { New-Item -Path $destDir -ItemType Directory -Force | Out-Null } # If destination file exists and is newer or same, skip it: if (Test-Path $destFile) { $destItem = Get-Item $destFile -ErrorAction SilentlyContinue if ($destItem -and $destItem.LastWriteTime -ge $_.LastWriteTime) { # Skip this file $skipMsg = "Skipped (up-to-date): $relativePath" Write-Verbose $skipMsg Add-Content -Path $sessionLog -Value $skipMsg return } } # Copy the file (overwrite if needed): Copy-Item -Path $srcFile -Destination $destFile -ErrorAction Stop $filesCopied++ # Update progress bar and label: $percent = [Math]::Floor(($filesCopied / $totalFiles) * 100) $progressBar.Value = $filesCopied $label.Text = "$percent% ($filesCopied / $totalFiles files)" # Allow UI to update [System.Windows.Forms.Application]::DoEvents() } # If copy completes without error: "`nFolder backed up successfully: $folder" | Tee-Object -FilePath $sessionLog -Append Add-Content -Path $successLog -Value $folder } catch { # On any error during copy: "`nERROR copying folder ${folder}: $($_.Exception.Message)" | Tee-Object -FilePath $sessionLog -Append Add-Content -Path $failLog -Value $folder } } #endregion #region Cleanup # Close the progress form when done: $form.Close() "`nBackup process complete. Files copied: $filesCopied out of $totalFiles." | Tee-Object -FilePath $sessionLog -Append # Optionally, display final status message: Write-Host "`nBackup finished. Check logs for details:" -ForegroundColor Green Write-Host " Success log: $successLog" -ForegroundColor Green Write-Host " Failure log: $failLog" -ForegroundColor Red Write-Host " Session log: $sessionLog" -ForegroundColor Yellow #endregion