#!/usr/bin/env bash # scripts/restore.sh — Database restore helper for Orion and Gitea # # Usage: # bash scripts/restore.sh orion ~/backups/orion/daily/orion_20260214_030000.sql.gz # bash scripts/restore.sh gitea ~/backups/gitea/daily/gitea_20260214_030000.sql.gz # # What it does: # 1. Stops app containers (keeps DB running) # 2. Drops and recreates the database # 3. Restores from the .sql.gz backup # 4. Runs Alembic migrations (Orion only) # 5. Restarts all containers set -euo pipefail # ============================================================================= # Configuration # ============================================================================= ORION_APP_DIR="${HOME}/apps/orion" # ============================================================================= # Functions # ============================================================================= log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" } usage() { echo "Usage: $0 " echo "" echo " target: 'orion' or 'gitea'" echo " backup-file: path to .sql.gz file" echo "" echo "Examples:" echo " $0 orion ~/backups/orion/daily/orion_20260214_030000.sql.gz" echo " $0 gitea ~/backups/gitea/daily/gitea_20260214_030000.sql.gz" exit 1 } restore_orion() { local backup_file="$1" local container="orion-db-1" local db_name="orion_db" local db_user="orion_user" log "=== Restoring Orion database ===" # Stop app containers (keep DB and Redis running) log "Stopping Orion app containers..." cd "${ORION_APP_DIR}" docker compose --profile full stop api celery-worker celery-beat flower redis-exporter 2>/dev/null || true # Drop and recreate database log "Dropping and recreating ${db_name}..." docker exec "${container}" psql -U "${db_user}" -d postgres -c \ "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '${db_name}' AND pid <> pg_backend_pid();" 2>/dev/null || true docker exec "${container}" dropdb -U "${db_user}" --if-exists "${db_name}" docker exec "${container}" createdb -U "${db_user}" "${db_name}" # Restore log "Restoring from ${backup_file}..." gunzip -c "${backup_file}" | docker exec -i "${container}" psql -U "${db_user}" -d "${db_name}" --quiet # Run migrations log "Running Alembic migrations..." docker compose --profile full start api 2>/dev/null || \ docker compose --profile full up -d api # Wait for API container to be healthy before running migrations log "Waiting for API container to be ready..." for i in $(seq 1 12); do if docker compose --profile full exec -T db pg_isready -U orion_user -d orion_db > /dev/null 2>&1; then log "Database is ready (attempt $i/12)" break fi [ "$i" -eq 12 ] && { log "WARNING: database may not be ready, attempting migration anyway"; } sleep 5 done docker compose --profile full exec -T -e PYTHONPATH=/app api python -m alembic upgrade heads # Restart all log "Restarting all services..." docker compose --profile full up -d log "=== Orion restore complete ===" } restore_gitea() { local backup_file="$1" local container="gitea-db" local db_name="gitea" local db_user="gitea" local gitea_dir="${HOME}/gitea" log "=== Restoring Gitea database ===" # Stop Gitea container (keep DB running) log "Stopping Gitea..." cd "${gitea_dir}" docker compose stop gitea 2>/dev/null || true # Drop and recreate database log "Dropping and recreating ${db_name}..." docker exec "${container}" psql -U "${db_user}" -d postgres -c \ "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '${db_name}' AND pid <> pg_backend_pid();" 2>/dev/null || true docker exec "${container}" dropdb -U "${db_user}" --if-exists "${db_name}" docker exec "${container}" createdb -U "${db_user}" "${db_name}" # Restore log "Restoring from ${backup_file}..." gunzip -c "${backup_file}" | docker exec -i "${container}" psql -U "${db_user}" -d "${db_name}" --quiet # Restart Gitea log "Restarting Gitea..." docker compose up -d log "=== Gitea restore complete ===" } # ============================================================================= # Main # ============================================================================= if [ $# -lt 2 ]; then usage fi TARGET="$1" BACKUP_FILE="$2" # Validate backup file if [ ! -f "${BACKUP_FILE}" ]; then log "ERROR: Backup file not found: ${BACKUP_FILE}" exit 1 fi if [[ ! "${BACKUP_FILE}" == *.sql.gz ]]; then log "ERROR: Expected a .sql.gz file, got: ${BACKUP_FILE}" exit 1 fi # Confirm log "WARNING: This will DROP and RECREATE the ${TARGET} database!" log "Backup file: ${BACKUP_FILE}" read -rp "Continue? (y/N) " confirm if [[ "${confirm}" != [yY] ]]; then log "Aborted." exit 0 fi case "${TARGET}" in orion) restore_orion "${BACKUP_FILE}" ;; gitea) restore_gitea "${BACKUP_FILE}" ;; *) log "ERROR: Unknown target '${TARGET}'. Use 'orion' or 'gitea'." usage ;; esac