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
250 lines
10 KiB
Bash
250 lines
10 KiB
Bash
#!/bin/bash
|
|
# extras/restore_borg.sh — interactive restore from a Borg archive.
|
|
# Installed to ~/docker/borg-backup/restore_borg.sh by the borg-backup installer.
|
|
#
|
|
# Run as root:
|
|
# sudo ./restore_borg.sh interactive
|
|
# sudo ./restore_borg.sh --list list all archives and exit
|
|
#
|
|
# Reads backup.conf from the same directory (chmod 600, root-only).
|
|
set -uo pipefail
|
|
|
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
CONF="${BACKUP_CONF:-$HERE/backup.conf}"
|
|
|
|
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m'
|
|
info() { echo -e "${BLUE}[INFO]${NC} $*"; }
|
|
ok() { echo -e "${GREEN}[OK]${NC} $*"; }
|
|
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
|
err() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
|
die() { err "$*"; exit 1; }
|
|
|
|
# ── Preflight ─────────────────────────────────────────────────────────────────
|
|
[ "${EUID:-$(id -u)}" -eq 0 ] || die "Run as root: sudo $0"
|
|
[ -f "$CONF" ] || die "backup.conf not found: $CONF"
|
|
command -v borg >/dev/null 2>&1 || die "borg not found — install: sudo apt install borgbackup"
|
|
|
|
# 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_BASE="$ACTUAL_HOME/docker"
|
|
|
|
# ── Destination picker (skipped when only one dest) ───────────────────────────
|
|
read -ra _DEST_ARR <<< "${DEST_NAMES:-default}"
|
|
|
|
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 ────────────────────────────────────────────────────────────────────
|
|
if [ "${1:-}" = "--list" ]; then
|
|
echo ""
|
|
info "Archives in $BORG_REPO (dest: $ACTIVE_DEST):"
|
|
echo ""
|
|
b list 2>/dev/null | sort -t- -k2 -r
|
|
echo ""
|
|
exit 0
|
|
fi
|
|
|
|
# ── Interactive restore ───────────────────────────────────────────────────────
|
|
echo ""
|
|
echo "╔═══════════════════════════════════════════════════════╗"
|
|
echo "║ Borg Restore ║"
|
|
echo "╚═══════════════════════════════════════════════════════╝"
|
|
echo ""
|
|
[ "${#_DEST_ARR[@]}" -gt 1 ] && info "Using destination: $ACTIVE_DEST ($BORG_REPO)"
|
|
|
|
info "Loading archive list..."
|
|
ARCHIVE_LIST=$(b list --format '{archive}{NL}' 2>/dev/null)
|
|
[ -z "$ARCHIVE_LIST" ] && die "No archives found. Run a backup first: sudo $HERE/backup_borg.sh"
|
|
|
|
# Group archives by service prefix (everything before the timestamp)
|
|
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 ""
|
|
for i in "${!SERVICES[@]}"; do
|
|
svc="${SERVICES[$i]}"
|
|
latest=$(echo "$ARCHIVE_LIST" | grep "^${svc}-" | sort | tail -1)
|
|
count=$(echo "$ARCHIVE_LIST" | grep -c "^${svc}-")
|
|
printf " %2d) %-28s latest: %s | %s archive(s)\n" \
|
|
"$((i+1))" "$svc" "${latest#${svc}-}" "$count"
|
|
done
|
|
echo ""
|
|
|
|
read -rp "Select service [1-${#SERVICES[@]}] or q to quit: " SEL
|
|
[[ "$SEL" =~ ^[qQ]$ ]] && echo "Cancelled." && exit 0
|
|
[[ "$SEL" =~ ^[0-9]+$ ]] && [ "$SEL" -ge 1 ] && [ "$SEL" -le "${#SERVICES[@]}" ] \
|
|
|| die "Invalid selection: $SEL"
|
|
|
|
SELECTED_SVC="${SERVICES[$((SEL-1))]}"
|
|
ok "Service: $SELECTED_SVC"
|
|
|
|
# ── Pick an archive ───────────────────────────────────────────────────────────
|
|
echo ""
|
|
echo "Available archives (most recent first):"
|
|
echo ""
|
|
mapfile -t ARCHIVES < <(echo "$ARCHIVE_LIST" | grep "^${SELECTED_SVC}-" | sort -r | head -20)
|
|
[ "${#ARCHIVES[@]}" -eq 0 ] && die "No archives found for $SELECTED_SVC."
|
|
|
|
for i in "${!ARCHIVES[@]}"; do
|
|
note=""; [ "$i" -eq 0 ] && note=" ← latest"
|
|
ts="${ARCHIVES[$i]#${SELECTED_SVC}-}"
|
|
printf " %2d) %s%s\n" "$((i+1))" "$ts" "$note"
|
|
done
|
|
echo ""
|
|
|
|
read -rp "Select archive [1-${#ARCHIVES[@]}, Enter = latest]: " ARCH_SEL
|
|
ARCH_SEL="${ARCH_SEL:-1}"
|
|
[[ "$ARCH_SEL" =~ ^[0-9]+$ ]] && [ "$ARCH_SEL" -ge 1 ] && [ "$ARCH_SEL" -le "${#ARCHIVES[@]}" ] \
|
|
|| die "Invalid selection: $ARCH_SEL"
|
|
|
|
SELECTED_ARCHIVE="${ARCHIVES[$((ARCH_SEL-1))]}"
|
|
ok "Archive: $SELECTED_ARCHIVE"
|
|
|
|
TARGET_DIR="$DOCKER_BASE/$SELECTED_SVC"
|
|
COMPOSE_FILE="$TARGET_DIR/docker-compose.yml"
|
|
|
|
# ── Choose restore mode ───────────────────────────────────────────────────────
|
|
echo ""
|
|
echo "Restore mode:"
|
|
echo ""
|
|
echo " 1) Inspect — extract to /tmp so you can browse without touching live data"
|
|
echo " 2) Restore — move current data aside, restore archive in its place"
|
|
echo " old data kept as .pre-restore-DATE (easy rollback)"
|
|
echo ""
|
|
read -rp "Select [1/2] or q to quit: " MODE
|
|
[[ "$MODE" =~ ^[qQ]$ ]] && echo "Cancelled." && exit 0
|
|
|
|
case "$MODE" in
|
|
1)
|
|
TEMP_DIR="/tmp/borg-inspect-$(date +%Y%m%d-%H%M%S)"
|
|
mkdir -p "$TEMP_DIR"
|
|
echo ""
|
|
info "Extracting $SELECTED_ARCHIVE to $TEMP_DIR (nothing live is changed)..."
|
|
if ( cd "$TEMP_DIR" && b extract --progress "$BORG_REPO::$SELECTED_ARCHIVE" ); then
|
|
echo ""
|
|
ok "Done. Browse the extracted files:"
|
|
echo ""
|
|
echo " ls -la $TEMP_DIR"
|
|
echo " Service directory: $TEMP_DIR${TARGET_DIR}"
|
|
echo ""
|
|
echo " When finished:"
|
|
echo " rm -rf $TEMP_DIR"
|
|
else
|
|
rm -rf "$TEMP_DIR" 2>/dev/null || true
|
|
die "Extraction failed — no changes were made."
|
|
fi
|
|
;;
|
|
|
|
2)
|
|
STOPPED=false
|
|
if [ -f "$COMPOSE_FILE" ] \
|
|
&& docker ps --format '{{.Names}}' 2>/dev/null | grep -qx "$SELECTED_SVC"; then
|
|
echo ""
|
|
warn "Container '$SELECTED_SVC' is currently running."
|
|
read -rp " Stop it before restoring? Recommended to avoid corruption. (Y/n): " STOP_YN
|
|
if [[ ! "$STOP_YN" =~ ^[Nn]$ ]]; then
|
|
info "Stopping $SELECTED_SVC..."
|
|
docker compose -f "$COMPOSE_FILE" down 2>/dev/null \
|
|
|| docker stop "$SELECTED_SVC" 2>/dev/null \
|
|
|| warn "Could not stop $SELECTED_SVC — continuing anyway."
|
|
STOPPED=true
|
|
ok "$SELECTED_SVC stopped."
|
|
else
|
|
warn "Restoring with $SELECTED_SVC running — consistency not guaranteed."
|
|
fi
|
|
fi
|
|
|
|
ASIDE="${TARGET_DIR}.pre-restore-$(date +%Y%m%d-%H%M%S)"
|
|
echo ""
|
|
if [ -e "$TARGET_DIR" ]; then
|
|
info "Moving current data aside → $(basename "$ASIDE")"
|
|
mv "$TARGET_DIR" "$ASIDE"
|
|
ok "Current data saved at: $ASIDE"
|
|
else
|
|
warn "$TARGET_DIR does not exist — restoring fresh."
|
|
fi
|
|
|
|
mkdir -p "$TARGET_DIR"
|
|
info "Restoring $SELECTED_ARCHIVE → $TARGET_DIR ..."
|
|
EXTRACT_PATH="${TARGET_DIR#/}"
|
|
if ( cd / && b extract --progress "$BORG_REPO::$SELECTED_ARCHIVE" "$EXTRACT_PATH" ); then
|
|
ok "Restore complete."
|
|
else
|
|
err "Restore failed — rolling back to original data."
|
|
rm -rf "$TARGET_DIR" 2>/dev/null || true
|
|
if [ -e "$ASIDE" ]; then
|
|
mv "$ASIDE" "$TARGET_DIR"
|
|
ok "Original data recovered from aside copy."
|
|
fi
|
|
if [ "$STOPPED" = true ] && [ -f "$COMPOSE_FILE" ]; then
|
|
docker compose -f "$COMPOSE_FILE" up -d 2>/dev/null || true
|
|
fi
|
|
exit 1
|
|
fi
|
|
|
|
if [ "$STOPPED" = true ] && [ -f "$COMPOSE_FILE" ]; then
|
|
echo ""
|
|
read -rp " Start '$SELECTED_SVC' now? (Y/n): " START_YN
|
|
if [[ ! "$START_YN" =~ ^[Nn]$ ]]; then
|
|
info "Starting $SELECTED_SVC..."
|
|
docker compose -f "$COMPOSE_FILE" up -d 2>/dev/null \
|
|
&& ok "$SELECTED_SVC started." \
|
|
|| warn "Start failed — check: docker compose -f $COMPOSE_FILE logs"
|
|
fi
|
|
fi
|
|
|
|
echo ""
|
|
echo "═══════════════════════════════════════════════════════"
|
|
echo " RESTORE COMPLETE"
|
|
echo "═══════════════════════════════════════════════════════"
|
|
echo ""
|
|
echo " Restored from : $SELECTED_ARCHIVE"
|
|
echo " Restored to : $TARGET_DIR"
|
|
[ -e "$ASIDE" ] && echo " Previous data : $ASIDE"
|
|
echo ""
|
|
if [ -e "$ASIDE" ]; then
|
|
echo " Keep the restore (delete aside copy when satisfied):"
|
|
echo " rm -rf \"$ASIDE\""
|
|
echo ""
|
|
echo " Roll back to previous data:"
|
|
[ -f "$COMPOSE_FILE" ] && echo " docker compose -f $COMPOSE_FILE down"
|
|
echo " rm -rf \"$TARGET_DIR\""
|
|
echo " mv \"$ASIDE\" \"$TARGET_DIR\""
|
|
[ -f "$COMPOSE_FILE" ] && echo " docker compose -f $COMPOSE_FILE up -d"
|
|
fi
|
|
echo ""
|
|
;;
|
|
|
|
*)
|
|
echo "Cancelled."
|
|
exit 0
|
|
;;
|
|
esac
|