119 lines
3.4 KiB
Bash
119 lines
3.4 KiB
Bash
#!/usr/bin/env bash
|
|
# offload-backup.sh — back up /mnt/data directly to a USB drive using restic.
|
|
#
|
|
# Run on the Pi (see homey-offload-backup in the dev shell).
|
|
# Scans for plugged-in USB partitions, lets you pick one, mounts it if needed,
|
|
# initialises a restic repo on it, and runs a backup of all service data dirs.
|
|
#
|
|
# The restic password is read from the sops-managed secret at runtime;
|
|
# no S3 credentials are needed — this is a direct local backup.
|
|
#
|
|
# Usage: sudo bash offload-backup.sh
|
|
|
|
set -euo pipefail
|
|
|
|
REPO_NAME="homey-backup"
|
|
DATA_DIR="/mnt/data"
|
|
PASSWORD_FILE="/run/secrets/restic/password"
|
|
|
|
BACKUP_PATHS=(
|
|
"$DATA_DIR/openldap"
|
|
"$DATA_DIR/authelia"
|
|
"$DATA_DIR/gitea"
|
|
"$DATA_DIR/nextcloud"
|
|
"$DATA_DIR/jellyfin"
|
|
"$DATA_DIR/transmission"
|
|
)
|
|
|
|
EXCLUDE_ARGS=(
|
|
--exclude "$DATA_DIR/nextcloud/db"
|
|
--exclude "$DATA_DIR/restic-cache"
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Find USB partitions
|
|
# ---------------------------------------------------------------------------
|
|
echo "Scanning for USB drives..."
|
|
mapfile -t USB_PARTS < <(
|
|
lsblk -o NAME,SIZE,TRAN,LABEL,MOUNTPOINT -rn \
|
|
| awk '$3 == "usb" && $2 != "" {print $1, $2, $4, $5}'
|
|
)
|
|
|
|
if [ "${#USB_PARTS[@]}" -eq 0 ]; then
|
|
echo "No USB partitions found. Plug in a USB drive and try again." >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo ""
|
|
echo "Available USB partitions:"
|
|
for i in "${!USB_PARTS[@]}"; do
|
|
read -r dev size label mount <<< "${USB_PARTS[$i]}"
|
|
label="${label:-(no label)}"
|
|
mount="${mount:-(not mounted)}"
|
|
printf " [%d] /dev/%s %s label=%s mount=%s\n" \
|
|
"$((i + 1))" "$dev" "$size" "$label" "$mount"
|
|
done
|
|
echo ""
|
|
read -rp "Select a partition [1-${#USB_PARTS[@]}]: " CHOICE
|
|
|
|
if ! [[ "$CHOICE" =~ ^[0-9]+$ ]] \
|
|
|| [ "$CHOICE" -lt 1 ] \
|
|
|| [ "$CHOICE" -gt "${#USB_PARTS[@]}" ]; then
|
|
echo "Invalid selection." >&2
|
|
exit 1
|
|
fi
|
|
|
|
read -r SELECTED_DEV _ _ EXISTING_MOUNT <<< "${USB_PARTS[$((CHOICE - 1))]}"
|
|
SELECTED_DEV="/dev/$SELECTED_DEV"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Mount if needed
|
|
# ---------------------------------------------------------------------------
|
|
MOUNTED_HERE=false
|
|
MOUNT_DIR=""
|
|
|
|
if [ -n "$EXISTING_MOUNT" ]; then
|
|
MOUNT_DIR="$EXISTING_MOUNT"
|
|
echo "Using existing mount at $MOUNT_DIR"
|
|
else
|
|
MOUNT_DIR=$(mktemp -d)
|
|
echo "Mounting $SELECTED_DEV at $MOUNT_DIR..."
|
|
mount "$SELECTED_DEV" "$MOUNT_DIR"
|
|
MOUNTED_HERE=true
|
|
fi
|
|
|
|
cleanup() {
|
|
if [ "$MOUNTED_HERE" = true ] && [ -n "$MOUNT_DIR" ]; then
|
|
echo "Unmounting $MOUNT_DIR..."
|
|
umount "$MOUNT_DIR"
|
|
rmdir "$MOUNT_DIR"
|
|
fi
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Initialise restic repo if this is the first run
|
|
# ---------------------------------------------------------------------------
|
|
REPO="$MOUNT_DIR/$REPO_NAME"
|
|
|
|
if ! restic -r "$REPO" --password-file "$PASSWORD_FILE" snapshots &>/dev/null 2>&1; then
|
|
echo "Initialising restic repository at $REPO..."
|
|
restic -r "$REPO" --password-file "$PASSWORD_FILE" init
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Run the backup
|
|
# ---------------------------------------------------------------------------
|
|
echo ""
|
|
echo "Backing up to $REPO..."
|
|
restic -r "$REPO" \
|
|
--password-file "$PASSWORD_FILE" \
|
|
--cache-dir "$DATA_DIR/restic-cache" \
|
|
backup \
|
|
"${BACKUP_PATHS[@]}" \
|
|
"${EXCLUDE_ARGS[@]}"
|
|
|
|
echo ""
|
|
echo "Snapshots on this drive:"
|
|
restic -r "$REPO" --password-file "$PASSWORD_FILE" snapshots
|