feat: backup test script, ntfy notifications, and error categorization

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
This commit is contained in:
Claude
2026-06-04 20:03:30 +00:00
parent 925ded308f
commit 862ecf10e9
7 changed files with 627 additions and 20 deletions
+49 -7
View File
@@ -20,12 +20,40 @@ source "$CONF"
ACTUAL_USER="${SUDO_USER:-${USER:-$(id -un)}}"
ACTUAL_HOME="$(getent passwd "$ACTUAL_USER" 2>/dev/null | cut -d: -f6 || echo "/home/$ACTUAL_USER")"
DOCKER_DIR="$ACTUAL_HOME/docker"
HOST="$(hostname -s 2>/dev/null || hostname)"
log() { echo "[$(date '+%F %T')] $*"; }
repo_for() { local var="DEST_${1}_REPO"; echo "${!var:-}"; }
pass_for() { local var="DEST_${1}_PASSPHRASE"; echo "${!var:-}"; }
dest_for_svc() { local var="SVC_${1//-/_}"; echo "${!var:-${DEST_DEFAULT:-default}}"; }
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.*does not exist\|not a borg\|is not a valid"; then
echo "repository not found — re-run the backup installer"
elif echo "$txt" | grep -qi "passphrase\|wrong key\|bad 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
}
repo_for() { local var="DEST_${1}_REPO"; echo "${!var:-}"; }
pass_for() { local var="DEST_${1}_PASSPHRASE"; echo "${!var:-}"; }
dest_for_svc() { local var="SVC_${1//-/_}"; echo "${!var:-${DEST_DEFAULT:-default}}"; }
b_for() {
local dest="$1"; shift
@@ -55,6 +83,9 @@ esac
log "===== Borg backup starting ====="
rc=0
TS="$(date +%Y-%m-%dT%H-%M-%S)"
declare -a FAILED_SVCS=()
_ERR="$(mktemp)"
trap 'rm -f "$_ERR"' EXIT
for svc_dir in "$DOCKER_DIR"/*/; do
[ -f "${svc_dir}docker-compose.yml" ] || continue
@@ -77,10 +108,13 @@ for svc_dir in "$DOCKER_DIR"/*/; do
log "Archiving $svc$dest::$ARCHIVE ..."
if b_for "$dest" create \
--compression=zstd,6 --exclude-caches --stats \
"::$ARCHIVE" "$svc_dir" 2>&1 | while IFS= read -r line; do log " $line"; done; then
"::$ARCHIVE" "$svc_dir" 2>"$_ERR" | while IFS= read -r line; do log " $line"; done; then
log "OK $svc (Minecraft, no downtime)"
else
log "WARNING: archive failed for $svc"; rc=1
_reason="$(categorize_error "$(cat "$_ERR")")"
log "WARNING: archive failed for $svc$_reason"
FAILED_SVCS+=("$svc: $_reason")
rc=1
fi
else
STOPPED=false
@@ -95,10 +129,13 @@ for svc_dir in "$DOCKER_DIR"/*/; do
log "Archiving $svc$dest::$ARCHIVE ..."
if b_for "$dest" create \
--compression=zstd,6 --exclude-caches --stats \
"::$ARCHIVE" "$svc_dir" 2>&1 | while IFS= read -r line; do log " $line"; done; then
"::$ARCHIVE" "$svc_dir" 2>"$_ERR" | while IFS= read -r line; do log " $line"; done; then
log "OK $svc"
else
log "WARNING: archive failed for $svc"; rc=1
_reason="$(categorize_error "$(cat "$_ERR")")"
log "WARNING: archive failed for $svc$_reason"
FAILED_SVCS+=("$svc: $_reason")
rc=1
fi
if [ "$STOPPED" = true ]; then
@@ -127,7 +164,12 @@ done
if [ "$rc" -eq 0 ]; then
log "===== Borg backup complete ====="
ntfy_send "✓ Borg backup complete" "$HOST: all services archived successfully" \
"low" "white_check_mark"
else
log "===== Borg backup finished WITH WARNINGS (see above) ====="
_ntfy_msg="$HOST: Borg backup failures:"
for _s in "${FAILED_SVCS[@]}"; do _ntfy_msg+=$'\n'"$_s"; done
ntfy_send "✗ Borg backup FAILED" "$_ntfy_msg" "urgent" "rotating_light"
fi
exit "$rc"