151 lines
4.1 KiB
Bash
151 lines
4.1 KiB
Bash
#!/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 <backup-zip>" >&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}"
|