Major additions: onboarding tour system, correlation-id middleware, media error handler, restore script, env validation script, Dockerignore files. Updates across 70+ admin components for improved UX and error handling. Bunker Admin
281 lines
9.9 KiB
Bash
Executable File
281 lines
9.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# =============================================================================
|
|
# Changemaker Lite V2 — Restore Script
|
|
# Restores from a backup archive created by backup.sh.
|
|
# Usage: ./scripts/restore.sh --archive PATH [--skip-db] [--skip-uploads]
|
|
# [--skip-listmonk] [--dry-run] [--force]
|
|
# =============================================================================
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
|
|
|
# --- Colors ---
|
|
RED='\033[0;31m'
|
|
YELLOW='\033[1;33m'
|
|
GREEN='\033[0;32m'
|
|
CYAN='\033[0;36m'
|
|
NC='\033[0m'
|
|
|
|
error() { echo -e "${RED}ERROR:${NC} $1"; }
|
|
warn() { echo -e "${YELLOW}WARN:${NC} $1"; }
|
|
info() { echo -e "${CYAN}INFO:${NC} $1"; }
|
|
ok() { echo -e "${GREEN}OK:${NC} $1"; }
|
|
|
|
# --- Defaults ---
|
|
ARCHIVE=""
|
|
SKIP_DB=false
|
|
SKIP_UPLOADS=false
|
|
SKIP_LISTMONK=false
|
|
DRY_RUN=false
|
|
FORCE=false
|
|
|
|
# --- Parse args ---
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--archive) ARCHIVE="$2"; shift 2 ;;
|
|
--skip-db) SKIP_DB=true; shift ;;
|
|
--skip-uploads) SKIP_UPLOADS=true; shift ;;
|
|
--skip-listmonk) SKIP_LISTMONK=true; shift ;;
|
|
--dry-run) DRY_RUN=true; shift ;;
|
|
--force) FORCE=true; shift ;;
|
|
--help)
|
|
echo "Usage: $0 --archive PATH [OPTIONS]"
|
|
echo ""
|
|
echo "Options:"
|
|
echo " --archive PATH Path to backup .tar.gz archive (required)"
|
|
echo " --skip-db Skip main PostgreSQL restore"
|
|
echo " --skip-uploads Skip uploads directory restore"
|
|
echo " --skip-listmonk Skip Listmonk database restore"
|
|
echo " --dry-run Validate archive without restoring"
|
|
echo " --force Skip confirmation prompt"
|
|
echo ""
|
|
exit 0 ;;
|
|
*) error "Unknown option: $1"; exit 1 ;;
|
|
esac
|
|
done
|
|
|
|
if [[ -z "$ARCHIVE" ]]; then
|
|
error "Missing --archive PATH argument"
|
|
echo " Usage: $0 --archive /path/to/backup.tar.gz"
|
|
exit 1
|
|
fi
|
|
|
|
if [[ ! -f "$ARCHIVE" ]]; then
|
|
error "Archive not found: $ARCHIVE"
|
|
exit 1
|
|
fi
|
|
|
|
# --- Load .env ---
|
|
if [ -f "$PROJECT_DIR/.env" ]; then
|
|
while IFS='=' read -r key value; do
|
|
[[ -z "$key" || "$key" =~ ^[[:space:]]*# ]] && continue
|
|
key="$(echo "$key" | xargs)"
|
|
value="${value%\"}" ; value="${value#\"}"
|
|
value="${value%\'}" ; value="${value#\'}"
|
|
if [[ "$key" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]]; then
|
|
export "$key=$value"
|
|
fi
|
|
done < "$PROJECT_DIR/.env"
|
|
fi
|
|
|
|
# --- Derived vars ---
|
|
PG_CONTAINER="${PG_CONTAINER:-changemaker-v2-postgres}"
|
|
PG_USER="${V2_POSTGRES_USER:-changemaker}"
|
|
PG_DB="${V2_POSTGRES_DB:-changemaker_v2}"
|
|
LISTMONK_PG_CONTAINER="${LISTMONK_PG_CONTAINER:-listmonk-db}"
|
|
LISTMONK_PG_USER="${LISTMONK_DB_USER:-listmonk}"
|
|
LISTMONK_PG_DB="${LISTMONK_DB_NAME:-listmonk}"
|
|
UPLOADS_DIR="${PROJECT_DIR}/assets/uploads"
|
|
|
|
APP_CONTAINERS="changemaker-v2-api changemaker-v2-admin changemaker-media-api changemaker-v2-nginx"
|
|
|
|
echo ""
|
|
echo "=========================================="
|
|
echo " Changemaker Lite V2 — Restore"
|
|
echo "=========================================="
|
|
echo ""
|
|
|
|
# --- 1. Extract and validate archive ---
|
|
info "Extracting archive: $(basename "$ARCHIVE")"
|
|
TEMP_DIR="$(mktemp -d)"
|
|
trap 'rm -rf "$TEMP_DIR"' EXIT
|
|
|
|
tar -xzf "$ARCHIVE" -C "$TEMP_DIR"
|
|
|
|
# Find the extracted backup directory (single child of temp dir)
|
|
BACKUP_DIR="$(find "$TEMP_DIR" -mindepth 1 -maxdepth 1 -type d | head -1)"
|
|
if [[ -z "$BACKUP_DIR" ]]; then
|
|
error "Archive does not contain a backup directory"
|
|
exit 1
|
|
fi
|
|
|
|
# Validate manifest
|
|
MANIFEST="${BACKUP_DIR}/manifest.json"
|
|
if [[ ! -f "$MANIFEST" ]]; then
|
|
error "manifest.json not found in archive"
|
|
exit 1
|
|
fi
|
|
|
|
ok "Manifest found"
|
|
echo " Backup: $(python3 -c "import json,sys; m=json.load(open(sys.argv[1])); print(m.get('backup_name','unknown'))" "$MANIFEST" 2>/dev/null || basename "$BACKUP_DIR")"
|
|
|
|
# --- 2. Verify checksums ---
|
|
info "Verifying file integrity..."
|
|
INTEGRITY_OK=true
|
|
while IFS= read -r entry; do
|
|
FILE=$(echo "$entry" | python3 -c "import json,sys; print(json.load(sys.stdin)['file'])")
|
|
EXPECTED=$(echo "$entry" | python3 -c "import json,sys; print(json.load(sys.stdin)['sha256'])")
|
|
FILE_PATH="${BACKUP_DIR}/${FILE}"
|
|
|
|
if [[ ! -f "$FILE_PATH" ]]; then
|
|
warn "Missing file: $FILE"
|
|
continue
|
|
fi
|
|
|
|
ACTUAL="$(sha256sum "$FILE_PATH" 2>/dev/null | cut -d' ' -f1 || shasum -a 256 "$FILE_PATH" | cut -d' ' -f1)"
|
|
if [[ "$EXPECTED" != "$ACTUAL" ]]; then
|
|
error "Checksum mismatch: $FILE"
|
|
INTEGRITY_OK=false
|
|
else
|
|
ok " $FILE checksum verified"
|
|
fi
|
|
done < <(python3 -c "import json,sys; [print(json.dumps(f)) for f in json.load(open(sys.argv[1]))['files']]" "$MANIFEST")
|
|
|
|
if ! $INTEGRITY_OK; then
|
|
error "Archive integrity check failed. Aborting."
|
|
exit 1
|
|
fi
|
|
|
|
# --- Show what will be restored ---
|
|
echo ""
|
|
info "Components to restore:"
|
|
[[ -f "${BACKUP_DIR}/v2-postgres.sql.gz" ]] && ! $SKIP_DB && echo " - V2 PostgreSQL (${PG_DB})"
|
|
[[ -f "${BACKUP_DIR}/gancio-postgres.sql.gz" ]] && ! $SKIP_DB && echo " - Gancio PostgreSQL"
|
|
[[ -f "${BACKUP_DIR}/listmonk-postgres.sql.gz" ]] && ! $SKIP_LISTMONK && echo " - Listmonk PostgreSQL (${LISTMONK_PG_DB})"
|
|
[[ -f "${BACKUP_DIR}/uploads.tar.gz" ]] && ! $SKIP_UPLOADS && echo " - Uploads directory"
|
|
echo ""
|
|
|
|
if $DRY_RUN; then
|
|
ok "Dry run complete. Archive is valid."
|
|
exit 0
|
|
fi
|
|
|
|
# --- Confirmation ---
|
|
if ! $FORCE; then
|
|
echo -e "${RED}WARNING: This will OVERWRITE existing databases and uploads!${NC}"
|
|
read -p "Continue with restore? (y/N): " CONFIRM
|
|
if [[ ! "$CONFIRM" =~ ^[Yy]$ ]]; then
|
|
echo "Aborted."
|
|
exit 0
|
|
fi
|
|
fi
|
|
|
|
# --- 3. Stop application containers ---
|
|
info "Stopping application containers..."
|
|
for container in $APP_CONTAINERS; do
|
|
if docker ps --format '{{.Names}}' | grep -q "^${container}$"; then
|
|
docker stop "$container" >/dev/null 2>&1 && echo " Stopped $container" || true
|
|
fi
|
|
done
|
|
echo ""
|
|
|
|
# --- 4. Restore V2 PostgreSQL ---
|
|
if [[ -f "${BACKUP_DIR}/v2-postgres.sql.gz" ]] && ! $SKIP_DB; then
|
|
info "Restoring V2 PostgreSQL (${PG_DB})..."
|
|
if docker ps --format '{{.Names}}' | grep -q "^${PG_CONTAINER}$"; then
|
|
# Terminate existing connections and recreate database
|
|
docker exec "$PG_CONTAINER" psql -U "$PG_USER" -d postgres -c \
|
|
"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname='${PG_DB}' AND pid <> pg_backend_pid();" >/dev/null 2>&1 || true
|
|
docker exec "$PG_CONTAINER" psql -U "$PG_USER" -d postgres -c \
|
|
"DROP DATABASE IF EXISTS \"${PG_DB}\";" >/dev/null 2>&1
|
|
docker exec "$PG_CONTAINER" psql -U "$PG_USER" -d postgres -c \
|
|
"CREATE DATABASE \"${PG_DB}\";" >/dev/null 2>&1
|
|
# Restore dump
|
|
gunzip -c "${BACKUP_DIR}/v2-postgres.sql.gz" | docker exec -i "$PG_CONTAINER" psql -U "$PG_USER" -d "$PG_DB" >/dev/null 2>&1
|
|
ok "V2 PostgreSQL restored"
|
|
else
|
|
error "Container ${PG_CONTAINER} not running"
|
|
fi
|
|
fi
|
|
|
|
# --- 4b. Restore Gancio PostgreSQL ---
|
|
if [[ -f "${BACKUP_DIR}/gancio-postgres.sql.gz" ]] && ! $SKIP_DB; then
|
|
info "Restoring Gancio PostgreSQL..."
|
|
if docker ps --format '{{.Names}}' | grep -q "^${PG_CONTAINER}$"; then
|
|
docker exec "$PG_CONTAINER" psql -U "$PG_USER" -d postgres -c \
|
|
"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname='gancio' AND pid <> pg_backend_pid();" >/dev/null 2>&1 || true
|
|
docker exec "$PG_CONTAINER" psql -U "$PG_USER" -d postgres -c \
|
|
"DROP DATABASE IF EXISTS gancio;" >/dev/null 2>&1
|
|
docker exec "$PG_CONTAINER" psql -U "$PG_USER" -d postgres -c \
|
|
"CREATE DATABASE gancio;" >/dev/null 2>&1
|
|
gunzip -c "${BACKUP_DIR}/gancio-postgres.sql.gz" | docker exec -i "$PG_CONTAINER" psql -U "$PG_USER" -d gancio >/dev/null 2>&1
|
|
ok "Gancio PostgreSQL restored"
|
|
else
|
|
warn "V2 PostgreSQL container not running, skipping Gancio restore"
|
|
fi
|
|
fi
|
|
|
|
# --- 5. Restore Listmonk PostgreSQL ---
|
|
if [[ -f "${BACKUP_DIR}/listmonk-postgres.sql.gz" ]] && ! $SKIP_LISTMONK; then
|
|
info "Restoring Listmonk PostgreSQL (${LISTMONK_PG_DB})..."
|
|
if docker ps --format '{{.Names}}' | grep -q "^${LISTMONK_PG_CONTAINER}$"; then
|
|
docker exec "$LISTMONK_PG_CONTAINER" psql -U "$LISTMONK_PG_USER" -d postgres -c \
|
|
"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname='${LISTMONK_PG_DB}' AND pid <> pg_backend_pid();" >/dev/null 2>&1 || true
|
|
docker exec "$LISTMONK_PG_CONTAINER" psql -U "$LISTMONK_PG_USER" -d postgres -c \
|
|
"DROP DATABASE IF EXISTS \"${LISTMONK_PG_DB}\";" >/dev/null 2>&1
|
|
docker exec "$LISTMONK_PG_CONTAINER" psql -U "$LISTMONK_PG_USER" -d postgres -c \
|
|
"CREATE DATABASE \"${LISTMONK_PG_DB}\";" >/dev/null 2>&1
|
|
gunzip -c "${BACKUP_DIR}/listmonk-postgres.sql.gz" | docker exec -i "$LISTMONK_PG_CONTAINER" psql -U "$LISTMONK_PG_USER" -d "$LISTMONK_PG_DB" >/dev/null 2>&1
|
|
ok "Listmonk PostgreSQL restored"
|
|
else
|
|
warn "Container ${LISTMONK_PG_CONTAINER} not running, skipping"
|
|
fi
|
|
fi
|
|
|
|
# --- 6. Restore uploads ---
|
|
if [[ -f "${BACKUP_DIR}/uploads.tar.gz" ]] && ! $SKIP_UPLOADS; then
|
|
info "Restoring uploads..."
|
|
UPLOADS_PARENT="$(dirname "$UPLOADS_DIR")"
|
|
mkdir -p "$UPLOADS_PARENT"
|
|
tar -xzf "${BACKUP_DIR}/uploads.tar.gz" -C "$UPLOADS_PARENT"
|
|
ok "Uploads restored to $UPLOADS_DIR"
|
|
fi
|
|
|
|
echo ""
|
|
|
|
# --- 7. Run migrations (catch up if code is ahead of backup) ---
|
|
info "Running Prisma migrations..."
|
|
cd "$PROJECT_DIR"
|
|
docker compose run --rm --no-deps --entrypoint "" api npx prisma migrate deploy 2>&1 \
|
|
&& ok "Migrations applied" \
|
|
|| warn "Migration apply had warnings"
|
|
|
|
# --- 8. Restart application containers ---
|
|
info "Restarting application containers..."
|
|
docker compose up -d 2>&1 | tail -5
|
|
echo ""
|
|
|
|
# --- 9. Health check ---
|
|
info "Waiting for API health check..."
|
|
HEALTHY=false
|
|
for i in $(seq 1 20); do
|
|
if docker compose exec -T api wget -q --spider http://localhost:4000/api/health 2>/dev/null; then
|
|
HEALTHY=true
|
|
break
|
|
fi
|
|
sleep 3
|
|
done
|
|
|
|
if $HEALTHY; then
|
|
ok "API is healthy"
|
|
else
|
|
warn "API health check timed out (60s). Check logs: docker compose logs api"
|
|
fi
|
|
|
|
echo ""
|
|
echo "=========================================="
|
|
echo -e " ${GREEN}Restore complete!${NC}"
|
|
echo " Archive: $(basename "$ARCHIVE")"
|
|
echo "=========================================="
|