setup.sh's run_service() looks up install_<name> using the literal
hyphenated registered name, not an underscore-converted one. borg-backup,
calibre-web, gaming-backup, and stirling-pdf all used underscored function
names and were therefore uninstallable ("has no install_<name>"). Same
fix already applied to ai-gpu/ai-stack; this closes out the rest of the
repo-wide audit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nb2vJ8W7bHKx1JXVvpCraH
501 lines
26 KiB
Bash
501 lines
26 KiB
Bash
#!/bin/bash
|
|
# services/gaming-backup.sh — Frequent gaming-saves backup (no service downtime).
|
|
# Part of the modular post-install system (sourced by setup.sh).
|
|
#
|
|
# Can also be run standalone on any machine:
|
|
# sudo bash gaming-backup.sh
|
|
# (Docker must already be installed when run standalone)
|
|
#
|
|
# Backs up the things you can't re-download — progress, saved games, user data:
|
|
# • Minecraft worlds / player data (every <id>/data instance under $DOCKER_DIR)
|
|
# • Emulator saves & save states ($GAME_STORAGE_DIR/saves) [gaming box]
|
|
# • Emulator BIOS / firmware ($GAME_STORAGE_DIR/bios) [gaming box]
|
|
# • RetroArch shaders & overlays ($GAME_STORAGE_DIR/retroarch) [gaming box]
|
|
# • ES-DE scraped artwork ($GAME_STORAGE_DIR/media) [gaming box]
|
|
# • Steam user data & game saves ($GAME_STORAGE_DIR/steam) [gaming box]
|
|
# • Wolf state (/etc/wolf — config + profile_data) [gaming box]
|
|
#
|
|
# Unlike the 'backup' service, nothing is stopped — Minecraft worlds are flushed
|
|
# to disk (save-all) and snapshotted live. Run this hourly or every few hours for
|
|
# frequent save protection; use 'backup' nightly for full service recovery.
|
|
#
|
|
# It does NOT back up ROMs or Steam game installs — those are re-downloadable.
|
|
#
|
|
# Safe to re-run: it reconnects to an existing repository and refreshes the
|
|
# config, policies, worker script and timer.
|
|
|
|
# ── Standalone bootstrap ──────────────────────────────────────────────────────
|
|
# Detected when the script is executed directly rather than sourced by setup.sh.
|
|
# Sets up helpers and globals, then defers execution until after the function
|
|
# definition at the bottom of this file.
|
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
|
[[ "$(id -u)" == "0" ]] || { echo "Run with sudo: sudo bash $0"; exit 1; }
|
|
|
|
_SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
_COMMON="$_SELF_DIR/../lib/common.sh"
|
|
|
|
if [[ -f "$_COMMON" ]]; then
|
|
# Full repo present — use the real helpers (picks up ~/docker/.config too)
|
|
# shellcheck source=../lib/common.sh
|
|
source "$_COMMON"
|
|
else
|
|
# One-off copy — inline minimal stubs so the script works without the repo
|
|
log_info() { echo -e "\033[0;34m[INFO]\033[0m $*"; }
|
|
log_success() { echo -e "\033[0;32m[OK]\033[0m $*"; }
|
|
log_warning() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
|
|
log_error() { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; }
|
|
|
|
ensure_docker_dir_ownership() {
|
|
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$@" 2>/dev/null || true
|
|
}
|
|
|
|
# Match common.sh's eval-based pattern so local vars in install_* are set correctly
|
|
prompt_text() {
|
|
local _q="$1" _def="$2" _var="$3" _r
|
|
[[ "${UNATTENDED:-false}" == "true" ]] && { eval "$_var='$_def'"; return; }
|
|
read -r -p " $_q " _r
|
|
eval "$_var='${_r:-$_def}'"
|
|
}
|
|
|
|
prompt_yn() {
|
|
local _q="$1" _def="$2" _var="$3" _r
|
|
[[ "${UNATTENDED:-false}" == "true" ]] && { eval "$_var='$_def'"; return; }
|
|
read -r -p " $_q " _r
|
|
eval "$_var='${_r:-$_def}'"
|
|
}
|
|
|
|
generate_password() {
|
|
local _len="${1:-32}"
|
|
tr -dc 'A-Za-z0-9' < /dev/urandom | head -c "$_len"
|
|
}
|
|
fi
|
|
|
|
# Globals — ACTUAL_USER/ACTUAL_HOME must come before DOCKER_DIR
|
|
# ($HOME under sudo is /root, not the real user's home)
|
|
ACTUAL_USER="${ACTUAL_USER:-${SUDO_USER:-$USER}}"
|
|
ACTUAL_HOME="$(getent passwd "$ACTUAL_USER" 2>/dev/null | cut -d: -f6 || echo "${HOME:-/root}")"
|
|
DOCKER_DIR="${DOCKER_DIR:-$ACTUAL_HOME/docker}"
|
|
DRY_RUN="${DRY_RUN:-false}"
|
|
UNATTENDED="${UNATTENDED:-false}"
|
|
SITE_TZ="${SITE_TZ:-$(cat /etc/timezone 2>/dev/null || echo UTC)}"
|
|
SITE_DOMAIN="${SITE_DOMAIN:-example.com}"
|
|
SITE_CADDY_NET="${SITE_CADDY_NET:-caddy_net}"
|
|
CADDY_REMOTE_HOST="${CADDY_REMOTE_HOST:-}"
|
|
|
|
register_service() { :; } # no-op — no wizard to register into
|
|
_RUN_STANDALONE=1
|
|
fi
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
register_service gaming-backup backup "Gaming saves backup (Minecraft worlds, emulator saves, Steam)"
|
|
|
|
install_gaming-backup() {
|
|
log_info "Setting up gaming saves backup (Kopia)..."
|
|
|
|
# ── Repo-conventional paths ──────────────────────────────────────────────
|
|
local BACKUP_DIR="$DOCKER_DIR/gaming-backup"
|
|
local CONF_FILE="$BACKUP_DIR/backup.conf" # editable settings
|
|
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"
|
|
local DEFAULT_REPO="$ACTUAL_HOME/backups/gaming-kopia"
|
|
|
|
echo ""
|
|
echo "╔═══════════════════════════════════════════════════════╗"
|
|
echo "║ Gaming Backup Setup · Kopia ║"
|
|
echo "║ Minecraft worlds · game saves · user data ║"
|
|
echo "╚═══════════════════════════════════════════════════════╝"
|
|
echo ""
|
|
echo " Backs up progress / saves / user data — NOT ROMs or game installs."
|
|
echo " Nothing is stopped: Minecraft worlds are flushed to disk then snapshotted."
|
|
echo " Run frequently (hourly or every few hours) without disrupting gameplay."
|
|
echo " Use the 'backup' service for full service recovery (nightly)."
|
|
echo ""
|
|
|
|
# ── DRY-RUN ──────────────────────────────────────────────────────────────
|
|
if [ "$DRY_RUN" = true ]; then
|
|
echo "[DRY-RUN] Would install Kopia from the official apt repository"
|
|
echo "[DRY-RUN] Would create $BACKUP_DIR (owned by $ACTUAL_USER)"
|
|
echo "[DRY-RUN] Would create/connect a Kopia repository at $DEFAULT_REPO"
|
|
echo "[DRY-RUN] Would write config $CONF_FILE and worker $WORKER"
|
|
echo "[DRY-RUN] Would install systemd service+timer $SVC_NAME (cron fallback)"
|
|
echo "[DRY-RUN] Would optionally run the first backup"
|
|
return 0
|
|
fi
|
|
|
|
# ── 1. Ensure Kopia is installed ─────────────────────────────────────────
|
|
if ! command -v kopia >/dev/null 2>&1; then
|
|
log_info "Kopia not found — installing from the official apt repository..."
|
|
if command -v apt-get >/dev/null 2>&1; then
|
|
install -d -m 0755 /etc/apt/keyrings
|
|
if curl -fsSL https://kopia.io/signing-key \
|
|
| gpg --dearmor --yes -o /etc/apt/keyrings/kopia-keyring.gpg; then
|
|
echo "deb [signed-by=/etc/apt/keyrings/kopia-keyring.gpg] http://packages.kopia.io/apt/ stable main" \
|
|
> /etc/apt/sources.list.d/kopia.list
|
|
apt-get update -y && apt-get install -y kopia
|
|
fi
|
|
fi
|
|
fi
|
|
if ! command -v kopia >/dev/null 2>&1; then
|
|
log_error "Kopia is still not installed."
|
|
echo " Install it manually, then re-run this service:"
|
|
echo " https://kopia.io/docs/installation/"
|
|
return 1
|
|
fi
|
|
local KOPIA_BIN; KOPIA_BIN="$(command -v kopia)"
|
|
log_success "Kopia: $("$KOPIA_BIN" --version 2>/dev/null | head -1)"
|
|
|
|
mkdir -p "$BACKUP_DIR" || return 1
|
|
ensure_docker_dir_ownership "$BACKUP_DIR"
|
|
|
|
# ── 2. What to back up ───────────────────────────────────────────────────
|
|
echo ""
|
|
echo "═══════════════════════════════════════════════════════"
|
|
echo " WHAT TO BACK UP"
|
|
echo "═══════════════════════════════════════════════════════"
|
|
echo ""
|
|
|
|
local DEFAULT_MCBASE="$DOCKER_DIR"
|
|
echo " Each Minecraft instance's world is backed up from its <id>/data folder;"
|
|
echo " all instances under this folder are detected automatically."
|
|
echo " (type 'none' to exclude Minecraft)"
|
|
local MC_BASE_DIR=""
|
|
prompt_text " Folder containing Minecraft instance(s) [${DEFAULT_MCBASE}]:" "$DEFAULT_MCBASE" MC_BASE_DIR
|
|
if [ "$MC_BASE_DIR" = none ]; then
|
|
MC_BASE_DIR=""
|
|
else
|
|
MC_BASE_DIR="${MC_BASE_DIR/#\~/$ACTUAL_HOME}"; MC_BASE_DIR="${MC_BASE_DIR%/}"
|
|
local _found=() d
|
|
for d in "$MC_BASE_DIR"/*/; do
|
|
[ -f "${d}Dockerfile" ] && grep -qs itzg "${d}Dockerfile" && [ -d "${d}data" ] \
|
|
&& _found+=("$(basename "$d")")
|
|
done
|
|
if [ "${#_found[@]}" -gt 0 ]; then
|
|
log_success " Detected Minecraft instance(s): ${_found[*]}"
|
|
else
|
|
log_warning " No instances detected yet under $MC_BASE_DIR — picked up once created."
|
|
fi
|
|
fi
|
|
|
|
# ── Optional gaming-box sources ──────────────────────────────────────────
|
|
echo ""
|
|
echo " The following are only relevant on a gaming box (Wolf + emulators +"
|
|
echo " Steam). On a plain homelab server you can leave them disabled."
|
|
local DEFAULT_STORAGE="$ACTUAL_HOME/drives/games"
|
|
local GAME_STORAGE_DIR=""
|
|
prompt_text " Game storage dir (ROMs/Steam/saves live here) [${DEFAULT_STORAGE}]:" "$DEFAULT_STORAGE" GAME_STORAGE_DIR
|
|
GAME_STORAGE_DIR="${GAME_STORAGE_DIR/#\~/$ACTUAL_HOME}"
|
|
GAME_STORAGE_DIR="${GAME_STORAGE_DIR%/}"
|
|
|
|
echo ""
|
|
local _a=""
|
|
prompt_yn " Back up emulator saves ($GAME_STORAGE_DIR/saves)? (y/N):" "n" _a
|
|
local BACKUP_SAVES; BACKUP_SAVES=$([[ "$_a" =~ ^[Yy]$ ]] && echo yes || echo no)
|
|
prompt_yn " Back up emulator BIOS/firmware ($GAME_STORAGE_DIR/bios)? (y/N):" "n" _a
|
|
local BACKUP_BIOS; BACKUP_BIOS=$([[ "$_a" =~ ^[Yy]$ ]] && echo yes || echo no)
|
|
prompt_yn " Back up RetroArch shaders & overlays ($GAME_STORAGE_DIR/retroarch)? (y/N):" "n" _a
|
|
local BACKUP_RA_SHADERS; BACKUP_RA_SHADERS=$([[ "$_a" =~ ^[Yy]$ ]] && echo yes || echo no)
|
|
prompt_yn " Back up Steam user data/saves (game installs excluded)? (y/N):" "n" _a
|
|
local BACKUP_STEAM; BACKUP_STEAM=$([[ "$_a" =~ ^[Yy]$ ]] && echo yes || echo no)
|
|
prompt_yn " Back up ES-DE scraped artwork ($GAME_STORAGE_DIR/media)? (y/N):" "n" _a
|
|
local BACKUP_MEDIA; BACKUP_MEDIA=$([[ "$_a" =~ ^[Yy]$ ]] && echo yes || echo no)
|
|
echo ""
|
|
echo " /etc/wolf includes pairing/config AND profile_data — where Wolf stores"
|
|
echo " every app's home dir (ES-DE settings, controller mappings, RetroArch"
|
|
echo " saves & save states, standalone-emulator saves)."
|
|
prompt_yn " Back up Wolf state (/etc/wolf)? (y/N):" "n" _a
|
|
local BACKUP_WOLF; BACKUP_WOLF=$([[ "$_a" =~ ^[Yy]$ ]] && echo yes || echo no)
|
|
local WOLF_STATE_DIR="/etc/wolf"
|
|
|
|
# ── 3. Repository location ────────────────────────────────────────────────
|
|
echo ""
|
|
echo "═══════════════════════════════════════════════════════"
|
|
echo " BACKUP REPOSITORY (local)"
|
|
echo "═══════════════════════════════════════════════════════"
|
|
echo ""
|
|
echo " Backups are stored in a local Kopia repository. You can mirror it to"
|
|
echo " another computer or the cloud later (see backup.conf, REMOTE_* lines)."
|
|
echo " Put it on a DIFFERENT drive from your data if you can."
|
|
echo ""
|
|
local REPO_DIR=""
|
|
prompt_text " Repository path [${DEFAULT_REPO}]:" "$DEFAULT_REPO" REPO_DIR
|
|
REPO_DIR="${REPO_DIR/#\~/$ACTUAL_HOME}"
|
|
REPO_DIR="${REPO_DIR%/}"
|
|
|
|
echo ""
|
|
echo " The repository is encrypted. A strong password is generated and stored"
|
|
echo " in backup.conf (root-only). KEEP A COPY — without it backups cannot be"
|
|
echo " restored, even by you."
|
|
echo ""
|
|
local KOPIA_PASSWORD=""
|
|
if [ "$UNATTENDED" = true ]; then
|
|
echo " [auto] Generating a random repository password."
|
|
else
|
|
read -rsp " Repository password [Enter = auto-generate]: " KOPIA_PASSWORD; echo
|
|
fi
|
|
if [ -z "$KOPIA_PASSWORD" ]; then
|
|
KOPIA_PASSWORD="$(generate_password 32)"
|
|
log_info " Generated a random repository password (saved in backup.conf)."
|
|
fi
|
|
|
|
# ── 4. Retention + schedule ───────────────────────────────────────────────
|
|
echo ""
|
|
echo "═══════════════════════════════════════════════════════"
|
|
echo " SCHEDULE & RETENTION"
|
|
echo "═══════════════════════════════════════════════════════"
|
|
echo ""
|
|
echo " 1) Daily at 03:00"
|
|
echo " 2) Every 6 hours"
|
|
echo " 3) Hourly (recommended for active gaming)"
|
|
echo " 4) Custom (systemd OnCalendar)"
|
|
echo ""
|
|
local _sch=""
|
|
prompt_text " How often? [3]:" "3" _sch
|
|
local ONCALENDAR SCHED_LABEL
|
|
case "${_sch:-3}" in
|
|
1) ONCALENDAR="*-*-* 03:00:00"; SCHED_LABEL="daily at 03:00" ;;
|
|
2) ONCALENDAR="*-*-* 00,06,12,18:00:00"; SCHED_LABEL="every 6 hours" ;;
|
|
4) prompt_text " OnCalendar expression:" "hourly" ONCALENDAR; SCHED_LABEL="$ONCALENDAR" ;;
|
|
*) ONCALENDAR="hourly"; SCHED_LABEL="hourly" ;;
|
|
esac
|
|
|
|
echo ""
|
|
local KEEP_LATEST=""
|
|
prompt_text " How many recent snapshots to keep (latest)? [24]:" "24" KEEP_LATEST
|
|
local KEEP_DAILY=7 KEEP_WEEKLY=4 KEEP_MONTHLY=6
|
|
|
|
# ── Notifications (ntfy) ─────────────────────────────────────────────────
|
|
echo ""
|
|
echo "═══════════════════════════════════════════════════════"
|
|
echo " NOTIFICATIONS (optional)"
|
|
echo "═══════════════════════════════════════════════════════"
|
|
echo ""
|
|
echo " Receive a push notification after every backup (and on failures)."
|
|
echo " Uses ntfy — free and self-hostable. Create a topic at https://ntfy.sh"
|
|
echo ""
|
|
local NTFY_URL="" NTFY_TOKEN=""
|
|
prompt_text " ntfy topic URL (blank to skip):" "" NTFY_URL
|
|
if [ -n "$NTFY_URL" ]; then
|
|
prompt_text " ntfy access token (blank if public/no auth):" "" NTFY_TOKEN
|
|
fi
|
|
|
|
# ── 5. Write backup.conf ──────────────────────────────────────────────────
|
|
log_info "Writing $CONF_FILE ..."
|
|
tee "$CONF_FILE" >/dev/null << CONFEOF
|
|
# ── gaming-backup config (read by gaming-backup.sh) ──────────────────────────
|
|
# Generated on $(date '+%F %T'). Safe to hand-edit.
|
|
|
|
KOPIA="$KOPIA_BIN"
|
|
KOPIA_CONFIG="$KOPIA_CONFIG"
|
|
KOPIA_CACHE_DIR="$CACHE_DIR"
|
|
# Repository encryption password — KEEP A COPY somewhere safe.
|
|
KOPIA_PASSWORD='$KOPIA_PASSWORD'
|
|
|
|
# ── Sources (progress / saves / user data only) ──────────────────────────────
|
|
# MC_BASE_DIR holds Minecraft instances; every <id>/data with an itzg Dockerfile
|
|
# is snapshotted automatically (covers multi-server setups).
|
|
MC_BASE_DIR="$MC_BASE_DIR"
|
|
GAME_STORAGE_DIR="$GAME_STORAGE_DIR"
|
|
WOLF_STATE_DIR="$WOLF_STATE_DIR"
|
|
BACKUP_SAVES="$BACKUP_SAVES" # \$GAME_STORAGE_DIR/saves
|
|
BACKUP_BIOS="$BACKUP_BIOS" # \$GAME_STORAGE_DIR/bios (emulator BIOS/firmware — user-provided)
|
|
BACKUP_RA_SHADERS="$BACKUP_RA_SHADERS" # \$GAME_STORAGE_DIR/retroarch/{shaders,overlays} (cores excluded — re-downloadable)
|
|
BACKUP_STEAM="$BACKUP_STEAM" # \$GAME_STORAGE_DIR/steam (game installs excluded by policy)
|
|
BACKUP_MEDIA="$BACKUP_MEDIA" # \$GAME_STORAGE_DIR/media (ES-DE scraped artwork)
|
|
BACKUP_WOLF="$BACKUP_WOLF" # /etc/wolf — config + profile_data
|
|
|
|
# ── Optional offsite mirror ──────────────────────────────────────────────────
|
|
# Mirror the WHOLE repository to another computer or the cloud after each run.
|
|
# Leave REMOTE_TYPE=none to stay local-only.
|
|
#
|
|
# SFTP: REMOTE_TYPE="sftp" REMOTE_ARGS="--host H --username U --path /srv/..."
|
|
# B2: REMOTE_TYPE="b2" REMOTE_ARGS="--bucket B --key-id K --key A"
|
|
# S3: REMOTE_TYPE="s3" REMOTE_ARGS="--bucket B --endpoint E --access-key K --secret-access-key S"
|
|
# rclone: REMOTE_TYPE="rclone" REMOTE_ARGS="--remote-path myremote:gaming-kopia"
|
|
#
|
|
REMOTE_TYPE="none"
|
|
REMOTE_ARGS=""
|
|
|
|
# ── Notifications (ntfy) ─────────────────────────────────────────────────────
|
|
# Set NTFY_URL to receive backup success/failure alerts.
|
|
# Leave blank to disable. NTFY_TOKEN is optional (for private topics).
|
|
NTFY_URL="$NTFY_URL"
|
|
NTFY_TOKEN="$NTFY_TOKEN"
|
|
CONFEOF
|
|
chown root:root "$CONF_FILE" 2>/dev/null || true
|
|
chmod 600 "$CONF_FILE"
|
|
log_success "backup.conf written (chmod 600 — contains the repo password)"
|
|
|
|
# ── 6. Create / connect the repository, set policies ─────────────────────
|
|
log_info "Preparing repository at $REPO_DIR ..."
|
|
mkdir -p "$REPO_DIR" "$CACHE_DIR" "$(dirname "$KOPIA_CONFIG")"
|
|
|
|
kp() { env KOPIA_PASSWORD="$KOPIA_PASSWORD" "$KOPIA_BIN" --config-file="$KOPIA_CONFIG" "$@"; }
|
|
|
|
if kp repository status >/dev/null 2>&1; then
|
|
log_success "Already connected to a repository."
|
|
elif test -e "$REPO_DIR/kopia.repository.f"; then
|
|
log_info "Existing repository found — connecting..."
|
|
kp repository connect filesystem --path="$REPO_DIR" --cache-directory="$CACHE_DIR" \
|
|
|| { log_error "Failed to connect to existing repository."; return 1; }
|
|
log_success "Connected to existing repository."
|
|
else
|
|
log_info "Creating new repository..."
|
|
kp repository create filesystem --path="$REPO_DIR" --cache-directory="$CACHE_DIR" \
|
|
|| { log_error "Failed to create repository."; return 1; }
|
|
log_success "Repository created."
|
|
fi
|
|
|
|
log_info "Applying global policy (zstd compression + retention)..."
|
|
kp policy set --global --compression=zstd >/dev/null
|
|
kp policy set --global \
|
|
--keep-latest="$KEEP_LATEST" \
|
|
--keep-daily="$KEEP_DAILY" \
|
|
--keep-weekly="$KEEP_WEEKLY" \
|
|
--keep-monthly="$KEEP_MONTHLY" \
|
|
--keep-annual=0 --keep-hourly=0 >/dev/null
|
|
log_success "Retention: keep latest $KEEP_LATEST, $KEEP_DAILY daily, $KEEP_WEEKLY weekly, $KEEP_MONTHLY monthly"
|
|
|
|
if [ "$BACKUP_STEAM" = yes ]; then
|
|
log_info "Setting Steam ignore rules (excluding game installs, keeping saves)..."
|
|
kp policy set "$GAME_STORAGE_DIR/steam" \
|
|
--add-ignore='**/steamapps/common' \
|
|
--add-ignore='**/steamapps/downloading' \
|
|
--add-ignore='**/steamapps/shadercache' \
|
|
--add-ignore='**/steamapps/temp' \
|
|
--add-ignore='**/steamapps/workshop' \
|
|
--add-ignore='**/depotcache' >/dev/null 2>&1 \
|
|
|| log_warning "Could not pre-set Steam ignore policy (applies on first snapshot)."
|
|
fi
|
|
|
|
# ── 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 "backup_gaming.sh installed"
|
|
|
|
# ── Copy the interactive restore script ───────────────────────────────────
|
|
local RESTORE_SRC="${HERE:-}/extras/restore_kopia.sh"
|
|
local RESTORE_DEST="$BACKUP_DIR/restore_gaming.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.sh installed"
|
|
else
|
|
log_warning "extras/restore_kopia.sh not found — restore script not installed"
|
|
fi
|
|
|
|
# ── Install test script ───────────────────────────────────────────────────
|
|
local TEST_SCRIPT="$BACKUP_DIR/test_backup.sh"
|
|
local TEST_SRC="${HERE:-}/extras/test_backup.sh"
|
|
if [ -f "$TEST_SRC" ]; then
|
|
cp "$TEST_SRC" "$TEST_SCRIPT"
|
|
chmod +x "$TEST_SCRIPT"
|
|
chown root:root "$TEST_SCRIPT" 2>/dev/null || true
|
|
log_success "test_backup.sh installed"
|
|
else
|
|
log_warning "extras/test_backup.sh not found — test script not installed"
|
|
fi
|
|
|
|
# ── 8. Install systemd timer (fallback: cron) ────────────────────────────
|
|
local AUTORUN=""
|
|
if command -v systemctl >/dev/null 2>&1 && [ -d /run/systemd/system ]; then
|
|
log_info "Installing systemd service + timer ($SCHED_LABEL)..."
|
|
tee "/etc/systemd/system/${SVC_NAME}.service" >/dev/null << UNITEOF
|
|
[Unit]
|
|
Description=Post-install gaming backup (Minecraft world + game saves via Kopia)
|
|
After=docker.service network-online.target
|
|
Wants=docker.service
|
|
|
|
[Service]
|
|
Type=oneshot
|
|
ExecStart=/bin/bash $WORKER run
|
|
UNITEOF
|
|
|
|
tee "/etc/systemd/system/${SVC_NAME}.timer" >/dev/null << UNITEOF
|
|
[Unit]
|
|
Description=Schedule gaming backup ($SCHED_LABEL)
|
|
|
|
[Timer]
|
|
OnCalendar=$ONCALENDAR
|
|
Persistent=true
|
|
RandomizedDelaySec=300
|
|
|
|
[Install]
|
|
WantedBy=timers.target
|
|
UNITEOF
|
|
|
|
systemctl daemon-reload
|
|
systemctl enable --now "${SVC_NAME}.timer"
|
|
log_success "Timer enabled: $SCHED_LABEL"
|
|
AUTORUN="systemctl list-timers ${SVC_NAME}.timer"
|
|
else
|
|
log_warning "systemd not detected — installing a cron job instead."
|
|
local CRON
|
|
case "${_sch:-3}" in
|
|
1) CRON="0 3 * * *" ;;
|
|
2) CRON="0 0,6,12,18 * * *" ;;
|
|
*) CRON="0 * * * *" ;;
|
|
esac
|
|
echo "$CRON root /bin/bash $WORKER run >> /var/log/${SVC_NAME}.log 2>&1" \
|
|
> "/etc/cron.d/${SVC_NAME}"
|
|
log_success "Cron job installed: $CRON"
|
|
AUTORUN="cat /etc/cron.d/${SVC_NAME}"
|
|
fi
|
|
|
|
# ── 9. First backup now? ─────────────────────────────────────────────────
|
|
echo ""
|
|
local _now=""
|
|
prompt_yn " Run the first gaming backup now? (Y/n):" "y" _now
|
|
if [[ ! "$_now" =~ ^[Nn]$ ]]; then
|
|
/bin/bash "$WORKER" run || log_warning "First backup reported warnings — check the output above."
|
|
fi
|
|
|
|
# ── Summary ──────────────────────────────────────────────────────────────
|
|
echo ""
|
|
echo "═══════════════════════════════════════════════════════"
|
|
echo " GAMING BACKUP CONFIGURED"
|
|
echo "═══════════════════════════════════════════════════════"
|
|
echo ""
|
|
echo " Repository : $REPO_DIR (encrypted, dedup + zstd)"
|
|
echo " Schedule : $SCHED_LABEL"
|
|
echo " Config : $CONF_FILE"
|
|
echo " Worker : $WORKER"
|
|
echo " Backing up :"
|
|
[ -n "$MC_BASE_DIR" ] && echo " • Minecraft worlds $MC_BASE_DIR/*/data (all instances)"
|
|
[ "$BACKUP_SAVES" = yes ] && echo " • Emulator saves $GAME_STORAGE_DIR/saves"
|
|
[ "$BACKUP_BIOS" = yes ] && echo " • Emulator BIOS $GAME_STORAGE_DIR/bios"
|
|
[ "$BACKUP_RA_SHADERS" = yes ] && echo " • RA shaders/overlays $GAME_STORAGE_DIR/retroarch/{shaders,overlays}"
|
|
[ "$BACKUP_STEAM" = yes ] && echo " • Steam user data $GAME_STORAGE_DIR/steam (game installs excluded)"
|
|
[ "$BACKUP_MEDIA" = yes ] && echo " • ES-DE scraped art $GAME_STORAGE_DIR/media"
|
|
[ "$BACKUP_WOLF" = yes ] && echo " • Wolf state $WOLF_STATE_DIR (config + profile_data)"
|
|
echo " NOT backed up: ROMs, Steam game installs (re-downloadable)."
|
|
echo ""
|
|
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 ""
|
|
echo " Backup test (restore latest, compare, restore-back):"
|
|
echo " sudo $TEST_SCRIPT test most recent backup"
|
|
echo " sudo $TEST_SCRIPT --list list testable sources"
|
|
echo " sudo $TEST_SCRIPT <source> test a specific source"
|
|
[ -n "${NTFY_URL:-}" ] && echo "" && echo " Notifications: $NTFY_URL"
|
|
echo ""
|
|
echo " $AUTORUN"
|
|
echo ""
|
|
echo " Tip: also install the 'backup' service for nightly full-service recovery."
|
|
echo ""
|
|
log_warning "Save your repository password (in backup.conf) somewhere safe —"
|
|
log_warning "without it the encrypted backups cannot be restored."
|
|
echo ""
|
|
log_success "Gaming backup configured."
|
|
}
|
|
|
|
# Run immediately when executed directly (deferred until after function definition)
|
|
[[ "${_RUN_STANDALONE:-0}" == 1 ]] && install_gaming-backup
|