refactor: move backup/restore worker scripts to extras/ as source files

Replace embedded heredocs in the three backup service installers with
cp from versioned source files in extras/:

  extras/backup_kopia.sh   — Kopia worker (was inline in services/backup.sh)
  extras/backup_borg.sh    — Borg worker  (was inline in services/borg-backup.sh)
  extras/backup_gaming.sh  — gaming saves worker (was inline in services/gaming-backup.sh)
  extras/restore_kopia.sh  — unified Kopia restore (multi-dest + single-dest)
  extras/restore_borg.sh   — unified Borg restore with destination picker

Each installer now does `cp extras/<script>.sh $DIR/<script>.sh` instead of
writing the script inline. Workers and restore scripts are now readable in the
repo rather than buried in heredocs.

Restore scripts are installed flat into the service directory root:
  ~/docker/backup/backup_kopia.sh   ~/docker/backup/restore_kopia.sh
  ~/docker/borg-backup/backup_borg.sh   ~/docker/borg-backup/restore_borg.sh
  ~/docker/gaming-backup/backup_gaming.sh   ~/docker/gaming-backup/restore_kopia.sh

The new restore scripts handle destination selection internally, so a single
script replaces the old per-destination restore/<dest>/ layout.

Also fixes `local` used outside a function in restore_kopia.sh and
restore_borg.sh (destination picker loop), and removes the now-superseded
extras/restore_kopia_backup.sh and extras/restore_borg_backup.sh.

https://claude.ai/code/session_019XgsQ13XKm4Zj3cNsDNwHj
This commit is contained in:
Claude
2026-06-04 18:50:09 +00:00
parent 159c6608a9
commit e55449442f
8 changed files with 508 additions and 533 deletions
+120
View File
@@ -0,0 +1,120 @@
#!/bin/bash
# extras/backup_kopia.sh — Kopia backup worker for all Docker services.
# Installed to ~/docker/backup/backup_kopia.sh by the backup service installer.
#
# sudo ./backup_kopia.sh run a full backup cycle
# sudo ./backup_kopia.sh snapshots list all snapshots (all repos)
# sudo ./backup_kopia.sh policy show retention policies
#
# Minecraft instances: flush to disk (save-all) then snapshot — no downtime.
# All other services: stop → snapshot → restart for consistency.
# 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 backup)"; exit 1; }
# shellcheck source=/dev/null
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"
log() { echo "[$(date '+%F %T')] $*"; }
kp_for() {
local dest="$1"; shift
local cfg_var="DEST_${dest}_CONFIG" pw_var="DEST_${dest}_PASSWORD"
local cfg="${!cfg_var:-}" pw="${!pw_var:-}"
[ -n "$cfg" ] || { log "Unknown destination: $dest"; return 1; }
env KOPIA_PASSWORD="$pw" "$KOPIA" --config-file="$cfg" "$@"
}
dest_for_svc() {
local var="SVC_${1//-/_}"
echo "${!var:-${DEST_DEFAULT:-default}}"
}
is_minecraft() { [ -f "${1}Dockerfile" ] && grep -qs itzg "${1}Dockerfile"; }
case "${1:-run}" in
snapshots)
for dest in ${DEST_NAMES:-default}; do
echo ""; echo "── dest: $dest ──"
kp_for "$dest" snapshot list 2>/dev/null || true
done
exit 0 ;;
policy)
for dest in ${DEST_NAMES:-default}; do
echo ""; echo "── dest: $dest ──"
kp_for "$dest" policy show --global 2>/dev/null || true
done
exit 0 ;;
esac
log "===== Backup starting ====="
rc=0
for svc_dir in "$DOCKER_DIR"/*/; do
[ -f "${svc_dir}docker-compose.yml" ] || continue
svc="$(basename "$svc_dir")"
[[ "$svc" == "backup" || "$svc" == "borg-backup" || "$svc" == "gaming-backup" ]] && continue
dest="$(dest_for_svc "$svc")"
_repo_var="DEST_${dest}_REPO"
[ -n "${!_repo_var:-}" ] || { log "SKIP $svc — dest '$dest' not configured in conf"; continue; }
if is_minecraft "$svc_dir"; then
if docker ps --format '{{.Names}}' 2>/dev/null | grep -qx "$svc"; then
log "Flushing Minecraft world '$svc' (save-all, no downtime)..."
docker exec "$svc" mc-send-to-console save-all flush 2>/dev/null \
|| docker exec "$svc" rcon-cli save-all 2>/dev/null || true
sleep 5
fi
log "Snapshotting $svc (dest: $dest)..."
if kp_for "$dest" snapshot create --description="backup: $svc" "$svc_dir"; then
log "OK $svc (Minecraft, no downtime)"
else
log "WARNING: snapshot failed for $svc"; rc=1
fi
else
STOPPED=false
if docker ps --format '{{.Names}}' 2>/dev/null | grep -qx "$svc"; then
log "Stopping $svc..."
docker compose -f "${svc_dir}docker-compose.yml" down 2>/dev/null \
|| docker stop "$svc" 2>/dev/null \
|| log "WARNING: could not stop $svc — snapshotting live (consistency not guaranteed)"
STOPPED=true
fi
log "Snapshotting $svc (dest: $dest)..."
if kp_for "$dest" snapshot create --description="backup: $svc" "$svc_dir"; then
log "OK $svc"
else
log "WARNING: snapshot failed for $svc"; rc=1
fi
if [ "$STOPPED" = true ]; then
log "Starting $svc..."
docker compose -f "${svc_dir}docker-compose.yml" up -d 2>/dev/null \
|| log "WARNING: could not restart $svc — run: docker compose -f ${svc_dir}docker-compose.yml up -d"
fi
fi
done
if [ "${REMOTE_TYPE:-none}" != "none" ] && [ -n "${REMOTE_TYPE:-}" ]; then
for dest in ${DEST_NAMES:-default}; do
log "Mirroring '$dest' offsite ($REMOTE_TYPE)..."
# shellcheck disable=SC2086
kp_for "$dest" repository sync-to "$REMOTE_TYPE" $REMOTE_ARGS \
|| { log "WARNING: mirror failed for '$dest'"; rc=1; }
done
fi
if [ "$rc" -eq 0 ]; then
log "===== Backup complete ====="
else
log "===== Backup finished WITH WARNINGS (see above) ====="
fi
exit "$rc"