#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" cd "$ROOT_DIR" BACKUP_SOURCE="${1:-}" STORAGE_HOST_PATH="${STORAGE_HOST_PATH:-./storage}" BACKUP_DIR="${BACKUP_DIR:-${STORAGE_HOST_PATH%/}/backups}" DATABASE_URL="${DATABASE_URL:-}" STOPPED_SERVICES=0 if [[ -z "$BACKUP_SOURCE" || ! -f "$BACKUP_SOURCE" ]]; then echo "Usage: scripts/restore.sh " >&2 exit 1 fi if [[ -z "$DATABASE_URL" ]]; then echo "DATABASE_URL ist erforderlich." >&2 exit 1 fi if ! command -v pg_restore >/dev/null 2>&1; then echo "pg_restore ist nicht verfuegbar." >&2 exit 1 fi mkdir -p "$STORAGE_HOST_PATH" "$BACKUP_DIR" python3 - "$BACKUP_SOURCE" <<'PY' from pathlib import Path import sys import zipfile path = Path(sys.argv[1]).resolve() required = {"manifest.json", "database.dump"} try: with zipfile.ZipFile(path) as archive: names = set(archive.namelist()) except zipfile.BadZipFile as exc: raise SystemExit(f"Ungueltige ZIP-Datei: {exc}") from exc missing = [name for name in required if name not in names] if not any(name == "storage/" or name.startswith("storage/") for name in names): missing.append("storage/") if missing: raise SystemExit(f"Backup unvollstaendig: {', '.join(missing)}") PY echo "WARNUNG: Dieser Restore kann Datenbank- und Storage-Daten dauerhaft ueberschreiben." echo "Backup: $BACKUP_SOURCE" read -r -p "Bitte exakt 'ICH VERSTEHE DAS RISIKO' eingeben: " confirmation if [[ "$confirmation" != "ICH VERSTEHE DAS RISIKO" ]]; then echo "Restore abgebrochen." exit 0 fi if command -v docker >/dev/null 2>&1; then read -r -p "Olympus-Dienste jetzt per docker compose stoppen? [y/N] " stop_reply if [[ "$stop_reply" =~ ^[Yy]$ ]]; then docker compose stop athena hermes STOPPED_SERVICES=1 fi fi WORK_DIR="$(mktemp -d "${BACKUP_DIR%/}/tmp.restore.XXXXXX")" trap 'rm -rf "$WORK_DIR"; if [[ "$STOPPED_SERVICES" -eq 1 ]]; then docker compose start hermes athena; fi' EXIT python3 - "$BACKUP_SOURCE" "$WORK_DIR" <<'PY' from pathlib import Path import sys import zipfile archive_path = Path(sys.argv[1]).resolve() target_dir = Path(sys.argv[2]).resolve() with zipfile.ZipFile(archive_path) as archive: archive.extractall(target_dir) PY SNAPSHOT_PATH="${BACKUP_DIR%/}/pre-restore-storage-$(date +%Y%m%d-%H%M%S).tar.gz" export STORAGE_HOST_PATH SNAPSHOT_PATH python3 - <<'PY' from pathlib import Path import os import tarfile storage = Path(os.environ["STORAGE_HOST_PATH"]).resolve() snapshot = Path(os.environ["SNAPSHOT_PATH"]).resolve() storage.mkdir(parents=True, exist_ok=True) with tarfile.open(snapshot, "w:gz") as archive: for path in sorted(storage.rglob("*")): if path.name == "backups" and path.is_dir(): continue if "backups" in path.parts: continue archive.add(path, arcname=path.relative_to(storage)) PY PG_RESTORE_URL="$(python3 - <<'PY' import os from urllib.parse import urlsplit, urlunsplit database_url = os.environ["DATABASE_URL"] parts = urlsplit(database_url) scheme = parts.scheme.split("+", 1)[0] print(urlunsplit((scheme, parts.netloc, parts.path, parts.query, parts.fragment))) PY )" pg_restore \ --clean \ --if-exists \ --no-owner \ --no-privileges \ --dbname="$PG_RESTORE_URL" \ "$WORK_DIR/database.dump" export WORK_DIR python3 - <<'PY' from pathlib import Path import os import shutil storage_target = Path(os.environ["STORAGE_HOST_PATH"]).resolve() storage_source = (Path(os.environ["WORK_DIR"]).resolve() / "storage") storage_target.mkdir(parents=True, exist_ok=True) for path in storage_target.iterdir(): if path.name == "backups": continue if path.is_dir(): shutil.rmtree(path) else: path.unlink() for source_path in sorted(storage_source.rglob("*")): relative = source_path.relative_to(storage_source) destination = storage_target / relative if source_path.is_dir(): destination.mkdir(parents=True, exist_ok=True) continue destination.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(source_path, destination) PY echo "Restore abgeschlossen. Storage-Snapshot: ${SNAPSHOT_PATH}"