#!/bin/sh # # J.P.Boggis 25/11/2004: Script to backup selected files/directories using # 'rsync' from multiple sources, creating an archive # history of modified and deleted files. # # # License: # ~~~~~~~~ # 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. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # long with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # For further information, see: http://www.gnu.org/licenses/gpl.html # # # History: # ~~~~~~~~ # J.P.Boggis 25/11/2004 v1.0 - Initial version. # J.P.Boggis 02/02/2005 v1.1 - Updated to use separate config file. # J.P.Boggis 09/02/2005 v1.2 - Improved reporting and remote performance. # J.P.Boggis 10/02/2005 v1.3 - Option to compress previous archives. # J.P.Boggis 04/03/2005 v1.4 - Added multiplex option and pre/post commands. # J.P.Boggis 29/03/2005 v1.5 - Lock file added to detect script running. # J.P.Boggis 05/04/2005 v1.6 - Added MANUAL_[NAME]. # J.P.Boggis 19/05/2005 v1.7 - Option added to zip previous archives. # J.P.Boggis 20/05/2005 v1.8 - Added EXCLUDE_[NAME]= option. # J.P.Boggis 08/07/2005 v1.9 - Added RSYNC_RETRIES for failed transfers. # J.P.Boggis 11/07/2005 v2.0 - Added RSYNC_RETRY_DELAY and RSYNC_TIMEOUT. # J.P.Boggis 13/07/2005 v2.1 - Added ability to do incremental backups # using hardlinks. # J.P.Boggis 05/08/2005 v2.2 - Added DEST_CREATE_[NAME]= option. # J.P.Boggis 16/11/2005 v2.3 - Support for multiple hosts in SOURCEPATH. # J.P.Boggis 23/01/2006 v2.4 - Destination path is now recursively created # (If directory structure doesn't exist) and # DEST_CREATE_[NAME]=1 for source. Default # for DEST_CREATE (If not set for source) # can now be set globally. # J.P.Boggis 24/01/2006 v2.5 - Option to purge previous archives for all # sources when disk utilisation is exceeded # (Rather than just the current source) # added. Oldest sources are removed first. # J.P.Boggis 17/03/2006 v2.6 - Added TIMEOUT_[NAME] option to set custom # timeout for a source. # J.P.Boggis 27/03/2006 v2.7 - Purge lock files added to prevent conflicts # when multiple multiplexed instances are # purging previous archives at the same time. # Path on remote source is now checked for # accessibility and files before preceeding. # J.P.Boggis 02/05/2006 v2.8 - Previous archives are now purged (If # necessary) while the backup is waiting for # the correct start time. This helps prevent # the backup from being delayed while waiting # for old files to be deleted. To best # utilise this feature, multi_backup_rsync # should be called via cron several hours # before the backups are due to start. # J.P.Boggis 19/05/2006 v2.9 - Elapsed time added to purge log messages. # J.P.Boggis 29/06/2006 v3.0 - Check for files in remote directory will # now try other hostname (If more than one # has been specified) if connection to the # first picked at random fails. # J.P.Boggis 26/07/2006 v3.1 - expr is now used instead of $[] for # arithmetic, to increase compatibility # with shells other than bash. # # # # Usage: # ~~~~~~ #:> USAGE: multi_backup_rsync [start|cron|silent|multiplex] [] #:> multi_backup_rsync run [ALL|[,][...]] #:> #:> start - Start backup of all sources. #:> cron - Same as start, but for use in cronjobs. #:> Only errors will be output. #:> silent - Same as cron, but with absolutely no output #:> (For use with summary/errors via E-mail.) #:> multiplex - Used internally to start multiplexed sources. #:> #:> run - Manually run backups for specified sources. #:> BACKUP_DAYS and MULTIPLEX will be ignored #:> (Sources specified are run sequentially #:> in the order specified.) # # # Backup sources are configured in /etc/multi_backup_rsync.sources, which # should contain the following: # # SOURCES="NAME [NAME] [...]" # # List of defined sources that will be backed up, e.g: # # SOURCES="SERVER1 SERVER2 SERVER3" # # # MOUNT_[NAME]="[mount path]" # MOUNTCMD_[NAME]="[mount command-line]" # UMOUNTCMD_[NAME]="[unmount command-line]" # # If [mount path] is specified, this will be mounted prior to the # backup and unmounted at the end. This is used to mount NFS, # Windows/Samba, NetWare, etc. volumes locally for backup. # # The mount must be configured in /etc/fstab or the command-lines # for mounting/unmounting must specified using MOUNTCMD_[NAME] and # UMOUNTCMD_[NAME] (In addition to the mount path), e.g: # # MOUNT_SERVER2="/servers/server2" # MOUNTCMD_SERVER3="ncpmount -S SERVER3 -A server3.local -U .admin.tree # -C /servers/server3" # UMOUNTCMD_SERVER3="ncpumount /servers/server3" # # NOTE: For NetWare, use CRON.NLM to backup NDS on a daily basis via # 'dsrepair -rc' and trustee assignments via 'trustbar volume: -B' # # # SOURCEPATH_[NAME]="[host[;host[;...]]:][path]" # # Path to the source files that you wish to backup. For ssh accessible # sources, this will generally be host.name:/ so that the source files # are relative to root on the remote server. For pre-mounted volumes # (See above), this needs to be /path/to/mounted/volume # # If a remote host has multiple links (E.g: Two Internet connections), # multiple hostnames can be specified as a ; separated list. A host # will be picked at random for each source file/directory. This also # enables fault tolerance - If the connection fails, times out, etc., # one of the other hostname(s) will be picked at random for each retry. # # E.g: SOURCEPATH_SERVER1="server1.local:/" # SOURCEPATH_USER1="user1@server1.local:" # SOURCEPATH_MULTI="server.domain1.tld;server.domain2.tld:/" # SOURCEPATH_LOCAL="/home/user2" # # # SOURCEFILES_[NAME]="[file:]name [[file:]name] [...]" # # Files/directories within above source path to backup, e.g: # # SOURCEFILES_SERVER1="etc root home var file:test.txt" # # If the source is not prefixed with 'file:', it will be treated as a # directory and backed up recursively. If prefixed with file: it will # be treated as a file and backed up individually (Wildcards may be # specified for file:, e.g: file:/etc/*.conf # # # DEST_[NAME]="[backup path]" # # Local path to copy backups to, e.g: # # DEST_SERVER1="/backup/server1" # # The following will be created under this directory: # # incremental - For incremental backups, contains daily, weekly # and monthly incremental backups in the format # m(N)-w(N)-d(N). # # The current backup can be found in current, which # is a symlink to m0-w0-d1. This contains a full # complete copy of the specified source files/ # directories from the source server. The other # incremental directories contain changed/deleted # file and hardlinks to those that haven't changed. # # current - For non-incremental backups, contains the current # backup files. This is a complete copy of the # specified source files/directories from the # source server. # # previous - Contains dated archives of modified/deleted files. # These are in the format YYYY-MM-DD and compressed # to save disk space. # # NOTE: These archives DO NOT contain unchanged # files. # # log - Contains backup logs: # # summary - Brief summary of backup. # errors - Summary of backup errors (If any.) # results - Full results log of backup. # # # EXCLUDE_[NAME]="pattern [pattern] [...]" # # Files that match the specified patterns will be excluded from # the backup of the specified source. # # For COMPRESS_[NAME]=2, pattern is a regexp that will be used in # conjunction with grep to filter the file list, so . in filenames # should be protected appropriately. # # E.g: \\.tmp\$ ~\$ Excludes files ending in .tmp and ~ # # For other COMPRESS_[NAME] settings, pattern is a rsync --exclude # pattern. See the manual page and/or documentation for the rsync # --exclude option for further information. # # E.g: *.tmp *~ Excludes files ending in .tmp and ~ # # # LOCAL_[NAME]=0|1 # # If backing up a local source (I.e: Local mounted disk or fast LAN # host), set this to 1 to use rsync options optimised for local # copying (No compression.) # # # MANUAL_[NAME]=0|1 # # If this is set to 1, the source can only be ran manually, using # 'multi_backup_rsync run NAME'. # # # MULTIPLEX_[NAME]=0|1 # # If multiple sources are being backed up, setting this to 1 for each # source will allow these sources to be backed up simultaneously. # Non-multiplex sources will be backed up sequentially. # # # DELAY_[NAME]=seconds # # Sets the minimum delay between each ssh/rsync command. This is for # use with hosts with anti-SSH attack measures implemented, where # multiple SSH connections over a short period of time will result # in a firewall host block. # # # TIME_[NAME]="hh:mm" # # For multiplexed sources, wait until the specified time before starting # backup job. This allows individual jobs to be delayed until the # specified time (24-hour format.) # # # BANDWIDTH_[NAME]=value # # Limit bandwidth to specified value Kbps (Bits per second) # # # TIMEOUT_[NAME]=value # # Set custom timeout value (Setting this to 0 will disable the default # (RSYNC_TIMEOUT) and use rsync's built-in default only.) # # # INCREMENTAL_BACKUPS_[NAME]=0|1 # INCREMENTAL_DAILY_[NAME]=value # INCREMENTAL_WEEKLY_[NAME]=value # INCREMENTAL_MONTHLY_[NAME]=value # # Overrides incremental backup settings for the named backup source # (Defaults defined below will be used otherwise.) # # # RSYNC_OPTIONS_[NAME]="[options]" # # Alternative rsync options for this source. RSYNC_OPTIONS are used # by default if these are not specified for a particular source. # # E.g: RSYNC_OPTIONS_SERVER1="-abzrv" # # # SSH_PORT_[NAME]=value # # Specifies an alternative port to use for SSH instead of the default # port 22. This is useful for hosts where SSH is listening on a # non-standard port, or access to the standard port has been # restricted to stop SSH attacks. # # # PRE_EXEC_[NAME]="command [; command][; ...]" # POST_EXEC_[NAME]="command [; command]; ...]" # # Commands to execute before and after backup of the source. For # example, this can be used to run mysqldump to backup MySQL # databases prior to backing up the source. # # For backups of remote hosts, the command(s) will be executed on # the remote host using ssh. # # # KEEP_[NAME]=value # # Maximum number of previous archives to keep (Providing disk # utilisation is not exceeded.) If set to 0, no limit will be # applied. # # # BACKUP_DAYS_[NAME]="day [day] [...]" # # Days when this source will be backed up (0 is Sunday, 1 is Monday, # etc.) E.g: "1 2 3 4 5" will only backup the source during weekdays. # If this is not specified, every day will be assumed. # # # UTILISATION_[NAME]=value # # Alternative utilisation value for destination backup path. When # this is exceeded, previous archives will be deleted to free up # disk space. If this is not specified, the value for # PURGE_UTILISATION is used. # # E.g: UTILISATION_SERVER1=75 # # # DEST_CREATE_[NAME]=0|1 # # If set to 0, the destination directory (DEST_[NAME]) will not be # created if it does not exist. Useful if the destination directory # is on a removable external disk, preventing creation and a full # backup onto the local disk if it's not mounted for any reason. # # # PURGE_OLD_DIRS="/path/dir [/path/dir] [...]" # # When PURGE_ALL_SOURCES=1, this option specifies additional directories # to purge in addition to the destination directories of current defined # sources (SOURCES=...) This can be used to purge previous archives of # old sources that are no-longer backed up (E.g: Server removed from # network.) # # # E-mail report options: # ~~~~~~~~~~~~~~~~~~~~~~ # EMAIL_RCPT="name@domain.tld" # # Send summary and/or error report to specified recipient. # # EMAIL_FROM="name@domain.tld" # # Sender address to use for above E-mail, e.g: backup@domain.tld # # EMAIL_SUBJECT="Subject prefix" # # Subject for above E-mail, e.g: "Backup summary" # # # NOTE: For automated and unattended backup, this script should be # called from a cron job. If backing up remote servers, create # SSH certificates to allow rsync to copy files from the remote # hosts without prompting for a password. # Source definitions SOURCEDEFS="/etc/multi_backup_rsync.sources" # Mount destination path for archives (If external (E.g: USB) disk and not # currently mounted.) If required, specify this option in above source # definitions file. Multiple paths can be specified separated by spaces. # MOUNT_DEST="" # Number of logs from previous backups to keep (In log directory) KEEP_LOGS=7 # Show summary at end? SHOW_SUMMARY=1 # Show errors at end? SHOW_ERRORS=1 # Send summary via E-mail? EMAIL_SUMMARY=1 # Send errors by E-mail? EMAIL_ERRORS=1 # Enable incremental backups using hardlinks? This creates dated directories # containing a complete snapshot of the backup source (Making restore/browsing # considerably easier.) Disk space is conserved by creating hardlinks to # unchanged files. # INCREMENTAL_BACKUPS=1 # Number of daily incremental backups to keep # INCREMENTAL_DAILY=7 # Number of weekly incremental backups to keep # INCREMENTAL_WEEKLY=5 # Number of monthly incremental backups to keep # INCREMENTAL_MONTHLY=12 # Compress previous backup archive directories to save space? # (NOTE: Incremental backups using hardlinks cannot be compressed.) # COMPRESS_BACKUPS=1 # For previous archive directories, use zip instead of gzip? # COMPRESS_BACKUPS_ZIP=0 # Use bzip2? This will give better compression than gzip, but at the # expense of more CPU time (Please ensure that this is available.) # COMPRESS_BACKUPS_BZIP2=0 # Default rsync options RSYNC_OPTIONS_REMOTE="-rlptgoDvz --delete --delete-excluded --hard-links --numeric-ids --stats" RSYNC_OPTIONS_LOCAL="-rlptgoDv --delete --delete-excluded --hard-links --numeric-ids --stats" # If rsync fails, retry the transfer upto the specified number of times. # # rsync can sometimes fail for large transfers with 'connection unexpectedly # closed' (Even for a local transfer across a LAN.) # RSYNC_RETRIES=3 # Delay time between each retry # RSYNC_RETRY_DELAY="5m" # Rsync timeout due to inactivity in seconds (0 = Use rsync default.) # RSYNC_TIMEOUT=0 # Create destination directory structure by default? # DEST_CREATE=1 # Default number of previous archives to keep (0 = No limit.) # (Not applicable to incremental backups.) # KEEP_PREVIOUS=0 # When disk utilisation on a given backup volume exceeds the percentage # value below, previous backup archives (Oldest first) will be deleted # to free up disk space for the pending backup. # PURGE_UTILISATION=90 # Minimum number of previous backup archives to keep when purging # PURGE_MINIMUM=2 # Search all source destination directories for previous backups and purge # the oldest? If not, the oldest backups in the current source will be # purged only. # PURGE_ALL_SOURCES=1 # Maximum time to wait for purge of source by another instance # PURGE_TIMEOUT=`expr 60 \* 60 ` # Purge previous archives (If necessary) while waiting for correct backup # start time? This helps prevent the backup from being delayed while waiting # for old files to be deleted. # PURGE_WHILE_WAITING=1 # Temporary file for purge list # TMP_PURGE_LIST="/tmp/.mbackup.purge.list" # Tempory log file for purge while waiting for backup to start # TMP_PURGE_LOG="/tmp/.mbackup.purge.log" # Default E-mail subject EMAIL_SUBJECT="Backup report for \$SOURCE (\$ERRORS \$ERRORS_TITLE) on \$START_TIME..." # Default E-mail sender EMAIL_FROM="backup@localhost" # Date format for logs LOG_DATE="%d/%m/%Y %H:%M:%S" # Temporary file for E-mail message TMP_EMAIL="/tmp/.backup.report" # Name prefix for lock files and temporary files # TMP_NAME="/tmp/.mbackuprsync" # To prevent multiple instances of backing up the same source, a lock file # is created while each source is running. This is updated regularly # during the backup. The lock will be considered expired if it has # not been updated for the time period specified below (In hours.) # LOCK_TIME=2 # Remove old log files (Assumed 1 file = 1 day) # # USAGE: RemoveOldLogs # function RemoveOldLogs { if [ "$1" != "" ] && [ "$2" != "" ] && [ $2 -gt 1 ]; then FILES="`ls -r $1 2>&1 |grep -v \"No such file or directory\"`" FILE_COUNT=1 for FILE in $FILES; do if [ $FILE_COUNT -ge $2 ]; then if [ -e "$FILE" ] && [ -d "$FILE" ]; then if [ $ERRORS_ONLY -eq 0 ]; then echo "$SOURCE: Removing old log file $DIR..."; fi echo "`date +\"$LOG_DATE\"`: Removing old log file $FILE..." >> $LOGFILE rm $FILE >> $LOGFILE 2>&1 fi fi FILE_COUNT=`expr $FILE_COUNT + 1` done fi } # Rotate incremental backups # # USAGE: RotateBackups # function RotateBackups { ROTATE_PATH="$1" ROTATE_DAILY="$2" ROTATE_WEEKLY="$3" ROTATE_MONTHLY="$4" # Incremental backup directory exists? if [ ! -e "$ROTATE_PATH" ] || [ ! -d "$ROTATE_PATH" ]; then return fi # Ensure current backup contains files (Otherwise do not rotate backups) if [ "`ls -d -r $1/m0-w0-d1/* 2>&1 |grep -v \"No such file or directory\"`" = "" ]; then return fi # Ensure # Rotate backups for ROTATE_TYPE in m w d; do case $ROTATE_TYPE in d) ROTATE_INTERVAL=`expr 60 \* 60 \* 12` ROTATE_INTFILE="$ROTATE_PATH/.last.daily" ROTATE_COUNT="$ROTATE_DAILY" ROTATE_PREFIX="m0-w0-d" ROTATE_SUFFIX="" ROTATE_ROLL_COUNT=0 ROTATE_ROLL_PREFIX="" ROTATE_ROLL_SUFFIX="" ;; w) ROTATE_INTERVAL=`expr 60 \* 60 \* 24 \* 7` ROTATE_INTFILE="$ROTATE_PATH/.last.weekly" ROTATE_COUNT="$ROTATE_WEEKLY" ROTATE_PREFIX="m0-w" ROTATE_SUFFIX="-d0" ROTATE_ROLL_COUNT="$ROTATE_DAILY" ROTATE_ROLL_PREFIX="m0-w0-d" ROTATE_ROLL_SUFFIX="" ;; m) ROTATE_INTERVAL=`expr 60 \* 60 \* 24 \* 30` ROTATE_INTFILE="$ROTATE_PATH/.last.monthly" ROTATE_COUNT="$ROTATE_MONTHLY" ROTATE_PREFIX="m" ROTATE_SUFFIX="-w0-d0" ROTATE_ROLL_COUNT="$ROTATE_WEEKLY" ROTATE_ROLL_PREFIX="m0-w" ROTATE_ROLL_SUFFIX="-d0" ;; *) ROTATE_COUNT=0 ROTATE_ROLL_COUNT=0 esac if [ $ROTATE_COUNT -gt 0 ]; then # Check rotate whether interval has passed if [ -e "$ROTATE_INTFILE" ]; then ROTATE_LASTINT="`cat $ROTATE_INTFILE`" else ROTATE_LASTINT="" fi if [ "$ROTATE_LASTINT" = "" ] || [ "$ROTATE_LASTINT" = "0" ]; then if [ "$ROTATE_TYPE" = "d" ]; then ROTATE_LASTINT=0 else ROTATE_LASTINT="`date +%s`" echo "$ROTATE_LASTINT" > $ROTATE_INTFILE fi fi if [ `date +%s` -ge `expr $ROTATE_LASTINT + $ROTATE_INTERVAL` ]; then echo "`date +%s`" > $ROTATE_INTFILE # Rotate existing backups, removing oldest ROTATE_COUNTER=`expr $ROTATE_COUNT + 1` while [ $ROTATE_COUNTER -gt 0 ]; do if [ -e "$ROTATE_PATH/${ROTATE_PREFIX}${ROTATE_COUNTER}${ROTATE_SUFFIX}" ]; then if [ $ROTATE_COUNTER -ge $ROTATE_COUNT ]; then rm -rf "$ROTATE_PATH/${ROTATE_PREFIX}${ROTATE_COUNTER}${ROTATE_SUFFIX}" >>$LOGERRORS 2>&1 else mv $ROTATE_PATH/${ROTATE_PREFIX}${ROTATE_COUNTER}${ROTATE_SUFFIX} $ROTATE_PATH/${ROTATE_PREFIX}`expr $ROTATE_COUNTER + 1`${ROTATE_SUFFIX} >>$LOGERRORS 2>&1 if [ "${ROTATE_PREFIX}${ROTATE_COUNTER}${ROTATE_SUFFIX}" = "m0-w0-d1" ] && [ -e "$ROTATE_PATH/${ROTATE_PREFIX}`expr $ROTATE_COUNTER + 1`${ROTATE_SUFFIX}" ] && [ ! -e "$ROTATE_PATH/${ROTATE_PREFIX}${ROTATE_COUNTER}${ROTATE_SUFFIX}" ]; then ln -sf $ROTATE_PATH/${ROTATE_PREFIX}`expr $ROTATE_COUNTER + 1`${ROTATE_SUFFIX} $ROTATE_PATH/current fi fi fi ROTATE_COUNTER=`expr $ROTATE_COUNTER - 1` done # Rollover oldest backup from next group ROTATE_COUNTER=$ROTATE_ROLL_COUNT ROTATE_FOUND=0 while [ $ROTATE_COUNTER -gt 0 ] && [ $ROTATE_FOUND -eq 0 ]; do if [ -e "$ROTATE_PATH/${ROTATE_ROLL_PREFIX}${ROTATE_COUNTER}${ROTATE_ROLL_SUFFIX}" ]; then cp -al $ROTATE_PATH/${ROTATE_ROLL_PREFIX}${ROTATE_COUNTER}${ROTATE_ROLL_SUFFIX} $ROTATE_PATH/${ROTATE_PREFIX}1${ROTATE_SUFFIX} >>$LOGERRORS 2>&1 ROTATE_FOUND=1 fi ROTATE_COUNTER=`expr $ROTATE_COUNTER - 1` done fi fi done } # Wait for purge of source directory by another instance to complete function WaitPurge { PURGE_WAITED=0 if [ -e "$1" ]; then echo "Purge: $2 is being purged by another instance. Waiting for purge to complete..." >> $LOGFILE PURGE_WAITED=1 WAIT_PURGE=1 while [ $WAIT_PURGE -eq 1 ]; do sleep 1m if [ ! -e "$1" ]; then WAIT_PURGE=0 else PURGE_TIME="`cat $1`" if [ "$PURGE_TIME" = "" ] || [ `date +%s` -gt `expr $PURGE_TIME + $PURGE_TIMEOUT` ]; then echo "Purge: Timeout waiting for purge of $2 by another instance." >> $LOGFILE PURGE_WAITED=0 WAIT_PURGE=0 fi fi done fi } # Free up space by deleting previous backup archives (Oldest first) until # disk space utilisation is less than specified utilisation # # USAGE: PurgeBackups # function PurgeBackups { PURGED=0 PURGE_START_TIME="`date +%s`" # Temporary purge file list TMP_PURGE_LIST_FILE="$TMP_PURGE_LIST.`date +%s`" # Keep only specified number of previous backup archives # (Not applicable to incremental backups) # if [ "$3" != "" ] && [ $3 -gt 1 ]; then NOTIFIED=0 BACKUPS="`ls -d -r $1/*-*-* 2>&1 |grep -v \"No such file or directory\"`" BACKUP_COUNT=1 for DIR in $BACKUPS; do if [ $BACKUP_COUNT -ge $3 ]; then CORRECT_PATH="`echo \"$DIR\" |grep -E \"^$1\"`" if [ -e "$DIR" ] && [ "$CORRECT_PATH" != "" ]; then if [ $NOTIFIED -eq 0 ]; then if [ $ERRORS_ONLY -eq 0 ]; then echo "$SOURCE: Old archive count is greater than $3, purging old archives..."; fi echo "Purge: Old archive count is greater than $3, purging old archives..." >> $LOGFILE NOTIFIED=1 fi if [ $ERRORS_ONLY -eq 0 ]; then echo "$SOURCE: Purging old archive $DIR..."; fi DIR_TMP_PURGE="`echo "$DIR" |sed -e "s~/~-~g"`" WaitPurge "$TMP_NAME.$DIR_TMP_PURGE.purge" "$DIR" if [ "$PURGE_WAITED" != "1" ]; then echo "Purge: Purging old archive $DIR..." >> $LOGFILE echo "`date +%s`" > $TMP_NAME.$DIR_TMP_PURGE.purge rm -r "$DIR" >/dev/null 2>&1 rm "$TMP_NAME.$DIR_TMP_PURGE.purge" >/dev/null 2>&1 PURGED=1 # Update lock file if [ -e "$TMP_NAME.$SOURCE.lock" ]; then echo "`date +%s`" > $TMP_NAME.$SOURCE.lock fi fi fi fi BACKUP_COUNT=`expr $BACKUP_COUNT + 1` done fi # Update lock file if [ -e "$TMP_NAME.$SOURCE.lock" ]; then echo "`date +%s`" > $TMP_NAME.$SOURCE.lock fi # Disk utilisation exceeded? Purge previous backup # archives until this is below specified utilisation. # CONTINUE_PURGE=1 PURGE_COUNT=0 while [ $CONTINUE_PURGE -eq 1 ]; do PURGE_COUNT=`expr $PURGE_COUNT + 1` CONTINUE_PURGE=0 SOURCES_PURGED=0 eval CHECK_DEST="\$DEST_$SOURCE" if [ "$CHECK_DEST" != "" ] && [ -d "$CHECK_DEST" ]; then UTILISATION="`df $CHECK_DEST |grep -v "Filesystem" |sed -e "s/[[:space:]]\+/ /g" -e "s/%//g" |cut -d' ' -f5`" if [ $UTILISATION -gt $2 ]; then if [ $PURGE_COUNT -gt 1 ]; then PURGE_STILL=" still"; else PURGE_STILL=""; fi if [ $ERRORS_ONLY -eq 0 ]; then echo "$SOURCE: Disk utilisation of '$CHECK_DEST' is$PURGE_STILL greater than $2."; fi echo "Purge: Disk utilisation of '$CHECK_DEST' is$PURGE_STILL greater than $2." >> $LOGFILE # Purge oldest previous backup BACKUPS="" if [ $PURGE_ALL_SOURCES -eq 1 ]; then # Produce list of oldest previous backups for all sources PURGE_DIR_LIST="$PURGE_OLD_DIRS" for PURGE_SRC in $ORIGINAL_SOURCES; do eval PURGE_SRC_DIR="\$DEST_$PURGE_SRC" PURGE_DIR_LIST="$PURGE_DIR_LIST $PURGE_SRC_DIR" done PURGE_DIR_LIST="$PURGE_DIR_LIST $MOUNT_DEST" echo -n "" > $TMP_PURGE_LIST_FILE for DIR in $PURGE_DIR_LIST; do if [ -e "$DIR" ] && [ -d "$DIR" ]; then find $DIR -iregex ^.*/incremental/m[0-9]+-w[0-9]+-d[0-9]+\$ -prune -printf "%f #-DIR-# %p\n" -o -iregex ^.*/previous/[0-9]+-[0-9]+-[0-9]+.*\$ -prune -printf "%f #-DIR-# %p\n" |grep -v "/incremental/m0-w0-d1" >> $TMP_PURGE_LIST_FILE fi done BACKUPS="`cat $TMP_PURGE_LIST_FILE |sort -r -u |sed -e \"s/^.\+#-DIR-#[[:space:]]\+//g\"`" BACKUPS_COUNT="`echo \"$BACKUPS\" |wc -l`" if [ $BACKUPS_COUNT -eq 1 ]; then BACKUPS_COUNT=2; fi fi if [ "$BACKUPS" = "" ]; then # Purge oldest previous backup of current source BACKUPS="`ls $CHECK_DEST/previous/*-*-* 2>&1 |grep -v \"No such file or directory\" ; ls -r -d $CHECK_DEST/incremental/*-*-* 2>&1 |grep -v \"No such file or directory\"`" BACKUPS_COUNT="`echo \"$BACKUPS\" |wc -l`" fi if [ "$BACKUPS_COUNT" != "" ] && [ $BACKUPS_COUNT -gt 1 ] && [ $BACKUPS_COUNT -gt $PURGE_MINIMUM ]; then PURGE_COUNT=0 for DIR in $BACKUPS; do if [ -e "$DIR" ]; then if [ $PURGE_COUNT -lt 1 ]; then if [ $ERRORS_ONLY -eq 0 ]; then echo "$SOURCE: Purging $DIR..."; fi DIR_TMP_PURGE="`echo "$DIR" |sed -e "s~/~-~g"`" WaitPurge "$TMP_NAME.$DIR_TMP_PURGE.purge" "$DIR" if [ "$PURGE_WAITED" != "1" ]; then echo "Purge: Purging $DIR..." >> $LOGFILE echo "`date +%s`" > $TMP_NAME.$DIR_TMP_PURGE.purge rm -r "$DIR" >/dev/null 2>&1 rm "$TMP_NAME.$DIR_TMP_PURGE.purge" >/dev/null 2>&1 PURGE_COUNT=`expr $PURGE_COUNT + 1` PURGED=1 # Update lock file if [ -e "$TMP_NAME.$SOURCE.lock" ]; then echo "`date +%s`" > $TMP_NAME.$SOURCE.lock fi fi fi fi done if [ $PURGE_COUNT -gt 0 ]; then SOURCES_PURGED=`expr $SOURCES_PURGED + 1` fi fi if [ $SOURCES_PURGED -lt 1 ]; then if [ $ERRORS_ONLY -eq 0 ]; then echo "$SOURCE: No purgable previous backups available."; fi echo "Purge: No purgable previous backups available." >> $LOGFILE fi fi fi if [ $SOURCES_PURGED -eq 1 ]; then CONTINUE_PURGE=1 fi done if [ -e "$TMP_PURGE_LIST_FILE" ]; then rm $TMP_PURGE_LIST_FILE; fi # Update lock file if [ -e "$TMP_NAME.$SOURCE.lock" ]; then echo "`date +%s`" > $TMP_NAME.$SOURCE.lock fi # Purge time if [ $PURGED = "1" ]; then PURGE_ELAPSED=`expr \`date +%s\` - $PURGE_START_TIME` echo -e "Purge: Purging took `expr $PURGE_ELAPSED / \( 60 \* 60 \* 24 \)`d `expr \( $PURGE_ELAPSED % \( 60 \* 60 \* 24 \) \) / \( 60 \* 60 \)`h `expr \( $PURGE_ELAPSED % \( 60 \* 60 \) \) / 60`m `expr $PURGE_ELAPSED % 60`s\n" >> $LOGFILE fi } # Work out percentage (Val1 / Val2 * 100) # # USAGE: percent # # Result returned in $? and $PercentVal # function percentage { PercentVal=0 if [ "$1" != "" ] && [ "$2" != "" ] && [ "$1" != "N/A" ] && [ "$2" != "N/A" ] && [ $2 -ne 0 ]; then if [ $1 -lt 0 ]; then PercentVal1=`expr 0 - $1`; else PercentVal1=$1; fi if [ $2 -lt 0 ]; then PercentVal2=`expr 0 - $2`; else PercentVal2=$2; fi PercentVal=`expr \( \( $PercentVal1 \* 100 \) / $PercentVal2 \)` fi return $PercentVal } # Create directories under specified location. # # USAGE: CreateDirs [/][...] # function CreateDirs { if [ "$1" != "" ] && [ "$2" != "" ]; then CREATE_DIRLIST="`echo \"$1/$2\" |sed \"s/[[:space:]]\+/_.-/g\" |sed -e \"s~/~ ~g\"`" CREATE_PATH="" for CREATE_DIR in $CREATE_DIRLIST; do CREATE_DIR="`echo \"$CREATE_DIR\" |sed -e \"s/_\.-/ /g\"`" if [ ! -e "$CREATE_PATH/$CREATE_DIR" ]; then mkdir "$CREATE_PATH/$CREATE_DIR" >>$LOGFILE 2>&1 fi CREATE_PATH="$CREATE_PATH/$CREATE_DIR" done fi } # Wait for specified time # function Delay { if [ "$1" != "" ] && [ $1 -gt 0 ]; then if [ "$LAST_TIME" = "" ]; then LAST_TIME=`expr \`date +%s\` - $1`; fi TIME_NOW="`date +%s`" TIME_INTERVAL=`expr $TIME_NOW - $LAST_TIME` if [ $TIME_INTERVAL -gt $1 ]; then TIME_DELAY=$1 else TIME_DELAY=`expr $1 - $TIME_INTERVAL` fi if [ $TIME_DELAY -gt 0 ]; then if [ $ERRORS_ONLY -eq 0 ]; then echo "$SOURCE: (Delay) Waiting for $TIME_DELAY second(s)..."; fi echo -e "\n`date +\"$LOG_DATE\"`: (Delay) Waiting for $TIME_DELAY second(s)..." >> $LOGFILE sleep ${TIME_DELAY}s fi LAST_TIME="`date +%s`" fi } # Execute pre/post command(s) # # USAGE: PrePostExec "command [; command][; ...]" # function PrePostExec { if [ "$1" != "" ] && [ "$2" != "" ]; then BACKUP_SOURCE="$1" BACKUP_CMD="$2" BACKUP_HOST="`echo \"$BACKUP_SOURCE\" |sed -e \"s/^\(.\+\):.*\$/\1/g\"`" if [ "$BACKUP_HOST" != "" ] && [ "$BACKUP_HOST" != "$BACKUP_SOURCE" ]; then Delay $SOURCE_DELAY ssh $RndItem $BACKUP_CMD else $BACKUP_CMD fi fi } # Pick random value from list. # # USAGE: RandomItem "item1 item2 [...]" "current_item" # # Result returned $RndItem # function RandomItem { RndItem="" if [ "$1" != "" ]; then ItemCnt=0 ItemLoop=0 ItemLast="$2" for Itm in $1; do ItemCnt=`expr $ItemCnt + 1`; done if [ "$ItemCnt" != "" ] && [ $ItemCnt -gt 1 ]; then while [ "$RndItem" = "" ] && [ $ItemLoop -le 100 ]; do ItemNo=`expr $RANDOM % $ItemCnt` ItemPos=0 for Itm in $1; do if [ $ItemPos -le $ItemNo ] && [ "$Itm" != "$2" ]; then RndItem="$Itm" fi ItemPos=`expr $ItemPos + 1` done ItemLoop=`expr $ItemLoop + 1` done else RndItem="$1" fi fi } # Return source path to host picked at random. # # USAGE: RandomSourcePath "" # # UNIQUE: 1 = Ensure new random host is different from last picked host. # 0 = Pick any host at random. # # Result returned $RANDOMPATH # function RandomSourcePath { SOURCEPATH_HOST="`echo \"$1\" |sed -e \"s/^\(.\+\):\(.*\)\$/\1/g\"`" SOURCEPATH_PATH="`echo \"$1\" |sed -e \"s/^\(.\+\):\(.*\)\$/\2/g\"`" RANDOMPATH="$1" if [ "$SOURCEPATH_HOST" != "" ] && [ "$SOURCEPATH_HOST" != "$SOURCEPATH" ] && [ "$SOURCEPATH_PATH" != "$SOURCEPATH" ]; then SOURCEPATH_HOST="`echo \"$SOURCEPATH_HOST\" |sed -e \"s/;/ /g\"`" if [ "$2" = "1" ]; then RandomItem "$SOURCEPATH_HOST" "$LAST_HOST" else RandomItem "$SOURCEPATH_HOST" "" fi if [ "$RndItem" != "" ]; then LAST_HOST="$RndItem" RANDOMPATH="$RndItem:$SOURCEPATH_PATH" else RANDOMPATH="" fi fi } # Send message via E-mail # # USAGE: SendMessage "Message" # function SendMessage { if [ "$2" = "1" ]; then if [ "$ERRORS" = "" ]; then ERRORS=0; fi ERRORS=`expr $ERRORS + 1` if [ $ERRORS -eq 1 ]; then ERRORS_TITLE="error"; else ERRORS_TITLE="errors"; fi fi eval MAIL_SUBJECT=\"$EMAIL_SUBJECT\" echo "$1" |mail -a "From: <$EMAIL_FROM>" -a "To: <$EMAIL_RCPT>" -a "Reply-to: <$EMAIL_FROM>" -s "$MAIL_SUBJECT" $EMAIL_RCPT } # Read sources from /etc/multi_backup_rsync.sources if [ -e "$SOURCEDEFS" ]; then source $SOURCEDEFS else echo -e "\nError: Backup source defininitions file '$SOURCEDEFS' not found.\n" exit 1 fi # No sources? if [ "$SOURCES" = "" ]; then echo -e "\nError: No sources (SOURCES=) to backup defined in '$SOURCEDEFS'.\n" exit 1 fi # Compression options if [ $COMPRESS_BACKUPS_BZIP2 -eq 1 ]; then TAR_OPT="j" TAR_EXT="bz2" ZIP_CMD="bzip2" ZIP_EXT="bz2" UNZIP_CMD="bunzip2" else TAR_OPT="z" TAR_EXT="gz" ZIP_CMD="gzip" ZIP_EXT="gz" UNZIP_CMD="gunzip" fi MULTIPLEX=0 ERRORS_ONLY=0 MANUAL_COMPRESS=0 MANUAL_DECOMPRESS=0 RUN_SOURCES="" MANUAL_RUN=0 AUTO_RUN=0 RUN_ALL=0 ORIGINAL_SOURCES="$SOURCES" if [ "$1" = "start" ] || [ "$1" = "START" ]; then # Automatically start backup of all sources AUTO_RUN=1 elif [ "$1" = "cron" ] || [ "$1" = "CRON" ]; then # No general output when ran in cron job? AUTO_RUN=1 ERRORS_ONLY=1 SHOW_SUMMARY=0 elif [ "$1" = "silent" ] || [ "$1" = "SILENT" ]; then # Totally silent (For use in cron job with E-mail report of summary/errors.) AUTO_RUN=1 ERRORS_ONLY=1 SHOW_SUMMARY=0 SHOW_ERRORS=0 elif [ "$1" = "multiplex" ] || [ "$1" = "MULTIPLEX" ]; then # Multiplex source? MULTIPLEX=1 if [ "$3" != "" ]; then ERRORS_ONLY="$3"; fi if [ "$4" != "" ]; then SHOW_SUMMARY="$4"; fi if [ "$5" != "" ]; then SHOW_ERRORS="$5"; fi elif [ "$1" = "run" ] || [ "$1" = "RUN" ]; then # Manually run specified sources RUN_SOURCES="`echo \"$2\" |sed -e \"s/,/ /g\"`" MANUAL_RUN=1 if [ "$RUN_SOURCES" = "" ]; then echo -e "\nError: Please specify the name(s) of sources to run (Or ALL for all sources.)\n" exit 1 fi if [ "$RUN_SOURCES" = "ALL" ]; then RUN_ALL=1 else SOURCES="$RUN_SOURCES" fi else # Show usage echo "" cat $0 |grep "#:>" |grep -v "cat \$0" |sed -e "s/#:>[[:space:]]\?//g" echo "" exit 1 fi # Able to create temp files? if [ "$TMP_NAME" != "" ]; then echo "Test" > $TMP_NAME.test if [ -e "$TMP_NAME.test" ]; then rm $TMP_NAME.test >/dev/null 2>&1 else echo -e "\nError: Unable to create temporary files ($TMP_NAME.test)\n" exit 1 fi fi # Mount destination archive directory(s) for DIR in $MOUNT_DEST; do if [ "$DIR" != "" ]; then MOUNTED="`mount |grep \"$DIR\"`" if [ "$MOUNTED" = "" ]; then mount $DIR > /dev/null 2>&1 fi fi done # Loop through sources and backup if [ $ERRORS_ONLY -eq 0 ]; then echo ""; fi Processed=0 for SOURCE in $SOURCES; do if [ $RUN_ALL -eq 1 ] || [ $MULTIPLEX -ne 1 ] || [ "$SOURCE" = "$2" ]; then # Set variables Processed=`expr $Processed + 1` BACKUP_DATE="`date +%Y-%m-%d`" BACKUP_SECS="`date +%s`" eval SOURCEPATH="\$SOURCEPATH_$SOURCE" eval SOURCEFILES="\$SOURCEFILES_$SOURCE" eval MOUNT="\$MOUNT_$SOURCE" eval MOUNTCMD="\$MOUNTCMD_$SOURCE" eval UMOUNTCMD="\$UMOUNT_$SOURCE" eval DEST="\$DEST_$SOURCE" eval SOURCE_EXCLUDE="\$EXCLUDE_$SOURCE" eval SOURCE_COMPRESS="\$COMPRESS_$SOURCE" eval SOURCE_BANDWIDTH="\$BANDWIDTH_$SOURCE" eval SOURCE_TIMEOUT="\$TIMEOUT_$SOURCE" eval SOURCE_MANUAL="\$MANUAL_$SOURCE" eval SOURCE_MULTIPLEX="\$MULTIPLEX_$SOURCE" eval SOURCE_KEEP="\$KEEP_$SOURCE" eval SOURCE_DAYS="\$BACKUP_DAYS_$SOURCE" eval SOURCE_LOCAL="\$LOCAL_$SOURCE" eval SOURCE_DELAY="\$DELAY_$SOURCE" eval SOURCE_TIME="\$TIME_$SOURCE" eval SOURCE_INCREMENTAL_BACKUPS="\$INCREMENTAL_BACKUPS_$SOURCE" eval SOURCE_INCREMENTAL_DAILY="\$INCREMENTAL_DAILY_$SOURCE" eval SOURCE_INCREMENTAL_WEEKLY="\$INCREMENTAL_WEEKLY_$SOURCE" eval SOURCE_INCREMENTAL_MONTHLY="\$INCREMENTAL_MONTHLY_$SOURCE" eval SOURCE_RSYNC_OPTIONS="\$RSYNC_OPTIONS_$SOURCE" eval SOURCE_SSH_PORT="\$SSH_PORT_$SOURCE" eval SOURCE_UTILISATION=\$UTILISATION_$SOURCE eval SOURCE_RECURSE_DIRS="\$RECURSE_DIRS_$SOURCE" eval SOURCE_PRE_EXEC="\$PRE_EXEC_$SOURCE" eval SOURCE_POST_EXEC="\$POST_EXEC_$SOURCE" eval SOURCE_DEST_CREATE="\$DEST_CREATE_$SOURCE" DECOMPRESS_TIME=0 RSYNC_TIME=0 COMPRESS_TIME=0 ERRORS=0 ERRORS_TITLE="errors" # Use default incremental backup options? if [ "$SOURCE_INCREMENTAL_BACKUPS" = "" ]; then SOURCE_INCREMENTAL_BACKUPS="$INCREMENTAL_BACKUPS" fi if [ "$SOURCE_INCREMENTAL_DAILY" = "" ]; then SOURCE_INCREMENTAL_DAILY="$INCREMENTAL_DAILY" fi if [ "$SOURCE_INCREMENTAL_WEEKLY" = "" ]; then SOURCE_INCREMENTAL_WEEKLY="$INCREMENTAL_WEEKLY" fi if [ "$SOURCE_INCREMENTAL_MONTHLY" = "" ]; then SOURCE_INCREMENTAL_MONTHLY="$INCREMENTAL_MONTHLY" fi # Exclusions list RSYNC_EXCLUDE="" EXCLUDE_MODIFIER="" if [ "$SOURCE_EXCLUDE" != "" ]; then for EXCLUDE in $SOURCE_EXCLUDE; do if [ "$EXCLUDE" != "" ]; then if [ "$EXCLUDE" = "+" ]; then EXCLUDE_MODIFIER="+" elif [ "$EXCLUDE" = "-" ]; then EXCLUDE_MODIFIER="-" else if [ "$EXCLUDE_MODIFIER" = "" ]; then RSYNC_EXCLUDE="$RSYNC_EXCLUDE --exclude=$EXCLUDE" else RSYNC_EXCLUDE="$RSYNC_EXCLUDE --exclude='$EXCLUDE_MODIFIER $EXCLUDE'" fi EXCLUDE_MODIFIER="" fi fi done fi # Limit bandwidth? if [ "$SOURCE_BANDWIDTH" != "" ] && [ $SOURCE_BANDWIDTH -gt 0 ]; then SOURCE_BANDWIDTH=" --bwlimit=$SOURCE_BANDWIDTH" else SOURCE_BANDWIDTH="" fi # Alternative port for SSH? if [ "$SOURCE_SSH_PORT" = "" ] || [ $SOURCE_SSH_PORT -lt 1 ]; then SOURCE_SSH_PORT="" fi # Alternative list of directories to sync recursively if [ "$SOURCE_RECURSE_DIRS" = "" ]; then SOURCE_RECURSE_DIRS="$RECURSE_DIRS" fi # Create destination directory structure if it doesn't exist? if [ "$SOURCE_DEST_CREATE" = "" ]; then SOURCE_DEST_CREATE="$DEST_CREATE" fi # Use default keep previous archives value? if [ "$SOURCE_KEEP" = "" ]; then SOURCE_KEEP="$KEEP_PREVIOUS" fi # Use default purge disk utilisation? if [ "$SOURCE_UTILISATION" = "" ]; then SOURCE_UTILISATION="$PURGE_UTILISATION" fi # Manual run backup or multiplexed source? if [ $MANUAL_RUN -eq 1 ]; then SOURCE_MULTIPLEX=0; fi if [ "$SOURCE_MULTIPLEX" = "" ]; then SOURCE_MULTIPLEX=0; fi if [ "$SOURCE_MANUAL" = "" ]; then SOURCE_MANUAL=0; fi # Custom timeout? IO_TIMEOUT=" --timeout=$RSYNC_TIMEOUT" if [ "$SOURCE_TIMEOUT" != "" ]; then if [ "$SOURCE_TIMEOUT" = "0" ]; then IO_TIMEOUT="" else IO_TIMEOUT=" --timeout=$SOURCE_TIMEOUT" fi elif [ "$RSYNC_TIMEOUT" = "0" ]; then IO_TIMEOUT="" fi # Backup this source today? BACKUP_SOURCE=0 if [ "$SOURCE_DAYS" != "" ]; then TODAY="`date +%w`" for DAY in $SOURCE_DAYS; do if [ "$DAY" = "$TODAY" ]; then BACKUP_SOURCE=1 fi done else BACKUP_SOURCE=1 fi if [ $MANUAL_RUN -eq 1 ]; then BACKUP_SOURCE=1; fi # Wait until specified time before backing up source? if [ "$SOURCE_TIME" != "" ] && [ $BACKUP_SOURCE -eq 1 ] && [ $MULTIPLEX -eq 1 ] && [ $SOURCE_MULTIPLEX -eq 1 ]; then ORIGINAL_TIME="$SOURCE_TIME" SOURCE_TIME="`echo \"$SOURCE_TIME\" |sed -e \"s/:/ /g\" |sed -e \"s/0\+\([0-9]\+\)/\1/g\"`" SOURCE_HOUR="`echo \"$SOURCE_TIME\" |sed -e \"s/^\([0-9]\+\) \([0-9]\+\)\$/\1/g\"`" SOURCE_MIN="`echo \"$SOURCE_TIME\" |sed -e \"s/^\([0-9]\+\) \([0-9]\+\)\$/\2/g\"`" if [ "$SOURCE_HOUR" = "" ] || [ $SOURCE_HOUR -lt 0 ] || [ $SOURCE_HOUR -gt 23 ]; then echo -e "\nError: Invalid hour '$SOURCE_HOUR' specified for delay time '$ORIGINAL_TIME'. Please specify 0-23.\n" BACKUP_SOURCE=0 fi if [ "$SOURCE_MIN" = "" ] || [ $SOURCE_MIN -lt 0 ] || [ $SOURCE_MIN -gt 59 ]; then echo -e "\nError: Invalid minute '$SOURCE_MIN' specified for delay time '$ORIGINAL_TIME'. Please specify 0-59.\n" BACKUP_SOURCE=0 fi WAIT_TIME=0 CURRENT_HOUR=-1 CURRENT_MIN=-1 WAIT_TIME_PURGE=$PURGE_WHILE_WAITING while [ $CURRENT_HOUR -ne $SOURCE_HOUR ] || [ $CURRENT_MIN -lt $SOURCE_MIN ]; do if [ $CURRENT_HOUR -eq -1 ] && [ $CURRENT_MIN -eq -1 ]; then if [ $ERRORS_ONLY -eq 0 ]; then echo "$SOURCE: (Delay) Waiting until $ORIGINAL_TIME before starting backup..."; fi else sleep 1m WAIT_TIME=`expr $WAIT_TIME + 1` if [ $WAIT_TIME -gt 1500 ]; then echo -e "\nError: Start time $ORIGINAL_TIME not reached within 25 hours. Backup aborted.\n" SOURCE_HOUR="$CURRENT_HOUR" SOURCE_MIN="$CURRENT_MIN" BACKUP_SOURCE=0 fi fi if [ $WAIT_TIME_PURGE -eq 1 ]; then # Purge previous backups while waiting for correct backup time? LOGFILE="$TMP_PURGE_LOG.$BACKUP_SECS" if [ $SOURCE_INCREMENTAL_BACKUPS -eq 1 ]; then PurgeBackups "$DEST/incremental" $SOURCE_UTILISATION 0 else PurgeBackups "$DEST/previous" $SOURCE_UTILISATION $SOURCE_KEEP fi fi CURRENT_HOUR="`date +%H |sed -e \"s/0\+\([0-9]\+\)/\1/g\"`" CURRENT_MIN="`date +%M |sed -e \"s/0\+\([0-9]\+\)/\1/g\"`" done; fi # Manual run only if [ $SOURCE_MANUAL -eq 1 ] && [ $MANUAL_RUN -eq 0 ]; then BACKUP_SOURCE=0 fi # Small random delay at start if [ $MANUAL_RUN -ne 1 ]; then sleep `expr $RANDOM % 60`s fi # Check lock file if [ -e "$TMP_NAME.$SOURCE.lock" ]; then LOCK="`cat $TMP_NAME.$SOURCE.lock`" if [ "$LOCK" != "" ] && [ $LOCK -gt 0 ]; then NOW="`date +%s`" if [ `expr $NOW - $LOCK` -lt `expr $LOCK_TIME \* 60 \* 60` ]; then echo "$SOURCE: Active lock file exists - Backup of source skipped." SendMessage "Error: Active lock file exists - Backup of source skipped." 1 BACKUP_SOURCE=0 fi fi fi if [ $BACKUP_SOURCE -ne 1 ]; then # Don't backup this source BACKUP_SOURCE=0 elif [ $MULTIPLEX -ne 1 ] && [ $SOURCE_MULTIPLEX -eq 1 ]; then # Multiplex this source if [ $ERRORS_ONLY -eq 0 ]; then echo "$SOURCE: Initiating multiplexed backup..."; fi sh $0 multiplex "$SOURCE" "$ERRORS_ONLY" "$SHOW_SUMMARY" "$SHOW_ERRORS" & else # Backup this source if [ "$SOURCEFILES" = "" ]; then echo -e "$SOURCE: No source files/directories (SOURCEFILES_$SOURCE=) specified for source $SOURCE in '$SOURCEDEFS' (Source doesn't exist?) Valid sources: $ORIGINAL_SOURCES\n" elif [ "$DEST" = "" ]; then echo -e "$SOURCE: No backup destination path (DEST_$SOURCE=) specified for source $SOURCE in '$SOURCEDEFS'.\n" else # Update lock file echo "`date +%s`" > $TMP_NAME.$SOURCE.lock # Randomise source host in source path RandomSourcePath "$SOURCEPATH" 0 # Create destination directories DirError=0 if [ $SOURCE_INCREMENTAL_BACKUPS -eq 1 ]; then DIR_LIST="$DEST/incremental $DEST/log" else DIR_LIST="$DEST/current $DEST/previous $DEST/log" fi if [ "$SOURCE_DEST_CREATE" = "1" ]; then DEST_DIRS="" DEST_PATH="" for DIRS in `echo "$DEST" |sed -e "s/[[:space:]]\+/_.-/g" |sed -e "s~/~ ~g"`; do DIRNAME="`echo \"$DIRS\" |sed -e \"s/_.-/ /g\"`" DEST_PATH="$DEST_PATH/$DIRNAME" DEST_DIRS="$DEST_DIRS $DEST_PATH" done; DIR_LIST="$DEST_DIRS $DIR_LIST" fi for DIRS in $DIR_LIST; do if [ ! -e "$DIRS" ]; then mkdir $DIRS if [ ! -e "$DIRS" ]; then echo "$SOURCE: Unable to create destination directory '$DIRS' for source $SOURCE." DirError=1 fi fi done # Update lock file echo "`date +%s`" > $TMP_NAME.$SOURCE.lock if [ $DirError -eq 0 ]; then # Execute pre command(s) if [ "$SOURCE_PRE_EXEC" != "" ]; then PrePostExec "$RANDOMPATH" "$SOURCE_PRE_EXEC" fi # Use default rsync options? if [ "$SOURCE_RSYNC_OPTIONS" = "" ]; then if [ "$SOURCE_LOCAL" != "" ] && [ $SOURCE_LOCAL -eq 1 ]; then SOURCE_RSYNC_OPTIONS="$RSYNC_OPTIONS_LOCAL" else SOURCE_RSYNC_OPTIONS="$RSYNC_OPTIONS_REMOTE" fi fi # Set log file LOGFILE="$DEST/log/$BACKUP_DATE-results.log" LOGERRORS="$DEST/log/$BACKUP_DATE-errors.log" LOGSUMMARY="$DEST/log/$BACKUP_DATE-summary.log" if [ -e "$LOGFILE" ]; then rm $LOGFILE; fi if [ -e "$LOGERROR" ]; then rm $LOGERROR; fi if [ -e "$LOGSUMMARY" ]; then rm $LOGSUMMARY; fi # Remove old logs RemoveOldLogs $DEST/log/\*-\*-\*-results.log $KEEP_LOGS RemoveOldLogs $DEST/log/\*-\*-\*-errors.log $KEEP_LOGS RemoveOldLogs $DEST/log/\*-\*-\*-summary.log $KEEP_LOGS # Update lock file echo "`date +%s`" > $TMP_NAME.$SOURCE.lock # Log start time START_TIME="`date`" if [ $MULTIPLEX -eq 1 ]; then echo -e "`date +\"$LOG_DATE\"`: Multiplexed backup of '$SOURCEFILES' from $RANDOMPATH ($SOURCE) to $DEST started at $START_TIME...\n" > $LOGFILE else echo -e "`date +\"$LOG_DATE\"`: Backup of '$SOURCEFILES' from $RANDOMPATH ($SOURCE) to $DEST started at $START_TIME...\n" > $LOGFILE fi # Append any purge log messages to log file if [ -e "$TMP_PURGE_LOG.$BACKUP_SECS" ]; then cat "$TMP_PURGE_LOG.$BACKUP_SECS" >> $LOGFILE rm "$TMP_PURGE_LOG.$BACKUP_SECS" > /dev/null 2>&1 fi if [ -e "$DEST" ]; then # Mount specified source volume IsMounted=1 if [ "$MOUNT" != "" ]; then if [ $ERRORS_ONLY -eq 0 ]; then echo "$SOURCE: Mounting $MOUNT..."; fi echo "`date +\"$LOG_DATE\"`: Mounting $MOUNT..." >> $LOGFILE if [ "$UMOUNTCMD" = "" ]; then umount $MOUNT > /dev/null 2>&1 else $UMOUNTCMD > /dev/null 2>&1 fi if [ "$MOUNTCMD" = "" ]; then mount $MOUNT >> $LOGFILE 2>&1 else mount $MOUNTCMD >> $LOGFILE 2>&1 fi MOUNTED_VOLUME="`mount |grep "$MOUNT" |grep -v grep`" MOUNTED_FILES="`ls $MOUNT 2>&1 |grep -v "No such file or directory"`" if [ "$MOUNTED_VOLUME" = "" ] || [ "$MOUNTED_FILES" = "" ]; then echo "$SOURCE: Unable to mount $MOUNT or mounted volume contains no files/directories." echo "Error: Unable to mount $MOUNT or mounted volume contains no files/directories." >> $LOGFILE SendMessage "Error: Unable to mount $MOUNT or mounted volume contains no files/directories." 1 IsMounted=0 fi fi if [ $IsMounted -eq 1 ]; then # Backup source files SOURCECOUNT=1 SOURCELIST="" for FILE in $SOURCEFILES; do if [ "$FILE" != "" ]; then FILE_TYPE="`echo \"$FILE\" |sed -e \"s/^\(.\+\):\(.\+\)\$/\1/g\"`" FILE_NAME="`echo \"$FILE\" |sed -e \"s/^\(.\+\):\(.\+\)\$/\2/g\"`" if [ "$FILE_TYPE" != "$FILE" ] && [ "$FILE_NAME" != "$FILE" ]; then if [ "$FILE_TYPE" = "dir" ] || [ "$FILE_TYPE" = "DIR" ]; then FILE_IS_DIR=1 FILE="$FILE_NAME" elif [ "$FILE_TYPE" = "file" ] || [ "$FILE_TYPE" = "FILE" ]; then FILE_IS_DIR=0 FILE="$FILE_NAME" else echo "$SOURCE: Unknown file type '$FILE_TYPE' - Please specify either dir:$FILE_NAME or file:$FILE_NAME" echo "Error: Unknown file type '$FILE_TYPE' - Please specify either dir:$FILE_NAME or file:$FILE_NAM" >> $LOGFILE FILE="" fi else FILE_IS_DIR=1 fi fi if [ "$FILE" != "" ]; then if [ $SOURCECOUNT -gt 1 ] && [ $ERRORS_ONLY -eq 0 ]; then echo ""; fi if [ "$RANDOMPATH" != "" ]; then RANDOMPATH_HOST="" RANDOMPATH_LAST="(NULL)" IsMounted=0 RetryCount=$RSYNC_RETRIES while [ $IsMounted -eq 0 ] && [ $RetryCount -gt 0 ] && [ "$RANDOMPATH" != "" ] && [ "$RANDOMPATH_LAST" != "$RANDOMPATH" ]; do if [ "$SOURCE_COMPRESS" != "" ] && [ $SOURCE_COMPRESS -eq 2 ]; then FILE="`echo \"$FILE\" |sed -e \"s~^/\+~~g\"`" else FILE="/`echo \"$FILE\" |sed -e \"s~^/\+~~g\"`" fi IsMounted=1 # Check path exists on remote host and contains files RANDOMPATH_HOST="`echo "$RANDOMPATH" |sed -e "s/:.\+\$//g"`" RANDOMPATH_PATH="`echo "$RANDOMPATH" |sed -e "s/^.\+://g"`" if [ "$RANDOMPATH_HOST" != "$RANDOMPATH" ] && [ "$RANDOMPATH_PATH" != "$RANDOMPATH" ]; then if [ "$SOURCE_SSH_PORT" != "" ]; then MOUNTED_FILES="`ssh -p $SOURCE_SSH_PORT $RANDOMPATH_HOST ls $RANDOMPATH_PATH$FILE 2>&1 |grep -v "No such file or directory"`" else MOUNTED_FILES="`ssh $RANDOMPATH_HOST ls $RANDOMPATH_PATH$FILE 2>&1 |grep -v "No such file or directory"`" fi MOUNTED_FILES_SSH="`echo "$MOUNTED_FILES" |grep "^ssh:"`" if [ "$MOUNTED_FILES_SSH" != "" ]; then if [ $ERRORS_ONLY -eq 0 ]; then echo "$SOURCE: (Retry `expr $RSYNC_RETRIES - $RetryCount + 1` of $RSYNC_RETRIES) Unable to backup $FILE: $MOUNTED_FILES_SSH" fi if [ $RetryCount -gt 1 ]; then echo "Retry: `expr $RSYNC_RETRIES - $RetryCount + 1` of $RSYNC_RETRIES - Unable to backup $FILE: $MOUNTED_FILES_SSH" >> $LOGFILE else echo "Error: (Retry `expr $RSYNC_RETRIES - $RetryCount + 1` of $RSYNC_RETRIES) Unable to backup $FILE: $MOUNTED_FILES_SSH" >> $LOGFILE fi IsMounted=0 elif [ "$MOUNTED_FILES" = "" ]; then if [ $ERRORS_ONLY -eq 0 ]; then echo "$SOURCE: $RANDOMPATH_PATH$FILE contains no files/directories." fi echo "Error: $RANDOMPATH_PATH$FILE contains no files/directories." >> $LOGFILE RetryCount=1 IsMounted=0 fi fi if [ $IsMounted -eq 0 ]; then # Randomise source host in source path RANDOMPATH_LAST="$RANDOMPATH" RandomSourcePath "$SOURCEPATH" 1 fi RetryCount=`expr $RetryCount - 1` done; fi if [ $IsMounted -eq 1 ]; then if [ $ERRORS_ONLY -eq 0 ]; then if [ $SOURCECOUNT -gt 1 ]; then echo ""; fi echo "$SOURCE: Backing up $FILE via $RANDOMPATH on `date`..." fi echo -e "\n`date +\"$LOG_DATE\"`: Backing up $FILE via $RANDOMPATH on `date`...\n" >> $LOGFILE SOURCE_START="`date`" # Update lock file echo "`date +%s`" > $TMP_NAME.$SOURCE.lock # Rotate incremental backups? if [ $SOURCE_INCREMENTAL_BACKUPS -eq 1 ] && [ $SOURCECOUNT -eq 1 ]; then RotateBackups "$DEST/incremental" $SOURCE_INCREMENTAL_DAILY $SOURCE_INCREMENTAL_WEEKLY $SOURCE_INCREMENTAL_MONTHLY # Create new current backup directory if [ ! -e "$DEST/incremental/m0-w0-d1" ]; then # Non-incremental backup converted to incremental? if [ -e "$DEST/current" ] && [ -d "$DEST/current" ] && [ ! -e "$DEST/incremental/m0-w0-d2" ]; then mv $DEST/current $DEST/incremental/m0-w0-d2 >>$LOGERRORS 2>&1 fi mkdir "$DEST/incremental/m0-w0-d1" >>$LOGERRORS 2>&1 fi # Hard-link copy previous backup into new current backup directory if [ -e "$DEST/incremental/m0-w0-d2" ]; then cp -al $DEST/incremental/m0-w0-d2/* $DEST/incremental/m0-w0-d1 >>$LOGERRORS 2>&1 fi fi # Update lock file echo "`date +%s`" > $TMP_NAME.$SOURCE.lock # Purge previous backups? if [ $SOURCE_INCREMENTAL_BACKUPS -eq 1 ]; then PurgeBackups "$DEST/incremental" $SOURCE_UTILISATION 0 else PurgeBackups "$DEST/previous" $SOURCE_UTILISATION $SOURCE_KEEP fi # Update lock file echo "`date +%s`" > $TMP_NAME.$SOURCE.lock # Setup background job to update lock file # while rsync is taking place # echo "1" > $TMP_NAME.$SOURCE.updatelock sh -c "while [ -e $TMP_NAME.$SOURCE.updatelock ]; do echo \"\`date +%s\`\" > $TMP_NAME.$SOURCE.lock; sleep 1s; done" & # Copy file(s) via rsync Delay $SOURCE_DELAY TIME_START="`date +%s`" RSYNC_RETRY=1 RSYNC_RETRY_COUNT=1 while [ $RSYNC_RETRY -eq 1 ]; do # Copy files via rsync RSYNC_RETRY=0 if [ "$SOURCE_SSH_PORT" != "" ]; then if [ $SOURCE_INCREMENTAL_BACKUPS -eq 1 ]; then rsync $SOURCE_RSYNC_OPTIONS$SOURCE_BANDWIDTH$RSYNC_EXCLUDE$IO_TIMEOUT --rsh="ssh -p $SOURCE_SSH_PORT" "$RANDOMPATH$FILE" "$DEST/incremental/m0-w0-d1" >> $TMP_NAME.$SOURCE.rsync 2>&1 else rsync $SOURCE_RSYNC_OPTIONS$SOURCE_BANDWIDTH$RSYNC_EXCLUDE$IO_TIMEOUT --rsh="ssh -p $SOURCE_SSH_PORT" --backup --backup-dir=$DEST/previous/$BACKUP_DATE "$RANDOMPATH$FILE" "$DEST/current" >> $TMP_NAME.$SOURCE.rsync 2>&1 fi else if [ $SOURCE_INCREMENTAL_BACKUPS -eq 1 ]; then rsync $SOURCE_RSYNC_OPTIONS$SOURCE_BANDWIDTH$RSYNC_EXCLUDE$IO_TIMEOUT "$RANDOMPATH$FILE" "$DEST/incremental/m0-w0-d1" >> $TMP_NAME.$SOURCE.rsync 2>&1 else rsync $SOURCE_RSYNC_OPTIONS$SOURCE_BANDWIDTH$RSYNC_EXCLUDE$IO_TIMEOUT --backup --backup-dir=$DEST/previous/$BACKUP_DATE "$RANDOMPATH$FILE" "$DEST/current" >> $TMP_NAME.$SOURCE.rsync 2>&1 fi fi if [ -e "$TMP_NAME.$SOURCE.rsync" ]; then cat $TMP_NAME.$SOURCE.rsync >> $LOGFILE fi # Transfer failed/error - Retry? if [ "`grep -i -E \"^.*(rsync.*connection[[:space:]]+unexpectedly[[:space:]]+closed|rsync[[:space:]]+error.*some[[:space:]]+files[[:space:]]+could[[:space:]]+not[[:space:]]+be[[:space:]]+transferred|rsync[[:space:]]+warning.*some[[:space:]]+files[[:space:]]+vanished[[:space:]]+before[[:space:]]+they[[:space:]]+could[[:space:]]+be[[:space:]]+transferred|rsync[[:space:]]+error.*timeout[[:space:]]+in[[:space:]]+data[[:space:]]+send/receive|connection[[:space:]]+timed[[:space:]]+out|file[[:space:]]+has[[:space:]]+vanished:[[:space:]]+|WARNING:.*failed[[:space:]]+verification[[:space:]]+-+[[:space:]]+update[[:space:]]+discarded|rsync[[:space:]]+error:[[:space:]]+unexplained[[:space:]]+error).*\$\" $TMP_NAME.$SOURCE.rsync`" != "" ]; then if [ $RSYNC_RETRY_COUNT -le $RSYNC_RETRIES ]; then RandomSourcePath "$SOURCEPATH" 1 echo "Retry: $RSYNC_RETRY_COUNT of $RSYNC_RETRIES - Restarting transfer of $FILE from $SOURCE via $RANDOMPATH at `date`..." >> $LOGFILE RSYNC_RETRY=1 sleep $RSYNC_RETRY_DELAY else echo "Retry: $RSYNC_RETRY_COUNT of $RSYNC_RETRIES - Maximum number of retries exceeded for $FILE from $SOURCE at `date`." >> $LOGFILE fi fi if [ -e "$TMP_NAME.$SOURCE.rsync" ]; then rm $TMP_NAME.$SOURCE.rsync fi RSYNC_RETRY_COUNT=`expr $RSYNC_RETRY_COUNT + 1` done # Log end time TIME_END="`date +%s`" RSYNC_TIME=`expr $RSYNC_TIME + \( $TIME_END - $TIME_START \)` SOURCECOUNT=`expr $SOURCECOUNT + 1` # Stop background lock file updating process if [ -e $TMP_NAME.$SOURCE.updatelock ]; then rm $TMP_NAME.$SOURCE.updatelock sleep 1s fi # Update lock file echo "`date +%s`" > $TMP_NAME.$SOURCE.lock # Remove lock file rm $TMP_NAME.$SOURCE.lock >/dev/null 2>&1 # Source end time and duration SOURCE_END="`date`" SOURCE_START_INT="`date --date=\"$SOURCE_START\" +%s`" SOURCE_END_INT="`date --date=\"$SOURCE_END\" +%s`" SOURCE_ELAPSED=`expr $SOURCE_END_INT - $SOURCE_START_INT` if [ $ERRORS_ONLY -eq 0 ]; then echo "$SOURCE: Backup of $FILE completed on $SOURCE_END (`expr $SOURCE_ELAPSED / \( 60 \* 60 \* 24 \)`d `expr \( $SOURCE_ELAPSED % \( 60 \* 60 \* 24 \) \) / \( 60 \* 60 \)`h `expr \( $SOURCE_ELAPSED % \( 60 \* 60 \) \) / 60`m `expr $SOURCE_ELAPSED % 60`s)."; fi echo "`date +\"$LOG_DATE\"`: Backup of $FILE completed on $SOURCE_END (`expr $SOURCE_ELAPSED / \( 60 \* 60 \* 24 \)`d `expr \( $SOURCE_ELAPSED % \( 60 \* 60 \* 24 \) \) / \( 60 \* 60 \)`h `expr \( $SOURCE_ELAPSED % \( 60 \* 60 \) \) / 60`m `expr $SOURCE_ELAPSED % 60`s)." >> $LOGFILE if [ $ERRORS_ONLY -eq 0 ]; then echo ""; fi fi # Randomise source host in source path RandomSourcePath "$SOURCEPATH" 0 fi done if [ $SOURCECOUNT -gt 1 ]; then # Update current symlink for incremental backups if [ $SOURCE_INCREMENTAL_BACKUPS -eq 1 ]; then if [ -e "$DEST/incremental/m0-w0-d1" ]; then rm $DEST/incremental/current >>$LOGERRORS 2>&1 ln -sf $DEST/incremental/m0-w0-d1 $DEST/incremental/current >>$LOGERRORS 2>&1 fi fi fi # Unmount specified volume if [ "$MOUNT" != "" ]; then cd / if [ "$UMOUNTCMD" = "" ]; then umount $MOUNT >> $LOGFILE 2>&1 else $UMOUNTCMD >> $LOGFILE 2>&1 fi fi if [ $SOURCECOUNT -gt 1 ]; then # Compress previous archives if [ $COMPRESS_BACKUPS -eq 1 ] && [ $SOURCE_INCREMENTAL_BACKUPS -ne 1 ]; then if [ -e "$DEST/previous/$BACKUP_DATE" ] && [ -d "$DEST/previous/$BACKUP_DATE" ]; then if [ $ERRORS_ONLY -eq 0 ]; then echo "$SOURCE: Compressing $DEST/previous/$BACKUP_DATE..."; fi echo -e "\n`date +\"$LOG_DATE\"`: Compressing $DEST/previous/$BACKUP_DATE..." >> $LOGFILE cd $DEST/previous/$BACKUP_DATE if [ "`pwd`" = "$DEST/previous/$BACKUP_DATE" ]; then ZIP_FILE_SIZE="`du -s \"$DEST/previous/$BACKUP_DATE\" |cut -f1`" if [ $COMPRESS_BACKUPS_ZIP -eq 1 ] && [ "$ZIP_FILE_SIZE" != "" ] && [ $ZIP_FILE_SIZE -gt 2000000 ]; then # Use gzip/bzip if file >2Gb # (Too big for zip) # COMPRESS_BACKUPS_ZIP=0 fi if [ $COMPRESS_BACKUPS_ZIP -eq 1 ]; then zip -b /tmp -r $DEST/previous/$BACKUP_DATE.zip * >> $LOGFILE 2>&1 if [ -e $DEST/previous/$BACKUP_DATE.zip ]; then cd .. rm -r $DEST/previous/$BACKUP_DATE fi else tar c${TAR_OPT}f $DEST/previous/$BACKUP_DATE.tar.${TAR_EXT} * >> $LOGFILE 2>&1 fi if [ -e "$DEST/previous/$BACKUP_DATE.tar.$TAR_EXT" ] && [ -e "$DEST/previous/$BACKUP_DATE" ] && [ -d "$DEST/previous/$BACKUP_DATE" ]; then rm -r $DEST/previous/$BACKUP_DATE >> $LOGFILE 2>&1 fi fi fi fi fi # Execute post command(s) if [ "$SOURCE_POST_EXEC" != "" ]; then PrePostExec "$RANDOMPATH" "$SOURCE_POST_EXEC" fi # Remove lock file if [ -e "$TMP_NAME.$SOURCE.lock" ]; then rm $TMP_NAME.$SOURCE.lock >/dev/null 2>&1 fi fi fi fi # Log finish time END_TIME="`date`" echo -e "\nSummary for backup of '$SOURCEFILES' from $RANDOMPATH ($SOURCE) to $DEST...\n" >> $LOGFILE # Summary: Script start time/date echo -e "Start time:\t$START_TIME\n" >> $LOGFILE # Summary: Time spent rsync'ing files echo -e "Rsync time:\t`expr $RSYNC_TIME / \( 60 \* 60 \* 24 \)`d `expr \( $RSYNC_TIME % \( 60 \* 60 \* 24 \) \) / \( 60 \* 60 \)`h `expr \( $RSYNC_TIME % \( 60 \* 60 \) \) / 60`m `expr $RSYNC_TIME % 60`s" >> $LOGFILE # Determine total time START_INT="`date --date=\"$START_TIME\" +%s`" END_INT="`date --date=\"$END_TIME\" +%s`" ELAPSED_TIME=`expr $END_INT - $START_INT` # Determine script processing time SCRIPT_TIME=`expr $ELAPSED_TIME - $RSYNC_TIME` # Summary: Time spent processing script echo -e "Script time:\t`expr $SCRIPT_TIME / \( 60 \* 60 \* 24 \)`d `expr \( $SCRIPT_TIME % \( 60 \* 60 \* 24 \) \) / \( 60 \* 60 \)`h `expr \( $SCRIPT_TIME % \( 60 \* 60 \) \) / 60`m `expr $SCRIPT_TIME % 60`s" >> $LOGFILE # Summary: Total overall time echo -e "Total time:\t`expr $ELAPSED_TIME / \( 60 \* 60 \* 24 \)`d `expr \( $ELAPSED_TIME % \( 60 \* 60 \* 24 \) \) / \( 60 \* 60 \)`h `expr \( $ELAPSED_TIME % \( 60 \* 60 \) \) / 60`m `expr $ELAPSED_TIME % 60`s\n" >> $LOGFILE # Summary: Finish time/date echo -e "Finish time:\t$END_TIME" >> $LOGFILE # Generate summary/errors if [ -e "$LOGFILE" ]; then # Errors grep -E -i "((bunzip2?|bzip2?|error|retry|gunzip|gzip|mv|rm|rsync|tar|warning):|failed to open|file has vanished|permission denied|send_files failed)" $LOGFILE |grep -v -E -i "^.*rsync.*mkdir.*previous.*failed.*File.*exist" > $LOGERRORS if [ -e "$LOGERRORS" ] && [ `wc -l $LOGERRORS |cut -f1 -d' '` -lt 1 ]; then rm $LOGERRORS > /dev/null 2>&1 fi # Summary: Backup session echo "Backup session summary:" >> $LOGSUMMARY echo "~~~~~~~~~~~~~~~~~~~~~~~" >> $LOGSUMMARY echo -e "Source Name:\t$SOURCE" >> $LOGSUMMARY echo -e "Source Path:\t$SOURCEPATH" >> $LOGSUMMARY echo -e "Source Files:\t$SOURCEFILES" >> $LOGSUMMARY echo -e "Destination:\t$DEST" >> $LOGSUMMARY # Summary: Transfer grep -E "^[[:digit:][:space:]:-]\+:[[:space:]]\+(Backing up .+\.\.\.|Downloading file list from|[0-9]+Kb reduced to [0-9]+Kb|Purging .+\.\.\.|Disk utilisation of .+ is greater than|Removing old log file .+\.\.\.)" $LOGFILE |sed -e "s/^\(Backing \)/\n\1/g" -e "s/^\(Backup of \)/\n\1/g" >> $LOGSUMMARY TOTAL_SENT=0 TOTAL_RCVD=0 TOTAL_RATE=0 TOTAL_SIZE=0 TOTAL_SPEED=0 TOTAL_FILES=0 COUNTER=0 for VAL in `cat $LOGFILE |grep -E "^(sent [0-9]+ byte|total size is [0-9]+)" |sed -e "s/[^0-9[:space:]]//g" |sed -e "s/^[[:space:]]\+//g" |sed -e "s/[[:space:]]\+/ /g"`; do VAL="`echo \"$VAL\" |sed -e \"s/^0\{1,2\}//g\" |sed -e \"s/^000\+[0-9]\+/0/g\"`" if [ "$VAL" = "" ]; then VAL=0; fi case $COUNTER in 0) TOTAL_SENT=`expr $TOTAL_SENT + $VAL` ;; 1) TOTAL_RCVD=`expr $TOTAL_RCVD + $VAL` ;; 2) TOTAL_RATE=`expr $TOTAL_RATE + $VAL` ;; 3) TOTAL_SIZE=`expr $TOTAL_SIZE + $VAL` ;; 4) TOTAL_SPEED=`expr $TOTAL_SPEED + $VAL` TOTAL_FILES=`expr $TOTAL_FILES + 1` ;; esac COUNTER=`expr $COUNTER + 1` if [ $COUNTER -gt 4 ]; then COUNTER=0; fi done if [ $TOTAL_FILES -gt 0 ]; then echo -e "\nRsync Transfer:" >> $LOGSUMMARY echo "~~~~~~~~~~~~~~~" >> $LOGSUMMARY echo -e "Bytes Sent:\t$TOTAL_SENT" >> $LOGSUMMARY echo -e "Bytes Rcvd:\t$TOTAL_RCVD" >> $LOGSUMMARY RATE_DECIMAL=`expr \( $TOTAL_RATE / $TOTAL_FILES \) / 100` RATE_FRACTION=`expr \( $TOTAL_RATE / $TOTAL_FILES \) % 100` if [ $RATE_FRACTION -lt 10 ]; then RATE_FRACTION="0$RATE_FRACTION"; fi echo -e "Avg. Rate:\t$RATE_DECIMAL.$RATE_FRACTION bytes/sec" >> $LOGSUMMARY echo -e "Total Xfer:\t`expr $TOTAL_SENT + $TOTAL_RCVD`" >> $LOGSUMMARY echo -e "Total Size:\t$TOTAL_SIZE" >> $LOGSUMMARY SPEED_DECIMAL=`expr \( $TOTAL_SPEED / $TOTAL_FILES \) / 100` SPEED_FRACTION=`expr \( $TOTAL_SPEED / $TOTAL_FILES \) % 100` if [ $SPEED_FRACTION -lt 10 ]; then SPEED_FRACTION="0$SPEED_FRACTION"; fi echo -e "Avg Speedup:\t$SPEED_DECIMAL.$SPEED_FRACTION" >> $LOGSUMMARY fi # Summary: Duration echo -e "\nDuration:" >> $LOGSUMMARY echo "~~~~~~~~~" >> $LOGSUMMARY grep -E "^[[:space:]]*(Start|Finish|Total|Compr\\.|Decmp\\.|Rsync|Script) time:" $LOGFILE >> $LOGSUMMARY # Summary: Archives purged? PURGED="`cat $LOGFILE |grep -E -i \"Purge:\"`" if [ "$PURGED" != "" ]; then echo -e "\nPurge:" >> $LOGSUMMARY echo "~~~~~~" >> $LOGSUMMARY echo "$PURGED" >> $LOGSUMMARY fi # Show summary? if [ $SHOW_SUMMARY -eq 1 ] && [ -e "$LOGSUMMARY" ]; then if [ $ERRORS_ONLY -eq 0 ]; then echo ""; fi cat $LOGSUMMARY fi # Show errors? if [ $SHOW_ERRORS -eq 1 ] && [ -e "$LOGERRORS" ] && [ "`wc -l $LOGERRORS |cut -d' ' -f1`" -gt 0 ]; then if [ $ERRORS_ONLY -eq 0 ]; then echo -e "\nErrors/Warnings:" echo "~~~~~~~~~~~~~~~~" else echo -e "$SOURCE errors:\n" fi cat $LOGERRORS if [ $ERRORS_ONLY -eq 0 ]; then echo ""; fi else if [ $SHOW_ERRORS -eq 1 ]; then echo ""; fi fi # Error count if [ -e "$LOGERRORS" ]; then ERRORS="`wc -l $LOGERRORS |cut -d' ' -f1`" if [ "$ERRORS" = "" ]; then ERRORS=0; fi if [ $ERRORS -eq 1 ]; then ERRORS_TITLE="error"; else ERRORS_TITLE="errors"; fi fi # E-mail summary and/or errors? if [ $EMAIL_SUMMARY -eq 1 ] || [ $EMAIL_ERRORS -eq 1 ]; then if [ "$EMAIL_RCPT" != "" ] && [ "$EMAIL_FROM" != "" ]; then if [ -e $TMP_EMAIL ]; then rm $TMP_EMAIL >/dev/null 2>&1; fi NEWLINE=0 # Summary if [ $EMAIL_SUMMARY -eq 1 ] && [ -e "$LOGSUMMARY" ]; then cat $LOGSUMMARY >> $TMP_EMAIL NEWLINE=1 fi # Errors if [ $NEWLINE -eq 1 ]; then echo "" >> $TMP_EMAIL; fi if [ $EMAIL_ERRORS -eq 1 ] && [ -e "$LOGERRORS" ]; then if [ $NEWLINE -eq 1 ]; then echo "" >> $TMP_EMAIL; fi echo -e "\nErrors/Warnings:" >> $TMP_EMAIL echo "~~~~~~~~~~~~~~~~" >> $TMP_EMAIL cat $LOGERRORS >> $TMP_EMAIL fi # Send E-mail if [ -e "$TMP_EMAIL" ]; then eval MAIL_SUBJECT=\"$EMAIL_SUBJECT\" mail -a "From: <$EMAIL_FROM>" -a "To: <$EMAIL_RCPT>" -a "Reply-to: <$EMAIL_FROM>" -s "$MAIL_SUBJECT" $EMAIL_RCPT < $TMP_EMAIL rm $TMP_EMAIL >/dev/null 2>&1 fi fi fi fi fi fi fi done exit 0