extras/test_backup.sh — new unified test script (Kopia + Borg):
• Stops container, moves live data aside, restores latest backup,
compares restored vs live with diff -rq (content, not timestamps),
moves live data back and restarts container
• PASS = restore succeeded; diff output is informational (files changed
since last backup are normal)
• FAIL = restore command failed or target empty after restore
• --list flag, CLI service arg, interactive picker
• Handles both full-service dirs and sub-path sources (gaming-backup)
• Cleanup trap always restores live data even on error
• Sends ntfy notification on pass and fail
extras/backup_kopia.sh, backup_borg.sh, backup_gaming.sh:
• ntfy_send() + categorize_error() helpers added
• Each snapshot/archive failure captures stderr and categorizes:
disk full, remote unreachable, repository not found, wrong passphrase,
permission denied, unknown error
• Single ntfy notification at end: success (low priority) or failure
(urgent) with per-service failure reasons listed
• backup_borg.sh: changed 2>&1 | pipe to 2>"$_ERR" | so stdout logs
cleanly and stderr is captured for error categorization
services/backup.sh, borg-backup.sh, gaming-backup.sh:
• New ntfy prompt section in installer (URL + optional token)
• NTFY_URL / NTFY_TOKEN written to backup.conf
• test_backup.sh copied from extras/ into service dir
• Summary updated to show test_backup.sh commands and ntfy URL
https://claude.ai/code/session_019XgsQ13XKm4Zj3cNsDNwHj
135 lines
5.3 KiB
Bash
135 lines
5.3 KiB
Bash
#!/bin/bash
|
|
# extras/backup_gaming.sh — Kopia gaming-saves worker (no service downtime).
|
|
# Installed to ~/docker/gaming-backup/backup_gaming.sh by the gaming-backup installer.
|
|
#
|
|
# sudo ./backup_gaming.sh run a backup now
|
|
# sudo ./backup_gaming.sh snapshots list snapshots
|
|
# sudo ./backup_gaming.sh policy show retention/ignore policy
|
|
#
|
|
# Nothing is stopped: Minecraft worlds are flushed to disk (save-all) first.
|
|
# Reads backup.conf from the same directory.
|
|
set -uo pipefail
|
|
|
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
CONF="${BACKUP_CONF:-$HERE/backup.conf}"
|
|
[ -f "$CONF" ] || { echo "Config not found: $CONF (re-run: sudo setup.sh gaming-backup)"; exit 1; }
|
|
# shellcheck source=/dev/null
|
|
source "$CONF"
|
|
|
|
export KOPIA_PASSWORD
|
|
HOST="$(hostname -s 2>/dev/null || hostname)"
|
|
log() { echo "[$(date '+%F %T')] $*"; }
|
|
k() { "$KOPIA" --config-file="$KOPIA_CONFIG" "$@"; }
|
|
|
|
ntfy_send() {
|
|
local title="$1" msg="$2" priority="${3:-default}" tags="${4:-}"
|
|
[ -z "${NTFY_URL:-}" ] && return 0
|
|
local -a _args=(-fsS -o /dev/null)
|
|
_args+=(-H "Title: $title" -H "Priority: $priority")
|
|
[ -n "$tags" ] && _args+=(-H "Tags: $tags")
|
|
[ -n "${NTFY_TOKEN:-}" ] && _args+=(-H "Authorization: Bearer $NTFY_TOKEN")
|
|
curl "${_args[@]}" -d "$msg" "$NTFY_URL" 2>/dev/null || true
|
|
}
|
|
|
|
categorize_error() {
|
|
local txt="$1"
|
|
if echo "$txt" | grep -qi "no space left\|disk quota exceeded"; then
|
|
echo "disk full — backup destination is out of space"
|
|
elif echo "$txt" | grep -qi "connection refused\|network unreachable\|no route to host\|ssh.*connect\|timed out\|host unreachable"; then
|
|
echo "remote unreachable — check network / destination host"
|
|
elif echo "$txt" | grep -qi "repository.*not.*exist\|not a valid kopia\|not connected"; then
|
|
echo "repository not found — re-run the gaming-backup installer"
|
|
elif echo "$txt" | grep -qi "passphrase\|wrong key\|cannot decrypt"; then
|
|
echo "wrong passphrase — check backup.conf"
|
|
elif echo "$txt" | grep -qi "permission denied\|access denied"; then
|
|
echo "permission denied — check file permissions"
|
|
else
|
|
echo "error — see system logs on $HOST"
|
|
fi
|
|
}
|
|
|
|
if ! k repository status >/dev/null 2>&1; then
|
|
log "ERROR: not connected to a repository — re-run the gaming-backup service"
|
|
ntfy_send "✗ Gaming backup FAILED" \
|
|
"$HOST: cannot connect to Kopia repository — re-run gaming-backup installer" \
|
|
"urgent" "rotating_light"
|
|
exit 1
|
|
fi
|
|
|
|
case "${1:-run}" in
|
|
snapshots) k snapshot list; exit 0 ;;
|
|
policy) k policy show --global; exit 0 ;;
|
|
esac
|
|
|
|
log "===== Gaming backup starting ====="
|
|
|
|
if [ -n "${MC_BASE_DIR:-}" ] && command -v docker >/dev/null 2>&1; then
|
|
_flushed=0
|
|
for d in "$MC_BASE_DIR"/*/; do
|
|
[ -f "${d}Dockerfile" ] && grep -qs itzg "${d}Dockerfile" || continue
|
|
name="$(basename "$d")"
|
|
if docker ps --format '{{.Names}}' 2>/dev/null | grep -qx "$name"; then
|
|
log "Flushing Minecraft world '$name' (save-all)..."
|
|
docker exec "$name" mc-send-to-console save-all flush 2>/dev/null \
|
|
|| docker exec "$name" rcon-cli save-all 2>/dev/null || true
|
|
_flushed=1
|
|
fi
|
|
done
|
|
[ "$_flushed" = 1 ] && sleep 5
|
|
fi
|
|
|
|
rc=0
|
|
declare -a FAILED_LABELS=()
|
|
_ERR="$(mktemp)"
|
|
trap 'rm -f "$_ERR"' EXIT
|
|
|
|
snap() {
|
|
local label="$1" path="$2"
|
|
if [ -z "$path" ] || [ ! -e "$path" ]; then
|
|
log "skip $label — not found: ${path:-<unset>}"; return
|
|
fi
|
|
log "Snapshotting $label: $path"
|
|
if ! k snapshot create --description="gaming: $label" "$path" 2>"$_ERR"; then
|
|
_reason="$(categorize_error "$(cat "$_ERR")")"
|
|
log "WARNING: snapshot failed for $label — $_reason"
|
|
FAILED_LABELS+=("$label: $_reason")
|
|
rc=1
|
|
fi
|
|
}
|
|
|
|
if [ -n "${MC_BASE_DIR:-}" ]; then
|
|
for d in "$MC_BASE_DIR"/*/; do
|
|
[ -f "${d}Dockerfile" ] && grep -qs itzg "${d}Dockerfile" && [ -d "${d}data" ] || continue
|
|
nm="$(basename "$d")"
|
|
case "$nm" in minecraft*) lbl="$nm" ;; *) lbl="minecraft-$nm" ;; esac
|
|
snap "$lbl" "${d}data"
|
|
done
|
|
fi
|
|
[ "${BACKUP_SAVES:-no}" = yes ] && snap "emulator-saves" "$GAME_STORAGE_DIR/saves"
|
|
[ "${BACKUP_STEAM:-no}" = yes ] && snap "steam-userdata" "$GAME_STORAGE_DIR/steam"
|
|
[ "${BACKUP_MEDIA:-no}" = yes ] && snap "es-de-media" "$GAME_STORAGE_DIR/media"
|
|
[ "${BACKUP_WOLF:-no}" = yes ] && snap "wolf-state" "$WOLF_STATE_DIR"
|
|
|
|
if [ "${REMOTE_TYPE:-none}" != "none" ] && [ -n "${REMOTE_TYPE:-}" ]; then
|
|
log "Mirroring repository to remote ($REMOTE_TYPE)..."
|
|
# shellcheck disable=SC2086
|
|
if ! k repository sync-to "$REMOTE_TYPE" $REMOTE_ARGS 2>"$_ERR"; then
|
|
_reason="$(categorize_error "$(cat "$_ERR")")"
|
|
log "WARNING: remote mirror failed — $_reason"
|
|
FAILED_LABELS+=("mirror: $_reason")
|
|
rc=1
|
|
fi
|
|
fi
|
|
|
|
if [ "$rc" -eq 0 ]; then
|
|
log "===== Gaming backup complete ====="
|
|
ntfy_send "✓ Gaming backup complete" "$HOST: all saves backed up successfully" \
|
|
"low" "white_check_mark"
|
|
else
|
|
log "===== Gaming backup finished WITH WARNINGS ====="
|
|
_ntfy_msg="$HOST: gaming backup failures:"
|
|
for _s in "${FAILED_LABELS[@]}"; do _ntfy_msg+=$'\n'"• $_s"; done
|
|
ntfy_send "✗ Gaming backup FAILED" "$_ntfy_msg" "urgent" "rotating_light"
|
|
fi
|
|
exit "$rc"
|