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
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
[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"
|
||||
Reference in New Issue
Block a user