#!/usr/bin/env bash set -Eeuo pipefail umask 077 OUTPUT=${1:-} RECIPIENT_FILE=${AGE_RECIPIENT_FILE:-/etc/mike-ai/recovery.age-recipient} OPENWEBUI_VOLUME=${OPENWEBUI_VOLUME:-mike-ai_open-webui-data} OPENWEBUI_CONTAINER=${OPENWEBUI_CONTAINER:-mike-ai-open-webui} STACK_DIR=${STACK_DIR:-/opt/mike-ai/stack} die() { printf 'FEHLER: %s\n' "$*" >&2; exit 1; } [[ $EUID -eq 0 ]] || die "Bitte als root ausführen." [[ -n $OUTPUT ]] || die "Aufruf: $0 /sicheres/offhost-ziel/athena-recovery-YYYYMMDD.tar.age" [[ -s $RECIPIENT_FILE ]] || die "Age-Empfängerdatei fehlt: $RECIPIENT_FILE" command -v age >/dev/null || die "age ist nicht installiert." command -v docker >/dev/null || die "Docker ist nicht installiert." recipient=$(awk '/^age1[[:alnum:]]+$/ {print; exit}' "$RECIPIENT_FILE") [[ -n $recipient ]] || die "Keine gültige öffentliche age-Adresse gefunden." install -d -m 0700 "$(dirname "$OUTPUT")" [[ ! -e $OUTPUT ]] || die "Zieldatei existiert bereits: $OUTPUT" stage=$(mktemp -d /tmp/mike-ai-recovery.XXXXXX) sqlite_snapshot="" cleanup() { [[ -z $sqlite_snapshot ]] || rm -f -- "$sqlite_snapshot" rm -rf "$stage" } trap cleanup EXIT mkdir -p "$stage/rootfs" "$stage/payload" for source in \ /etc/mike-ai \ /root/mike-ai-install.env \ /usr/local/bin/runraid \ /opt/mike-ai/stack/docs \ /data/mike-ai-platform-context; do [[ -e $source ]] || continue rsync -aR "$source" "$stage/rootfs/" done tar -C "$stage/rootfs" -czf "$stage/payload/host-config.tar.gz" . volume_path=$(docker volume inspect -f '{{.Mountpoint}}' "$OPENWEBUI_VOLUME") [[ -s $volume_path/webui.db ]] || die "OpenWebUI-Datenbank fehlt oder ist leer." # OpenWebUI uses SQLite. The online backup API creates a transactionally # consistent snapshot while the service remains available. The archive omits # the live DB/WAL/SHM and stores that snapshot under the canonical DB name. [[ $(docker inspect -f '{{.State.Running}}' "$OPENWEBUI_CONTAINER" 2>/dev/null || true) == true ]] || \ die "OpenWebUI läuft nicht; Online-Datenbanksicherung nicht möglich." sqlite_snapshot="$volume_path/.mike-ai-recovery-webui.db" rm -f -- "$sqlite_snapshot" docker exec -i "$OPENWEBUI_CONTAINER" python - <<'PY' import os import sqlite3 source = "/app/backend/data/webui.db" snapshot = "/app/backend/data/.mike-ai-recovery-webui.db" if os.path.exists(snapshot): os.unlink(snapshot) with sqlite3.connect(source) as src, sqlite3.connect(snapshot) as dst: src.backup(dst) with sqlite3.connect(snapshot) as check: result = check.execute("PRAGMA integrity_check").fetchone() if not result or result[0] != "ok": raise SystemExit("SQLite integrity_check failed") PY [[ -s $sqlite_snapshot ]] || die "Konsistenter OpenWebUI-Snapshot wurde nicht erzeugt." tar -C "$volume_path" \ --exclude='./webui.db' \ --exclude='./webui.db-wal' \ --exclude='./webui.db-shm' \ --transform='s#\.mike-ai-recovery-webui\.db#webui.db#' \ -czf "$stage/payload/openwebui-data.tar.gz" . tar -tzf "$stage/payload/openwebui-data.tar.gz" ./webui.db >/dev/null 2>&1 || \ die "OpenWebUI-Archiv enthält den konsistenten Datenbanksnapshot nicht." source_commit=unknown [[ ! -s $STACK_DIR/.mike-ai-source-commit ]] || source_commit=$(<"$STACK_DIR/.mike-ai-source-commit") cat >"$stage/payload/METADATA" <SHA256SUMS tar -czf "$stage/bundle.tar.gz" \ host-config.tar.gz openwebui-data.tar.gz METADATA SHA256SUMS ) age -r "$recipient" -o "$OUTPUT.partial" "$stage/bundle.tar.gz" mv "$OUTPUT.partial" "$OUTPUT" chmod 0600 "$OUTPUT" printf 'RECOVERY_BUNDLE_OK %s\n' "$OUTPUT"