#!/bin/bash # # RSBAK3 is Copyright (C) 2003, 2004 LINBIT . # # Written by Clifford Wolf . # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. A copy of the GNU General Public # License can be found at COPYING. ver=0.2 exec 2>&1 umask 007 # logfiles should not be readable by "anyone" verbose=0 if [ "$1" = "-v" ]; then verbose=1; shift fi if [ -n "$RSBAK3_VERBOSE" ]; then verbose=$RSBAK3_VERBOSE fi errlog() { logger -p user.crit -t "rsbak3" "$*" } if [ $verbose = 1 -o -z "$1" -o ! -f "$1" ]; then vecho() { echo "$@"; } else vecho() { :; } fi if [ -z "$DONT_SHOW_RSBAK3_COPY" ]; then vecho vecho "RSBAK3 $ver is Copyright (C) 2003 LINBIT ." vecho "Written by Clifford Wolf ." vecho "This is free software with ABSOLUTELY NO WARRANTY." vecho fi export DONT_SHOW_RSBAK3_COPY=1 export RSBAK3_VERBOSE=$verbose if [ -z "$1" -o ! -f "$1" ]; then echo "Usage: $0 [-v] config-file [ config-name ]" echo; exit 1 fi if [ ! -z "$3" ]; then rc=0; f="$1"; shift for cfg; do "$0" "$f" "$cfg" || rc=1 done exit $rc fi if [ "${2/\*/}" != "$2" ]; then rc=0; found=0 for cfg in $( grep '^\[' "$1" | tr -d '[]' | grep -v '\*' ); do if [[ "$cfg" == $2 ]]; then "$0" "$1" "$cfg" || rc=1 found=1 fi done if [ $found -eq 0 ]; then echo "$2:$this: Can't find configs matching >>$2<< in config file $1!" echo; exit 1 fi exit $rc fi if [ -z "$2" ]; then rc=0 for cfg in $( grep '^\[' "$1" | tr -d '[]' | grep -v '\*' ); do "$0" "$1" "$cfg" || rc=1 done exit $rc fi master="" backupdir="" generations="" rsopt="" current="" foundcfg=0 this=$( date '+%Y%m%d-%H%M%S' ) vecho "[ $2:$this ]" # it's easier to use that var than escaping the character... t="'" while read tag value do [ "$tag" != "${tag#\#}" ] && continue if [ "$tag" = "[" ]; then current="${value%% *}" continue fi [[ "$2" == $current ]] || continue [ "$2" == "$current" ] && foundcfg=1 case "$tag" in master) master="$value" ;; backup-dir) backupdir="$value" ;; generations) generations="$value" ;; password) export RSYNC_PASSWORD="$value" ;; password-file|exclude|include|bwlimit) rsopt="$rsopt --$tag='${value//$t/$t\\$t$t}'" ;; exclude-from|include-from) rsopt="$rsopt --$tag='${value//$t/$t\\$t$t}'" ;; include-tree) x=""; while read y; do x="$x/$y" rsopt="$rsopt --include='${x//$t/$t\\$t$t}'" done < <( echo "$value" | tr '/' '\n' | grep . ) ;; cvs-exclude|compress|whole-file) rsopt="$rsopt --$tag" ;; rsh-command) rsopt="$rsopt --rsh='${value//$t/$t\\$t$t}'" ;; system-exclude) rsopt="$rsopt --exclude='/tmp/**'" rsopt="$rsopt --exclude='/dev/**'" rsopt="$rsopt --exclude='.journal'" rsopt="$rsopt --exclude='lost+found/'" rsopt="$rsopt --exclude='/proc/**'" rsopt="$rsopt --exclude='/sys/**'" ;; rsync-option) rsopt="$rsopt $value" ;; '') ;; *) echo "$2:$this: Syntax error in config file: $tag $value!" echo; exit 1 ;; esac done < "$1" if [ $foundcfg = 0 ]; then echo "$2:$this: Can't find config >>$2<< in config file $1!" echo; exit 1 fi if [ -z "$master" ]; then echo "$2:$this: No >>master<< entry in config file!" echo; exit 1 fi if [ -z "$backupdir" ]; then echo "$2:$this: No >>backup-dir<< entry in config file!" echo; exit 1 fi if [ -z "$generations" ]; then echo "$2:$this: No >>generations<< entry in config file!" echo; exit 1 fi if ! cd "$backupdir" || ! mkdir -p "$2/generation_0" || ! cd "$2/generation_0" then echo "$2:$this: Error while creating $backupdir/$2/generation_0" errlog "$2:$this: Error while creating $backupdir/$2/generation_0" echo; exit 1 fi set -C if ! echo "Already running with PID $$ [$0 $*]." > LOCKFILE then echo "Found lockfile $PWD/LOCKFILE:" errlog "Found lockfile $PWD/LOCKFILE" cat LOCKFILE; echo; exit 1 fi set +C trap "rm -f $PWD/LOCKFILE" EXIT last=$( ls -d [0-9]*.bak 2> /dev/null | tail -1 ) rm -rf "$this.new" rsopt="$rsopt -O '--log-format=%i %9l %n%L'" if [ -d "$last" ]; then vecho "Preparing incremental backup using ${last%.bak} ..." rsopt="$rsopt '--link-dest=$PWD/$last/'" fi vecho "Running rsync (output redirected to logfile) ..." # vecho "rsync '$master' '$this.new' --archive -v --stats" \ # "--delete-excluded --ignore-errors --delete $rsopt" # magic bash $SECONDS START_TIME=$SECONDS date "+STARTED: %F %T" > $this.log eval 'rsync "$master" "$this.new" --archive -v --stats' \ '--delete-excluded --ignore-errors --delete' \ "$rsopt" >> $this.log 2> $this.err < /dev/null rsync_rc=$? TOTAL_TIME=$[SECONDS - START_TIME] date "+FINISHED: %F %T" >> $this.log echo "TOTAL TIME: $TOTAL_TIME s" >> $this.log # ignore error 23 = Partial transfer due to error # ignore error 24 = Partial transfer due to vanished source files if [ $rsync_rc != 0 -a $rsync_rc != 23 -a $rsync_rc != 24 ] then echo "$2:$this: rsync returned error $rsync_rc, see $this.log and $this.err:" errlog "$2:$this: rsync returned error $rsync_rc, see $this.log and $this.err." echo; tail "$this.log" "$this.err"; echo; exit 1 fi if [ $verbose = 1 ]; then tail -n2 "$this.log" | tr -s ' ' fi mv "$this.log" "$this.new/rsync.log" mv "$this.err" "$this.new/rsync.err" mv "$this.new" "$this.bak" rm -f ../latest ln -s "generation_0/$this.bak" ../latest c=0 for gen in $generations; do eval "gen_${c}_num=${gen%:*}" eval "gen_${c}_rot=${gen#*:}" (( c++ )) done for gen in 0 1 2 3 4 5 6 7 8 9; do [ -d ../generation_$gen ] || break cd ../generation_$gen eval "num=\$gen_${gen}_num" eval "rot=\$gen_${gen}_rot" (( next = gen + 1 )) if eval "[ -z \"\$gen_${next}_num\" ]" then last=1; else last=0; fi gencount="$( [ -s GENCOUNT ] && egrep '^[0-9]+$' GENCOUNT | head -1 )" [ -z "$gencount" ] && gencount=$rot for dir in $( ls -r -d [0-9]*.bak 2> /dev/null | tail -n +$(($num+1)) ) do (( gencount = $gencount + 1 )) if [ $gencount -ge $rot -a $last -eq 0 ]; then vecho "Moving to next generation: [$gen] ${dir%.bak} ..." mkdir -p ../generation_$next mv $dir ../generation_$next/ gencount=0 else vecho "Removing outdated backup: [$gen] ${dir%.bak} ..." rm -rf $dir fi done echo $gencount > GENCOUNT done vecho