Skip to content

Commit 0bafcfd

Browse files
authored
Script backwards compatible, using pg_restore (#517)
1 parent 9400e53 commit 0bafcfd

File tree

1 file changed

+126
-23
lines changed

1 file changed

+126
-23
lines changed

scripts/backup/db_backup.sh

Lines changed: 126 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,14 @@
88
# ./SocialPredict backup --latest
99
# ./SocialPredict backup --restore </path/to/backup.dump.gz>
1010
# ./SocialPredict backup --restore-latest
11+
# ./SocialPredict backup --inspect </path/to/backup.dump.gz>
1112
# ./SocialPredict backup --help
1213
#
14+
# Environment overrides (optional):
15+
# PRESERVE_OWNERS=1 # keep original owners/privileges on restore (default: off)
16+
# RESTORE_DB=<name> # restore into this DB instead of POSTGRES_DATABASE
17+
# RESTORE_JOBS=<n> # pg_restore parallel jobs (-j), e.g. 4
18+
#
1319
# Notes:
1420
# - Must be called via ./SocialPredict (enforced by CALLED_FROM_SOCIALPREDICT guard)
1521
# - Uses .env for DB container/name/user/pass/ports
@@ -29,15 +35,13 @@ set -euo pipefail
2935
: "${POSTGRES_DATABASE:?POSTGRES_DATABASE not set}"
3036
: "${POSTGRES_PORT:=5432}" # default for in-container access
3137

32-
# Determine SCRIPT_DIR (root/scripts/backup) and BACKUP_ROOT (../backups relative to root)
38+
# Determine paths
3339
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
3440
ROOT_DIR="$(dirname "$SCRIPT_DIR")" # .../scripts
3541
ROOT_DIR="$(dirname "$ROOT_DIR")" # repo root
36-
# readlink -f is not available on macOS by default; do a portable resolution:
3742
abs_path() { (cd "$1" 2>/dev/null && pwd -P) || return 1; }
3843
PARENT_OF_ROOT="$(dirname "$(abs_path "$ROOT_DIR")")"
3944
BACKUP_ROOT="$PARENT_OF_ROOT/backups"
40-
4145
mkdir -p "$BACKUP_ROOT"
4246

4347
# --- Helpers ------------------------------------------------------------------
@@ -66,15 +70,45 @@ need_container_running() {
6670
fi
6771
}
6872

73+
# psql wrapper; tries DB, then postgres, then template1
74+
psql_try() {
75+
local db="$1"; shift
76+
docker exec -e PGPASSWORD="$POSTGRES_PASSWORD" -i "$POSTGRES_CONTAINER_NAME" \
77+
psql -v ON_ERROR_STOP=1 -h localhost -p "$POSTGRES_PORT" -U "$POSTGRES_USER" -d "$db" "$@"
78+
}
79+
80+
psql_smart() {
81+
# Try connecting to configured DB; if missing, fall back to postgres/template1 purely for control commands.
82+
if psql_try "$POSTGRES_DATABASE" -c "SELECT 1;" >/dev/null 2>&1; then
83+
psql_try "$POSTGRES_DATABASE" "$@"
84+
return
85+
fi
86+
if psql_try postgres -c "SELECT 1;" >/dev/null 2>&1; then
87+
psql_try postgres "$@"
88+
return
89+
fi
90+
psql_try template1 "$@"
91+
}
92+
6993
pg_dump_cmd() {
70-
# pg_dump inside container; output to stdout; custom format; gzip on host
71-
# Use in-container localhost & port; auth via PGPASSWORD env
72-
echo "PGPASSWORD='${POSTGRES_PASSWORD}' pg_dump -U '${POSTGRES_USER}' -h 'localhost' -p '${POSTGRES_PORT}' -d '${POSTGRES_DATABASE}' -Fc"
94+
# Custom format; output to stdout; auth via env
95+
local db="$1"
96+
echo "PGPASSWORD='${POSTGRES_PASSWORD}' pg_dump -U '${POSTGRES_USER}' -h 'localhost' -p '${POSTGRES_PORT}' -d '${db}' -Fc"
7397
}
7498

7599
pg_restore_cmd() {
76-
# pg_restore inside container; restore into same database; clean objects first
77-
echo "PGPASSWORD='${POSTGRES_PASSWORD}' pg_restore -U '${POSTGRES_USER}' -h 'localhost' -p '${POSTGRES_PORT}' -d '${POSTGRES_DATABASE}' --clean --if-exists"
100+
# Build pg_restore with safe defaults; accepts <target-db> as $1
101+
local target_db="$1"; shift || true
102+
local flags=( --clean --if-exists )
103+
# Unless preserving owners, ignore owners & privileges (portable across clusters/roles)
104+
if [ "${PRESERVE_OWNERS:-0}" != "1" ]; then
105+
flags+=( --no-owner --no-privileges )
106+
fi
107+
# Optional parallel jobs
108+
if [ -n "${RESTORE_JOBS:-}" ]; then
109+
flags+=( -j "$RESTORE_JOBS" )
110+
fi
111+
echo "PGPASSWORD='${POSTGRES_PASSWORD}' pg_restore -U '${POSTGRES_USER}' -h 'localhost' -p '${POSTGRES_PORT}' -d '${target_db}' ${flags[*]}"
78112
}
79113

80114
latest_backup_file() {
@@ -83,21 +117,37 @@ latest_backup_file() {
83117

84118
print_usage() {
85119
cat <<EOF
86-
Usage: ./SocialPredict backup [--save | --list | --latest | --restore <file> | --restore-latest | --help]
120+
Usage: ./SocialPredict backup [--save | --list | --latest | --restore <file> | --restore-latest | --inspect <file> | --help]
87121
88-
--save Create a new backup in $BACKUP_ROOT
89-
--list List available backups (newest first)
90-
--latest Print the path to the newest backup
91-
--restore <file> Restore the given .dump.gz into the running DB (with confirmation)
92-
--restore-latest Restore the newest backup (with confirmation)
93-
--help Show this help
122+
--save Create a new backup in $BACKUP_ROOT
123+
--list List available backups (newest first)
124+
--latest Print the path to the newest backup
125+
--restore <file> Restore the given .dump.gz into the running DB (with confirmation)
126+
--restore-latest Restore the newest backup (with confirmation)
127+
--inspect <file> Show owner/privilege hints and extensions present in the dump
128+
--help Show this help
129+
130+
Environment overrides:
131+
PRESERVE_OWNERS=1 Keep original owners/privileges (default: skip owners/privs)
132+
RESTORE_DB=<name> Restore into this database instead of \$POSTGRES_DATABASE
133+
RESTORE_JOBS=<n> Use pg_restore -j <n> parallel jobs
94134
95135
Backups:
96136
socialpredict_backup_${APP_ENV}_YYYYmmdd_HHMMSS.dump.gz
97137
socialpredict_backup_${APP_ENV}_YYYYmmdd_HHMMSS.dump.gz.sha256
98138
EOF
99139
}
100140

141+
ensure_db_exists() {
142+
local db="$1"
143+
# If DB exists, nothing to do
144+
if psql_smart -tAc "SELECT 1 FROM pg_database WHERE datname='${db}'" | grep -q 1; then
145+
return
146+
fi
147+
echo "Target database '${db}' not found. Creating it (OWNER=${POSTGRES_USER})..."
148+
psql_smart -c "CREATE DATABASE \"${db}\" OWNER \"${POSTGRES_USER}\";"
149+
}
150+
101151
# --- Actions ------------------------------------------------------------------
102152
do_save() {
103153
need_container_running
@@ -107,8 +157,14 @@ do_save() {
107157
tmpfile="${file}.partial"
108158

109159
echo "Creating backup: $file"
160+
# Validate we can connect with provided user
161+
if ! psql_smart -c "SELECT current_user, current_database();" >/dev/null 2>&1; then
162+
echo "ERROR: Cannot connect as POSTGRES_USER='${POSTGRES_USER}'. Check your .env credentials/role."
163+
exit 1
164+
fi
165+
110166
# Run pg_dump inside the container; stream to host; compress
111-
if ! docker exec -i "$POSTGRES_CONTAINER_NAME" bash -c "$(pg_dump_cmd) " | gzip -c > "$tmpfile"; then
167+
if ! docker exec -i "$POSTGRES_CONTAINER_NAME" bash -c "$(pg_dump_cmd "$POSTGRES_DATABASE") " | gzip -c > "$tmpfile"; then
112168
echo "ERROR: pg_dump failed."
113169
rm -f "$tmpfile"
114170
exit 1
@@ -140,14 +196,14 @@ do_latest() {
140196
}
141197

142198
confirm_restore() {
143-
local file="$1"
144-
echo "About to RESTORE into database '${POSTGRES_DATABASE}' inside container '${POSTGRES_CONTAINER_NAME}'."
199+
local file="$1" target_db="$2"
200+
echo "About to RESTORE into database '${target_db}' inside container '${POSTGRES_CONTAINER_NAME}'."
145201
echo "This will overwrite existing data for that database."
146202
echo
147203
echo "Backup file: $file"
148204
echo
149-
read -r -p "Type EXACT database name (${POSTGRES_DATABASE}) to proceed, or anything else to abort: " answer
150-
if [ "$answer" != "$POSTGRES_DATABASE" ]; then
205+
read -r -p "Type EXACT database name (${target_db}) to proceed, or anything else to abort: " answer
206+
if [ "$answer" != "$target_db" ]; then
151207
echo "Aborted."
152208
exit 1
153209
fi
@@ -157,7 +213,10 @@ do_restore_file() {
157213
local file="$1"
158214
[ -f "$file" ] || { echo "ERROR: Backup file not found: $file"; exit 1; }
159215
need_container_running
160-
confirm_restore "$file"
216+
217+
# Decide target DB
218+
local target_db="${RESTORE_DB:-$POSTGRES_DATABASE}"
219+
confirm_restore "$file" "$target_db"
161220

162221
# Verify checksum if present
163222
local sumfile="${file}.sha256"
@@ -173,9 +232,19 @@ do_restore_file() {
173232
echo "Checksum OK."
174233
fi
175234

176-
echo "Restoring..."
235+
# Basic connection sanity
236+
if ! psql_smart -c "SELECT current_user;" >/dev/null 2>&1; then
237+
echo "ERROR: Cannot connect to Postgres as POSTGRES_USER='${POSTGRES_USER}'."
238+
echo "Hint: If you see 'role \"postgres\" does not exist', set POSTGRES_USER in .env to the actual superuser for this cluster."
239+
exit 1
240+
fi
241+
242+
# Ensure target DB exists (pg_restore needs it)
243+
ensure_db_exists "$target_db"
244+
245+
echo "Restoring (owners/privileges $( [ "${PRESERVE_OWNERS:-0}" = "1" ] && echo "PRESERVED" || echo "IGNORED" ))..."
177246
# Decompress on host; stream into pg_restore in container
178-
if ! gunzip -c "$file" | docker exec -i "$POSTGRES_CONTAINER_NAME" bash -c "$(pg_restore_cmd) "; then
247+
if ! gunzip -c "$file" | docker exec -i "$POSTGRES_CONTAINER_NAME" bash -c "$(pg_restore_cmd "$target_db") "; then
179248
echo "ERROR: pg_restore failed."
180249
exit 1
181250
fi
@@ -193,6 +262,35 @@ do_restore_latest() {
193262
do_restore_file "$latest"
194263
}
195264

265+
do_inspect() {
266+
local file="$1"
267+
[ -f "$file" ] || { echo "ERROR: Backup file not found: $file"; exit 1; }
268+
269+
# Decompress to a temp (pg_restore -l needs a seekable archive file)
270+
local tmp
271+
tmp="$(mktemp -t sp_dump_XXXXXX.dump)"
272+
trap 'rm -f "$tmp"' EXIT
273+
gunzip -c "$file" > "$tmp"
274+
275+
echo "== Dump inspection =="
276+
echo "File: $file"
277+
echo
278+
echo "-- Owners referenced --"
279+
if ! pg_restore -l "$tmp" | grep -Eo 'OWNER TO [^;]+' | sort -u; then
280+
echo "(none found or not applicable)"
281+
fi
282+
echo
283+
echo "-- Privilege statements (GRANT/REVOKE) --"
284+
if ! pg_restore -l "$tmp" | grep -E 'GRANT |REVOKE ' | sort -u | sed -n '1,200p'; then
285+
echo "(none found or not applicable)"
286+
fi
287+
echo
288+
echo "-- Extensions requested --"
289+
if ! pg_restore -l "$tmp" | grep -i 'EXTENSION - ' | sed -n '1,200p'; then
290+
echo "(none found)"
291+
fi
292+
}
293+
196294
# --- Main ---------------------------------------------------------------------
197295
ACTION="${1:-"--help"}"
198296
case "$ACTION" in
@@ -213,6 +311,11 @@ case "$ACTION" in
213311
--restore-latest)
214312
do_restore_latest
215313
;;
314+
--inspect)
315+
shift || true
316+
[ -n "${1:-}" ] || { echo "ERROR: --inspect requires a file path"; exit 1; }
317+
do_inspect "$1"
318+
;;
216319
--help|-h)
217320
print_usage
218321
;;

0 commit comments

Comments
 (0)