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
+22 -160
View File
@@ -11,9 +11,9 @@
# New services are auto-discovered on every run — no reconfiguration needed.
#
# Creates: ~/docker/backup/
# backup.conf settings + per-service destination map (chmod 600)
# backup.sh worker (run directly or via systemd timer)
# restore/<dest>/ restore_kopia_backup.sh + backup.conf per destination
# backup.conf settings + per-service destination map (chmod 600)
# backup_kopia.sh worker (run directly or via systemd timer)
# restore_kopia.sh interactive restore helper
register_service backup backup "Encrypted backup of all Docker services (full restore)"
@@ -22,9 +22,8 @@ install_backup() {
local DIR="$DOCKER_DIR/backup"
local CONF_FILE="$DIR/backup.conf"
local WORKER="$DIR/backup.sh"
local RESTORE_DIR="$DIR/restore"
local RESTORE_SRC="${HERE:-}/extras/restore_kopia_backup.sh"
local WORKER="$DIR/backup_kopia.sh"
local RESTORE="$DIR/restore_kopia.sh"
local SVC_NAME="post-install-backup"
echo ""
@@ -248,7 +247,7 @@ install_backup() {
KEEP_LATEST="${KEEP_LATEST:-7}"
# ── 7. Create dirs + init Kopia repos ────────────────────────────────────
mkdir -p "$DIR" "$RESTORE_DIR"
mkdir -p "$DIR"
ensure_docker_dir_ownership "$DIR"
local repo pw cfg
@@ -288,7 +287,7 @@ install_backup() {
echo "# ── backup.conf ────────────────────────────────────────────────────────────"
echo "# Generated $(date '+%F %T'). Safe to hand-edit."
echo "# Worker : sudo $WORKER"
echo "# Restore: sudo $RESTORE_DIR/<dest>/restore_kopia_backup.sh"
echo "# Restore: sudo $RESTORE"
echo ""
echo "KOPIA=\"$KOPIA_BIN\""
echo ""
@@ -326,158 +325,23 @@ install_backup() {
chmod 600 "$CONF_FILE"
log_success "backup.conf written (chmod 600)"
# ── 9. Generate worker script ─────────────────────────────────────────────
log_info "Writing worker $WORKER ..."
cat > "$WORKER" << 'WORKEREOF'
#!/bin/bash
# Generated by the backup installer.
# Backs up full ~/docker/<service>/ directories via Kopia.
# Minecraft instances: flush to disk (save-all) then snapshot — no downtime.
# All other services: stop → snapshot → restart for consistency.
#
# sudo ./backup.sh run a full backup cycle
# sudo ./backup.sh snapshots list all snapshots (all repos)
# sudo ./backup.sh policy show retention policies
#
# 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 the backup service)"; 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}}"
}
# Detect itzg Minecraft instances by their Dockerfile signature.
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" == "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
# ── Minecraft: flush world to disk, snapshot without stopping ────────
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
# ── All other services: stop → snapshot → restart ─────────────────
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"
WORKEREOF
# ── 9. Install worker script ─────────────────────────────────────────────
log_info "Installing worker $WORKER ..."
cp "${HERE:-}/extras/backup_kopia.sh" "$WORKER"
chmod +x "$WORKER"
chown root:root "$WORKER" 2>/dev/null || true
log_success "backup.sh written"
log_success "backup_kopia.sh installed"
# ── 10. Restore scripts (one per destination) ────────────────────────────
# ── 10. Install restore script ────────────────────────────────────────────
local RESTORE_SRC="${HERE:-}/extras/restore_kopia.sh"
if [ -f "$RESTORE_SRC" ]; then
for dn in "${DEST_NAMES_ARR[@]}"; do
local dest_rdir="$RESTORE_DIR/$dn"
mkdir -p "$dest_rdir"
cp "$RESTORE_SRC" "$dest_rdir/restore_kopia_backup.sh"
chmod +x "$dest_rdir/restore_kopia_backup.sh"
{
echo "# backup.conf for backup destination '$dn'"
echo "# Read by restore_kopia_backup.sh in this directory."
echo "KOPIA=\"$KOPIA_BIN\""
echo "KOPIA_CONFIG=\"${DEST_CONFIGS[$dn]}\""
printf "KOPIA_PASSWORD='%s'\n" "${DEST_PASSWORDS[$dn]}"
} > "$dest_rdir/backup.conf"
chown root:root "$dest_rdir/backup.conf" 2>/dev/null || true
chmod 600 "$dest_rdir/backup.conf"
log_success "restore/$dn/ ready"
done
cp "$RESTORE_SRC" "$RESTORE"
chmod +x "$RESTORE"
chown root:root "$RESTORE" 2>/dev/null || true
log_success "restore_kopia.sh installed"
else
log_warning "extras/restore_kopia_backup.sh not found — restore scripts not installed"
log_warning "Copy it manually: cp extras/restore_kopia_backup.sh $RESTORE_DIR/<dest>/"
log_warning "extras/restore_kopia.sh not found — restore script not installed"
log_warning "Copy it manually: cp extras/restore_kopia.sh $RESTORE"
fi
# ── 11. Systemd timer ────────────────────────────────────────────────────
@@ -559,11 +423,9 @@ SVCEOF
echo " sudo $WORKER back up now"
echo " sudo $WORKER snapshots list all snapshots"
echo ""
echo " Restore (per destination):"
for dn in "${DEST_NAMES_ARR[@]}"; do
echo " sudo $RESTORE_DIR/$dn/restore_kopia_backup.sh"
echo " sudo $RESTORE_DIR/$dn/restore_kopia_backup.sh --list"
done
echo " Restore:"
echo " sudo $RESTORE"
echo " sudo $RESTORE --list"
echo ""
[ -n "$AUTORUN" ] && echo " $AUTORUN" && echo ""
log_warning "Save your passwords (in backup.conf) somewhere safe —"
+21 -183
View File
@@ -11,9 +11,9 @@
# Vorta GUI, SSH remote repos out of the box, widely packaged.
#
# Creates: ~/docker/borg-backup/
# backup.conf settings + per-dest repo/passphrase (chmod 600)
# borg-backup.sh worker (run directly or via systemd timer)
# restore/<dest>/ restore_borg_backup.sh + backup.conf per destination
# backup.conf settings + per-dest repo/passphrase (chmod 600)
# backup_borg.sh worker (run directly or via systemd timer)
# restore_borg.sh interactive restore helper
register_service borg-backup backup "Encrypted backup of all Docker services via Borg"
@@ -22,9 +22,8 @@ install_borg_backup() {
local DIR="$DOCKER_DIR/borg-backup"
local CONF_FILE="$DIR/backup.conf"
local WORKER="$DIR/borg-backup.sh"
local RESTORE_DIR="$DIR/restore"
local RESTORE_SRC="${HERE:-}/extras/restore_borg_backup.sh"
local WORKER="$DIR/backup_borg.sh"
local RESTORE="$DIR/restore_borg.sh"
local SVC_NAME="post-install-borg-backup"
echo ""
@@ -193,7 +192,7 @@ install_borg_backup() {
KEEP_MONTHLY="${KEEP_MONTHLY:-3}"
# ── 7. Create dirs + init Borg repos ─────────────────────────────────────
mkdir -p "$DIR" "$RESTORE_DIR"
mkdir -p "$DIR"
ensure_docker_dir_ownership "$DIR"
local repo pw
@@ -232,7 +231,7 @@ install_borg_backup() {
echo "# ── backup.conf ────────────────────────────────────────────────────────────"
echo "# Generated $(date '+%F %T'). Safe to hand-edit."
echo "# Worker : sudo $WORKER"
echo "# Restore: sudo $RESTORE_DIR/<dest>/restore_borg_backup.sh"
echo "# Restore: sudo $RESTORE"
echo ""
echo "BORG=\"$BORG_BIN\""
echo ""
@@ -268,182 +267,23 @@ install_borg_backup() {
chmod 600 "$CONF_FILE"
log_success "backup.conf written (chmod 600)"
# ── 9. Generate worker script ─────────────────────────────────────────────
log_info "Writing worker $WORKER ..."
cat > "$WORKER" << 'WORKEREOF'
#!/bin/bash
# Generated by the borg-backup installer.
# Backs up full ~/docker/<service>/ directories via Borg.
# Minecraft instances: flush to disk (save-all) then archive — no downtime.
# All other services: stop → archive → restart for consistency.
#
# sudo ./borg-backup.sh run a full backup cycle
# sudo ./borg-backup.sh list list all archives in all repos
# sudo ./borg-backup.sh info show repo info for all destinations
#
# 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 the borg-backup service)"; 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
# ── Minecraft: flush world to disk, archive without stopping ─────────
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
# ── All other services: stop → archive → restart ──────────────────
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
# Prune old archives for this service in its destination repo
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
# Compact each repo to reclaim space freed by pruning
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"
WORKEREOF
# ── 9. Install worker script ─────────────────────────────────────────────
log_info "Installing worker $WORKER ..."
cp "${HERE:-}/extras/backup_borg.sh" "$WORKER"
chmod +x "$WORKER"
chown root:root "$WORKER" 2>/dev/null || true
log_success "borg-backup.sh written"
log_success "backup_borg.sh installed"
# ── 10. Restore scripts (one per destination) ─────────────────────────────
# ── 10. Install restore script ────────────────────────────────────────────
local RESTORE_SRC="${HERE:-}/extras/restore_borg.sh"
if [ -f "$RESTORE_SRC" ]; then
for dn in "${DEST_NAMES_ARR[@]}"; do
local dest_rdir="$RESTORE_DIR/$dn"
mkdir -p "$dest_rdir"
cp "$RESTORE_SRC" "$dest_rdir/restore_borg_backup.sh"
chmod +x "$dest_rdir/restore_borg_backup.sh"
{
echo "# backup.conf for borg-backup destination '$dn'"
echo "# Read by restore_borg_backup.sh in this directory."
echo "BORG=\"$BORG_BIN\""
echo "BORG_REPO=\"${DEST_REPOS[$dn]}\""
printf "BORG_PASSPHRASE='%s'\n" "${DEST_PASSWORDS[$dn]}"
} > "$dest_rdir/backup.conf"
chown root:root "$dest_rdir/backup.conf" 2>/dev/null || true
chmod 600 "$dest_rdir/backup.conf"
log_success "restore/$dn/ ready"
done
cp "$RESTORE_SRC" "$RESTORE"
chmod +x "$RESTORE"
chown root:root "$RESTORE" 2>/dev/null || true
log_success "restore_borg.sh installed"
else
log_warning "extras/restore_borg_backup.sh not found — restore scripts not installed"
log_warning "Copy it manually: cp extras/restore_borg_backup.sh $RESTORE_DIR/<dest>/"
log_warning "extras/restore_borg.sh not found — restore script not installed"
log_warning "Copy it manually: cp extras/restore_borg.sh $RESTORE"
fi
# ── 11. Systemd timer ─────────────────────────────────────────────────────
@@ -525,10 +365,8 @@ SVCEOF
echo " sudo $WORKER info repo stats"
echo ""
echo " Restore:"
for dn in "${DEST_NAMES_ARR[@]}"; do
echo " sudo $RESTORE_DIR/$dn/restore_borg_backup.sh"
echo " sudo $RESTORE_DIR/$dn/restore_borg_backup.sh --list"
done
echo " sudo $RESTORE"
echo " sudo $RESTORE --list"
echo ""
log_warning "IMPORTANT — back up your Borg key and passphrase now."
echo " The key is stored in the repo itself (repokey-blake2 encryption)."
+11 -96
View File
@@ -26,7 +26,7 @@ install_gaming_backup() {
# ── Repo-conventional paths ──────────────────────────────────────────────
local BACKUP_DIR="$DOCKER_DIR/gaming-backup"
local CONF_FILE="$BACKUP_DIR/backup.conf" # editable settings
local WORKER="$BACKUP_DIR/gaming-backup.sh" # generated worker
local WORKER="$BACKUP_DIR/backup_gaming.sh" # worker script
local KOPIA_CONFIG="/etc/gaming-backup/repository.config"
local CACHE_DIR="/var/cache/gaming-backup"
local SVC_NAME="post-install-gaming-backup"
@@ -273,108 +273,23 @@ CONFEOF
|| log_warning "Could not pre-set Steam ignore policy (applies on first snapshot)."
fi
# ── 7. Generate the worker script ────────────────────────────────────────
log_info "Writing worker $WORKER ..."
cat > "$WORKER" << 'WORKEREOF'
#!/bin/bash
# Generated by the gaming-backup service — frequent game-save snapshots via Kopia.
# Nothing is stopped: Minecraft worlds are flushed to disk (save-all) first.
#
# sudo ./gaming-backup.sh run a backup now
# sudo ./gaming-backup.sh snapshots list snapshots
# sudo ./gaming-backup.sh policy show retention/ignore policy
#
# Reads settings from backup.conf next to this script.
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 the gaming-backup service)"; exit 1; }
# shellcheck source=/dev/null
source "$CONF"
export KOPIA_PASSWORD
log() { echo "[$(date '+%F %T')] $*"; }
k() { "$KOPIA" --config-file="$KOPIA_CONFIG" "$@"; }
if ! k repository status >/dev/null 2>&1; then
log "ERROR: not connected to a repository — re-run the gaming-backup service"
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 ====="
# Flush each running Minecraft world to disk first so snapshots are consistent.
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
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"; then
log "WARNING: snapshot failed for $label"; 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; then
log "WARNING: remote mirror failed"; rc=1
fi
fi
if [ "$rc" -eq 0 ]; then log "===== Gaming backup complete ====="; else log "===== Gaming backup finished WITH WARNINGS ====="; fi
exit "$rc"
WORKEREOF
# ── 7. Install the worker script ────────────────────────────────────────
log_info "Installing worker $WORKER ..."
cp "${HERE:-}/extras/backup_gaming.sh" "$WORKER"
chmod +x "$WORKER"
chown root:root "$WORKER" 2>/dev/null || true
log_success "gaming-backup.sh written"
log_success "backup_gaming.sh installed"
# ── Copy the interactive restore script ───────────────────────────────────
local RESTORE_SRC="${HERE:-}/extras/restore_kopia_backup.sh"
local RESTORE_DEST="$BACKUP_DIR/restore_kopia_backup.sh"
local RESTORE_SRC="${HERE:-}/extras/restore_kopia.sh"
local RESTORE_DEST="$BACKUP_DIR/restore_kopia.sh"
if [ -f "$RESTORE_SRC" ]; then
cp "$RESTORE_SRC" "$RESTORE_DEST"
chmod +x "$RESTORE_DEST"
chown root:root "$RESTORE_DEST" 2>/dev/null || true
log_success "restore_kopia_backup.sh installed"
log_success "restore_kopia.sh installed"
else
log_warning "extras/restore_kopia_backup.sh not found — restore script not installed"
log_warning "extras/restore_kopia.sh not found — restore script not installed"
fi
# ── 8. Install systemd timer (fallback: cron) ────────────────────────────
@@ -452,8 +367,8 @@ UNITEOF
echo " Commands:"
echo " sudo $WORKER back up now"
echo " sudo $WORKER snapshots list snapshots"
echo " sudo $RESTORE_DEST interactive restore"
echo " sudo $RESTORE_DEST --list list all snapshot sources"
echo " sudo $RESTORE_DEST interactive restore"
echo " sudo $RESTORE_DEST --list list all snapshot sources"
echo " $AUTORUN"
echo ""
echo " Tip: also install the 'backup' service for nightly full-service recovery."