HOWTO: Backup script for external USB Drives that are hot pluggable

Hello all,

i have implemented the functionality to autodetect any ext4 (or any other filesystem spedified) connected disks. You can either still specify an UUID to back up to or let the script select any ext4 disk. It will automatically cycle through the connected disks by saving the last used UUID into the $DATAFILE.

I also moved my script location to a seperate dataset called Scripts as I read that the filesystem of Truenas is not a safe location as it might be deleted on an update.

I might have overengineered the saving functionality of the last UUID but had fun while doing it and either on future amendments it might be useful or other scripts I will work on in the future.

Thanks again for your previous inputs and scripts to copy. @Arwen I copied your script documentation format :+1:
I might still implement the error codes or any bugfixes. For now this is already more then sufficient for me :slight_smile:

Feel free to use any parts of the script for yourself!

I will also edit the first post, once I have the rights to do so. Could anyone help me with this please?

#!/bin/bash
#ident  "TrueNAS backup to USB disks  0.02     2025/08/03    Matthias Geffert"
#
#   Script name:
#       rsync_backup.sh
#
#   Description:
#	  Perform the following:
#	 - Create a log file
#	 - Either backups to a specified UUID given as a parameter ./rsync_backup.sh 
#	   e.g. ./rsync_backup.sh e9624b97-f9f1-4048-af11-a91b7dc85d0c
#	   Or backups to any connected disk with the defined filesystem in configuration
#	   section and changes the disk each time the script runs
#
#   Usage:
#	   e.g. ./rsync_backup.sh
#	   e.g. ./rsync_backup.sh e9624b97-f9f1-4048-af11-a91b7dc85d0c
#
#   Author info:
#       Matthias Geffert
#       (C) Copyright Matthias Geffert
#
#   Permission and fees:
#       You have the right to run this script without any warranty. If you
#       find it useful, let me know. If it breaks something, I am not
#       responsible. If you modify this script, do not alter my credits,
#       add your own.
#
#   Notes:
#     	It is assumed that the backup will fit on the destination. Thus,
#	      checking log file after backup is helpful to detect errors.
#
#   History:
#       2025/07/31 - Matthias Geffert - First version of script
#       2025/08/04 - Matthias Geffert - Added automatic UUID selction and
#                    read/write functionality to save last used UUID
#
#   To be added:
#     - creation of exit codes
#
##############################################################################

# debug information for UUID selection
#exec 3>&1 1>"/mnt/Data/Scripts/test.log" 2>&1

##############
#Configuration
##############

DATAFILE="/mnt/Data/Scripts/rsync_backup_data.txt"
LOGFOLDER="/mnt/Data/Dokumente/backup_logs"
SOURCEFOLDERS=("/mnt/Data/Dokumente" "/mnt/Data/Bilder" "/mnt/Data/Backup" "/mnt/Data/Syncthing" "mnt/Data/Scripts")
DESTFOLDER="/mnt/backupdisk"
FILESYSTEMTYPE="ext4"

##############
#Functions
##############

#check if UUID disk is mounted
isUuidMounted() { findmnt --source UUID="$CURUUID" >/dev/null;} #UUID only

#Read data from $DATAFILE
readData() {
  SEARCHCONTENT=$1
  if test -f $DATAFILE; then
    #Read file content into arrays
    mapfile -t FILECONTENTNAMES < <( cat $DATAFILE | cut -d'=' -f1)
    mapfile -t FILECONTENTVALUES < <( cat $DATAFILE | cut -d'=' -f2)
    #Search for Parameter 
    COUNTCONTENT=${#FILECONTENTNAMES[@]}
    for (( i=0; i<$COUNTCONTENT; i++ ));
    do
      if [ "${FILECONTENTNAMES[$i]}" = "$SEARCHCONTENT" ]; then
        #Return Parameter value
        echo ${FILECONTENTVALUES[$i]}
      fi
    done
  
  else
    echo "File not found"
  fi
}

#Save data to $DATAFILE
saveData() {
  PARAMETER=$1
  PARAMETERCONTENT=$2

  if test -f $DATAFILE; then
    #Read file content into arrays
    mapfile -t FILECONTENTNAMES < <( cat $DATAFILE | cut -d'=' -f1)
    mapfile -t FILECONTENTVALUES < <( cat $DATAFILE | cut -d'=' -f2)
    #Search for parameter to write
    COUNTCONTENT=${#FILECONTENTNAMES[@]}
    for (( i=0; i<$COUNTCONTENT; i++ ));
    do
      if [ "${FILECONTENTNAMES[$i]}" = "$PARAMETER" ]; then
        FILECONTENTVALUES[$i]=$PARAMETERCONTENT
        FOUNDPARAMETER=1
      fi
    done
    #write parameter
    COUNTCONTENT=${#FILECONTENTNAMES[@]}
    WRITEARRAY=
    for (( i=0; i<$COUNTCONTENT; i++ ));
    do
      WRITEARRAY+=${FILECONTENTNAMES[$i]}"="${FILECONTENTVALUES[$i]}"\n"
    done
    #if parameter has not been found
    if [[ ! $FOUNDPARAMETER ]]; then
      WRITEARRAY+=$PARAMETER"="$PARAMETERCONTENT"\n"
    fi
  #if file has not been found
  else
    WRITEARRAY+=$PARAMETER"="$PARAMETERCONTENT"\n"
  fi
  printf ${WRITEARRAY[@]} > $DATAFILE
}

######################
#Start of backupscript
######################

#Check if UUID has been given as parameter, otherwise select one of all connected ext4 disks
if [ "$1" != "" ]; then
  CURUUID="$1"
else
  mapfile -t ALLUUIDS < <( sudo blkid -tTYPE=$FILESYSTEMTYPE -sUUID | cut -d'"' -f2)
  #mapfile -t ALLUUIDS < <( sudo blkid -tTYPE=ext4 -sUUID | cut -d'"' -f2)
  CURUUID=""
  #check if there was saved a previously backup UUID
  if test -f $DATAFILE; then
    LASTUUID=$(readData "lastUUID")
    echo "LASTUUID:"$LASTUUID
    if [ ! $LASTUUID = "" ]; then
      #loop through UUIDs to check what is the next UUID
      COUNTUUIDS=${#ALLUUIDS[@]}
      for (( i=0; i<COUNTUUIDS; i++ ));
      do
        if [ "${ALLUUIDS[$i]}" = "$LASTUUID" ]; then
          CURUUID=${ALLUUIDS[$i + 1]}
        fi
      done 
      #if the File for storage of UUID has not been found or UUID was at the end of array use the first UUID
      if [ "$CURUUID" = "File not found" ] || [ "$CURUUID" = "" ]; then
        CURUUID=${ALLUUIDS[0]}
      fi
    #if LASTUUID is empty
    else
      CURUUID=${ALLUUIDS[0]}
    fi
  #if $DATAFILE does not exist 
  else
    CURUUID=${ALLUUIDS[0]}
  fi
  #save current used UUID
  saveData "lastUUID" $CURUUID
fi

#debug info for UUID selection
echo "array0  :"${ALLUUIDS[0]}
echo "array1  :"${ALLUUIDS[1]}
echo "array2  :"${ALLUUIDS[2]}
echo "array3  :"${ALLUUIDS[3]}
echo "LASTUUID:"$LASTUUID
echo "CURUUID :"$CURUUID
#set Logfilename
LOGDATE=$(date +"%Y%m%d-%H%M%S")
LOGNAME=$LOGDATE"_"$CURUUID
#mount UUID Disk
mkdir -p /mnt/backupdisk
mount UUID=$CURUUID /mnt/backupdisk
#create logfile
mkdir -p $LOGFOLDER
LOGFILE=$LOGFOLDER"/"$LOGNAME".log"
exec 3>&1 1>"$LOGFILE" 2>&1
#Start backup
echo "Start: "$(date -R)
echo "UUID:  "$CURUUID
echo "#############################################################"
echo
#check if disk is mounted
if isUuidMounted $CURUUID;
  then
    echo   "Disk is mounted"
    echo
    echo "#############################################################"
    echo "Truenas Configuration Backup"
    echo "#############################################################"
    echo
    mkdir -p $DESTFOLDER"/truenas_config"
    rsync -rltgoDv /var/db/system/configs-*/ $DESTFOLDER"/truenas_config"
    #do backup of all specified folders
    for FOLDER in "${SOURCEFOLDERS[@]}"
    do
      echo
      echo "#############################################################"
      echo "Sourcefolder: " $FOLDER
      echo "#############################################################"
      echo
      mkdir -p $FOLDER
      rsync -rltgoDv $FOLDER $DESTFOLDER
    done
  else
    echo   "Disk is not mounted"
fi
#unmount disk
umount /mnt/backupdisk

echo
echo "#############################################################"
echo "End: "$(date -R)
echo "#############################################################"

exit 0

3 Likes