e9c7e9df1a
- 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
98 lines
3.9 KiB
PowerShell
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"
|