[CmdletBinding()] param( [string]$DestinationDirectory ) Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" $ProjectRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot "..")) if (-not $DestinationDirectory) { $DestinationDirectory = Join-Path $ProjectRoot "backups" } $DestinationDirectory = [IO.Path]::GetFullPath($DestinationDirectory) $Sources = @{ Environment = Join-Path $ProjectRoot ".env" Calendars = Join-Path $ProjectRoot "secrets/calendar_ical_urls.json" Token = Join-Path $ProjectRoot "secrets/organizer_access_token.txt" } foreach ($source in $Sources.GetEnumerator()) { if (-not (Test-Path -LiteralPath $source.Value -PathType Leaf)) { throw "Required configuration is missing. Run scripts/configure.ps1 before creating a backup." } } New-Item -ItemType Directory -Path $DestinationDirectory -Force | Out-Null $timestamp = Get-Date -Format "yyyyMMdd-HHmmss" $suffix = [Guid]::NewGuid().ToString("N").Substring(0, 8) $archivePath = Join-Path $DestinationDirectory "hobbs-organizer-config-$timestamp-$suffix.zip" $stagingPath = Join-Path $DestinationDirectory (".backup-staging-" + [Guid]::NewGuid().ToString("N")) $stagingFullPath = [IO.Path]::GetFullPath($stagingPath) $destinationPrefix = $DestinationDirectory.TrimEnd([IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar) + [IO.Path]::DirectorySeparatorChar if (-not $stagingFullPath.StartsWith($destinationPrefix, [StringComparison]::OrdinalIgnoreCase)) { throw "Refusing to create a staging directory outside the selected backup directory." } try { New-Item -ItemType Directory -Path (Join-Path $stagingPath "config") -Force | Out-Null New-Item -ItemType Directory -Path (Join-Path $stagingPath "secrets") -Force | Out-Null Copy-Item -LiteralPath $Sources.Environment -Destination (Join-Path $stagingPath "config/environment.env") Copy-Item -LiteralPath $Sources.Calendars -Destination (Join-Path $stagingPath "secrets/calendar_ical_urls.json") Copy-Item -LiteralPath $Sources.Token -Destination (Join-Path $stagingPath "secrets/organizer_access_token.txt") $manifest = @( "format=HOBBS_ORGANIZER_CONFIG_BACKUP_V1", "created_utc=$((Get-Date).ToUniversalTime().ToString('o'))", "contains_secrets=true" ) -join [Environment]::NewLine [IO.File]::WriteAllText( (Join-Path $stagingPath "manifest.txt"), $manifest + [Environment]::NewLine, (New-Object System.Text.UTF8Encoding($false)) ) Compress-Archive -Path (Join-Path $stagingPath "*") -DestinationPath $archivePath -CompressionLevel Optimal if ($PSVersionTable.PSEdition -eq "Desktop" -or [Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([Runtime.InteropServices.OSPlatform]::Windows)) { $userName = [Security.Principal.WindowsIdentity]::GetCurrent().Name & icacls.exe $archivePath "/inheritance:r" "/grant:r" "${userName}:(F)" | Out-Null if ($LASTEXITCODE -ne 0) { throw "Could not restrict backup archive permissions." } } else { & chmod 600 -- $archivePath if ($LASTEXITCODE -ne 0) { throw "Could not restrict backup archive permissions." } } } finally { if (Test-Path -LiteralPath $stagingFullPath) { Remove-Item -LiteralPath $stagingFullPath -Recurse -Force } } Write-Host "Created a protected configuration backup at: $archivePath" Write-Host "The archive contains secrets. Store it in an encrypted or access-controlled backup system."