#!/bin/bash # Licensed GNU GPLv3 or later: http://www.gnu.org/licenses/lgpl.html # die with descriptive error messages SCRIPTNAME=`basename $0` ; STARTPWD=`pwd` function die { e="$1"; shift; [ -n "$*" ] && echo "$SCRIPTNAME: $*" >&2; exit "$e" ; } function warn { [ -n "$*" ] && echo "$SCRIPTNAME: warning: $*" >&2; } # default config CWD= SOURCES= INC=false DRY=false FPREFIX=bak- FPOSTFIX=-snap IPREFIX=bak- IPOSTFIX=-rinc RSYNC_OPTIONS="--partial" RSYNC_QUIET='-v --progress' # -i -v=verbose NOP=-p # used as "NOP" rsync option EXCLUDEFILE=$NOP SSHACCOUNT= SSHPORT= SSHKEYFILE= unset LINKDESTS # array of --link-dest arguments if [ -z "$RSYNC_BINARY" ]; then BINRSYNC=rsync else BINRSYNC=`readlink -f "$RSYNC_BINARY"` [ -x "$BINRSYNC" ] || die 1 "Failed to execute rsync binary: $BINRSYNC" fi # usage and help function usagedie { # exitcode message... e="$1"; shift; [ -n "$*" ] && echo "$SCRIPTNAME: $*" >&2 echo "Usage: $SCRIPTNAME [options] sources..." echo "OPTIONS:" echo " --inc make reverse incremental backup" echo " --dry run and show rsync with --dry-run option" echo " --help print usage summary" echo " -C backup directory (default: '.')" echo " -E file with rsync exclude list" echo " -l ssh user name to use (see ssh(1) -l)" echo " -i ssh identity key file to use (see ssh(1) -i)" echo " -P ssh port to use on the remote system" echo " -L hardlink dest files from /" echo " -o output directory name (default: 'bak')" echo " -q, --quiet suppress progress information" echo " -c perform checksum based file content comparisons" echo " --one-file-system" echo " -x disable crossing of filesystem boundaries" echo " --version script and rsync versions" echo "DESCRIPTION:" echo " This script creates full or reverse incremental backups using the" echo " rsync(1) command. Backup directory names contain the date and time" echo " of each backup run to allow sorting and selective pruning." echo " At the end of each successful backup run, a symlink '*-current' is" echo " updated to always point at the latest backup. To reduce remote file" echo " transfers, the '-L' option can be used (possibly multiple times) to" echo " specify existing local file trees from which files will be" echo " hard-linked into the backup." echo " Full Backups:" echo " Upon each invocation, a new backup directory is created that contains" echo " all files of the source system. Hard links are created to files of" echo " previous backups where possible, so extra storage space is only required" echo " for contents that changed between backups." echo " Incremental Backups:" echo " In incremental mode, the most recent backup is always a full backup," echo " while the previous full backup is degraded to a reverse incremental" echo " backup, which only contains differences between the current and the" echo " last backup." echo " RSYNC_BINARY Environment variable used to override the rsync binary path." exit "$e" } # parse options [ -z "$*" ] && usagedie 0 parse_options=1 while test $# -ne 0 -a $parse_options = 1; do case "$1" in --dry-run|--dry) DRY=true ;; # simulation --inc) INC=true ;; -c) RSYNC_OPTIONS="$RSYNC_OPTIONS -c" ;; -x|--one-file-system) RSYNC_OPTIONS="$RSYNC_OPTIONS -x" ;; -q|--quiet) RSYNC_QUIET=-q ;; --help) usagedie 0 ;; -C) CWD="$2" ; shift ;; -E) EXCLUDEFILE="$2" ; shift ;; -o) IPREFIX="$2"- ; FPREFIX="$2"- ; shift ;; -l) SSHACCOUNT="$2" ; shift ;; -i) SSHKEYFILE="$2" ; shift ;; -P) SSHPORT="$2" ; shift ;; -L) LINKDESTS[$[${#LINKDESTS[@]}+1]]="$2" ; shift ;; --version) echo "sayebackup.sh version 0.0.1"; $BINRSYNC --version | head -n1 ; exit 0 ;; --) parse_options=0 ;; -*) usagedie 1 "option not supported: $1" ;; *) parse_options=0 ; break ;; esac shift done [ -z "$*" ] && usagedie 1 "No sources specified" # operate in backup directory [ -n "$CWD" ] && { cd "$CWD"/. || die 2 "Invalid CWD: $CWD" ; } [ -z "$CWD" -a / = "`pwd`" ] && die 2 "Refusing to backup in / - invoked from cron without -C?" pwd | fgrep -q : && die 2 "CWD contains invalid special (rsync) character: :" # create --link-dest arguments with absolute pathnames for i in `seq ${#LINKDESTS[@]}` ; do ldest="${LINKDESTS[$i]}" [ "${ldest:0:1}" != "/" ] && ldest="$STARTPWD/$ldest" LINKDESTS[$i]="--link-dest=$ldest" done # setup definitions CURRENT="$FPREFIX""current" unset COMPRESS # only set on remote invocation RSHCOMMAND="--rsh=ssh -x -oBatchMode=yes -oStrictHostKeyChecking=no -oCompression=yes" [ -n "$SSHPORT" ] && RSHCOMMAND="$RSHCOMMAND -p $SSHPORT" [ -n "$SSHACCOUNT$SSHKEYFILE" ] && { [ -n "$SSHACCOUNT" ] && RSHCOMMAND="$RSHCOMMAND -l $SSHACCOUNT" [ "${SSHKEYFILE:0:1}" != "/" ] && SSHKEYFILE="$STARTPWD/$SSHKEYFILE" [ -n "$SSHKEYFILE" ] && RSHCOMMAND="$RSHCOMMAND -i $SSHKEYFILE -o IdentitiesOnly=yes" COMPRESS="--compress-level=7" } echo " $*" | fgrep -q : && COMPRESS="--compress-level=7" [ -n "$COMPRESS" ] && RSYNC_OPTIONS="$RSYNC_OPTIONS $COMPRESS --block-size=33000" # block-size=33000 needed for rsync<3.0.7, see: http://samba.anu.edu.au/rsync/FAQ.html#13 # inflate (token) returned -5 NOWDIR="$FPREFIX"`date +%Y-%m-%d-%H:%M:%S`"$FPOSTFIX" # assumed to be sortable # find last backup for hard links [ -L "$CURRENT" ] && { LAST=`readlink "$CURRENT"` } || { LAST=`find . -maxdepth 1 -name "$FPREFIX*$FPOSTFIX" | sort | tail -n1` } INCDIR=`printf %s "$LAST" | sed "s,^\(\./\)\?\($FPREFIX\)\?,$IPREFIX, ; s,\($FPOSTFIX\)\?$,$IPOSTFIX,"` # sanity checks [ -d "$NOWDIR" ] && die 3 "Target backup dir exists already: $NOWDIR" $INC && [ -d "$INCDIR" ] && die 3 "Incremental target exists already: $INCDIR" #$INC && [ ! -d "$LAST" ] && die 3 "Missing last backup dir: $LAST" # handle exclude files [ "$EXCLUDEFILE" != $NOP ] && { [ ! -e "$EXCLUDEFILE" ] && die 3 "Missing exclude file: $EXCLUDEFILE" EXCLUDEFILE="--exclude-from=$EXCLUDEFILE" } # prepare transfer directories LASTLINKDIR= if $DRY ; then TARGET_DIR="./$NOWDIR" MODE="$MODE --dry-run" else # create temporary working directory without special-: TMPDIR=`mktemp -d "./sayebackup_tmpXXXXXX"` || die 5 "$0: Failed to create temporary dir" trap 'rm -rf "$TMPDIR"' 0 HUP QUIT TRAP USR1 PIPE TERM # prepare incremental/full backups if $INC ; then # clone old backup with hard links to reduce transfers [ -d "$LAST" ] && { cp -al "$LAST" "$TMPDIR/full" || warn "Failed to fully clone backup dir: $LAST -> $TMPDIR/full" } LASTLINKDIR="--link-dest=../full --backup-dir=../incremental" MODE="-ab --del --ignore-errors" else # symlink old backup to reduce transfers (and avoid special-:) [ -n "$LAST" ] && { ln -s "../$LAST" "$TMPDIR/lasttransfer" LASTLINKDIR="--link-dest=../lasttransfer" } MODE="-aH" fi TARGET_DIR="$TMPDIR/full" fi # work around bogus "file vanished" messages, see https://bugzilla.samba.org/show_bug.cgi?id=3653 VANISHING_PATTERN='^(file has vanished: |rsync warning: some files vanished before they could be transferred)' # run rsync, destination full/, reusing lasttransfer/, backup dir is incremental/ ( $DRY && set -x # echo commands nice -n15 ionice -c3 \ $BINRSYNC $RSYNC_QUIET "$RSHCOMMAND" "$EXCLUDEFILE" $RSYNC_OPTIONS $MODE $LASTLINKDIR "${LINKDESTS[@]}" "$@" "$TARGET_DIR" ) \ 2> >(egrep -v "$VANISHING_PATTERN") RSYNC_CODE="$?" $DRY && exit 0 # handle rsync error codes case "$RSYNC_CODE" in 0) ;; # success 24) ;; # some source files vanished 25) ;; # deletion lmit reached 23) trap "" 0 HUP INT QUIT TRAP USR1 PIPE TERM # partial transfer (ENOSPC) die 7 "Incomplete backup (partial transfer) - retaining temporaries: $TMPDIR" ;; 20) trap "" 0 HUP INT QUIT TRAP USR1 PIPE TERM # interrupted (got SIGINT) die 7 "Interruption during rsync - retaining temporaries: $TMPDIR" ;; 130) trap "" 0 HUP INT QUIT TRAP USR1 PIPE TERM # interrupted (child died, SIGINT?) die 7 "Interruption during rsync - retaining temporaries: $TMPDIR" ;; *) die 7 "Error during rsync ($RSYNC_CODE) - purging temporaries..." ;; esac # make sure the file system contents have been writen out before renaming sync ; sync ; sync # rename newly synced backup dir mv "$TMPDIR/full" "$NOWDIR" || die 8 "Failed to create target backup dir: $TMPDIR/full -> $NOWDIR" # point to newly synced backup dir [ -L "$CURRENT" -o ! -e "$CURRENT" ] && { rm -f "$CURRENT" && ln -s "$NOWDIR" "$CURRENT" } # finish incremental backups $INC && { # rename newly created incremental dir [ -d "$TMPDIR"/incremental ] && { mv "$TMPDIR"/incremental "$INCDIR" || \ die 8 "Failed to create target backup dir: $TMPDIR/incremental -> $INCDIR" } # purge olddir, $INCDIR replaces it [ -w "$LAST" ] && { rm -rf "$LAST" || warn "Failed to purge left over dir: $LAST" ; } } # cleanup rm -f "$TMPDIR/lasttransfer" rmdir "$TMPDIR" # ensure everything is flushed to disk sync sync # some unixes use staggered syncing