# Backup-UDCData.ps1 - Capture UDC's CurrentData.json + ArchiveData/ to the # SFLD share, keyed by the PC's current machine number. Runs LOCALLY on the # old PC before retirement; the new PC restores during its first # placeholder-to-real machine-number assignment via Update-MachineNumber.ps1. # # DESIGN: backup lives at \\ root. Presence of CurrentData.json # at that root means "available to restore". Restore consumes by moving content # into \migrated\\, leaving the root empty so it can't # be replayed. # # USAGE: # - Direct: powershell -NoProfile -ExecutionPolicy Bypass -File Backup-UDCData.ps1 # - Wrapper: Backup-UDCData.bat (handles the bypass + elevation prompts) # - Remote (WinRM): ../powershell/remote-execution/Backup-UDCData-Remote.ps1 [CmdletBinding()] param( # Override auto-detection for unusual cases. Defaults to whatever UDC # currently has in C:\ProgramData\UDC\udc_settings.json. [string]$MachineNumber, # Override the share path. Defaults to the canonical SFLD location. [string]$BackupShareRoot = '\\tsgwp00525.wjs.geaerospace.net\shared\dt\shopfloor\backup\udc', # Local UDC data root. Default matches the vendor install path. [string]$UdcDataDir = 'C:\ProgramData\UDC', # Cred for the SFLD share. If omitted, the current session's identity # is used (works for SYSTEM-context runs and for local interactive # admin sessions where the user has cached SFLD creds via SFLD-Reg). [System.Management.Automation.PSCredential]$Credential, # If the destination already has a backup for this machine, append a # timestamped suffix instead of overwriting. Default: overwrite (latest # backup wins; the prior one only matters if the new PC didn't restore # before this re-backup). [switch]$KeepPriorBackup ) $ErrorActionPreference = 'Stop' $logDir = 'C:\Logs\UDC' if (-not (Test-Path $logDir)) { try { New-Item -Path $logDir -ItemType Directory -Force | Out-Null } catch { $logDir = $env:TEMP } } $logFile = Join-Path $logDir ("Backup-UDCData_$(Get-Date -Format 'yyyyMMdd-HHmmss').log") try { Start-Transcript -Path $logFile -Append -Force | Out-Null } catch {} function Log { param([string]$Msg, [string]$Level = 'INFO') $ts = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' Write-Host "[$ts][$Level] $Msg" } Log "===============================================" Log "Backup-UDCData starting" Log "Hostname: $env:COMPUTERNAME" Log "User: $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)" Log "UDC data dir: $UdcDataDir" Log "Backup share: $BackupShareRoot" Log "Log file: $logFile" Log "===============================================" # --- Resolve machine number --- if (-not $MachineNumber) { $settings = Join-Path $UdcDataDir 'udc_settings.json' if (-not (Test-Path $settings)) { Log "udc_settings.json not found at $settings - cannot auto-detect machine number" 'ERROR' Log "Re-run with -MachineNumber to override" try { Stop-Transcript | Out-Null } catch {} exit 2 } try { $json = Get-Content $settings -Raw | ConvertFrom-Json $MachineNumber = $json.GeneralSettings.MachineNumber } catch { Log "Failed to parse udc_settings.json: $_" 'ERROR' try { Stop-Transcript | Out-Null } catch {} exit 3 } } if (-not $MachineNumber -or $MachineNumber -eq '9999' -or $MachineNumber -notmatch '^\d+$') { Log "Invalid or placeholder machine number: '$MachineNumber'. Aborting." 'ERROR' Log "(Backups for placeholder 9999 would collide across PCs and aren't useful.)" try { Stop-Transcript | Out-Null } catch {} exit 4 } Log "Resolved machine number: $MachineNumber" # --- Verify source files exist --- $srcCurrent = Join-Path $UdcDataDir 'CurrentData.json' $srcArchive = Join-Path $UdcDataDir 'ArchiveData' $haveCurrent = Test-Path -LiteralPath $srcCurrent $haveArchive = Test-Path -LiteralPath $srcArchive if (-not $haveCurrent -and -not $haveArchive) { Log "Neither CurrentData.json nor ArchiveData/ exists under $UdcDataDir. Nothing to back up." 'WARN' try { Stop-Transcript | Out-Null } catch {} exit 0 } Log "CurrentData.json present: $haveCurrent" Log "ArchiveData/ present: $haveArchive" # --- Mount share if credentials supplied (otherwise rely on ambient auth) --- $psDrive = $null $dest = Join-Path $BackupShareRoot $MachineNumber if ($Credential) { try { $psDrive = New-PSDrive -Name UDCBKP -PSProvider FileSystem -Root $BackupShareRoot -Credential $Credential -ErrorAction Stop Log "Share mounted as UDCBKP: with explicit credentials" $dest = "UDCBKP:\$MachineNumber" } catch { Log "Failed to mount $BackupShareRoot with supplied credentials: $_" 'ERROR' try { Stop-Transcript | Out-Null } catch {} exit 5 } } # --- Create destination dir --- try { if (-not (Test-Path -LiteralPath $dest)) { New-Item -ItemType Directory -Path $dest -Force | Out-Null Log "Created destination: $dest" } } catch { Log "Failed to create destination $dest : $_" 'ERROR' if ($psDrive) { Remove-PSDrive -Name UDCBKP -Force } try { Stop-Transcript | Out-Null } catch {} exit 6 } # --- Optional: rotate prior backup at this bay if -KeepPriorBackup --- if ($KeepPriorBackup) { $existingMarker = Join-Path $dest 'CurrentData.json' if (Test-Path -LiteralPath $existingMarker) { $rotateName = Join-Path $dest ('superseded-' + (Get-Date -Format 'yyyyMMdd-HHmmss')) New-Item -ItemType Directory -Path $rotateName -Force | Out-Null Get-ChildItem $dest -Force | Where-Object { $_.Name -ne 'migrated' -and $_.Name -ne (Split-Path $rotateName -Leaf) } | ForEach-Object { Move-Item -LiteralPath $_.FullName -Destination $rotateName -Force } Log "Prior backup rotated into $rotateName (KeepPriorBackup=true)" } } # --- Copy CurrentData.json --- $copiedCurrent = $false $currentBytes = 0 if ($haveCurrent) { try { Copy-Item -LiteralPath $srcCurrent -Destination (Join-Path $dest 'CurrentData.json') -Force -ErrorAction Stop $currentBytes = (Get-Item -LiteralPath $srcCurrent).Length $copiedCurrent = $true Log "Copied CurrentData.json ($currentBytes bytes)" } catch { Log "Failed to copy CurrentData.json: $_" 'ERROR' } } # --- Copy ArchiveData/ recursively --- $copiedArchive = $false $archiveBytes = 0 $archiveFiles = 0 if ($haveArchive) { try { $destArchive = Join-Path $dest 'ArchiveData' if (Test-Path -LiteralPath $destArchive) { Remove-Item -LiteralPath $destArchive -Recurse -Force -ErrorAction Stop } Copy-Item -LiteralPath $srcArchive -Destination $destArchive -Recurse -Force -ErrorAction Stop $copiedArchive = $true $archiveItems = Get-ChildItem -LiteralPath $destArchive -Recurse -File -ErrorAction SilentlyContinue $archiveBytes = ($archiveItems | Measure-Object Length -Sum).Sum $archiveFiles = $archiveItems.Count Log "Copied ArchiveData/ ($archiveFiles files, $archiveBytes bytes)" } catch { Log "Failed to copy ArchiveData/: $_" 'ERROR' } } # --- Drop a small backup.manifest.json next to the data for forensics --- try { $manifest = [ordered]@{ BackedUpAt = (Get-Date -Format 'o') SourceHostname = $env:COMPUTERNAME SourceUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name MachineNumber = $MachineNumber CurrentDataPresent = $copiedCurrent CurrentDataBytes = $currentBytes ArchiveDataPresent = $copiedArchive ArchiveDataFiles = $archiveFiles ArchiveDataBytes = $archiveBytes } $manifest | ConvertTo-Json | Set-Content -Path (Join-Path $dest 'backup.manifest.json') -Encoding UTF8 Log "Wrote backup.manifest.json" } catch { Log "Manifest write failed (non-fatal): $_" 'WARN' } # --- Tear down mount --- if ($psDrive) { try { Remove-PSDrive -Name UDCBKP -Force } catch {} } Log "===============================================" Log "Backup complete:" Log " Bay: $MachineNumber" Log " Destination: $BackupShareRoot\$MachineNumber\" Log " CurrentData.json: $(if ($copiedCurrent) {'OK ('+$currentBytes+' bytes)'} else {'MISSING'})" Log " ArchiveData/: $(if ($copiedArchive) {'OK ('+$archiveFiles+' files, '+$archiveBytes+' bytes)'} else {'MISSING'})" Log "===============================================" try { Stop-Transcript | Out-Null } catch {} exit 0