133 lines
4.1 KiB
Bash
133 lines
4.1 KiB
Bash
#!/bin/bash
|
|
set -euo pipefail
|
|
|
|
RESTORE_TESTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
BORG_CONTAINER="${BORG_CONTAINER:-borg-ui}"
|
|
BORG_RESTORE_HOST_ROOT="${BORG_RESTORE_HOST_ROOT:-/mnt/user/appdata/borg-ui/restore}"
|
|
BORG_PASSPHRASE_FILE_DEFAULT="${BORG_PASSPHRASE_FILE_DEFAULT:-/mnt/user/appdata/secrets/borg_repo_passphrase.txt}"
|
|
|
|
require_cmd() {
|
|
command -v "$1" >/dev/null 2>&1 || {
|
|
echo "Missing command: $1" >&2
|
|
exit 1
|
|
}
|
|
}
|
|
|
|
require_path() {
|
|
[ -e "$1" ] || {
|
|
echo "Missing path: $1" >&2
|
|
exit 1
|
|
}
|
|
}
|
|
|
|
require_borg_container() {
|
|
docker inspect "$BORG_CONTAINER" >/dev/null 2>&1 || {
|
|
echo "Missing Borg container: $BORG_CONTAINER" >&2
|
|
exit 1
|
|
}
|
|
[ "$(docker inspect -f '{{.State.Running}}' "$BORG_CONTAINER" 2>/dev/null)" = "true" ] || {
|
|
echo "Borg container is not running: $BORG_CONTAINER" >&2
|
|
exit 1
|
|
}
|
|
docker exec "$BORG_CONTAINER" test -r /data/borg.db >/dev/null 2>&1 || {
|
|
echo "Missing borg-ui database in container: $BORG_CONTAINER:/data/borg.db" >&2
|
|
exit 1
|
|
}
|
|
docker exec "$BORG_CONTAINER" test -r /local/secrets/borg_repo_passphrase.txt >/dev/null 2>&1 || {
|
|
echo "Missing Borg passphrase in container: $BORG_CONTAINER:/local/secrets/borg_repo_passphrase.txt" >&2
|
|
echo "Host path exists, but borg-ui must mount it as /local/secrets/borg_repo_passphrase.txt." >&2
|
|
exit 1
|
|
}
|
|
}
|
|
|
|
latest_archive_name() {
|
|
require_borg_container
|
|
docker exec -i "$BORG_CONTAINER" python3 - <<'PY'
|
|
import sqlite3
|
|
conn = sqlite3.connect('/data/borg.db')
|
|
cur = conn.cursor()
|
|
cur.execute("select archive_name from backup_jobs where status in ('completed', 'completed_with_warnings') order by created_at desc limit 1")
|
|
row = cur.fetchone()
|
|
if not row:
|
|
raise SystemExit("No completed borg archive found")
|
|
print(row[0])
|
|
PY
|
|
}
|
|
|
|
borg_repo_url() {
|
|
require_borg_container
|
|
docker exec -i "$BORG_CONTAINER" python3 - <<'PY'
|
|
import sqlite3
|
|
conn = sqlite3.connect('/data/borg.db')
|
|
cur = conn.cursor()
|
|
cur.execute("select path from repositories where path is not null and path != '' order by id asc limit 1")
|
|
row = cur.fetchone()
|
|
if not row:
|
|
raise SystemExit("No borg repository configured")
|
|
print(row[0])
|
|
PY
|
|
}
|
|
|
|
borg_extract() {
|
|
local extract_dir="$1"
|
|
shift
|
|
local paths=("$@")
|
|
require_borg_container
|
|
docker exec -i "$BORG_CONTAINER" python3 - "$extract_dir" "${paths[@]}" <<'PY'
|
|
import os, sys, subprocess
|
|
extract_dir = sys.argv[1]
|
|
paths = sys.argv[2:]
|
|
import sqlite3
|
|
conn = sqlite3.connect('/data/borg.db')
|
|
cur = conn.cursor()
|
|
cur.execute("select path from repositories where path is not null and path != '' order by id asc limit 1")
|
|
repo = cur.fetchone()[0]
|
|
cur.execute("select archive_name from backup_jobs where status in ('completed', 'completed_with_warnings') order by created_at desc limit 1")
|
|
archive = cur.fetchone()[0]
|
|
with open('/local/secrets/borg_repo_passphrase.txt', 'r', encoding='utf-8') as f:
|
|
os.environ['BORG_PASSPHRASE'] = f.read().strip()
|
|
known_hosts = '/data/known_hosts'
|
|
if os.path.exists(known_hosts):
|
|
os.environ.setdefault(
|
|
'BORG_RSH',
|
|
f'ssh -o UserKnownHostsFile={known_hosts} -o StrictHostKeyChecking=yes',
|
|
)
|
|
os.makedirs(extract_dir, exist_ok=True)
|
|
os.chdir(extract_dir)
|
|
subprocess.run(['borg', 'extract', f'{repo}::{archive}', *paths], check=True)
|
|
PY
|
|
}
|
|
|
|
write_report() {
|
|
local report_file="$1"
|
|
shift
|
|
mkdir -p "$(dirname "$report_file")"
|
|
cat > "$report_file"
|
|
}
|
|
|
|
cleanup_compose() {
|
|
local compose_file="$1"
|
|
if [ -f "$compose_file" ]; then
|
|
docker compose -f "$compose_file" down >/dev/null 2>&1 || true
|
|
fi
|
|
}
|
|
|
|
# Hilfsfunktion: bei Fehler-Exit Restore-Lab-Pfad nicht loeschen, sondern in
|
|
# einen `_failed/<service>-<date>-<pid>`-Pfad umbenennen, damit Post-Mortem
|
|
# moeglich bleibt. Aufrufer setzt vor Erfolg `RESTORE_SUCCESS=1`.
|
|
RESTORE_FAILED_ROOT="${RESTORE_FAILED_ROOT:-/mnt/user/backups/restore-lab/_failed}"
|
|
preserve_on_failure() {
|
|
local service="$1"
|
|
local path="$2"
|
|
if [ ! -e "$path" ]; then
|
|
return 0
|
|
fi
|
|
mkdir -p "$RESTORE_FAILED_ROOT"
|
|
local target="$RESTORE_FAILED_ROOT/${service}-$(date +%F)-$$"
|
|
if mv "$path" "$target" 2>/dev/null; then
|
|
echo "preserved failed restore data: $target" >&2
|
|
else
|
|
echo "failed to preserve restore data: $path -> $target" >&2
|
|
fi
|
|
}
|