initial commit

This commit is contained in:
2025-11-25 12:27:53 +03:30
commit f9d16ab078
102 changed files with 11156 additions and 0 deletions

219
scripts/backup/restore.sh Executable file
View File

@@ -0,0 +1,219 @@
#!/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