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:
@@ -0,0 +1,133 @@
|
|||||||
|
#!/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"
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# extras/backup_gaming.sh — Kopia gaming-saves worker (no service downtime).
|
||||||
|
# Installed to ~/docker/gaming-backup/backup_gaming.sh by the gaming-backup installer.
|
||||||
|
#
|
||||||
|
# sudo ./backup_gaming.sh run a backup now
|
||||||
|
# sudo ./backup_gaming.sh snapshots list snapshots
|
||||||
|
# sudo ./backup_gaming.sh policy show retention/ignore policy
|
||||||
|
#
|
||||||
|
# Nothing is stopped: Minecraft worlds are flushed to disk (save-all) first.
|
||||||
|
# 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 gaming-backup)"; 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 ====="
|
||||||
|
|
||||||
|
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"
|
||||||
@@ -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"
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# extras/restore_borg_backup.sh — interactive restore from a Borg archive.
|
# extras/restore_borg.sh — interactive restore from a Borg archive.
|
||||||
# Installed to ~/docker/borg-backup/restore/<dest>/ by the borg-backup installer.
|
# Installed to ~/docker/borg-backup/restore_borg.sh by the borg-backup installer.
|
||||||
#
|
#
|
||||||
# Run as root:
|
# Run as root:
|
||||||
# sudo ./restore_borg_backup.sh interactive
|
# sudo ./restore_borg.sh interactive
|
||||||
# sudo ./restore_borg_backup.sh --list list all archives and exit
|
# sudo ./restore_borg.sh --list list all archives and exit
|
||||||
#
|
#
|
||||||
# Reads backup.conf from the same directory (chmod 600, root-only).
|
# Reads backup.conf from the same directory (chmod 600, root-only).
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
@@ -21,30 +21,50 @@ die() { err "$*"; exit 1; }
|
|||||||
|
|
||||||
# ── Preflight ─────────────────────────────────────────────────────────────────
|
# ── Preflight ─────────────────────────────────────────────────────────────────
|
||||||
[ "${EUID:-$(id -u)}" -eq 0 ] || die "Run as root: sudo $0"
|
[ "${EUID:-$(id -u)}" -eq 0 ] || die "Run as root: sudo $0"
|
||||||
[ -f "$CONF" ] || die "backup.conf not found: $CONF (run: sudo setup.sh borg-backup)"
|
[ -f "$CONF" ] || die "backup.conf not found: $CONF"
|
||||||
command -v borg >/dev/null 2>&1 || die "borg not found — install: sudo apt install borgbackup"
|
command -v borg >/dev/null 2>&1 || die "borg not found — install: sudo apt install borgbackup"
|
||||||
|
|
||||||
# shellcheck source=/dev/null
|
# shellcheck source=/dev/null
|
||||||
source "$CONF"
|
source "$CONF"
|
||||||
export BORG_PASSPHRASE BORG_REPO
|
|
||||||
b() { borg "$@"; }
|
|
||||||
|
|
||||||
b info 2>/dev/null | grep -q "Repository" \
|
|
||||||
|| die "Cannot connect to Borg repository at $BORG_REPO — check backup.conf."
|
|
||||||
|
|
||||||
ACTUAL_USER="${SUDO_USER:-${USER:-$(id -un)}}"
|
ACTUAL_USER="${SUDO_USER:-${USER:-$(id -un)}}"
|
||||||
ACTUAL_HOME="$(getent passwd "$ACTUAL_USER" 2>/dev/null | cut -d: -f6 || echo "/home/$ACTUAL_USER")"
|
ACTUAL_HOME="$(getent passwd "$ACTUAL_USER" 2>/dev/null | cut -d: -f6 || echo "/home/$ACTUAL_USER")"
|
||||||
DOCKER_BASE="$ACTUAL_HOME/docker"
|
DOCKER_BASE="$ACTUAL_HOME/docker"
|
||||||
|
|
||||||
# ── List all archives ──────────────────────────────────────────────────────────
|
# ── Destination picker (skipped when only one dest) ───────────────────────────
|
||||||
list_archives() {
|
read -ra _DEST_ARR <<< "${DEST_NAMES:-default}"
|
||||||
b list --format '{archive}{NL}' 2>/dev/null
|
|
||||||
}
|
if [ "${#_DEST_ARR[@]}" -gt 1 ]; then
|
||||||
|
echo ""
|
||||||
|
echo "Select backup destination:"
|
||||||
|
echo ""
|
||||||
|
for i in "${!_DEST_ARR[@]}"; do
|
||||||
|
_repo_var="DEST_${_DEST_ARR[$i]}_REPO"
|
||||||
|
printf " %d) %s (%s)\n" "$((i+1))" "${_DEST_ARR[$i]}" "${!_repo_var:-unknown}"
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
read -rp "Destination [1-${#_DEST_ARR[@]}]: " _DEST_SEL
|
||||||
|
[[ "$_DEST_SEL" =~ ^[0-9]+$ ]] && [ "$_DEST_SEL" -ge 1 ] && [ "$_DEST_SEL" -le "${#_DEST_ARR[@]}" ] \
|
||||||
|
|| die "Invalid selection"
|
||||||
|
ACTIVE_DEST="${_DEST_ARR[$((_DEST_SEL-1))]}"
|
||||||
|
else
|
||||||
|
ACTIVE_DEST="${_DEST_ARR[0]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
_REPO_VAR="DEST_${ACTIVE_DEST}_REPO"
|
||||||
|
_PASS_VAR="DEST_${ACTIVE_DEST}_PASSPHRASE"
|
||||||
|
export BORG_REPO="${!_REPO_VAR:-}"
|
||||||
|
export BORG_PASSPHRASE="${!_PASS_VAR:-}"
|
||||||
|
[ -n "$BORG_REPO" ] || die "No BORG_REPO found for destination '$ACTIVE_DEST'"
|
||||||
|
|
||||||
|
b() { borg "$@"; }
|
||||||
|
b info 2>/dev/null | grep -q "Repository" \
|
||||||
|
|| die "Cannot connect to Borg repository at $BORG_REPO — check backup.conf."
|
||||||
|
|
||||||
# ── --list ────────────────────────────────────────────────────────────────────
|
# ── --list ────────────────────────────────────────────────────────────────────
|
||||||
if [ "${1:-}" = "--list" ]; then
|
if [ "${1:-}" = "--list" ]; then
|
||||||
echo ""
|
echo ""
|
||||||
info "Archives in $BORG_REPO:"
|
info "Archives in $BORG_REPO (dest: $ACTIVE_DEST):"
|
||||||
echo ""
|
echo ""
|
||||||
b list 2>/dev/null | sort -t- -k2 -r
|
b list 2>/dev/null | sort -t- -k2 -r
|
||||||
echo ""
|
echo ""
|
||||||
@@ -54,17 +74,16 @@ fi
|
|||||||
# ── Interactive restore ───────────────────────────────────────────────────────
|
# ── Interactive restore ───────────────────────────────────────────────────────
|
||||||
echo ""
|
echo ""
|
||||||
echo "╔═══════════════════════════════════════════════════════╗"
|
echo "╔═══════════════════════════════════════════════════════╗"
|
||||||
echo "║ Borg Backup Restore ║"
|
echo "║ Borg Restore ║"
|
||||||
echo "╚═══════════════════════════════════════════════════════╝"
|
echo "╚═══════════════════════════════════════════════════════╝"
|
||||||
echo ""
|
echo ""
|
||||||
|
[ "${#_DEST_ARR[@]}" -gt 1 ] && info "Using destination: $ACTIVE_DEST ($BORG_REPO)"
|
||||||
|
|
||||||
info "Loading archive list from $BORG_REPO ..."
|
info "Loading archive list..."
|
||||||
ARCHIVE_LIST=$(list_archives)
|
ARCHIVE_LIST=$(b list --format '{archive}{NL}' 2>/dev/null)
|
||||||
[ -z "$ARCHIVE_LIST" ] && die "No archives found. Run a backup first: sudo ~/docker/borg-backup/borg-backup.sh"
|
[ -z "$ARCHIVE_LIST" ] && die "No archives found. Run a backup first: sudo $HERE/backup_borg.sh"
|
||||||
|
|
||||||
# ── Group archives by service prefix (everything before the first timestamp) ──
|
# Group archives by service prefix (everything before the timestamp)
|
||||||
# Archive names: <service>-YYYY-MM-DDTHH-MM-SS
|
|
||||||
# Extract unique service prefixes.
|
|
||||||
mapfile -t SERVICES < <(echo "$ARCHIVE_LIST" | sed 's/-[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]T.*//' | sort -u)
|
mapfile -t SERVICES < <(echo "$ARCHIVE_LIST" | sed 's/-[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]T.*//' | sort -u)
|
||||||
|
|
||||||
echo "Backed-up services:"
|
echo "Backed-up services:"
|
||||||
@@ -86,17 +105,15 @@ read -rp "Select service [1-${#SERVICES[@]}] or q to quit: " SEL
|
|||||||
SELECTED_SVC="${SERVICES[$((SEL-1))]}"
|
SELECTED_SVC="${SERVICES[$((SEL-1))]}"
|
||||||
ok "Service: $SELECTED_SVC"
|
ok "Service: $SELECTED_SVC"
|
||||||
|
|
||||||
# ── Pick an archive for that service ─────────────────────────────────────────
|
# ── Pick an archive ───────────────────────────────────────────────────────────
|
||||||
echo ""
|
echo ""
|
||||||
echo "Available archives (most recent first):"
|
echo "Available archives (most recent first):"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
mapfile -t ARCHIVES < <(echo "$ARCHIVE_LIST" | grep "^${SELECTED_SVC}-" | sort -r | head -20)
|
mapfile -t ARCHIVES < <(echo "$ARCHIVE_LIST" | grep "^${SELECTED_SVC}-" | sort -r | head -20)
|
||||||
[ "${#ARCHIVES[@]}" -eq 0 ] && die "No archives found for $SELECTED_SVC."
|
[ "${#ARCHIVES[@]}" -eq 0 ] && die "No archives found for $SELECTED_SVC."
|
||||||
|
|
||||||
for i in "${!ARCHIVES[@]}"; do
|
for i in "${!ARCHIVES[@]}"; do
|
||||||
note=""; [ "$i" -eq 0 ] && note=" ← latest"
|
note=""; [ "$i" -eq 0 ] && note=" ← latest"
|
||||||
# Parse timestamp from archive name: svc-YYYY-MM-DDTHH-MM-SS
|
|
||||||
ts="${ARCHIVES[$i]#${SELECTED_SVC}-}"
|
ts="${ARCHIVES[$i]#${SELECTED_SVC}-}"
|
||||||
printf " %2d) %s%s\n" "$((i+1))" "$ts" "$note"
|
printf " %2d) %s%s\n" "$((i+1))" "$ts" "$note"
|
||||||
done
|
done
|
||||||
@@ -110,10 +127,8 @@ ARCH_SEL="${ARCH_SEL:-1}"
|
|||||||
SELECTED_ARCHIVE="${ARCHIVES[$((ARCH_SEL-1))]}"
|
SELECTED_ARCHIVE="${ARCHIVES[$((ARCH_SEL-1))]}"
|
||||||
ok "Archive: $SELECTED_ARCHIVE"
|
ok "Archive: $SELECTED_ARCHIVE"
|
||||||
|
|
||||||
# ── Determine the Docker service directory ────────────────────────────────────
|
|
||||||
# Archive contains the full absolute path: home/user/docker/svc/ (without leading /)
|
|
||||||
# Derive expected restore target.
|
|
||||||
TARGET_DIR="$DOCKER_BASE/$SELECTED_SVC"
|
TARGET_DIR="$DOCKER_BASE/$SELECTED_SVC"
|
||||||
|
COMPOSE_FILE="$TARGET_DIR/docker-compose.yml"
|
||||||
|
|
||||||
# ── Choose restore mode ───────────────────────────────────────────────────────
|
# ── Choose restore mode ───────────────────────────────────────────────────────
|
||||||
echo ""
|
echo ""
|
||||||
@@ -127,21 +142,17 @@ read -rp "Select [1/2] or q to quit: " MODE
|
|||||||
[[ "$MODE" =~ ^[qQ]$ ]] && echo "Cancelled." && exit 0
|
[[ "$MODE" =~ ^[qQ]$ ]] && echo "Cancelled." && exit 0
|
||||||
|
|
||||||
case "$MODE" in
|
case "$MODE" in
|
||||||
|
|
||||||
# ── Inspect: extract to /tmp ──────────────────────────────────────────────────
|
|
||||||
1)
|
1)
|
||||||
TEMP_DIR="/tmp/borg-inspect-$(date +%Y%m%d-%H%M%S)"
|
TEMP_DIR="/tmp/borg-inspect-$(date +%Y%m%d-%H%M%S)"
|
||||||
mkdir -p "$TEMP_DIR"
|
mkdir -p "$TEMP_DIR"
|
||||||
echo ""
|
echo ""
|
||||||
info "Extracting $SELECTED_ARCHIVE to $TEMP_DIR (nothing live is changed)..."
|
info "Extracting $SELECTED_ARCHIVE to $TEMP_DIR (nothing live is changed)..."
|
||||||
# borg extract strips leading / — run from TEMP_DIR
|
|
||||||
if ( cd "$TEMP_DIR" && b extract --progress "$BORG_REPO::$SELECTED_ARCHIVE" ); then
|
if ( cd "$TEMP_DIR" && b extract --progress "$BORG_REPO::$SELECTED_ARCHIVE" ); then
|
||||||
echo ""
|
echo ""
|
||||||
ok "Done. Browse the extracted files:"
|
ok "Done. Browse the extracted files:"
|
||||||
echo ""
|
echo ""
|
||||||
echo " ls -la $TEMP_DIR"
|
echo " ls -la $TEMP_DIR"
|
||||||
echo ""
|
echo " Service directory: $TEMP_DIR${TARGET_DIR}"
|
||||||
echo " The service directory will be under: $TEMP_DIR${TARGET_DIR}"
|
|
||||||
echo ""
|
echo ""
|
||||||
echo " When finished:"
|
echo " When finished:"
|
||||||
echo " rm -rf $TEMP_DIR"
|
echo " rm -rf $TEMP_DIR"
|
||||||
@@ -151,10 +162,7 @@ case "$MODE" in
|
|||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
|
|
||||||
# ── Restore in place ──────────────────────────────────────────────────────────
|
|
||||||
2)
|
2)
|
||||||
COMPOSE_FILE="$TARGET_DIR/docker-compose.yml"
|
|
||||||
|
|
||||||
STOPPED=false
|
STOPPED=false
|
||||||
if [ -f "$COMPOSE_FILE" ] \
|
if [ -f "$COMPOSE_FILE" ] \
|
||||||
&& docker ps --format '{{.Names}}' 2>/dev/null | grep -qx "$SELECTED_SVC"; then
|
&& docker ps --format '{{.Names}}' 2>/dev/null | grep -qx "$SELECTED_SVC"; then
|
||||||
@@ -173,7 +181,6 @@ case "$MODE" in
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Move current data aside
|
|
||||||
ASIDE="${TARGET_DIR}.pre-restore-$(date +%Y%m%d-%H%M%S)"
|
ASIDE="${TARGET_DIR}.pre-restore-$(date +%Y%m%d-%H%M%S)"
|
||||||
echo ""
|
echo ""
|
||||||
if [ -e "$TARGET_DIR" ]; then
|
if [ -e "$TARGET_DIR" ]; then
|
||||||
@@ -184,11 +191,9 @@ case "$MODE" in
|
|||||||
warn "$TARGET_DIR does not exist — restoring fresh."
|
warn "$TARGET_DIR does not exist — restoring fresh."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# borg extract strips leading /: run from / to restore to original absolute path
|
|
||||||
mkdir -p "$TARGET_DIR"
|
mkdir -p "$TARGET_DIR"
|
||||||
info "Restoring $SELECTED_ARCHIVE → $TARGET_DIR ..."
|
info "Restoring $SELECTED_ARCHIVE → $TARGET_DIR ..."
|
||||||
# Extract only the service subdirectory to keep things scoped
|
EXTRACT_PATH="${TARGET_DIR#/}"
|
||||||
EXTRACT_PATH="${TARGET_DIR#/}" # strip leading / for borg
|
|
||||||
if ( cd / && b extract --progress "$BORG_REPO::$SELECTED_ARCHIVE" "$EXTRACT_PATH" ); then
|
if ( cd / && b extract --progress "$BORG_REPO::$SELECTED_ARCHIVE" "$EXTRACT_PATH" ); then
|
||||||
ok "Restore complete."
|
ok "Restore complete."
|
||||||
else
|
else
|
||||||
@@ -204,7 +209,6 @@ case "$MODE" in
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Restart container
|
|
||||||
if [ "$STOPPED" = true ] && [ -f "$COMPOSE_FILE" ]; then
|
if [ "$STOPPED" = true ] && [ -f "$COMPOSE_FILE" ]; then
|
||||||
echo ""
|
echo ""
|
||||||
read -rp " Start '$SELECTED_SVC' now? (Y/n): " START_YN
|
read -rp " Start '$SELECTED_SVC' now? (Y/n): " START_YN
|
||||||
@@ -225,8 +229,6 @@ case "$MODE" in
|
|||||||
echo " Restored to : $TARGET_DIR"
|
echo " Restored to : $TARGET_DIR"
|
||||||
[ -e "$ASIDE" ] && echo " Previous data : $ASIDE"
|
[ -e "$ASIDE" ] && echo " Previous data : $ASIDE"
|
||||||
echo ""
|
echo ""
|
||||||
echo " Verify your data, then:"
|
|
||||||
echo ""
|
|
||||||
if [ -e "$ASIDE" ]; then
|
if [ -e "$ASIDE" ]; then
|
||||||
echo " Keep the restore (delete aside copy when satisfied):"
|
echo " Keep the restore (delete aside copy when satisfied):"
|
||||||
echo " rm -rf \"$ASIDE\""
|
echo " rm -rf \"$ASIDE\""
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# extras/restore_kopia_backup.sh — interactive restore from a Kopia snapshot.
|
# extras/restore_kopia.sh — interactive restore from a Kopia snapshot.
|
||||||
# Installed to ~/docker/backup/ by the backup service installer.
|
# Installed to ~/docker/backup/ and ~/docker/gaming-backup/ by their installers.
|
||||||
#
|
#
|
||||||
# Run as root:
|
# Run as root:
|
||||||
# sudo ./restore_kopia_backup.sh interactive
|
# sudo ./restore_kopia.sh interactive
|
||||||
# sudo ./restore_kopia_backup.sh --list list all snapshot sources and exit
|
# sudo ./restore_kopia.sh --list list all snapshot sources and exit
|
||||||
#
|
#
|
||||||
# Reads backup.conf from the same directory. The repository password is stored
|
# Reads backup.conf from the same directory (chmod 600, root-only).
|
||||||
# there (chmod 600, root-only) — no password prompt needed.
|
# Supports both multi-destination (backup service) and single-destination
|
||||||
|
# (gaming-backup service) conf formats.
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
|
|
||||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
@@ -22,29 +23,61 @@ die() { err "$*"; exit 1; }
|
|||||||
|
|
||||||
# ── Preflight ─────────────────────────────────────────────────────────────────
|
# ── Preflight ─────────────────────────────────────────────────────────────────
|
||||||
[ "${EUID:-$(id -u)}" -eq 0 ] || die "Run as root: sudo $0"
|
[ "${EUID:-$(id -u)}" -eq 0 ] || die "Run as root: sudo $0"
|
||||||
[ -f "$CONF" ] || die "backup.conf not found: $CONF (run: sudo setup.sh backup)"
|
[ -f "$CONF" ] || die "backup.conf not found: $CONF"
|
||||||
command -v jq >/dev/null 2>&1 || die "jq is required — install it: sudo apt install jq"
|
command -v jq >/dev/null 2>&1 || die "jq is required — install it: sudo apt install jq"
|
||||||
|
|
||||||
# shellcheck source=/dev/null
|
# shellcheck source=/dev/null
|
||||||
source "$CONF"
|
source "$CONF"
|
||||||
export KOPIA_PASSWORD
|
|
||||||
|
# ── Normalise conf format ─────────────────────────────────────────────────────
|
||||||
|
# gaming-backup uses KOPIA_CONFIG/KOPIA_PASSWORD directly (single dest).
|
||||||
|
# backup service uses DEST_NAMES + DEST_<n>_CONFIG/PASSWORD (multi-dest).
|
||||||
|
# Normalise both to the multi-dest interface so the rest of the script is uniform.
|
||||||
|
if [ -z "${DEST_NAMES:-}" ]; then
|
||||||
|
DEST_NAMES="default"
|
||||||
|
DEST_default_CONFIG="${KOPIA_CONFIG:-}"
|
||||||
|
DEST_default_PASSWORD="${KOPIA_PASSWORD:-}"
|
||||||
|
fi
|
||||||
|
|
||||||
command -v "$KOPIA" >/dev/null 2>&1 || die "Kopia not found: $KOPIA"
|
command -v "$KOPIA" >/dev/null 2>&1 || die "Kopia not found: $KOPIA"
|
||||||
k() { "$KOPIA" --config-file="$KOPIA_CONFIG" "$@"; }
|
|
||||||
k repository status >/dev/null 2>&1 || die "Cannot connect to Kopia repository. Check backup.conf."
|
|
||||||
|
|
||||||
# ── Derive Docker dir (mirrors lib/common.sh logic) ───────────────────────────
|
|
||||||
ACTUAL_USER="${SUDO_USER:-${USER:-$(id -un)}}"
|
ACTUAL_USER="${SUDO_USER:-${USER:-$(id -un)}}"
|
||||||
ACTUAL_HOME="$(getent passwd "$ACTUAL_USER" 2>/dev/null | cut -d: -f6 || echo "/home/$ACTUAL_USER")"
|
ACTUAL_HOME="$(getent passwd "$ACTUAL_USER" 2>/dev/null | cut -d: -f6 || echo "/home/$ACTUAL_USER")"
|
||||||
DOCKER_BASE="$ACTUAL_HOME/docker"
|
DOCKER_BASE="$ACTUAL_HOME/docker"
|
||||||
|
|
||||||
|
# ── Destination picker (skipped when only one dest) ───────────────────────────
|
||||||
|
read -ra _DEST_ARR <<< "$DEST_NAMES"
|
||||||
|
|
||||||
|
if [ "${#_DEST_ARR[@]}" -gt 1 ]; then
|
||||||
|
echo ""
|
||||||
|
echo "Select backup destination:"
|
||||||
|
echo ""
|
||||||
|
for i in "${!_DEST_ARR[@]}"; do
|
||||||
|
_cfg_var="DEST_${_DEST_ARR[$i]}_CONFIG"
|
||||||
|
printf " %d) %s (%s)\n" "$((i+1))" "${_DEST_ARR[$i]}" "${!_cfg_var:-unknown}"
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
read -rp "Destination [1-${#_DEST_ARR[@]}]: " _DEST_SEL
|
||||||
|
[[ "$_DEST_SEL" =~ ^[0-9]+$ ]] && [ "$_DEST_SEL" -ge 1 ] && [ "$_DEST_SEL" -le "${#_DEST_ARR[@]}" ] \
|
||||||
|
|| die "Invalid selection"
|
||||||
|
ACTIVE_DEST="${_DEST_ARR[$((_DEST_SEL-1))]}"
|
||||||
|
else
|
||||||
|
ACTIVE_DEST="${_DEST_ARR[0]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
_CFG_VAR="DEST_${ACTIVE_DEST}_CONFIG"
|
||||||
|
_PW_VAR="DEST_${ACTIVE_DEST}_PASSWORD"
|
||||||
|
KOPIA_CONFIG="${!_CFG_VAR:-}"
|
||||||
|
KOPIA_PASSWORD="${!_PW_VAR:-}"
|
||||||
|
[ -n "$KOPIA_CONFIG" ] || die "No KOPIA_CONFIG found for destination '$ACTIVE_DEST'"
|
||||||
|
|
||||||
|
export KOPIA_PASSWORD
|
||||||
|
k() { "$KOPIA" --config-file="$KOPIA_CONFIG" "$@"; }
|
||||||
|
k repository status >/dev/null 2>&1 || die "Cannot connect to Kopia repository '$ACTIVE_DEST'. Check backup.conf."
|
||||||
|
|
||||||
# ── Snapshot helpers ──────────────────────────────────────────────────────────
|
# ── Snapshot helpers ──────────────────────────────────────────────────────────
|
||||||
all_snapshots_json() {
|
all_snapshots_json() { k snapshot list --all --json 2>/dev/null; }
|
||||||
k snapshot list --all --json 2>/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
# Given a source path like ~/docker/minecraft/data, return "service_name /path/to/compose.yml"
|
|
||||||
# or empty string if not a Docker service.
|
|
||||||
docker_info_for_path() {
|
docker_info_for_path() {
|
||||||
local path="$1"
|
local path="$1"
|
||||||
[[ "$path" == "$DOCKER_BASE"/* ]] || return 0
|
[[ "$path" == "$DOCKER_BASE"/* ]] || return 0
|
||||||
@@ -57,12 +90,9 @@ docker_info_for_path() {
|
|||||||
# ── --list ────────────────────────────────────────────────────────────────────
|
# ── --list ────────────────────────────────────────────────────────────────────
|
||||||
if [ "${1:-}" = "--list" ]; then
|
if [ "${1:-}" = "--list" ]; then
|
||||||
echo ""
|
echo ""
|
||||||
info "Loading snapshots..."
|
info "Loading snapshots (dest: $ACTIVE_DEST)..."
|
||||||
JSON=$(all_snapshots_json)
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Backup sources:"
|
all_snapshots_json | jq -r '
|
||||||
echo ""
|
|
||||||
echo "$JSON" | jq -r '
|
|
||||||
group_by(.source.path)[] |
|
group_by(.source.path)[] |
|
||||||
(.[0].source.path) as $p |
|
(.[0].source.path) as $p |
|
||||||
(.[0].description // "-") as $d |
|
(.[0].description // "-") as $d |
|
||||||
@@ -76,22 +106,20 @@ fi
|
|||||||
# ── Interactive restore ───────────────────────────────────────────────────────
|
# ── Interactive restore ───────────────────────────────────────────────────────
|
||||||
echo ""
|
echo ""
|
||||||
echo "╔═══════════════════════════════════════════════════════╗"
|
echo "╔═══════════════════════════════════════════════════════╗"
|
||||||
echo "║ Kopia Backup Restore ║"
|
echo "║ Kopia Restore ║"
|
||||||
echo "╚═══════════════════════════════════════════════════════╝"
|
echo "╚═══════════════════════════════════════════════════════╝"
|
||||||
echo ""
|
echo ""
|
||||||
|
[ "${#_DEST_ARR[@]}" -gt 1 ] && info "Using destination: $ACTIVE_DEST"
|
||||||
|
|
||||||
info "Loading snapshot index..."
|
info "Loading snapshot index..."
|
||||||
SNAP_JSON=$(all_snapshots_json)
|
SNAP_JSON=$(all_snapshots_json)
|
||||||
|
|
||||||
[ -z "$SNAP_JSON" ] || [ "$SNAP_JSON" = "[]" ] || [ "$SNAP_JSON" = "null" ] \
|
[ -z "$SNAP_JSON" ] || [ "$SNAP_JSON" = "[]" ] || [ "$SNAP_JSON" = "null" ] \
|
||||||
&& die "No snapshots found. Run a backup first: sudo $HERE/backup.sh"
|
&& die "No snapshots found. Run a backup first: sudo $HERE/backup_kopia.sh"
|
||||||
|
|
||||||
# Build source arrays (newest-first per source)
|
|
||||||
mapfile -t SRC_PATHS < <(echo "$SNAP_JSON" | jq -r 'group_by(.source.path)[] | .[0].source.path')
|
mapfile -t SRC_PATHS < <(echo "$SNAP_JSON" | jq -r 'group_by(.source.path)[] | .[0].source.path')
|
||||||
mapfile -t SRC_DESCS < <(echo "$SNAP_JSON" | jq -r 'group_by(.source.path)[] | .[0].description // "-"')
|
mapfile -t SRC_DESCS < <(echo "$SNAP_JSON" | jq -r 'group_by(.source.path)[] | .[0].description // "-"')
|
||||||
mapfile -t SRC_LATEST < <(echo "$SNAP_JSON" | jq -r 'group_by(.source.path)[] | .[0].startTime | split("T") | "\(.[0]) \(.[1][:8])"')
|
mapfile -t SRC_LATEST < <(echo "$SNAP_JSON" | jq -r 'group_by(.source.path)[] | .[0].startTime | split("T") | "\(.[0]) \(.[1][:8])"')
|
||||||
mapfile -t SRC_COUNTS < <(echo "$SNAP_JSON" | jq -r 'group_by(.source.path)[] | length')
|
mapfile -t SRC_COUNTS < <(echo "$SNAP_JSON" | jq -r 'group_by(.source.path)[] | length')
|
||||||
|
|
||||||
[ "${#SRC_PATHS[@]}" -eq 0 ] && die "No snapshot sources found."
|
[ "${#SRC_PATHS[@]}" -eq 0 ] && die "No snapshot sources found."
|
||||||
|
|
||||||
echo "What do you want to restore?"
|
echo "What do you want to restore?"
|
||||||
@@ -121,7 +149,6 @@ mapfile -t SNAP_IDS < <(echo "$SNAP_JSON" | jq -r --arg p "$SOURCE_PATH" '
|
|||||||
mapfile -t SNAP_TIMES < <(echo "$SNAP_JSON" | jq -r --arg p "$SOURCE_PATH" '
|
mapfile -t SNAP_TIMES < <(echo "$SNAP_JSON" | jq -r --arg p "$SOURCE_PATH" '
|
||||||
[.[] | select(.source.path == $p)] | sort_by(.startTime) | reverse | .[0:15] |
|
[.[] | select(.source.path == $p)] | sort_by(.startTime) | reverse | .[0:15] |
|
||||||
.[].startTime | split("T") | "\(.[0]) \(.[1][:8]) UTC"')
|
.[].startTime | split("T") | "\(.[0]) \(.[1][:8]) UTC"')
|
||||||
|
|
||||||
[ "${#SNAP_IDS[@]}" -eq 0 ] && die "No snapshots found for that source."
|
[ "${#SNAP_IDS[@]}" -eq 0 ] && die "No snapshots found for that source."
|
||||||
|
|
||||||
for i in "${!SNAP_IDS[@]}"; do
|
for i in "${!SNAP_IDS[@]}"; do
|
||||||
@@ -153,8 +180,6 @@ read -rp "Select [1/2] or q to quit: " MODE
|
|||||||
[[ "$MODE" =~ ^[qQ]$ ]] && echo "Cancelled." && exit 0
|
[[ "$MODE" =~ ^[qQ]$ ]] && echo "Cancelled." && exit 0
|
||||||
|
|
||||||
case "$MODE" in
|
case "$MODE" in
|
||||||
|
|
||||||
# ── Inspect: restore to /tmp, nothing touched ─────────────────────────────────
|
|
||||||
1)
|
1)
|
||||||
TEMP_DIR="/tmp/kopia-inspect-$(date +%Y%m%d-%H%M%S)"
|
TEMP_DIR="/tmp/kopia-inspect-$(date +%Y%m%d-%H%M%S)"
|
||||||
mkdir -p "$TEMP_DIR"
|
mkdir -p "$TEMP_DIR"
|
||||||
@@ -174,15 +199,12 @@ case "$MODE" in
|
|||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
|
|
||||||
# ── Restore in place: move aside, restore, offer rollback instructions ─────────
|
|
||||||
2)
|
2)
|
||||||
SINFO=$(docker_info_for_path "$SOURCE_PATH")
|
SINFO=$(docker_info_for_path "$SOURCE_PATH")
|
||||||
SVC_NAME="${SINFO%% *}"
|
SVC_NAME="${SINFO%% *}"
|
||||||
COMPOSE_FILE="${SINFO##* }"
|
COMPOSE_FILE="${SINFO##* }"
|
||||||
# If no match, both vars will be empty or equal
|
|
||||||
[ "$SVC_NAME" = "$COMPOSE_FILE" ] && SVC_NAME="" && COMPOSE_FILE=""
|
[ "$SVC_NAME" = "$COMPOSE_FILE" ] && SVC_NAME="" && COMPOSE_FILE=""
|
||||||
|
|
||||||
# Stop associated Docker service if running
|
|
||||||
STOPPED=false
|
STOPPED=false
|
||||||
if [ -n "$SVC_NAME" ] && [ -n "$COMPOSE_FILE" ] \
|
if [ -n "$SVC_NAME" ] && [ -n "$COMPOSE_FILE" ] \
|
||||||
&& docker ps --format '{{.Names}}' 2>/dev/null | grep -qx "$SVC_NAME"; then
|
&& docker ps --format '{{.Names}}' 2>/dev/null | grep -qx "$SVC_NAME"; then
|
||||||
@@ -201,7 +223,6 @@ case "$MODE" in
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Move current data aside
|
|
||||||
ASIDE="${SOURCE_PATH}.pre-restore-$(date +%Y%m%d-%H%M%S)"
|
ASIDE="${SOURCE_PATH}.pre-restore-$(date +%Y%m%d-%H%M%S)"
|
||||||
echo ""
|
echo ""
|
||||||
if [ -e "$SOURCE_PATH" ]; then
|
if [ -e "$SOURCE_PATH" ]; then
|
||||||
@@ -212,7 +233,6 @@ case "$MODE" in
|
|||||||
warn "Source path doesn't exist yet: $SOURCE_PATH — restoring fresh."
|
warn "Source path doesn't exist yet: $SOURCE_PATH — restoring fresh."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Restore snapshot
|
|
||||||
mkdir -p "$SOURCE_PATH"
|
mkdir -p "$SOURCE_PATH"
|
||||||
info "Restoring $SNAPSHOT_TIME → $SOURCE_PATH ..."
|
info "Restoring $SNAPSHOT_TIME → $SOURCE_PATH ..."
|
||||||
if k restore "$SNAPSHOT_ID" "$SOURCE_PATH"; then
|
if k restore "$SNAPSHOT_ID" "$SOURCE_PATH"; then
|
||||||
@@ -230,7 +250,6 @@ case "$MODE" in
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Restart container
|
|
||||||
if [ "$STOPPED" = true ] && [ -n "$COMPOSE_FILE" ]; then
|
if [ "$STOPPED" = true ] && [ -n "$COMPOSE_FILE" ]; then
|
||||||
echo ""
|
echo ""
|
||||||
read -rp " Start '$SVC_NAME' now? (Y/n): " START_YN
|
read -rp " Start '$SVC_NAME' now? (Y/n): " START_YN
|
||||||
@@ -251,8 +270,6 @@ case "$MODE" in
|
|||||||
echo " Restored to : $SOURCE_PATH"
|
echo " Restored to : $SOURCE_PATH"
|
||||||
[ -e "$ASIDE" ] && echo " Previous data : $ASIDE"
|
[ -e "$ASIDE" ] && echo " Previous data : $ASIDE"
|
||||||
echo ""
|
echo ""
|
||||||
echo " Verify your data, then:"
|
|
||||||
echo ""
|
|
||||||
if [ -e "$ASIDE" ]; then
|
if [ -e "$ASIDE" ]; then
|
||||||
echo " Keep the restore (delete aside copy when satisfied):"
|
echo " Keep the restore (delete aside copy when satisfied):"
|
||||||
echo " rm -rf \"$ASIDE\""
|
echo " rm -rf \"$ASIDE\""
|
||||||
+21
-159
@@ -12,8 +12,8 @@
|
|||||||
#
|
#
|
||||||
# Creates: ~/docker/backup/
|
# Creates: ~/docker/backup/
|
||||||
# backup.conf settings + per-service destination map (chmod 600)
|
# backup.conf settings + per-service destination map (chmod 600)
|
||||||
# backup.sh worker (run directly or via systemd timer)
|
# backup_kopia.sh worker (run directly or via systemd timer)
|
||||||
# restore/<dest>/ restore_kopia_backup.sh + backup.conf per destination
|
# restore_kopia.sh interactive restore helper
|
||||||
|
|
||||||
register_service backup backup "Encrypted backup of all Docker services (full restore)"
|
register_service backup backup "Encrypted backup of all Docker services (full restore)"
|
||||||
|
|
||||||
@@ -22,9 +22,8 @@ install_backup() {
|
|||||||
|
|
||||||
local DIR="$DOCKER_DIR/backup"
|
local DIR="$DOCKER_DIR/backup"
|
||||||
local CONF_FILE="$DIR/backup.conf"
|
local CONF_FILE="$DIR/backup.conf"
|
||||||
local WORKER="$DIR/backup.sh"
|
local WORKER="$DIR/backup_kopia.sh"
|
||||||
local RESTORE_DIR="$DIR/restore"
|
local RESTORE="$DIR/restore_kopia.sh"
|
||||||
local RESTORE_SRC="${HERE:-}/extras/restore_kopia_backup.sh"
|
|
||||||
local SVC_NAME="post-install-backup"
|
local SVC_NAME="post-install-backup"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
@@ -248,7 +247,7 @@ install_backup() {
|
|||||||
KEEP_LATEST="${KEEP_LATEST:-7}"
|
KEEP_LATEST="${KEEP_LATEST:-7}"
|
||||||
|
|
||||||
# ── 7. Create dirs + init Kopia repos ────────────────────────────────────
|
# ── 7. Create dirs + init Kopia repos ────────────────────────────────────
|
||||||
mkdir -p "$DIR" "$RESTORE_DIR"
|
mkdir -p "$DIR"
|
||||||
ensure_docker_dir_ownership "$DIR"
|
ensure_docker_dir_ownership "$DIR"
|
||||||
|
|
||||||
local repo pw cfg
|
local repo pw cfg
|
||||||
@@ -288,7 +287,7 @@ install_backup() {
|
|||||||
echo "# ── backup.conf ────────────────────────────────────────────────────────────"
|
echo "# ── backup.conf ────────────────────────────────────────────────────────────"
|
||||||
echo "# Generated $(date '+%F %T'). Safe to hand-edit."
|
echo "# Generated $(date '+%F %T'). Safe to hand-edit."
|
||||||
echo "# Worker : sudo $WORKER"
|
echo "# Worker : sudo $WORKER"
|
||||||
echo "# Restore: sudo $RESTORE_DIR/<dest>/restore_kopia_backup.sh"
|
echo "# Restore: sudo $RESTORE"
|
||||||
echo ""
|
echo ""
|
||||||
echo "KOPIA=\"$KOPIA_BIN\""
|
echo "KOPIA=\"$KOPIA_BIN\""
|
||||||
echo ""
|
echo ""
|
||||||
@@ -326,158 +325,23 @@ install_backup() {
|
|||||||
chmod 600 "$CONF_FILE"
|
chmod 600 "$CONF_FILE"
|
||||||
log_success "backup.conf written (chmod 600)"
|
log_success "backup.conf written (chmod 600)"
|
||||||
|
|
||||||
# ── 9. Generate worker script ─────────────────────────────────────────────
|
# ── 9. Install worker script ──────────────────────────────────────────────
|
||||||
log_info "Writing worker $WORKER ..."
|
log_info "Installing worker $WORKER ..."
|
||||||
cat > "$WORKER" << 'WORKEREOF'
|
cp "${HERE:-}/extras/backup_kopia.sh" "$WORKER"
|
||||||
#!/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
|
|
||||||
chmod +x "$WORKER"
|
chmod +x "$WORKER"
|
||||||
chown root:root "$WORKER" 2>/dev/null || true
|
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
|
if [ -f "$RESTORE_SRC" ]; then
|
||||||
for dn in "${DEST_NAMES_ARR[@]}"; do
|
cp "$RESTORE_SRC" "$RESTORE"
|
||||||
local dest_rdir="$RESTORE_DIR/$dn"
|
chmod +x "$RESTORE"
|
||||||
mkdir -p "$dest_rdir"
|
chown root:root "$RESTORE" 2>/dev/null || true
|
||||||
cp "$RESTORE_SRC" "$dest_rdir/restore_kopia_backup.sh"
|
log_success "restore_kopia.sh installed"
|
||||||
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
|
|
||||||
else
|
else
|
||||||
log_warning "extras/restore_kopia_backup.sh not found — restore scripts not installed"
|
log_warning "extras/restore_kopia.sh not found — restore script not installed"
|
||||||
log_warning "Copy it manually: cp extras/restore_kopia_backup.sh $RESTORE_DIR/<dest>/"
|
log_warning "Copy it manually: cp extras/restore_kopia.sh $RESTORE"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── 11. Systemd timer ────────────────────────────────────────────────────
|
# ── 11. Systemd timer ────────────────────────────────────────────────────
|
||||||
@@ -559,11 +423,9 @@ SVCEOF
|
|||||||
echo " sudo $WORKER back up now"
|
echo " sudo $WORKER back up now"
|
||||||
echo " sudo $WORKER snapshots list all snapshots"
|
echo " sudo $WORKER snapshots list all snapshots"
|
||||||
echo ""
|
echo ""
|
||||||
echo " Restore (per destination):"
|
echo " Restore:"
|
||||||
for dn in "${DEST_NAMES_ARR[@]}"; do
|
echo " sudo $RESTORE"
|
||||||
echo " sudo $RESTORE_DIR/$dn/restore_kopia_backup.sh"
|
echo " sudo $RESTORE --list"
|
||||||
echo " sudo $RESTORE_DIR/$dn/restore_kopia_backup.sh --list"
|
|
||||||
done
|
|
||||||
echo ""
|
echo ""
|
||||||
[ -n "$AUTORUN" ] && echo " $AUTORUN" && echo ""
|
[ -n "$AUTORUN" ] && echo " $AUTORUN" && echo ""
|
||||||
log_warning "Save your passwords (in backup.conf) somewhere safe —"
|
log_warning "Save your passwords (in backup.conf) somewhere safe —"
|
||||||
|
|||||||
+20
-182
@@ -12,8 +12,8 @@
|
|||||||
#
|
#
|
||||||
# Creates: ~/docker/borg-backup/
|
# Creates: ~/docker/borg-backup/
|
||||||
# backup.conf settings + per-dest repo/passphrase (chmod 600)
|
# backup.conf settings + per-dest repo/passphrase (chmod 600)
|
||||||
# borg-backup.sh worker (run directly or via systemd timer)
|
# backup_borg.sh worker (run directly or via systemd timer)
|
||||||
# restore/<dest>/ restore_borg_backup.sh + backup.conf per destination
|
# restore_borg.sh interactive restore helper
|
||||||
|
|
||||||
register_service borg-backup backup "Encrypted backup of all Docker services via Borg"
|
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 DIR="$DOCKER_DIR/borg-backup"
|
||||||
local CONF_FILE="$DIR/backup.conf"
|
local CONF_FILE="$DIR/backup.conf"
|
||||||
local WORKER="$DIR/borg-backup.sh"
|
local WORKER="$DIR/backup_borg.sh"
|
||||||
local RESTORE_DIR="$DIR/restore"
|
local RESTORE="$DIR/restore_borg.sh"
|
||||||
local RESTORE_SRC="${HERE:-}/extras/restore_borg_backup.sh"
|
|
||||||
local SVC_NAME="post-install-borg-backup"
|
local SVC_NAME="post-install-borg-backup"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
@@ -193,7 +192,7 @@ install_borg_backup() {
|
|||||||
KEEP_MONTHLY="${KEEP_MONTHLY:-3}"
|
KEEP_MONTHLY="${KEEP_MONTHLY:-3}"
|
||||||
|
|
||||||
# ── 7. Create dirs + init Borg repos ─────────────────────────────────────
|
# ── 7. Create dirs + init Borg repos ─────────────────────────────────────
|
||||||
mkdir -p "$DIR" "$RESTORE_DIR"
|
mkdir -p "$DIR"
|
||||||
ensure_docker_dir_ownership "$DIR"
|
ensure_docker_dir_ownership "$DIR"
|
||||||
|
|
||||||
local repo pw
|
local repo pw
|
||||||
@@ -232,7 +231,7 @@ install_borg_backup() {
|
|||||||
echo "# ── backup.conf ────────────────────────────────────────────────────────────"
|
echo "# ── backup.conf ────────────────────────────────────────────────────────────"
|
||||||
echo "# Generated $(date '+%F %T'). Safe to hand-edit."
|
echo "# Generated $(date '+%F %T'). Safe to hand-edit."
|
||||||
echo "# Worker : sudo $WORKER"
|
echo "# Worker : sudo $WORKER"
|
||||||
echo "# Restore: sudo $RESTORE_DIR/<dest>/restore_borg_backup.sh"
|
echo "# Restore: sudo $RESTORE"
|
||||||
echo ""
|
echo ""
|
||||||
echo "BORG=\"$BORG_BIN\""
|
echo "BORG=\"$BORG_BIN\""
|
||||||
echo ""
|
echo ""
|
||||||
@@ -268,182 +267,23 @@ install_borg_backup() {
|
|||||||
chmod 600 "$CONF_FILE"
|
chmod 600 "$CONF_FILE"
|
||||||
log_success "backup.conf written (chmod 600)"
|
log_success "backup.conf written (chmod 600)"
|
||||||
|
|
||||||
# ── 9. Generate worker script ─────────────────────────────────────────────
|
# ── 9. Install worker script ──────────────────────────────────────────────
|
||||||
log_info "Writing worker $WORKER ..."
|
log_info "Installing worker $WORKER ..."
|
||||||
cat > "$WORKER" << 'WORKEREOF'
|
cp "${HERE:-}/extras/backup_borg.sh" "$WORKER"
|
||||||
#!/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
|
|
||||||
chmod +x "$WORKER"
|
chmod +x "$WORKER"
|
||||||
chown root:root "$WORKER" 2>/dev/null || true
|
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
|
if [ -f "$RESTORE_SRC" ]; then
|
||||||
for dn in "${DEST_NAMES_ARR[@]}"; do
|
cp "$RESTORE_SRC" "$RESTORE"
|
||||||
local dest_rdir="$RESTORE_DIR/$dn"
|
chmod +x "$RESTORE"
|
||||||
mkdir -p "$dest_rdir"
|
chown root:root "$RESTORE" 2>/dev/null || true
|
||||||
cp "$RESTORE_SRC" "$dest_rdir/restore_borg_backup.sh"
|
log_success "restore_borg.sh installed"
|
||||||
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
|
|
||||||
else
|
else
|
||||||
log_warning "extras/restore_borg_backup.sh not found — restore scripts not installed"
|
log_warning "extras/restore_borg.sh not found — restore script not installed"
|
||||||
log_warning "Copy it manually: cp extras/restore_borg_backup.sh $RESTORE_DIR/<dest>/"
|
log_warning "Copy it manually: cp extras/restore_borg.sh $RESTORE"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── 11. Systemd timer ─────────────────────────────────────────────────────
|
# ── 11. Systemd timer ─────────────────────────────────────────────────────
|
||||||
@@ -525,10 +365,8 @@ SVCEOF
|
|||||||
echo " sudo $WORKER info repo stats"
|
echo " sudo $WORKER info repo stats"
|
||||||
echo ""
|
echo ""
|
||||||
echo " Restore:"
|
echo " Restore:"
|
||||||
for dn in "${DEST_NAMES_ARR[@]}"; do
|
echo " sudo $RESTORE"
|
||||||
echo " sudo $RESTORE_DIR/$dn/restore_borg_backup.sh"
|
echo " sudo $RESTORE --list"
|
||||||
echo " sudo $RESTORE_DIR/$dn/restore_borg_backup.sh --list"
|
|
||||||
done
|
|
||||||
echo ""
|
echo ""
|
||||||
log_warning "IMPORTANT — back up your Borg key and passphrase now."
|
log_warning "IMPORTANT — back up your Borg key and passphrase now."
|
||||||
echo " The key is stored in the repo itself (repokey-blake2 encryption)."
|
echo " The key is stored in the repo itself (repokey-blake2 encryption)."
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ install_gaming_backup() {
|
|||||||
# ── Repo-conventional paths ──────────────────────────────────────────────
|
# ── Repo-conventional paths ──────────────────────────────────────────────
|
||||||
local BACKUP_DIR="$DOCKER_DIR/gaming-backup"
|
local BACKUP_DIR="$DOCKER_DIR/gaming-backup"
|
||||||
local CONF_FILE="$BACKUP_DIR/backup.conf" # editable settings
|
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 KOPIA_CONFIG="/etc/gaming-backup/repository.config"
|
||||||
local CACHE_DIR="/var/cache/gaming-backup"
|
local CACHE_DIR="/var/cache/gaming-backup"
|
||||||
local SVC_NAME="post-install-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)."
|
|| log_warning "Could not pre-set Steam ignore policy (applies on first snapshot)."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── 7. Generate the worker script ────────────────────────────────────────
|
# ── 7. Install the worker script ─────────────────────────────────────────
|
||||||
log_info "Writing worker $WORKER ..."
|
log_info "Installing worker $WORKER ..."
|
||||||
cat > "$WORKER" << 'WORKEREOF'
|
cp "${HERE:-}/extras/backup_gaming.sh" "$WORKER"
|
||||||
#!/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
|
|
||||||
chmod +x "$WORKER"
|
chmod +x "$WORKER"
|
||||||
chown root:root "$WORKER" 2>/dev/null || true
|
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 ───────────────────────────────────
|
# ── Copy the interactive restore script ───────────────────────────────────
|
||||||
local RESTORE_SRC="${HERE:-}/extras/restore_kopia_backup.sh"
|
local RESTORE_SRC="${HERE:-}/extras/restore_kopia.sh"
|
||||||
local RESTORE_DEST="$BACKUP_DIR/restore_kopia_backup.sh"
|
local RESTORE_DEST="$BACKUP_DIR/restore_kopia.sh"
|
||||||
if [ -f "$RESTORE_SRC" ]; then
|
if [ -f "$RESTORE_SRC" ]; then
|
||||||
cp "$RESTORE_SRC" "$RESTORE_DEST"
|
cp "$RESTORE_SRC" "$RESTORE_DEST"
|
||||||
chmod +x "$RESTORE_DEST"
|
chmod +x "$RESTORE_DEST"
|
||||||
chown root:root "$RESTORE_DEST" 2>/dev/null || true
|
chown root:root "$RESTORE_DEST" 2>/dev/null || true
|
||||||
log_success "restore_kopia_backup.sh installed"
|
log_success "restore_kopia.sh installed"
|
||||||
else
|
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
|
fi
|
||||||
|
|
||||||
# ── 8. Install systemd timer (fallback: cron) ────────────────────────────
|
# ── 8. Install systemd timer (fallback: cron) ────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user