Files
hobbs-iac/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/scripts/backup.ps1
T
jeremy e9c7e9df1a feat: self-host Hobbs Family Organizer (Shield TV dashboard) on hf-pdocker-02:8340
- tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0: release bundle (Next.js app + Shield APK v1.4 + source)
- compose/hf-pdocker-02/hobbs-organizer: override (local-disk secrets, watchtower off), .env (port 8340), Shield provisioning script
- CLAUDE.md: service tree + internal ports
Trusted-LAN mode. Secrets live only on .100 /var/lib/hobbs-organizer/secrets (not in git).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E9NnVEico3WvFxLyjy1iW3
2026-11-04 18:27:22 -06:00

77 lines
3.4 KiB
PowerShell

[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."