#!/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 # ./restore.sh restore-db # # 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 < [args] Commands: list List dated backup directories under $BACKUPS_DIR restore-volume 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 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