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
134 lines
4.9 KiB
Bash
134 lines
4.9 KiB
Bash
#!/bin/bash
|
|
# extras/backup_borg.sh — Borg backup worker for all Docker services.
|
|
# Installed to ~/docker/borg-backup/backup_borg.sh by the borg-backup installer.
|
|
#
|
|
# sudo ./backup_borg.sh run a full backup cycle
|
|
# sudo ./backup_borg.sh list list all archives in all repos
|
|
# sudo ./backup_borg.sh info show repo info for all destinations
|
|
#
|
|
# Minecraft instances: flush to disk (save-all) then archive — no downtime.
|
|
# All other services: stop → archive → 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 borg-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')] $*"; }
|
|
|
|
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
|
|
local repo; repo="$(repo_for "$dest")"
|
|
local pass; pass="$(pass_for "$dest")"
|
|
[ -n "$repo" ] || { log "Unknown destination: $dest"; return 1; }
|
|
BORG_PASSPHRASE="$pass" BORG_REPO="$repo" "$BORG" "$@"
|
|
}
|
|
|
|
is_minecraft() { [ -f "${1}Dockerfile" ] && grep -qs itzg "${1}Dockerfile"; }
|
|
|
|
case "${1:-run}" in
|
|
list)
|
|
for dest in ${DEST_NAMES:-default}; do
|
|
echo ""; echo "── dest: $dest ($(repo_for "$dest")) ──"
|
|
b_for "$dest" list 2>/dev/null | sort -r | head -30 || true
|
|
done
|
|
exit 0 ;;
|
|
info)
|
|
for dest in ${DEST_NAMES:-default}; do
|
|
echo ""; echo "── dest: $dest ──"
|
|
b_for "$dest" info 2>/dev/null || true
|
|
done
|
|
exit 0 ;;
|
|
esac
|
|
|
|
log "===== Borg backup starting ====="
|
|
rc=0
|
|
TS="$(date +%Y-%m-%dT%H-%M-%S)"
|
|
|
|
for svc_dir in "$DOCKER_DIR"/*/; do
|
|
[ -f "${svc_dir}docker-compose.yml" ] || continue
|
|
svc="$(basename "$svc_dir")"
|
|
[[ "$svc" == "borg-backup" || "$svc" == "backup" || "$svc" == "gaming-backup" ]] && continue
|
|
|
|
dest="$(dest_for_svc "$svc")"
|
|
repo="$(repo_for "$dest")"
|
|
[ -n "$repo" ] || { log "SKIP $svc — dest '$dest' not configured"; continue; }
|
|
|
|
ARCHIVE="${svc}-${TS}"
|
|
|
|
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 "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
|
|
log "OK $svc (Minecraft, no downtime)"
|
|
else
|
|
log "WARNING: archive 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 — archiving live (consistency not guaranteed)"
|
|
STOPPED=true
|
|
fi
|
|
|
|
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
|
|
log "OK $svc"
|
|
else
|
|
log "WARNING: archive 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
|
|
|
|
log "Pruning old archives for $svc in '$dest'..."
|
|
b_for "$dest" prune \
|
|
--keep-daily="${KEEP_DAILY:-7}" \
|
|
--keep-weekly="${KEEP_WEEKLY:-4}" \
|
|
--keep-monthly="${KEEP_MONTHLY:-3}" \
|
|
--glob-archives="${svc}-*" \
|
|
--list 2>/dev/null \
|
|
|| log "WARNING: prune failed for $svc (non-fatal)"
|
|
done
|
|
|
|
for dest in ${DEST_NAMES:-default}; do
|
|
repo="$(repo_for "$dest")"
|
|
[ -n "$repo" ] || continue
|
|
log "Compacting repo '$dest'..."
|
|
b_for "$dest" compact 2>/dev/null || true
|
|
done
|
|
|
|
if [ "$rc" -eq 0 ]; then
|
|
log "===== Borg backup complete ====="
|
|
else
|
|
log "===== Borg backup finished WITH WARNINGS (see above) ====="
|
|
fi
|
|
exit "$rc"
|