#!/usr/bin/env bash set -euo pipefail SRC="${SRC:-/mnt/replicas}" DEST="${DEST:-/mnt2/homey-backup}" TIMESTAMP=$(date +%Y%m%d_%H%M%S) MANIFEST="$DEST/manifest.json" PVC_MAPPING=( "pvc-0310a337-9642-464b-a458-fcb3439328e7-fbc07d5a:ldap-pvc" "pvc-1cdc51ee-b965-4cab-baf7-077cc6df6f11-0fcfb9cd:authelia-pvc" "pvc-4888bf84-62c8-4340-adbc-cb31073d8fd2-d065d20b:gitea-pvc" "pvc-5c1f48e3-346f-4c35-8e6a-8fc0c4c3a842-96d72815:nextcloud-data-pvc" "pvc-c5b28179-1b9c-462a-be5b-05c4f0bb36ca-5f2dbf4d:nextcloud-postgres-pvc" "pvc-7f73ee94-5583-4e4a-9788-cba054214b1c-f767850a:radicale-pvc" "pvc-9e75f35a-27c3-4251-b25a-1a876f82f6c7-c9c8b185:jellyfin-config-pvc" "pvc-dfe2aa08-bbb8-423b-9001-fb6aea181597-baf06a7f:jellyfin-data-pvc" "pvc-dd4a069a-a638-49c0-8c95-f954510816e5-7e81a6f6:transmission-config-pvc" "pvc-e4ba414d-d9c2-4927-b0ae-f6bfb90ce311-a0963101:unknown-pvc-1" "pvc-ec6afe10-aca3-42ce-9d89-32fc4ac77f9a-8d6baa34:unknown-pvc-2" ) progress_bar() { local current=$1 local total=$2 local width=40 local percent=$((current * 100 / total)) local filled=$((current * width / total)) local empty=$((width - filled)) printf "\r[" printf "%${filled}s" | tr ' ' '=' printf "%${empty}s" | tr ' ' ' ' printf "] %3d%% (%d/%d)" "$percent" "$current" "$total" } get_pvc_name() { local pvc_id="$1" for mapping in "${PVC_MAPPING[@]}"; do if [[ "$mapping" == "$pvc_id:"* ]]; then echo "${mapping#*:}" return fi done echo "unknown" } echo "========================================" echo " Longhorn Volume Backup Tool" echo "========================================" echo "" echo "Source: $SRC" echo "Destination: $DEST" echo "Timestamp: $TIMESTAMP" echo "" mkdir -p "$DEST/volumes" mkdir -p "$DEST/metadata" VOLUMES=() TOTAL_SIZE=0 echo "Scanning volumes..." for pvc_dir in "$SRC"/*/; do pvc_name=$(basename "$pvc_dir") friendly_name=$(get_pvc_name "$pvc_name") VOLUMES+=("$pvc_name:$friendly_name") size=$(sudo du -sb "$pvc_dir" 2>/dev/null | awk '{print $1}' || echo "0") TOTAL_SIZE=$((TOTAL_SIZE + size)) printf " %-50s %s\n" "$friendly_name" "$(numfmt --to=iec-i --suffix=B "$size" 2>/dev/null || echo "${size}B")" done TOTAL_VOLUMES=${#VOLUMES[@]} echo "" echo "Found $TOTAL_VOLUMES volumes, total size: $(numfmt --to=iec-i --suffix=B "$TOTAL_SIZE")" echo "" read -p "Continue with backup? [y/N] " -n 1 -r echo "" if [[ ! $REPLY =~ ^[Yy]$ ]]; then echo "Aborted." exit 1 fi echo "" echo "Starting backup..." echo "" COPIED_SIZE=0 START_TIME=$(date +%s) for i in "${!VOLUMES[@]}"; do volume="${VOLUMES[$i]}" pvc_name="${volume%%:*}" friendly_name="${volume#*:}" CURRENT=$((i + 1)) progress_bar "$CURRENT" "$TOTAL_VOLUMES" echo " - $friendly_name" sudo rsync -a --no-owner --no-group --info=progress2 \ "$SRC/$pvc_name/" \ "$DEST/volumes/$pvc_name/" 2>&1 | while read -r line; do if [[ "$line" =~ to-chk=*([0-9]+)/([0-9]+) ]]; then printf "\r %s" "$line" fi done sudo chown -R "$USER:$USER" "$DEST/volumes/$pvc_name" 2>/dev/null || true if [[ -f "$SRC/$pvc_name/volume.meta" ]]; then sudo cp "$SRC/$pvc_name/volume.meta" "$DEST/metadata/${pvc_name}.meta" 2>/dev/null || true fi echo "" done echo "" echo "Generating manifest..." cat > "$MANIFEST" << EOF { "backup_timestamp": "$TIMESTAMP", "source_path": "$SRC", "destination_path": "$DEST", "total_volumes": $TOTAL_VOLUMES, "total_size_bytes": $TOTAL_SIZE, "volumes": [ EOF FIRST=true for volume in "${VOLUMES[@]}"; do pvc_name="${volume%%:*}" friendly_name="${volume#*:}" vol_size=$(sudo du -sb "$SRC/$pvc_name" 2>/dev/null | awk '{print $1}' || echo "0") vol_size_hr=$(numfmt --to=iec-i --suffix=B "$vol_size" 2>/dev/null || echo "${vol_size}B") head_file=$(sudo find "$DEST/volumes/$pvc_name" -name "volume-head-*.img" 2>/dev/null | head -1) head_file=$(basename "$head_file" 2>/dev/null || echo "") if [[ "$FIRST" == "true" ]]; then FIRST=false else echo "," >> "$MANIFEST" fi cat >> "$MANIFEST" << EOF { "pvc_id": "$pvc_name", "friendly_name": "$friendly_name", "size_bytes": $vol_size, "size_human": "$vol_size_hr", "volume_head": "$head_file", "backup_path": "volumes/$pvc_name" } EOF done cat >> "$MANIFEST" << EOF ] } EOF END_TIME=$(date +%s) DURATION=$((END_TIME - START_TIME)) echo "" echo "========================================" echo " Backup Complete!" echo "========================================" echo "" echo "Duration: $((DURATION / 60))m $((DURATION % 60))s" echo "Location: $DEST" echo "Manifest: $MANIFEST" echo "" echo "Backup size:" sudo du -sh "$DEST/volumes" echo "" echo "To mount a volume, run:" echo " ./scripts/mount-longhorn-volume.sh " echo "" echo "To restore a volume, run:" echo " ./scripts/restore-longhorn-volume.sh "