#!/usr/bin/env bash # Restores an encrypted backup produced by backup-postgres.sh into a target database. # Deliberately requires an EXPLICIT target — never silently restores over the live production # database ("what would you do if this ran the wrong way" is a real disaster-recovery failure # mode, so making that impossible to trigger by accident matters more than convenience here). # # Usage: ./restore-postgres.sh # # For a REAL production restore, target-db-url should point at a FRESH database created for # this purpose (docs/RUNBOOK_POSTGRES_RESTORE.md walks through creating it) — restoring into a # database that already has data merges/conflicts rather than cleanly replacing it. set -euo pipefail ENC_FILE="${1:?usage: restore-postgres.sh }" TARGET_DB_URL="${2:?usage: restore-postgres.sh }" : "${BACKUP_ENCRYPTION_PASSPHRASE:?BACKUP_ENCRYPTION_PASSPHRASE is required to decrypt}" [ -f "$ENC_FILE" ] || { echo "No such file: $ENC_FILE" >&2; exit 1; } TMPDIR="$(mktemp -d)" trap 'rm -rf "$TMPDIR"' EXIT DUMP_FILE="$TMPDIR/restore.sql.gz" echo "==> Decrypting $ENC_FILE" gpg --batch --yes --passphrase "$BACKUP_ENCRYPTION_PASSPHRASE" --decrypt --output "$DUMP_FILE" "$ENC_FILE" echo "==> Confirming target is reachable" psql "$TARGET_DB_URL" -c "SELECT 1" > /dev/null echo "==> Target database currently has these tables (should be empty/unimportant before you continue):" psql "$TARGET_DB_URL" -c "\dt" || true read -r -p "Type RESTORE to proceed against $TARGET_DB_URL: " CONFIRM [ "$CONFIRM" = "RESTORE" ] || { echo "Aborted."; exit 1; } echo "==> Restoring" gunzip -c "$DUMP_FILE" | psql "$TARGET_DB_URL" echo "==> Restore complete. Verify with infra/backup/verify-restore.sh $TARGET_DB_URL"