Skip to content

Instantly share code, notes, and snippets.

@zerolagtime
Created January 17, 2022 06:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zerolagtime/b7db8b6ab9b59a1fca662c5624b57bd1 to your computer and use it in GitHub Desktop.
Save zerolagtime/b7db8b6ab9b59a1fca662c5624b57bd1 to your computer and use it in GitHub Desktop.
#!/bin/bash
# Usage:
# 0 * * * * /home/zerolagtime/bin/do_backup.sh -t hourly -s /home/zerolagtime/Documents -d /data/backups/zerolagtime/Documents/ -l /home/zerolagtime/backups-documents.log -k Documents # JOB_ID_1
# 0 0 * * * /home/zerolagtime/bin/do_backup.sh -t daily -s /home/zerolagtime/Documents -d /data/backups/zerolagtime/Documents/ -l /home/zerolagtime/backups-documents.log -k Documents # JOB_ID_2
# 0 0 * * 1 /home/zerolagtime/bin/do_backup.sh -t monthly -s /home/zerolagtime/Documents -d /data/backups/zerolagtime/Documents/ -l /home/zerolagtime/backups-documents.log -K Documents # JOB_ID_3
VERSION="2.2"
DEBUG=0
LOGFILE=""
KEYWORD=""
EMAIL_USER="";
EMAIL_TEXT="";
DIRNAME="$(which /usr/bin/dirname| head -1 |awk '{print $1}')";
DATE="$(which date| head -1 |awk '{print $1}')";
PRINTF="$(which printf| head -1 |awk '{print $1}')";
ECHO=$(which echo| head -1 |awk '{print $1}');
NUM_ERRORS=0
ERRORS_LIST=""
now() {
$DATE +%m/%d/%Y\ %H:%M:%S
}
logit() {
n=$(now)
lvl=$1
shift
if [ -z "$KEYWORD" ]; then
str=$($PRINTF "[%s] [%5s] %s" "$n" "$lvl" "$*")
else
str=$($PRINTF "[%s] [%5s] [%s] %s" "$n" "$lvl" "$KEYWORD" "$*")
fi
if [ -z "$LOGFILE" ]; then
${ECHO} "$str"
else
${ECHO} "$str" >> $LOGFILE
fi
EMAIL_TEXT="${EMAIL_TEXT}
$str"
}
warn() {
logit "warn" "$*"
}
info() {
logit "info" "$*"
}
error() {
logit "ERROR" "$*"
NUM_ERRORS=$[$NUM_ERRORS + 1]
ERRORS_LIST="$*"
}
debug() {
if [ $DEBUG -ne 0 ]; then
logit "debug" "$*"
fi
}
email_log() {
if [ -n "$KEYWORD" ]; then
subject="Backup failed: $KEYWORD - $(${ECHO} "$ERRORS_LIST" |head -1 |cut -c1-30)"
msg="The $BACKUP_TYPE of $KEYWORD failed. The error was
$ERRORS_LIST"
else
subject="Backup failed: $(${ECHO} "$ERRORS_LIST" |head -1 |cut -c1-30)"
msg="Attempts to start the backup failed. The error was
$ERRORS_LIST"
fi
msg="
Details are below:
$EMAIL_MSG
-----------
run by user ${USER:?LOGNAME} on $HOSTNAME. command line was
$0 $*"
${ECHO} "$msg" | mail -s "$subject" $EMAIL_USER
}
exit_gracefully() {
code=$1;
if [ -n "$EMAIL_USER" -a -n "$ERRORS_LIST" ]; then
email_log;
fi
if [ -n "$lockfile" ]; then
${RM} -f $lockfile
fi
exit $code;
}
info "Starting up $(basename "$0") on $(uname -n) as ${USER:-$LOGNAME}"
# ------------- user configurable settings -----------------------------
BACKUPS_MAX_ARRAY[0]=4 # monthly max
BACKUPS_MAX_ARRAY[1]=3 # daily max
BACKUPS_MAX_ARRAY[2]=6 # hourly max
debug "Max monthly copies is $BACKUPS_MAX_ARRAY[0]"
debug "Max daily copies is $BACKUPS_MAX_ARRAY[1]"
debug "Max hourly copies is $BACKUPS_MAX_ARRAY[2]"
BACKUP_TYPE="" # choose monthly, daily, or hourly - default will cause an error
EXCLUDES_FROM="" # or --excludes-from=<file>
# ------------- system commands used by this script --------------------
ID=$(which id| head -1 |awk '{print $1}');
ECHO=$(which echo| head -1 |awk '{print $1}');
BASENAME="$(which /usr/bin/basename| head -1 |awk '{print $1}')";
GREP="$(which grep| head -1 |awk '{print $1}')";
STAT="$(which stat| head -1 |awk '{print $1}')";
AWK="$(which awk| head -1 |awk '{print $1}')";
DD=$(which dd| head -1 |awk '{print $1}');
HOSTNAME=$(hostname);
DRYRUN="" # set to "echo " to dry run, or empty to do it for real
RM="$DRYRUN$(which rm| head -1 |awk '{print $1}')";
MV="$DRYRUN$(which mv| head -1 |awk '{print $1}')";
CP="$DRYRUN$(which cp| head -1 |awk '{print $1}')";
SLEEP="$DRYRUN$(which sleep| head -1 |awk '{print $1}')";
RSYNC="$DRYRUN$(which rsync| head -1 |awk '{print $1}')";
MKDIR="$DRYRUN$(which mkdir| head -1 |awk '{print $1}')";
TOUCH="$DRYRUN$(which /usr/bin/touch| head -1 |awk '{print $1}')";
# keep the user from tainting input and assuming a path exists
unset PATH
# ------------- file locations -----------------------------------------
SRC=/var/www/
DEST=/data/backups/www/
# ------------- process command line arguments
validate_type() {
# --- treat the input as tainted
if [ -n "$1" ]; then
if [ "$1" == "monthly" ]; then
echo "monthly"
return 0
elif [ "$1" == "daily" ]; then
echo "daily"
return 0
elif [ "$1" == "hourly" ]; then
echo "hourly"
return 0
fi
fi
return 1
}
validate_dir() {
mode="$1"
dir="$2"
if [ "$mode" == "read" ]; then
if [ -d "$dir" -a -r "$dir" -a -x "$dir" ]; then
echo "$dir"
return 0
fi
fi
if [ "$mode" == "write" ]; then
if [ -d "$dir" -a -w "$dir" -a -x "$dir" ]; then
echo "$dir"
return 0
fi
fi
return 1
}
# make sure we're running as root
#if (( `$ID -u` != 0 )); then { $ECHO "Sorry, must be root. Exiting..."; exit; } fi
if [ "$($ID -u)" -ne 0 ]; then
warn "Not running as root. Some files may not be readable."
fi
usage="Usage: $0 -t backup_type [-s src_dir] [-d dest_dir] [-h]
[-l log_file] [-g] [-k keyword] [-n] [-e email_user]
Purpose: Provide tiered backups using rsync to a local drive.
Remote archives are not supported. Rsync will use hard links
to reduce disk usage overhead.
Arguments:
-t backup_type One of: monthly, daily, hourly
-s src_dir Source directory, with trailing /
-d dest_dir Destination directory
-l log_file File to write log messages into
-e email_user If failure, send an email to this user
-g Enable debug messages
-n Dry run. Don't take action. Enables debugging.
-k keyword Short word (no spaces) that ID's this backup set
-h Show help.
"
while getopts "nk:gl:t::s:d:he:" opt; do
case $opt in
t ) BACKUP_TYPE=$(validate_type "$OPTARG");
if [ $? -ne 0 ]; then
error "Invalid backup type $OPTARG";
exit 2;
fi;;
s ) SRC=$(validate_dir read "$OPTARG");
if [ $? -ne 0 ]; then
error "Source directory $OPTARG is not readable";
exit 2;
fi;;
e ) EMAIL_USER=$OPTARG;;
d ) DEST=$(validate_dir write "$OPTARG");
if [ $? -ne 0 ]; then
error "Destination directory $OPTARG is not writable";
exit 2;
fi;;
g ) DEBUG=1;;
l ) LOGFILE="$OPTARG";;
k ) KEYWORD="$OPTARG";;
n ) DRYRUN="echo"; DEBUG=1;
warn "Dry run mode set. No real action will be taken.";;
\? ) error "Invalid command line parameter. Showing proper usage.";
echo "$usage";
exit 1;;
h ) echo "$usage"
exit 1;;
esac
done
if [ $DEBUG -eq 1 ]; then
debug "Command line was $0 $*"
fi
shift $[$OPTIND - 1]
if [ -n "$LOGFILE" ]; then
err=$(validate_dir write $($DIRNAME "$LOGFILE") )
if [ $? -ne 0 ]; then
LOGFILE=""
error "Cannot create a log file in $($DIRNAME "$LOGFILE")"
info "Log messages have been directed back to stdout (-l ignored)"
fi
fi
if [ -z "$BACKUP_TYPE" ]; then
error "Invalid backup type: choose monthly, hourly, or daily."
exit 1
fi
if [ -z "$SRC" ]; then
error "Invalid source directory: Does it exist and do you have permission to read?"
exit 1
fi
if [ -z "$DEST" ]; then
error "Invalid destination directory: Does it exist and do you have permission"
error " write to it?"
exit 1
fi
BACKUPS_MAX=5
[ "$BACKUP_TYPE" == "monthly" ] && BACKUPS_MAX=${BACKUPS_MAX_ARRAY[0]}
[ "$BACKUP_TYPE" == "daily" ] && BACKUPS_MAX=${BACKUPS_MAX_ARRAY[1]}
[ "$BACKUP_TYPE" == "hourly" ] && BACKUPS_MAX=${BACKUPS_MAX_ARRAY[2]}
info "$BACKUP_TYPE backups will have $BACKUPS_MAX copies retained."
# ------------- compute the smaller backup type ------------------------
BACKUP_TYPE_SMALLER= # choose monthly, daily, or hourly
if [ $BACKUP_TYPE == "monthly" ]; then
BACKUP_TYPE_SMALLER=daily
elif [ $BACKUP_TYPE == "daily" ]; then
BACKUP_TYPE_SMALLER=hourly
else
BACKUP_TYPE_SMALLER=
fi
if [ -d "${DEST}/${BACKUP_TYPE}.${BACKUPS_MAX}" ]; then
debug "Preparing to delete the oldest backup set, number $BACKUPS_MAX, for $BACKUP_TYPE"
$MV "${DEST}/${BACKUP_TYPE}.${BACKUPS_MAX}" "${DEST}/${BACKUP_TYPE}.to_delete"
fi
# ------------- establish a lock file and wait if present ---------------
# --- Cheap lock, easy to spoof, but it works okay
# --- If you force root to run it above, you can put the lock somewhere
# --- safer
#set -x
if [ -w $HOME ]; then
lockfile=$HOME/.$($BASENAME "$DEST")_backup.lock
else
lockfile=/tmp/.$($BASENAME "$DEST")_backup.lock
fi
if [ -f $lockfile ]; then
${TOUCH} -d "yesterday" $lockfile.older
if [ $lockfile.older -nt $lockfile ]; then
warn "Deleting stale lock file $lockfile"
${RM} $lockfile
fi
${RM} $lockfile.older
fi
#set +x
debug "Using lockfile $lockfile to only allow one copy at a time to run."
max_wait=1 # in minutes
while [ -e $lockfile -a $max_wait -gt 0 ]; do
pid=$(${AWK} 'NR==1 && $1 ~ /^[0-9][0-9]*$/ {print $1}' $lockfile)
if [ -n "$pid" ]; then
if [ ! -d /proc/$pid ]; then
${RM} $lockfile
warn "PID $pid was gone. Lockfile removed."
next
fi
fi
warn "Lock file exists at $lockfile. Waiting 1 minute, $max_wait left."
$SLEEP 5
max_wait=$[max_wait - 1]
done
if [ -e $lockfile ]; then
error "Could not get lockfile $lockfile. Aborting $BACKUP_TYPE backup."
exit 1
fi
$TOUCH $lockfile
echo $$ >> $lockfile
trap "/bin/rm $lockfile; exit 0" 1 2 3 4 5 6 7 8 9 10 12 13 14 15
info "Lock file created to only allow one running copy: $lockfile"
info "Starting rotation of backup sets"
while [ $BACKUPS_MAX -ge 0 ]; do
oldest="${DEST}/${BACKUP_TYPE}.${BACKUPS_MAX}"
BACKUPS_MAX=$[$BACKUPS_MAX - 1]
old="${DEST}/${BACKUP_TYPE}.${BACKUPS_MAX}"
[ -d "$old" ] && $MV "$old" "$oldest" && \
debug "Moved $($BASENAME $old) to $($BASENAME $oldest)"
done
if [ -d "${DEST}/${BACKUP_TYPE}.to_delete" ]; then
info "Deleting oldest $BACKUP_TYPE backup set."
fi
$RM -fr "${DEST}/${BACKUP_TYPE}.to_delete"
if [ -n "$BACKUP_TYPE_SMALLER" ]; then
# can we promote a smaller backup to this current one
if [ -d "${DEST}/${BACKUP_TYPE_SMALLER}.0" ]; then
info "Promoting backup set ${BACKUP_TYPE_SMALLER}.0 to ${BACKUP_TYPE}.0"
$CP -al "${DEST}/${BACKUP_TYPE_SMALLER}.0" "${DEST}/${BACKUP_TYPE}.0"
if [ ! -d "${DEST}/${BACKUP_TYPE}.0" ]; then
warn "Promotion failed."
fi
else
info "Making a new backup folder ${DEST}/${BACKUP_TYPE}.0"
$MKDIR -p "${DEST}/${BACKUP_TYPE}.0"
fi
else
# only do rsync if it is the smallest backup type
if [ ! -d "${DEST}/${BACKUP_TYPE}.0" ] ; then
info "Creating backup directory ${DEST}/${BACKUP_TYPE}.0"
$MKDIR -p "${DEST}/${BACKUP_TYPE}.0"
if [ -d "${DEST}/${BACKUP_TYPE}.0" ]; then
debug "Successfully created ${DEST}/${BACKUP_TYPE}.0"
else
warn "Failed to create backup directory ${DEST}/${BACKUP_TYPE}.0"
fi
fi
${DD} if=/dev/zero bs=1024 count=1024 of="${DEST}/.test" 2>/dev/null
oops=$?
test_size=$($STAT -c "%s" "${DEST}/.test" 2>/dev/null)
if [ $oops -ne 0 -o "$test_size" != "1048576" ]; then
${RM} -f "${DEST}/.test"
error "Target device is full ${DEST}. Aborting."
exit 2;
fi
info "Starting rsync"
debug "$RSYNC -vaz $EXCLUDE_FROM --link-dest=../${BACKUP_TYPE}.1 ${SRC}/ ${DEST}/${BACKUP_TYPE}.0"
msg=$($RSYNC -vaz $EXCLUDE_FROM \
--link-dest=../${BACKUP_TYPE}.1 \
${SRC}/ ${DEST}/${BACKUP_TYPE}.0 2>&1 )
debug "RSYNC MSG: $msg"
${ECHO} "$msg" | ( IFS="
";
while read a; do
err=$(${ECHO} "$a" | $GREP --silent -i error)
if [ $? -eq 0 ]; then
warn "$a"
else
info "$a"
fi
done
)
#$RSYNC \
# -va --delete --delete-excluded \
# $EXCLUDE_FROM \
# ${SRC}/ ${DEST}/${BACKUP_TYPE}.0 ;
#
# update the mtime of $BACKUP_TYPE.0 to reflect the snapshot time
# $TOUCH ${DEST}/${BACKUP_TYPE}.0 ;
fi
if [ ! -d "${DEST}/${BACKUP_TYPE}.0" ]; then
#$TOUCH "${DEST}/${BACKUP_TYPE}.0"
$MKDIR -p "${DEST}/${BACKUP_TYPE}.0"
else
warn "${DEST}/${BACKUP_TYPE}.0 should have been a directory and isn't."
fi
$RM $lockfile
info "Lock file removed. Backup set $BACKUP_TYPE completed."
exit 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment