220 lines
7.0 KiB
Bash
Executable File
220 lines
7.0 KiB
Bash
Executable File
#!/bin/sh
|
|
set -eu
|
|
|
|
# Simple restore helper for backups produced by this stack.
|
|
# Usage:
|
|
# ./restore.sh list # list backups in ./backups
|
|
# ./restore.sh restore-volume <volume-name> <backup-archive-path>
|
|
# ./restore.sh restore-db <dump-file> <db-name> <db-user> <db-password>
|
|
#
|
|
# Notes:
|
|
# - This assumes you use `docker compose` in the repo root and the postgres service
|
|
# is named `postgres` in your compose stack. Adjust POSTGRES_SERVICE if different.
|
|
# - Stop services that use the target volume/database before restoring to avoid conflicts.
|
|
|
|
BACKUPS_DIR="${BACKUPS_DIR:-./backups}"
|
|
POSTGRES_SERVICE="${POSTGRES_SERVICE:-postgres}"
|
|
COMPOSE="docker compose"
|
|
|
|
require_bsdtar() {
|
|
if ! command -v bsdtar >/dev/null 2>&1; then
|
|
err "bsdtar not found. Please install libarchive/bsdtar in the current environment."
|
|
exit 2
|
|
fi
|
|
}
|
|
|
|
require_bsdtar
|
|
|
|
log() { printf '%s %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$*"; }
|
|
err() { log "ERROR: $*" >&2; }
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
Usage: $0 <command> [args]
|
|
|
|
Commands:
|
|
list
|
|
List dated backup directories under $BACKUPS_DIR
|
|
|
|
restore-volume <volume-name> <archive-path>
|
|
Extract a tar.gz archive into a named docker volume.
|
|
Example: $0 restore-volume odoo-db-data backups/2025-11-13/odoo_filestore_2025-11-13.tar.gz
|
|
|
|
restore-db <dump-file> <db-name> <db-user> <db-password>
|
|
Restore a SQL dump into the running Postgres service.
|
|
Example: $0 restore-db backups/2025-11-13/odoodb_2025-11-13.sql odoodb admin adminpass
|
|
|
|
EOF
|
|
}
|
|
|
|
list_backups() {
|
|
ls -1 "$BACKUPS_DIR" || true
|
|
}
|
|
|
|
restore_volume() {
|
|
volume="$1"
|
|
archive="$2"
|
|
|
|
if [ ! -f "$archive" ]; then
|
|
err "Archive not found: $archive"
|
|
exit 2
|
|
fi
|
|
|
|
# log "*** IMPORTANT: stop services that use volume '$volume' before running this restore."
|
|
# log "Proceeding in 5 seconds; press Ctrl-C to abort..."
|
|
# sleep 5
|
|
|
|
if [ "${IN_CONTAINER:-0}" = "1" ]; then
|
|
log "Running in-container restore: mapping volume name to mounted path."
|
|
target=""
|
|
|
|
case "$volume" in
|
|
opencloud-config*|*opencloud-config*) [ -d "/opencloud_config" ] && target="/opencloud_config" ;;
|
|
opencloud-data*|*opencloud-data*) [ -d "/opencloud_data" ] && target="/opencloud_data" ;;
|
|
odoo-config*|*odoo-config*) [ -d "/odoo_config" ] && target="/odoo_config" ;;
|
|
odoo-db-data*|*odoo-db-data*|*odoo*) [ -d "/odoo_db_data" ] && target="/odoo_db_data" ;;
|
|
gitea*|*gitea*) [ -d "/gitea_data" ] && target="/gitea_data" ;;
|
|
esac
|
|
|
|
if [ -z "$target" ]; then
|
|
err "Could not determine mount path for volume '$volume' inside container."
|
|
exit 4
|
|
fi
|
|
|
|
log "Extracting $archive into $target"
|
|
bsdtar --xattrs --same-owner --numeric-owner -xpf "$archive" -C "$target"
|
|
log "Restore finished. You may need to adjust ownership inside the target container if required."
|
|
return 0
|
|
fi
|
|
|
|
log "Restoring archive $archive into volume $volume"
|
|
docker run --rm -v "$volume":/data -v "$(pwd)/$archive":/backup.tar.gz alpine \
|
|
sh -c "apk add --no-cache libarchive-tools >/dev/null && bsdtar --xattrs --same-owner --numeric-owner -xpf /backup.tar.gz -C /data"
|
|
log "Restore finished. You may need to adjust ownership inside the target container if required."
|
|
}
|
|
|
|
restore_db() {
|
|
dumpfile="$1"
|
|
dbname="$2"
|
|
dbuser="$3"
|
|
dbpass="$4"
|
|
|
|
if [ ! -f "$dumpfile" ]; then
|
|
err "Dump file not found: $dumpfile"
|
|
exit 2
|
|
fi
|
|
|
|
host="${POSTGRES_HOST:-$POSTGRES_SERVICE}"
|
|
admin_db="${POSTGRES_ADMIN_DB:-postgres}"
|
|
admin_user="${POSTGRES_ADMIN_USER:-$dbuser}"
|
|
admin_pass="${POSTGRES_ADMIN_PASSWORD:-$dbpass}"
|
|
in_container="${IN_CONTAINER:-0}"
|
|
drop_existing="${DROP_EXISTING_DB:-1}"
|
|
|
|
stream_dump() {
|
|
case "$dumpfile" in
|
|
*.gz) gunzip -c "$dumpfile" ;;
|
|
*) cat "$dumpfile" ;;
|
|
esac
|
|
}
|
|
|
|
if [ "$in_container" = "1" ]; then
|
|
cont_id=""
|
|
else
|
|
cont_id="$($COMPOSE ps -q "$POSTGRES_SERVICE" || true)"
|
|
if [ -z "$cont_id" ]; then
|
|
err "Postgres service '$POSTGRES_SERVICE' not running. Start it with: $COMPOSE up -d $POSTGRES_SERVICE"
|
|
exit 3
|
|
fi
|
|
fi
|
|
|
|
run_psql_sql() {
|
|
user="$1"
|
|
pass="$2"
|
|
database="$3"
|
|
sql="$4"
|
|
|
|
if [ "$in_container" = "1" ]; then
|
|
PGPASSWORD="$pass" psql -h "$host" -U "$user" -d "$database" -v ON_ERROR_STOP=1 -v psql_restricted=off -tAc "$sql"
|
|
else
|
|
docker exec -i "$cont_id" env PGPASSWORD="$pass" psql -U "$user" -d "$database" -v ON_ERROR_STOP=1 -v psql_restricted=off -tAc "$sql"
|
|
fi
|
|
}
|
|
|
|
createdb_with_admin() {
|
|
if [ "$in_container" = "1" ]; then
|
|
PGPASSWORD="$admin_pass" createdb -h "$host" -U "$admin_user" -O "$dbuser" "$dbname"
|
|
else
|
|
docker exec -i "$cont_id" env PGPASSWORD="$admin_pass" createdb -U "$admin_user" -O "$dbuser" "$dbname"
|
|
fi
|
|
}
|
|
|
|
dropdb_with_admin() {
|
|
terminate_sql="SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname='$dbname' AND pid <> pg_backend_pid();"
|
|
run_psql_sql "$admin_user" "$admin_pass" "$admin_db" "$terminate_sql" >/dev/null 2>&1 || true
|
|
|
|
if [ "$in_container" = "1" ]; then
|
|
PGPASSWORD="$admin_pass" dropdb -h "$host" -U "$admin_user" "$dbname"
|
|
else
|
|
docker exec -i "$cont_id" env PGPASSWORD="$admin_pass" dropdb -U "$admin_user" "$dbname"
|
|
fi
|
|
}
|
|
|
|
ensure_database() {
|
|
db_exists=$(run_psql_sql "$admin_user" "$admin_pass" "$admin_db" "SELECT 1 FROM pg_database WHERE datname='$dbname'" 2>/dev/null | tr -d '[:space:]' || true)
|
|
if [ "$db_exists" = "1" ]; then
|
|
if [ "$drop_existing" = "1" ]; then
|
|
log "Database '$dbname' already exists. Dropping before restore (DROP_EXISTING_DB=1)."
|
|
if ! dropdb_with_admin 2>/dev/null; then
|
|
err "Failed to drop existing database '$dbname'. Ensure the configured credentials have DROP DATABASE privileges or set DROP_EXISTING_DB=0 to skip dropping."
|
|
return 4
|
|
fi
|
|
else
|
|
log "Database '$dbname' already exists; continuing without dropping (DROP_EXISTING_DB=0)."
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
log "Creating database '$dbname' owned by '$dbuser' using user '$admin_user'."
|
|
if createdb_with_admin 2>/dev/null; then
|
|
return 0
|
|
fi
|
|
|
|
err "Failed to create database '$dbname' with user '$admin_user'. Ensure the user has CREATEDB privileges or create the database manually."
|
|
return 4
|
|
}
|
|
|
|
log "Restoring SQL dump into $dbname on host/service ${host}."
|
|
log "*** IMPORTANT: stop users/applications that use the database or run in maintenance mode."
|
|
|
|
if ! ensure_database; then
|
|
return 4
|
|
fi
|
|
|
|
if [ "$in_container" = "1" ]; then
|
|
stream_dump | env PGPASSWORD="$dbpass" psql -h "$host" -U "$dbuser" -d "$dbname" -v ON_ERROR_STOP=1 -v psql_restricted=off >/dev/null
|
|
else
|
|
stream_dump | docker exec -i "$cont_id" env PGPASSWORD="$dbpass" psql -U "$dbuser" -d "$dbname" -v ON_ERROR_STOP=1 -v psql_restricted=off >/dev/null
|
|
fi
|
|
|
|
log "Database restore finished."
|
|
}
|
|
|
|
case "${1:-}" in
|
|
list)
|
|
list_backups
|
|
;;
|
|
restore-volume)
|
|
if [ $# -ne 3 ]; then usage; exit 2; fi
|
|
restore_volume "$2" "$3"
|
|
;;
|
|
restore-db)
|
|
if [ $# -ne 5 ]; then usage; exit 2; fi
|
|
restore_db "$2" "$3" "$4" "$5"
|
|
;;
|
|
*)
|
|
usage
|
|
exit 2
|
|
;;
|
|
esac
|