One Touch Backup

A while ago I bought myself a NAS. It had a neat function called “one touch backup” but I was missing some more sophisticated features, so I implemented my own one.

The following script detects new partitions (it ignores the ones which were connected when the script was started), mounts them, creates a new folder with a time stamp for the new backup and copies the content to the backup folder. Beeper script is used to get response from the device so no monitor or any other interface is needed.

When a flash disk is connected a folder will be created for each partition on that flash disk and the content will be copied. A sound will be played when the flash is connected. After the copying job is finished a sound will be played until the flash is removed. A successful removal is also announced by a different sound.

In case more flash disks are connected at once, first will be backed up. After finish and removal second will be backed up and so on. This works fine when a flash disk is being backup up and a second is inserted into the device. But if a third one is inserted before the first one finishes, there is no guarantee which one will be backed up next.

#!/bin/bash

LOG_FILE="/var/log/${APP_NAME}.log"
APP_NAME="ot-backup"
BACKUP_PATH="/mnt/backup/xerocomus"
BACKUP_DIR="one-touch-backup"
PID_FILE="/var/run/${APP_NAME}.pid"
MOUNT_POINT="/mnt/ot-backup"

TMP_PATH="/tmp/${APP_NAME}"
MAX_ATTEMPTS=10

function checkLogFile() {
	if [ ! -f ${LOG_FILE} ]; then
		touch ${LOG_FILE}
	fi
}

function debug() {
	MESSAGE=$1
	logger -is -f ${LOG_FILE} -p daemon.debug -t "${APP_NAME}" "${MESSAGE}"
}

function info() {
	MESSAGE=$1
	logger -is -f ${LOG_FILE} -p daemon.info -t "${APP_NAME}" "${MESSAGE}"
}

function error() {
	MESSAGE=$1
	logger -is -f ${LOG_FILE} -p daemon.error -t "${APP_NAME}" "${MESSAGE}"
	/usr/local/bin/beeper ERROR
	exit 1
}

function checkPath(){
	if [ ! -d ${BACKUP_PATH} ]; then
		mount -a
	fi

	if [ -d ${BACKUP_PATH} ]; then
		if [ ! -d ${BACKUP_PATH}/${BACKUP_DIR} ]; then
			mkdir -p ${BACKUP_PATH}/${BACKUP_DIR}
		fi
		if [ ! -d ${BACKUP_PATH}/${BACKUP_DIR} ]; then
                        error "Backup directory ${BACKUP_PATH} does not exist and could not be created."
                fi

	else
		error "Backup directory ${BACKUP_PATH} does not exist"
	fi
}

function checkPid() {
	if [ -f ${PID_FILE} ]; then
		PID=`cat ${PID_FILE}`
		if [ -n "`ps ax | cut -d" " -f1 | grep ${PID}`" ]; then
			error "Another $0 is still running with PID ${PID}"
		else
			rm -f ${PID_FILE}
		fi
	fi

	PID=$$
	echo ${PID} > ${PID_FILE}
}


function mount_usb(){
	DEVICE=$1
	LABEL=$2
	if [ -n "$3" ]; then
		ATTEMPT=$3
	else
		ATTEMPT=1
	fi

	if [ "${ATTEMPT}" -gt "${MAX_ATTEMPTS}" ]; then
		error "Mount of ${DEVICE} failed, attempts: ${MAX_ATTEMPTS}"
		while [[ -n `ls /dev/${DEVICE} 2> /dev/null` ]]; do
			/usr/local/bin/beeper ERROR
		done
	fi

	if [ -n "`mount | grep ${MOUNT_POINT}`" ]; then
		info "${MOUNT_POINT} mounted."
	else
		debug "Trying to mount /dev/${DEVICE} (${LABEL})... attempt ${ATTEMPT}"
		mount /dev/${DEVICE} ${MOUNT_POINT}
		sleep 1;
		mount_usb ${DEVICE} ${LABEL} `expr ${ATTEMPT} + 1`
	fi
}

function unmount_usb() {
	if [ -n "$1" ]; then
                ATTEMPT=$1
        else
                ATTEMPT=1
        fi

	if [ ! -d ${MOUNT_POINT} ]; then
		mkdir -p ${MOUNT_POINT}
	fi

	if [ "${ATTEMPT}" -gt "${MAX_ATTEMPTS}" ]; then
		DEVICE=`mount | grep ${MOUNT_POINT} | cut -d" " -f1`
		error "Unmount of ${DEVICE} failed, attempts: ${MAX_ATTEMPTS}"
        fi

	if [ -n "`mount | grep ${MOUNT_POINT}`" ]; then
		debug "Trying to unmount ${MOUNT_POINT}... attempt ${ATTEMPT}"
		umount ${MOUNT_POINT}
		sleep 1
		unmount_usb `expr ${ATTEMPT} + 1`
	else
		info "${MOUNT_POINT} unmounted."
	fi
}

function backup(){
	LABEL=$1
	TIMESTAMP=`date +%Y-%m-%d-%H-%M-%S`

	SRC="${MOUNT_POINT}/*"
	DEST="${BACKUP_PATH}/${BACKUP_DIR}/${LABEL}/${TIMESTAMP}/"

	info "Backup of ${LABEL} into ${DEST} started."
	mkdir -p ${DEST}
	if [ ! -d ${DEST} ]; then
		error "Could not create backup destination directory: ${DEST}"
	fi
	cp -a ${SRC} ${DEST}
	info "Backup of ${LABEL} ended."
}

checkLogFile
checkPid
unmount_usb
checkPath

ignored=""
for device_path in `ls /dev/sd*`; do
	device=`echo ${device_path} | cut -d"/" -f3`
	if [[ $device =~ ^sd[a-z][0-9]$ ]]; then
		if [[ $ignored != "" ]]; then ignored="${ignored}\n"; fi
		ignored="${ignored}${device}"
	fi
done
debug "Ignoring: $ignored"

# Loop
while true; do
	for device_path in `ls /dev/sd*`; do
		device=`echo ${device_path} | cut -d"/" -f3`
		if [[ $device =~ ^sd[a-z][0-9]$ ]]; then
			# Check if not ignored
			ignore=0
			for ignored_device in `echo -e ${ignored}`; do
				if [[ $ignored_device == $device ]]; then
					ignore=1
					break
				fi
			done

			# If not ignored and partition
			if [[ $ignore == 0 ]]; then
				disk=${device:0:3}
				debug "Detected disk ${disk}"
				sleep 1
				for part_path in `ls /dev/${disk}*`; do
					part=`echo ${part_path} | cut -d"/" -f3`
					if [[ $part =~ ^${disk}[0-9]$ ]]; then
						debug "Detected partition $part"

						# Detect label
						if [[ -n `ls -l /dev/disk/by-label | grep "../../${part}"` ]]; then
							label=`ls -l /dev/disk/by-label | grep "../../${part}" | awk '{print $9}'`
						elif [[ -n `ls -l /dev/disk/by-id | grep "../../${part}"` ]]; then
							label=`ls -l /dev/disk/by-id | grep "../../${part}" | awk '{print $9}'`
						else
							label=$part
						fi

						debug "Mounting $part ($label)"
						mount_usb ${part} ${label}

						if [ -n "`mount | grep ${MOUNT_POINT}`" ]; then
							/usr/local/bin/beeper CONNECT
							backup ${label}
							unmount_usb
							/usr/local/bin/beeper DISCONNECT
						fi
					fi
				done

				while [ -n "`ls /dev/${device} 2> /dev/null`" ]; do
					/usr/local/bin/beeper FINISHED
				done
				/usr/local/bin/beeper DISCONNECT
			fi
		fi
	done
	sleep 1
done

Leave a Reply

Your email address will not be published. Required fields are marked *