Created
January 17, 2022 06:01
-
-
Save zerolagtime/b7db8b6ab9b59a1fca662c5624b57bd1 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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