Files
hobbs-iac/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/scripts/restore.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

98 lines
3.9 KiB
PowerShell

[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$Archive,
[switch]$Force
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$ProjectRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot ".."))
$Archive = [IO.Path]::GetFullPath($Archive)
if (-not (Test-Path -LiteralPath $Archive -PathType Leaf)) {
throw "The backup archive does not exist."
}
if (-not $Force) {
$answer = (Read-Host "Restore will overwrite the current .env and runtime secrets. Continue? [y/N]").Trim().ToLowerInvariant()
if ($answer -notin @("y", "yes")) {
throw "Restore cancelled."
}
}
Add-Type -AssemblyName System.IO.Compression.FileSystem
$zip = [IO.Compression.ZipFile]::OpenRead($Archive)
try {
$allowedEntries = @(
"config/environment.env",
"secrets/calendar_ical_urls.json",
"secrets/organizer_access_token.txt",
"manifest.txt"
)
$totalBytes = 0L
foreach ($entry in $zip.Entries) {
$entryName = $entry.FullName.Replace("\", "/").TrimEnd("/")
if (-not $entryName) { continue }
if ($entryName -notin $allowedEntries -and $entryName -notin @("config", "secrets")) {
throw "The archive contains an unexpected path and will not be restored."
}
$totalBytes += $entry.Length
}
if ($totalBytes -gt 10MB) {
throw "The configuration archive is unexpectedly large."
}
}
finally {
$zip.Dispose()
}
$stagingPath = Join-Path $ProjectRoot (".restore-staging-" + [Guid]::NewGuid().ToString("N"))
$stagingFullPath = [IO.Path]::GetFullPath($stagingPath)
$projectPrefix = $ProjectRoot.TrimEnd([IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar) + [IO.Path]::DirectorySeparatorChar
if (-not $stagingFullPath.StartsWith($projectPrefix, [StringComparison]::OrdinalIgnoreCase)) {
throw "Refusing to create a restore staging directory outside the application directory."
}
try {
Expand-Archive -LiteralPath $Archive -DestinationPath $stagingPath
$restoredEnvironment = Join-Path $stagingPath "config/environment.env"
$restoredCalendars = Join-Path $stagingPath "secrets/calendar_ical_urls.json"
$restoredToken = Join-Path $stagingPath "secrets/organizer_access_token.txt"
foreach ($required in @($restoredEnvironment, $restoredCalendars, $restoredToken)) {
if (-not (Test-Path -LiteralPath $required -PathType Leaf)) {
throw "The archive is missing required configuration files."
}
}
$secretsDirectory = Join-Path $ProjectRoot "secrets"
New-Item -ItemType Directory -Path $secretsDirectory -Force | Out-Null
Copy-Item -LiteralPath $restoredEnvironment -Destination (Join-Path $ProjectRoot ".env") -Force
Copy-Item -LiteralPath $restoredCalendars -Destination (Join-Path $secretsDirectory "calendar_ical_urls.json") -Force
Copy-Item -LiteralPath $restoredToken -Destination (Join-Path $secretsDirectory "organizer_access_token.txt") -Force
$secretFiles = @(
(Join-Path $secretsDirectory "calendar_ical_urls.json"),
(Join-Path $secretsDirectory "organizer_access_token.txt")
)
if ($PSVersionTable.PSEdition -eq "Desktop" -or [Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([Runtime.InteropServices.OSPlatform]::Windows)) {
$userName = [Security.Principal.WindowsIdentity]::GetCurrent().Name
foreach ($secretFile in $secretFiles) {
& icacls.exe $secretFile "/inheritance:r" "/grant:r" "${userName}:(R,W)" | Out-Null
if ($LASTEXITCODE -ne 0) { throw "Could not restrict restored secret permissions." }
}
}
else {
& chmod 600 -- @secretFiles
if ($LASTEXITCODE -ne 0) { throw "Could not restrict restored secret permissions." }
}
}
finally {
if (Test-Path -LiteralPath $stagingFullPath) {
Remove-Item -LiteralPath $stagingFullPath -Recurse -Force
}
}
Write-Host "Configuration restored. Restart with: docker compose up -d --build"