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
3339SCRIPT_DIR=" $( cd " $( dirname " ${BASH_SOURCE[0]} " ) " && pwd ) "
3440ROOT_DIR=" $( dirname " $SCRIPT_DIR " ) " # .../scripts
3541ROOT_DIR=" $( dirname " $ROOT_DIR " ) " # repo root
36- # readlink -f is not available on macOS by default; do a portable resolution:
3742abs_path () { (cd " $1 " 2> /dev/null && pwd -P) || return 1; }
3843PARENT_OF_ROOT=" $( dirname " $( abs_path " $ROOT_DIR " ) " ) "
3944BACKUP_ROOT=" $PARENT_OF_ROOT /backups"
40-
4145mkdir -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+
6993pg_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
7599pg_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
80114latest_backup_file () {
@@ -83,21 +117,37 @@ latest_backup_file() {
83117
84118print_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
95135Backups:
96136 socialpredict_backup_${APP_ENV} _YYYYmmdd_HHMMSS.dump.gz
97137 socialpredict_backup_${APP_ENV} _YYYYmmdd_HHMMSS.dump.gz.sha256
98138EOF
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 ------------------------------------------------------------------
102152do_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
142198confirm_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 ---------------------------------------------------------------------
197295ACTION=" ${1:- " --help" } "
198296case " $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