add zfs backup script
authorAndrew Lorimer <andrew@lorimer.id.au>
Mon, 24 Nov 2025 09:32:58 +0000 (20:32 +1100)
committerAndrew Lorimer <andrew@lorimer.id.au>
Mon, 24 Nov 2025 09:32:58 +0000 (20:32 +1100)
backup-tank-2.sh [new file with mode: 0755]
diff --git a/backup-tank-2.sh b/backup-tank-2.sh
new file mode 100755 (executable)
index 0000000..0ac49d1
--- /dev/null
@@ -0,0 +1,146 @@
+#!/bin/bash
+
+MOUNT_POINT_PREFIX="/mnt/backup"
+TEST=1
+DEBUG=1
+
+# Check if the CSV file is provided
+if [ "$#" -ne 1 ]; then
+    echo "Usage: $0 <path_to_csv_file>"
+    exit 1
+fi
+
+CSV_FILE="$1"
+
+# Declare associative arrays to hold paths and rsync command for each drive
+declare -A INCLUDE_PATHS
+declare -A EXCLUDE_PATHS
+declare -A RSYNC_CMDS
+
+if [ $TEST -eq 1 ]; then
+    echo "Reading index $CSV_FILE"
+fi
+
+# Read the CSV file line by line
+while IFS=',' read -r FILE_PATH DRIVE _; do
+    # Trim whitespace
+    FILE_PATH=$(echo "$FILE_PATH" | xargs)
+    DRIVE=$(echo "$DRIVE" | xargs)
+
+    # Check if row is commented with a #
+    if [[ "$FILE_PATH" == "#"* ]]; then
+       continue
+    fi
+
+    # Check if path begins with a slash
+    if [[ "$FILE_PATH" != /* ]]; then
+      echo "File path $FILE_PATH is not relative - exiting"
+      exit 1
+    fi
+
+    # Determine the actual drive path
+    if [[ "$DRIVE" =~ ^- ]]; then
+        # Exclusion case
+        DRIVE=${DRIVE:1}  # Remove the leading '-'
+        EXCLUDE_PATHS["$FILE_PATH"]=1
+    else
+        # Inclusion case
+        INCLUDE_PATHS["$FILE_PATH"]="$DRIVE"
+        RSYNC_CMDS["$DRIVE"]=""
+    fi
+done < "$CSV_FILE"
+
+
+# Now process the paths to copy
+for FILE_PATH in "${!INCLUDE_PATHS[@]}"; do
+    DRIVE="${INCLUDE_PATHS[$FILE_PATH]}"
+    MOUNT_POINT="$MOUNT_POINT_PREFIX$DRIVE"
+
+    # Check if the mount point exists
+    if [[ $TEST -eq 0 && $(mountpoint -q "$MOUNT_POINT") ]]; then
+        echo "Nothing mounted to $MOUNT_POINT - skipping $FILE_PATH"
+        continue
+    fi
+
+    # Check if the path is excluded
+    if [[ -n "${EXCLUDE_PATHS[$FILE_PATH]}" ]]; then
+        echo "Excluding $FILE_PATH"
+        continue
+    fi
+
+    # Get parent directory
+    PARENT="$(dirname "$FILE_PATH")"
+    MKDIR_CMD="mkdir -p $MOUNT_POINT$PARENT"
+    if [ $DEBUG -eq 1 ]; then
+           echo "$MKDIR_CMD"
+    if [ $TEST -eq 0 ]; then 
+           eval "$MKDIR_CMD"
+    fi
+
+    # Check if the path is a directory
+    EXCLUDE_ARGS=()
+    if [ -d "$FILE_PATH" ]; then
+      for EXCLUDE_PATH in "${!EXCLUDE_PATHS[@]}"; do
+        if [[ "$EXCLUDE_PATH" =~ ^"$FILE_PATH"* ]]; then
+         if [ -d "$EXCLUDE_PATH" ] && [[ ! "$EXCLUDE_PATH" =~ /$ ]]; then
+           EXCLUDE_ARGS+=("--exclude=\"$EXCLUDE_PATH/\"")
+         else
+           EXCLUDE_ARGS+=("--exclude=\"$EXCLUDE_PATH\"")
+         fi
+          if [[ $TEST -eq 1 ]]; then
+            echo "Excluding $EXCLUDE_PATH from $FILE_PATH"
+          fi
+        fi
+      done
+    fi
+    RSYNC_CMD="rsync -Par"
+    if [[ $TEST -eq 1 ]]; then
+      RSYNC_CMD+="vn"
+    if [[ $DEBUG -eq 0 ]]; then
+      RSYNC_CMD+="q"
+    fi
+    RSYNC_CMD+=" "${EXCLUDE_ARGS[@]}" \"$FILE_PATH\" \"$MOUNT_POINT$PARENT/\""
+    if [[ -n "${RSYNC_CMDS[$DRIVE]}" ]]; then
+      RSYNC_CMDS["$DRIVE"]+=" & $RSYNC_CMD"
+    else
+      RSYNC_CMDS["$DRIVE"]="$RSYNC_CMD"
+    fi
+done
+
+for DRIVE in "${!RSYNC_CMDS[@]}"; do
+  if [[ $DEBUG -eq 1 ]]; then
+    echo "${RSYNC_CMDS[$DRIVE]}"
+  fi
+  if [[ $TEST -eq 0 ]]; then
+    eval "${RSYNC_CMDS[$DRIVE]}" &
+  fi
+done
+
+wait
+
+echo ""
+
+for DRIVE in "${!RSYNC_CMDS[@]}"; do
+  DRIVE_SERIAL="$(lsblk -o MOUNTPOINT,SERIAL | awk -v target="$MOUNT_POINT_PREFIX$DRIVE" '$1 == target { getline; print $1; exit }')"
+  echo "-------------------------------------------"
+  echo "Backup summary for Cold Backup Drive $DRIVE"
+  echo "-------------------------------------------"
+  echo ""
+  echo "S/N $DRIVE_SERIAL"
+  echo "Backup finished $(date)"
+  echo ""
+  for FILE_PATH in "${!INCLUDE_PATHS[@]}"; do
+    if [[ "${INCLUDE_PATHS[$FILE_PATH]}" == "$DRIVE" ]]; then
+      echo "$FILE_PATH ("$(du -sh "$FILE_PATH" 2>/dev/null | cut -f1)")"
+      if [ -d "$FILE_PATH" ]; then
+        for EXCLUDE_PATH in "${!EXCLUDE_PATHS[@]}"; do
+          if [[ "$EXCLUDE_PATH" =~ ^"$FILE_PATH"* ]]; then
+            echo "    Excluding .${EXCLUDE_PATH:${#FILE_PATH}} ($(du -sh "$EXCLUDE_PATH" 2>/dev/null | cut -f1))"
+          fi
+        done
+      fi
+    fi
+  done
+  echo ""
+  echo ""
+done