#!/usr/bin/env bash # Restore Athena's non-reproducible Docker state from the latest /data backup. set -Eeuo pipefail umask 077 ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" CHECK_ONLY=false if [[ ${1:-} == --check ]]; then CHECK_ONLY=true shift fi ARCHIVE=${1:-/data/docker-backups/athena-latest.tar.gz} die() { printf 'FEHLER: %s\n' "$*" >&2; exit 1; } [[ -s $ARCHIVE ]] || die "Backup fehlt: $ARCHIVE" validate_archive() { local listing normalized entry required gzip -t "$ARCHIVE" || die "Backup ist beschädigt: $ARCHIVE" listing=$(tar -tzf "$ARCHIVE") || die "Backup-Inhalt kann nicht gelesen werden." # docker-volume-backup stores the mounted sources below /backup. GNU tar # removes the leading slash while extracting; accept exactly that tree and # reject every unrelated or traversing member before extracting as root. while IFS= read -r entry; do normalized=${entry#./} normalized=${normalized#/} [[ $normalized == backup || $normalized == backup/* ]] || \ die "Unerwarteter Pfad im Backup: $entry" [[ /$normalized/ != */../* ]] || die "Unsicherer Pfad im Backup: $entry" done <<<"$listing" for required in backup/etc-mike-ai backup/volumes; do grep -Eq "^/?${required}(/|$)" <<<"$listing" || \ die "Backup enthält $required nicht." done } validate_archive if [[ $CHECK_ONLY == true ]]; then printf 'ATHENA_BACKUP_CHECK_OK %s\n' "$ARCHIVE" exit 0 fi [[ $EUID -eq 0 ]] || die "Bitte als root ausführen." command -v docker >/dev/null || die "Docker fehlt. Zuerst ./install.sh ausführen." work=$(mktemp -d /tmp/athena-restore.XXXXXX) trap 'rm -rf "$work"' EXIT # GNU tar removes the leading slash and extracts the single validated tree as # $work/backup. Keeping that root explicit avoids platform-dependent # --strip-components behaviour between GNU tar and bsdtar. tar -xzf "$ARCHIVE" -C "$work" backup_root=$work/backup [[ -d $backup_root/etc-mike-ai ]] || die "Backup enthält etc-mike-ai nicht." [[ -d $backup_root/volumes ]] || die "Backup enthält keine Docker-Volumes." # Stop only users of the restored volumes. WireGuard, SSH and networking stay up. for container in mike-ai-router mike-ai-profile-controller mike-ai-piper; do if [[ $(docker inspect -f '{{.State.Running}}' "$container" 2>/dev/null || true) == true ]]; then docker stop "$container" >/dev/null fi done install -d -m 0700 /etc/mike-ai rsync -a --delete "$backup_root/etc-mike-ai/" /etc/mike-ai/ restore_volume() { local volume=$1 source=$2 mountpoint [[ -d $source ]] || return 0 docker volume create "$volume" >/dev/null mountpoint=$(docker volume inspect -f '{{.Mountpoint}}' "$volume") [[ -n $mountpoint && -d $mountpoint ]] || die "Volume nicht zugreifbar: $volume" rsync -a --delete "$source/" "$mountpoint/" } restore_volume mike-ai_piper-data "$backup_root/volumes/piper-data" restore_volume mike-ai_router-state "$backup_root/volumes/router-state" restore_volume mike-ai_router-images "$backup_root/volumes/router-images" cd "$ROOT_DIR" ./manage.sh deploy core printf 'ATHENA_RESTORE_OK %s\n' "$ARCHIVE"