# Dumps the Siftlode Postgres database to backups\siftlode-.dump (custom format). # Run from the project root: .\scripts\backup.ps1 # # A dump is only HALF a backup. The other half is your .env — it holds TOKEN_ENCRYPTION_KEY, without # which every encrypted value in the restored database (Google credentials, SMTP password, Plex # token, stored refresh tokens) is unreadable. See docs/self-hosting.md § Backups. $ErrorActionPreference = "Stop" # The dump is moved with `docker compose cp`, never through the PowerShell pipeline. Piping a binary # stream corrupts it on Windows PowerShell 5.1 (text decoding), and on PowerShell 7 the old # `-Encoding Byte` workaround does not even exist any more — it throws "'Byte' is not a supported # encoding name", so this script simply failed there. $remote = "/tmp/siftlode-backup.dump" # Best-effort cleanup of the in-container dump. The try/catch is the load-bearing part: on Windows # PowerShell 5.1 ANY redirected native stderr (`2>$null` and `2>&1` alike) becomes an ErrorRecord, # and the $ErrorActionPreference above makes it TERMINATING — so one stray Compose warning would # kill the script here, masking the real diagnostic below or reporting a finished backup as a # failure. Do not remove the empty catch. A cleanup step must never decide the verdict. function Remove-RemoteDump($path) { try { docker compose exec -T db rm -f $path 2>&1 | Out-Null } catch { } } # `docker compose` finds docker-compose.yml on its own and honours $env:COMPOSE_FILE. The documented # self-host layout (docs/self-hosting.md § Get the files) has NEITHER — just the selfhost file. if (-not $env:COMPOSE_FILE -and -not (Test-Path "docker-compose.yml") -and (Test-Path "docker-compose.selfhost.yml")) { $env:COMPOSE_FILE = "docker-compose.selfhost.yml" } $user = if ($env:POSTGRES_USER) { $env:POSTGRES_USER } else { "siftlode" } $dbname = if ($env:POSTGRES_DB) { $env:POSTGRES_DB } else { "siftlode" } New-Item -ItemType Directory -Force -Path "backups" | Out-Null $ts = Get-Date -Format "yyyyMMdd-HHmmss" $out = "backups\siftlode-$ts.dump" docker compose exec -T db pg_dump -U $user -Fc $dbname -f $remote # Capture BEFORE the cleanup call: any native command overwrites $LASTEXITCODE, so interpolating it # after Remove-RemoteDump would report the cleanup's code (0) inside a branch that only runs when # pg_dump failed — a message contradicting itself. Same reason $cpCode is captured below. $dumpCode = $LASTEXITCODE if ($dumpCode -ne 0) { Remove-RemoteDump $remote throw "BACKUP FAILED — pg_dump returned $dumpCode; no file was written." } docker compose cp "db:$remote" $out $cpCode = $LASTEXITCODE Remove-RemoteDump $remote # Remove the partial file: `cp` streams, so a mid-stream failure leaves a truncated dump under # the final name — non-zero length, so the size check below would not catch it either. if ($cpCode -ne 0) { Remove-Item $out -ErrorAction SilentlyContinue throw "BACKUP FAILED — could not copy the dump out of the container (exit $cpCode)." } if (-not (Test-Path $out) -or (Get-Item $out).Length -eq 0) { if (Test-Path $out) { Remove-Item $out } throw "BACKUP FAILED — the dump came out empty; no backup was written." } Write-Output "Wrote $out" Write-Output "Remember: back up your .env too (TOKEN_ENCRYPTION_KEY) — this dump alone cannot be fully restored."