#!/usr/bin/env bash # Build a browser-downloadable, encrypted archive of irreplaceable Athena data. set -Eeuo pipefail umask 077 OUTPUT_DIR=${ATHENA_EXPORT_DIR:-/data/emergency-backups} RECIPIENT_FILE=${ATHENA_AGE_RECIPIENT_FILE:-/etc/mike-ai/recovery.age-recipient} STATE=/var/lib/mike-ai-disaster-backup/latest KEEP=${ATHENA_EXPORT_KEEP:-5} log() { printf '\n==> %s\n' "$*"; } die() { printf 'FEHLER: %s\n' "$*" >&2; exit 1; } [[ $EUID -eq 0 ]] || die "Bitte als root ausführen." [[ -s $RECIPIENT_FILE ]] || die "Age-Empfänger fehlt: $RECIPIENT_FILE" [[ $KEEP =~ ^[1-9][0-9]*$ ]] || die "ATHENA_EXPORT_KEEP muss positiv sein." for command in age zstd tar docker sha256sum flock; do command -v "$command" >/dev/null || die "$command fehlt." done exec 9>/run/lock/athena-export-backup.lock flock -n 9 || die "Ein exportierbares Backup läuft bereits." install -d -m 0755 "$OUTPUT_DIR" install -d -m 0700 "$STATE" log "Aktuellen Docker-Zustand sichern" docker exec mike-ai-backup backup latest=$(readlink -f /data/docker-backups/athena-latest.tar.gz) [[ -s $latest ]] || die "Docker-Zustandsbackup fehlt." gzip -t "$latest" || die "Docker-Zustandsbackup ist beschädigt." install -m 0600 "$latest" "$STATE/docker-state.tar.gz" stamp=$(date -u +%Y-%m-%dT%H-%M-%SZ) name="athena-portable-$stamp.tar.zst.age" partial="$OUTPUT_DIR/.$name.partial" target="$OUTPUT_DIR/$name" list=$(mktemp /tmp/athena-export-list.XXXXXX) trap 'rm -f "$list" "$partial"' EXIT # The encrypted export is roughly the size of its predecessor. Keeping all # generations until after writing the next one requires one extra archive of # free space and can deadlock a full backup disk. Release exactly one slot # before writing when the configured retention is already reached. If the new # export fails, KEEP-1 valid generations remain available. mapfile -t existing < <(find "$OUTPUT_DIR" -maxdepth 1 -type f \ -name 'athena-portable-*.tar.zst.age' -printf '%T@ %p\n' \ | sort -rn | cut -d' ' -f2-) while ((${#existing[@]} >= KEEP)); do last_index=$((${#existing[@]} - 1)) oldest=${existing[$last_index]} rm -f -- "$oldest" "$oldest.sha256" unset 'existing[last_index]' existing=("${existing[@]}") done add_path() { local path=${1#/} [[ ! -e /$path ]] || printf '%s\0' "$path" >>"$list" } # Reproducible model/HF caches are deliberately omitted. Everything below is # either a host configuration, project source, user input or generated result. add_path /etc/mike-ai add_path /opt/mike-ai add_path /var/lib/mike-ai-disaster-backup/latest add_path /data/voice/applio/logs add_path /data/voice/applio/datasets add_path /data/voice/applio/config.json # Preserve user-created Applio state while continuing to omit the large, # reproducible predictor/embedder/base-model caches. add_path /data/voice/applio/mikes-applio-ui add_path /data/voice/applio/models/pretraineds/custom add_path /data/voice/omnivoice/output add_path /data/voice/xvc/output add_path /data/voice/studio add_path /data/music add_path /data/audio add_path /data/llama-dashboard add_path /data/mike-ai-operator add_path /data/benchmarks add_path /data/model-benchmarks add_path /data/image-comparison add_path /data/backups add_path /data/deploy-backups log "Portables, verschlüsseltes Backup erzeugen" tar --create --numeric-owner --acls --xattrs -C / --null --files-from="$list" \ | zstd -T0 -3 \ | age -R "$RECIPIENT_FILE" -o "$partial" chmod 0644 "$partial" mv "$partial" "$target" sha256sum "$target" >"$target.sha256" chmod 0644 "$target.sha256" log "Nur die letzten $KEEP Generationen behalten" mapfile -t old < <(find "$OUTPUT_DIR" -maxdepth 1 -type f \ -name 'athena-portable-*.tar.zst.age' -printf '%T@ %p\n' | sort -rn | tail -n +$((KEEP + 1)) | cut -d' ' -f2-) for archive in "${old[@]}"; do rm -f -- "$archive" "$archive.sha256" done printf 'ATHENA_EXPORT_BACKUP_OK file=%s bytes=%s\n' "$target" "$(stat -c %s "$target")"