#!/bin/bash # # 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. # # Last updated: 21/02/2012 # # # 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. # J.P.Boggis 11/07/2008 v3.2 - Added PID check to lock file checks. # Kills the process if the lock file # hasn't been updated within LOCK_TIME. # J.P.Boggis 11/06/2009 v3.3 - Backup is now aborted if directory(s) # specified for MOUNT_DEST are not mounted # at the time the backup commences. # J.P.Boggis 11/06/2009 v3.4 - SYNC_OLDEST_FIRST option added. # J.P.Boggis 22/07/2009 v3.5 - IGNORE_ERRORS option added. # J.P.Boggis 11/08/2009 v3.6 - DEST2_[SOURCE] added to allow synchronisation # (Mirroring) to a secondary destination. # J.P.Boggis 16/12/2009 v3.7 - MOUNTSPEC_[NAME] added. # J.P.Boggis 29/01/2010 v3.8 - 'info' and 'sources' added. # J.P.Boggis 10/02/2010 v3.9 - 'status' added. # J.P.Boggis 24/02/2010 v4.0 - Removal of old logs now works correctly. # Added COMPRESS_FILES_[NAME]= option. # Added INCLUDE_[NAME]= option. # J.P.Boggis 04/05/2010 v4.1 - Added FAILOVER_[NAME]= option. # J.P.Boggis 07/05/2010 v4.2 - Added multiple DEST_[NAME] paths. # J.P.Boggis 12/05/2010 v4.3 - Added AVOID_BACKUP_HOURS # J.P.Boggis 19/05/2010 v4.4 - Multiple destination paths are now checked # for over utilisation and purged if necessary. # J.P.Boggis 27/01/2011 v4.5 - Last log message shown for info/status # J.P.Boggis 02/02/2011 v4.6 - Next path number for source is saved in /tmp # J.P.Boggis 14/02/2011 v4.7 - DEST_EXISTS_[NAME] option. # J.P.Boggis 24/03/2011 v4.8 - Mount status of directories listed in # MOUNT_DEST is now checked appropriately for # each source (Non-applicable dir that is not # mounted does not abort backup.) # J.P.Boggis 12/04/2011 v4.9 - Multiple source paths can now be specified # as well as hosts for failover. # J.P.Boggis 18/04/2011 v5.0 - Bug fixes in FailoverSourcePath # J.P.Boggis 21/04/2011 v5.1 - Bug fixes in AlternativeDestPath # J.P.Boggis 31/05/2011 v5.2 - Bugs fixed in AlternativeDestPath that # prevented first destination being chosen. # J.P.Boggis 18/08/2011 v5.3 - Bug in AlternativeDestPath that caused # DEST_EXISTS_[NAME]=0 to be ignored fixed. # J.P.Boggis 10/10/2011 v5.4 - Fixed /tmp/$SOURCE.next not being incremented. # J.P.Boggis 14/10/2011 v5.5 - Added running time to status report. Lock # file is now removed is backup is skipped # (Unable to mount source, etc.) # J.P.Boggis 27/10/2011 v5.6 - .next is updated after backup has completed. # J.P.Boggis 25/01/2012 v5.7 - Warns if no files in backup have been modified # in the past 48 hours. # J.P.Boggis 30/01/2012 v5.8 - WARN_DAYS_[NAME] option added. # SYNC_OLDEST_FIRST_[NAME] now checks each file # exists before attempting to rsync. # J.P.Boggis 31/01/2012 v5.9 - Errors and warnings are now reported # separately. # J.P.Boggis 10/02/2012 v6.0 - Others: section added to summary. # J.P.Boggis 21/02/2012 v6.1 - Warnings are no-longer logged as errors # # # Usage: # ~~~~~~ #:> USAGE: multi_backup_rsync [start|cron|silent|multiplex] [] #:> multi_backup_rsync [run|info|status] [ALL|[,][...]] #:?> multi_backup_rsync sources #:> #:> 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.) #:> #:> sources - Show list of available sources. #:> #:> status - Show status of source(s). #:> #:> info - Show information about source(s). # # # 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]" # MOUNTSPEC_[NAME]="/filespec" # # 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' # # MOUNTSPEC_[NAME] specifies a file specification (E.g: /*.dat) that # will be appended when listing files in the mount path to check whether # it is actually mounted or not (If no files are found, the backup will # be skipped.) # # # SOURCEPATH_[NAME]="[host[;host[;...]]:][path][: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, unless # FAILOVER_[NAME] is defined (See below.) # # Multiple hosts enables fault tolerance - If the connection fails, # times out, etc., one of the other hostname(s) will be used for each # retry. # # Multiple source paths may also be specified if the source is # duplicated on more than one disk (E.g: Remotely backing up # another archive with alternative paths specified for DEST_[NAME]) # # E.g: SOURCEPATH_SERVER1="server1.local:/" # SOURCEPATH_USER1="user1@server1.local:" # SOURCEPATH_MULTI="server.domain1.tld;server.domain2.tld:/" # SOURCEPATH_MPATH="server.domain.tld:/path1:/path2" # SOURCEPATH_LOCAL="/home/user2" # # # FAILOVER_[NAME]=0|1 # # When multiple hosts (Separated by ;) are specified for # SOURCEPATH_[NAME], this option enables failover selection of the host # instead of random. # # The first host in the list will always be used, but if the transfer # fails, the next host in the list will be used instead for the next # retry. # # # 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][;alternative backup path][...]" # # Local path to copy backups to, e.g: # # DEST_SERVER1="/backup/server1" # # If more than one path is specified, then the backup will be alternated # between the available paths based on the current date, initial random # selection and the source number (If multiple sources are defined and # being backed up), e.g: # # DEST_SERVER1="/backup/server1;/backup2/server1" # # This allows backups to be distributed between multiple disks for # data redundancy. # # # The following will be created under the destination path 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. # # # DEST_EXISTS_[NAME]=0|1 # # When using multiple destination paths for failover, this option # specifies that the destination directories already exist. In # this case, the backup will failover to the next destination in # the list if it is unable to access its first choice due to it # not being mounted or any other error. # # # DEST2_[NAME]="[secondary backup path]" # # Secondary path to synchronise the backup to after the primary backup # has completed. This allows the backup to be mirrored onto a second # drive/partition, e.g: # # DEST2_SERVER1="/backup2/server1" # # NOTE: No disk space checks are currently performed on the the # secondary path. The secondary drive/partition should # ideally have the same or greater available free space # as the primary. # # Multiple secondary destinations cannot be specified. # # # INCLUDE_[NAME]="pattern [pattern] [...]" # # Files that match the specified patterns will be included in # the backup of the specified source (rsync --include) # # E.g: important.dat Include file important.dat # # # EXCLUDE_[NAME]="pattern [pattern] [...]" # # Files that match the specified patterns will be excluded from # the backup of the specified source (rsync --exclude) # # E.g: *.tmp mydir/ Excludes files ending in .tmp and dir mydir # # # 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. # # # RETRY_DELAY_[NAME]=seconds # # Sets the minimum delay between each retry. This can be used to # allow sufficient time for open files to be closed. # # # 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.) # # # IGNORE_ERRORS_[NAME]="regexp" # # If set, rsync errors matching this regexp will be ignored, preventing # automatic retry (Unless other non-matching errors occur.) # # # 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.) # # # COMPRESS_FILES_[NAME]=0|1 # # Individually compresses each file within the archive using gzip. # This is useful for backups of large files that compress well and # are always changing (E.g: Backup of Exchange .pst's), making # hardlinks pointless. # # NOTE: When using this option, hardlinks will not be created for # the daily/weekly/monthly archive directories. # # There must be sufficient disk space to decompress each # SOURCEFILE. # # # 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. # # # SYNC_OLDEST_FIRST_[NAME]=0|1 # # If set to 1, each file will be individually syncronised in order # of oldest modification time first. After this has completed, a # normal sync will be performed to copy new files and/or delete # old files. # # # 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" # # WARN_DAYS_[NAME]=value # # Sets the time period to check for modified files in the backup # destination post-backup. If no files are changed within this # time period, a warning will be generated (Indicating possible # failed backup.) # # The default is 2 days and the warning can be optionally disabled # by setting this value to 0. # # # # 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. # Shell for spawning background processes BGSHELL="/bin/bash" # 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. # # If one or more of the listed destination paths cannot be mounted, then # sources which use that destination path will be aborted (Avoiding filling # up the local disk with the backup instead.) # MOUNT_DEST="" # Hours to try to avoid starting a new transfer (For non-local sources only.) # This helps avoid backups taking place during working hours, which may slow # down the Internet and/or WAN connectivity for users. # AVOID_BACKUP_HOURS="7 8 9 10 11 12 13 14 15 16 17 18" # 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" RSYNC_OPTIONS_DEST2="-rlptgoDv --delete --hard-links --numeric-ids" # 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 # Minimum delay time between each retry, in seconds # RSYNC_RETRY_DELAY="300" # Rsync timeout due to inactivity in seconds (0 = Use rsync default.) # RSYNC_TIMEOUT=0 # Apply failover by default to sources with multiple hosts defined, # instead of random selection? # FAILOVER_DEFAULT=0 # Create destination directory structure by default? # DEST_CREATE=1 # Select the first random path at random? # DEST_ALTERNATE_RANDOM=1 # If multiple destination paths are defined, alternate the path for each # source, based on its number in addition to the date? # DEST_ALTERNATE_BY_SOURCE=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, \$WARNINGS \$WARNINGS_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 # Send message via E-mail # # USAGE: SendMessage "Message" # function SendMessage { if [ "$2" = "ERROR" ]; then if [ "$ERRORS" = "" ]; then ERRORS=0; fi ERRORS=`expr $ERRORS + 1` if [ $ERRORS -eq 1 ]; then ERRORS_TITLE="error"; else ERRORS_TITLE="errors"; fi elif [ "$2" = "WARNING" ]; then if [ "$WARNINGS" = "" ]; then WARNINGS=0; fi WARNINGS=`expr $WARNINGS + 1` if [ $WARNINGS -eq 1 ]; then WARNINGS_TITLE="warning"; else WARNINGS_TITLE="warnings"; 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 } # Log message to log file $LOGFILE # # USAGE: LogMessage "Message" [ ] # # : GENERAL = General message # ERROR = Error message # WARNING = Warning message # : LOG = Send message to log file only # STATUS = Update status only # EMAIL = Send message via E-mail only # BOTH = Send message to log file and via E-mail # : STATUS = Update status file # NOSTATUS = Do not update # : TIMEDATE = Add time/date stamp to log file # NOTIMEDATE = Do not add time/date stamp # : SOURCE = Add name of source to console/E-mail message # NOSOURCE = Do not add name of source # : NL = Add trailing newline # NLNL = Add leading and trailing newline # : E = Interpret escape codes # function LogMessage { LM_MESSAGE="$1" LM_ISERROR="$2" LM_TARGET="$3" LM_UPDSTAT="$4" LM_TIMEDATE="$5" LM_ADDSOURCE="$6" LM_NEWLINE="$7" if [ "$8" = "E" ]; then LM_ESCAPE="-e" else LM_ESCAPE="" fi if [ "$LM_TARGET" != "STATUS" ] && [ "$LM_TARGET" != "EMAIL" ]; then if [ "$LM_NEWLINE" != "" ]; then if [ "$LM_NEWLINE" = "NL" ]; then LM_MESSAGE="$LM_MESSAGE\n" elif [ "$LM_NEWLINE" = "NLNL" ]; then LM_MESSAGE="\n$LM_MESSAGE\n" fi fi # Log to console if [ "$LM_ISERROR" = "ERROR" ] || [ "$LM_ISERROR" = "WARNING" ] || [ $ERRORS_ONLY -eq 0 ]; then if [ "$LM_ADDSOURCE" != "NOSOURCE" ] && [ "$SOURCE" != "" ]; then if [ "$LM_TIMEDATE" != "NOTIMEDATE" ]; then echo $LM_ESCAPE "`date +"%Y-%m-%d %H:%M:%S"`: $SOURCE: $LM_MESSAGE" else echo $LM_ESCAPE "$SOURCE: $LM_MESSAGE" fi else if [ "$LM_TIMEDATE" != "NOTIMEDATE" ]; then echo $LM_ESCAPE "`date +"%Y-%m-%d %H:%M:%S"`: $LM_MESSAGE" else echo $LM_ESCAPE "$LM_MESSAGE" fi fi fi # Log to log file if [ "$LOGFILE" != "" ]; then if [ "$LM_TIMEDATE" != "NOTIMEDATE" ]; then echo $LM_ESCAPE "`date +"%Y-%m-%d %H:%M:%S"`: $LM_MESSAGE" >> $LOGFILE else echo $LM_ESCAPE "$LM_MESSAGE" >> $LOGFILE fi fi LM_MESSAGE="$1" fi # Update status if [ "$LM_UPDSTAT" != "NOSTATUS" ] && [ "$TMP_NAME" != "" ] && [ "$SOURCE" != "" ]; then echo $LM_ESCAPE "`date +"%Y-%m-%d %H:%M:%S"`: $LM_MESSAGE" > $TMP_NAME.$SOURCE.status fi # Send by E-mail if [ "$LM_TARGET" = "EMAIL" ] || [ "$LM_TARGET" == "BOTH" ]; then SendMessage "$LM_MESSAGE" $LM_ISERROR fi } # Log message with trailing newline # function LogMessageNL { LogMessage "$1" $2 $3 $4 $5 $6 NL } # Log message with trailing newline and escape codes interpreted # function LogMessageNLE { LogMessage "$1" $2 $3 $4 $5 $6 NL E } # Log message with leading and trailing newline # function NLLogMessageNL { LogMessage "$1" $2 $3 $4 $5 $6 NLNL } # Remove old log files (Assumed 1 file = 1 day) # # USAGE: RemoveOldLogs # function RemoveOldLogs { if [ "$1" != "" ] && [ "$2" != "" ] && [ "$3" != "" ] && [ $3 -gt 1 ]; then if [ -e "$1" ] && [ -d "$1" ]; then pushd $1 2>&1 >/dev/null FILES="`ls -r *$2 2>&1 |grep -v \"No such file or directory\"`" FILE_COUNT=1 for FILE in $FILES; do if [ $FILE_COUNT -ge $3 ]; then if [ -e "$FILE" ]; then LogMessage "Removing old log file $FILE..." rm $1/$FILE >> $LOGFILE 2>&1 fi fi FILE_COUNT=`expr $FILE_COUNT + 1` done popd 2>&1 >/dev/null fi 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 # 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 LogMessage "Purge: $2 is being purged by another instance. Waiting for purge to complete..." 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 LogMessage "Purge: Timeout waiting for purge of $2 by another instance." ERROR 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 LogMessage "Purge: Old archive count is greater than $3, purging old archives..." NOTIFIED=1 fi LogMessage "Purge: Purging old archive $DIR..." DIR_TMP_PURGE="`echo "$DIR" |sed -e "s~/~-~g"`" WaitPurge "$TMP_NAME.$DIR_TMP_PURGE.purge" "$DIR" if [ "$PURGE_WAITED" != "1" ] && [ -e "$DIR" ]; then LogMessage "Purge: Purging old archive $DIR..." 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 BACKUPS="" # Purge oldest previous backup on over-utilised volume 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_SRC_DIR_LIST="`echo \"$PURGE_SRC_DIR\" |sed -e \"s/;/ /g\"`" for PDIR in $PURGE_SRC_DIR_LIST; do # Ensure that previous backup is on drive that is over utilised if [ -e "$PDIR" ]; then if [ -d "$PDIR" ]; then UTILISATION_PDIR="`df $PDIR |grep -v "Filesystem" |sed -e "s/[[:space:]]\+/ /g" -e "s/%//g" |cut -d' ' -f5`" if [ $UTILISATION_PDIR -gt $2 ]; then PURGE_DIR_LIST="$PURGE_DIR_LIST $PDIR" fi fi fi done done PURGE_DIR_LIST="$PURGE_DIR_LIST $MOUNT_DEST" else # Produce list of backups for current source only eval CHECK_DEST_SOURCE="\$DEST_$SOURCE" CHECK_DEST_SOURCE_LIST="`echo \"$CHECK_DEST_SOURCE\" |sed -e \"s/;/ /g\"`" for PURGE_SRC in $CHECK_DEST_SOURCE_LSIT; do eval PURGE_SRC_DIR="\$DEST_$PURGE_SRC" PURGE_SRC_DIR_LIST="`echo \"$PURGE_SRC_DIR\" |sed -e \"s/;/ /g\"`" for PDIR in $PURGE_SRC_DIR_LIST; do # Ensure that previous backup is on drive that is over utilised if [ -e "$PDIR" ]; then if [ -d "$PDIR" ]; then UTILISATION_PDIR="`df $PDIR |grep -v "Filesystem" |sed -e "s/[[:space:]]\+/ /g" -e "s/%//g" |cut -d' ' -f5`" if [ $UTILISATION_PDIR -gt $2 ]; then PURGE_DIR_LIST="$PURGE_DIR_LIST $PDIR" fi fi fi done done fi # Find directories to purge 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 if [ "$BACKUPS" = "" ]; then # Purge oldest previous backup of current source if directory list is empty 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 UTILISATION_PDIR=0 if [ -e "$DIR" ]; then if [ -d "$DIR" ]; then UTILISATION_PDIR="`df $DIR |grep -v "Filesystem" |sed -e "s/[[:space:]]\+/ /g" -e "s/%//g" |cut -d' ' -f5`" fi fi if [ $UTILISATION_PDIR -gt $2 ]; then LogMessage "Purge: Purging $DIR (${UTILISATION_PDIR}%)..." DIR_TMP_PURGE="`echo "$DIR" |sed -e "s~/~-~g"`" WaitPurge "$TMP_NAME.$DIR_TMP_PURGE.purge" "$DIR" if [ "$PURGE_WAITED" != "1" ] && [ -e "$DIR" ]; then LogMessage "Purge: Purging $DIR (${UTILISATION_PDIR}%)..." 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 fi done if [ $PURGE_COUNT -gt 0 ]; then SOURCES_PURGED=`expr $SOURCES_PURGED + 1` fi fi if [ $SOURCES_PURGED -gt 0 ]; 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 LogMessage "Delay: Waiting for $TIME_DELAY second(s)..." 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\"`" # Choose host at random 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" # Choose path at random SOURCEPATH_PATHS="`echo \"$SOURCEPATH_PATH\" |sed -e \"s/:/ /g\"`" if [ "$SOURCEPATH_PATHS" != "$SOURCEPATH_PATH" ]; then if [ "$2" = "1" ]; then RandomItem "$SOURCEPATH_PATHS" "$LAST_PATH" else RandomItem "$SOURCEPATH_PATHS" "" fi LAST_PATH="$RndItem" if [ "$RndItem" != "" ]; then RANDOMPATH="$LAST_HOST:$RndItem" else RANDOMPATH="$LAST_HOST:$SOURCEPATH_PATH" fi else RANDOMPATH="$LAST_HOST:$SOURCEPATH_PATH" fi else RANDOMPATH="" fi fi } # Return source path to host picked using failover. # # USAGE: FailoverSourcePath "" # # Result returned $RANDOMPATH # function FailoverSourcePath { SOURCEPATH_HOST="`echo \"$1\" |sed -e \"s/^\([^:]\+\):\(.*\)\$/\1/g\"`" SOURCEPATH_PATH="`echo \"$1\" |sed -e \"s/^\([^:]\+\):\(.*\)\$/\2/g\"`" # Choose path SOURCEPATH_PATHS="`echo \"$SOURCEPATH_PATH\" |sed -e \"s/:/ /g\"`" if [ "$SOURCEPATH_PATHS" != "$SOURCEPATH_PATH" ]; then if [ "$LAST_PATH" != "" ]; then PathFound=0 FIRST_PATH="" for Itm in $SOURCEPATH_PATHS; do if [ "$FIRST_PATH" = "" ]; then FIRST_PATH="$Itm" fi if [ $PathFound -eq 0 ]; then if [ "$Itm" != "$LAST_PATH" ]; then SOURCEPATH_PATH_PATH="$Itm" LAST_PATH="$Itm" PathFound=1 fi fi done if [ $PathFound -eq 0 ]; then if [ "$FIRST_PATH" != "" ]; then SOURCEPATH_PATH_PATH="$FIRST_PATH" LAST_PATH="$FIRST_PATH" else SOURCEPATH_PATH_PATH="$Itm" LAST_PATH="$Itm" fi fi else SOURCEPATH_PATH_PATH="" for Itm in $SOURCEPATH_PATHS; do if [ "$SOURCEPATH_PATH_PATH" = "" ]; then SOURCEPATH_PATH_PATH="$Itm" LAST_PATH="$Itm" fi done fi else SOURCEPATH_PATH_PATH="$SOURCEPATH_PATH" fi # Choose host RANDOMPATH="$1" if [ "$SOURCEPATH_HOST" != "" ] && [ "$SOURCEPATH_HOST" != "$SOURCEPATH" ] && [ "$SOURCEPATH_PATH" != "$SOURCEPATH" ]; then SOURCEPATH_HOST="`echo \"$SOURCEPATH_HOST\" |sed -e \"s/;/ /g\"`" if [ "$LAST_HOST" != "" ]; then HostFound=0 FIRST_HOST="" for Itm in $SOURCEPATH_HOST; do if [ "$FIRST_HOST" = "" ]; then FIRST_HOST="$Itm" fi if [ $HostFound -eq 0 ]; then if [ "$Itm" != "$LAST_HOST" ]; then RANDOMPATH="$Itm:$SOURCEPATH_PATH_PATH" LAST_HOST="$Itm" HostFound=1 fi fi done if [ $HostFound -eq 0 ]; then if [ "$FIRST_HOST" != "" ]; then RANDOMPATH="$FIRST_HOST:$SOURCEPATH_PATH_PATH" LAST_HOST="$FIRST_HOST" else RANDOMPATH="$Itm:$SOURCEPATH_PATH_PATH" LAST_HOST="$Itm" fi fi else RANDOMPATH="" for Itm in $SOURCEPATH_HOST; do if [ "$RANDOMPATH" = "" ]; then RANDOMPATH="$Itm:$SOURCEPATH_PATH_PATH" LAST_HOST="$Itm" fi done fi fi } # Choose destination path from list of alternatives. # # Parameter: 1 = Chosen destination folder must exist. # # Result returned $DEST # function AlternativeDestPath { DEST_PATHS="$DEST" FIRST_DEST="" if [ "$DEST" != "" ]; then DEST_LIST="`echo \"$DEST\" |sed -e \"s/;/ /g\"`" # Count number of defined destination paths ItmCnt=0 for Itm in $DEST_LIST; do ItmCnt=`expr $ItmCnt + 1`; done ItemCount=$ItmCnt if [ $ItemCount -gt 1 ]; then # Choose next path based on last saved number if [ "$SOURCE" != "" ] && [ -e "/tmp/.mbackuprsync.$SOURCE.next" ]; then ItmNo="`cat /tmp/.mbackuprsync.$SOURCE.next`" if [ "$ItmNo" != "" ] && [ $ItmNo -gt 0 ]; then ItmNo=$[ $ItmNo % $ItemCount ] else ItmNo="" fi else ItmNo="" fi # Choose next path based on current date and source number if [ "$ItmNo" = "" ]; then ItmNo=$[ `date +%s` / 60 / 60 / 24 ] if [ "$DEST_ALTERNATE_RANDOM" = "1" ] && [ "$DEST_ALTERNATE_RANDOMISED" = "" ]; then ItmNo=$[ $ItmNo + $RANDOM ] DEST_ALTERNATE_RANDOMISED=1 fi if [ "$SOURCE_NUMBER" != "" ] && [ "$DEST_ALTERNATE_BY_SOURCE" = "1" ]; then ItmNo=$[ $ItmNo + $SOURCE_NUMBER ] fi ItmNo=$[ $ItmNo % $ItemCount ] fi if [ "$ItmNo" = "" ]; then ItmNo=0; fi # Get selected path number from list Found=0 ItmCnt=0 Tries=$ItemCount FIRST_DEST_EXIST="" while test $Tries -gt 0; do for Itm in $DEST_LIST; do if [ $Found -eq 0 ]; then DEST="$Itm" fi if [ -e "$DEST" ] && [ -d "$DEST" ]; then if [ "$FIRST_DEST_EXIST" = "" ]; then FIRST_DEST_EXIST="$DEST" fi elif [ "$1" = "1" ]; then LogMessage "Error: Destination path $DEST does not exist or is not currently accessible." ERROR else if [ "$FIRST_DEST_EXIST" = "" ]; then FIRST_DEST_EXIST="$DEST" fi fi if [ $ItmCnt -eq $ItmNo ]; then if [ "$1" != "1" ]; then Found=1 Tries=1 elif [ -e "$DEST" ] && [ -d "$DEST" ]; then Found=1 Tries=1 fi fi ItmCnt=`expr $ItmCnt + 1` done Tries=$[ $Tries - 1 ] ItmNo=$[ $ItmNo + 1 ] if [ $ItmNo -ge $ItemCount ]; then ItmNo=0 fi done # Chosen path was not found in list. Use first path that was found if [ "$Found" != "1" ] && [ "$FIRST_DEST_EXIST" != "" ]; then DEST="$FIRST_DEST_EXIST" elif [ ! -e "$DEST" ] || [ ! -d "$DEST" ]; then DEST="$FIRST_DEST_EXIST" fi fi fi if [ "$DEST" = "" ]; then LogMessage "Error: No valid destination paths exist or none are currently accessible from the following specified list: $DEST_LIST" ERROR DEST="NO_VALID_DESTS" fi } # Wait if hour is within AVOID_BACKUP_HOURS list # function AvoidBackupHours { if [ $MANUAL_RUN -ne 1 ]; then HOUR="`date +%k`" Notified=0 for HR in $AVOID_BACKUP_HOURS; do if [ $HR -eq $HOUR ]; then if [ $Notified -eq 0 ]; then LogMessage "Delay: Avoiding backup during hours: $AVOID_BACKUP_HOURS" Notified=1 fi while [ $HR -eq $HOUR ]; do sleep 1m HOUR="`date +%k`" done fi done fi } # Recurse through directories and sync files individually by modification date (Oldest first) function RecurseDirOldest { OLDFILE="$FILE" if [ -d "$1" ]; then # Get file list from remote host if [ "$RANDOMPATH_HOST" != "$RANDOMPATH" ] && [ "$RANDOMPATH_PATH" != "$RANDOMPATH" ]; then if [ "$SOURCE_SSH_PORT" != "" ]; then REMOTE_FILE_LIST="`ssh -p $SOURCE_SSH_PORT $RANDOMPATH_HOST ls $RANDOMPATH_PATH$1 2>&1 |grep -v "No such file or directory" |sed -e "s/ /_.-/g"`" else REMOTE_FILE_LIST="`ssh $RANDOMPATH_HOST ls $RANDOMPATH_PATH$1 2>&1 |grep -v "No such file or directory" |sed -e "s/ /_.-/g"`" fi else REMOTE_FILE_LIST="" fi # Sync each file in directory for FILE in `ls "$1" -t -r |sed -e "s/ /_.-/g"`; do File="`echo $FILE |sed -e "s/_\.-/ /g"`" if [ -d "$1/$File" ]; then # Recurse into subdirectory RecurseDirOldest "$1/$File" else # Check file exists in remote file list if [ "$REMOTE_FILE_LIST" != "" ]; then FOUND=0 for RFILE in $REMOTE_FILE_LIST; do RFile="`echo $RFILE |sed -e "s/_\.-/ /g"`" if [ "$File" = "$RFILE" ]; then FOUND=1 fi done; else FOUND=1 fi if [ $FOUND -eq 1 ]; then if [ "$SOURCE_SSH_PORT" != "" ]; then if [ $SOURCE_INCREMENTAL_BACKUPS -eq 1 ]; then rsync $SOURCE_RSYNC_OPTIONS$SOURCE_BANDWIDTH$RSYNC_INCLUDE$RSYNC_EXCLUDE$IO_TIMEOUT --rsh="ssh -p $SOURCE_SSH_PORT" "$RANDOMPATH$1/$File" "$DEST/incremental/m0-w0-d1/$1/$File" |grep -v "No such file or directory" |grep -v "some files/attrs were not transferred" >> $TMP_NAME.$SOURCE.rsync 2>&1 else rsync $SOURCE_RSYNC_OPTIONS$SOURCE_BANDWIDTH$RSYNC_INCLUDE$RSYNC_EXCLUDE$IO_TIMEOUT --rsh="ssh -p $SOURCE_SSH_PORT" --backup --backup-dir=$DEST/previous/$BACKUP_DATE "$RANDOMPATH$1/$File" "$DEST/current/$1/$File" |grep -v "No such file or directory" |grep -v "some files/attrs were not transferred" >> $TMP_NAME.$SOURCE.rsync 2>&1 fi else if [ $SOURCE_INCREMENTAL_BACKUPS -eq 1 ]; then rsync $SOURCE_RSYNC_OPTIONS$SOURCE_BANDWIDTH$RSYNC_INCLUDE$RSYNC_EXCLUDE$IO_TIMEOUT "$RANDOMPATH$1/$File" "$DEST/incremental/m0-w0-d1/$1/$File" |grep -v "No such file or directory" |grep -v "some files/attrs were not transferred" >> $TMP_NAME.$SOURCE.rsync 2>&1 else rsync $SOURCE_RSYNC_OPTIONS$SOURCE_BANDWIDTH$RSYNC_INCLUDE$RSYNC_EXCLUDE$IO_TIMEOUT --backup --backup-dir=$DEST/previous/$BACKUP_DATE "$RANDOMPATH$1/$File" "$DEST/current/$1/$File" |grep -v "No such file or directory" |grep -v "some files/attrs were not transferred" >> $TMP_NAME.$SOURCE.rsync 2>&1 fi fi fi fi done fi FILE="$OLDFILE" } # 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 if [ "$6" != "" ]; then SOURCE_NUMBER=$6; fi elif [ "$1" = "run" ] || [ "$1" = "RUN" ] || [ "$1" = "info" ] || [ "$1" = "INFO" ] || [ "$1" = "status" ] || [ "$1" = "STATUS" ]; then # Manually run specified sources or show info RUN_SOURCES="`echo \"$2\" |sed -e \"s/,/ /g\"`" MANUAL_RUN=1 if [ "$1" = "status" ] || [ "$1" = "STATUS" ]; then SHOW_STATUS=1 SHOW_INFO=1 elif [ "$1" = "info" ] || [ "$1" = "INFO" ]; then SHOW_STATUS=0 SHOW_INFO=1 else SHOW_INFO=0 SHOW_INFO=0 fi if [ "$RUN_SOURCES" = "" ]; then if [ "$SHOW_INFO" = "1" ]; then if [ "$SHOW_STATUS" = "1" ]; then echo -e "\nError: Please specify the name(s) of sources to show status for (Or ALL for all sources.)\n" else echo -e "\nError: Please specify the name(s) of sources to show information for (Or ALL for all sources.)\n" fi else echo -e "\nError: Please specify the name(s) of sources to run (Or ALL for all sources.)\n" fi echo -e "Available sources: $SOURCES\n" exit 1 fi if [ "$RUN_SOURCES" = "all" ] || [ "$RUN_SOURCES" = "ALL" ]; then RUN_ALL=1 else SOURCES="$RUN_SOURCES" fi elif [ "$1" = "sources" ] || [ "$1" = "SOURCES" ] || [ "$1" = "source" ] || [ "$1" = "SOURCE" ]; then # Show available sources echo "$SOURCES" exit 1 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 [ "$SHOW_INFO" != "1" ] && [ "$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) if [ "$SHOW_INFO" != "1" ]; then 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 fi # 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` START_TIME="`date`" BACKUP_DATE="`date +%Y-%m-%d`" BACKUP_SECS="`date +%s`" eval SOURCEPATH="\$SOURCEPATH_$SOURCE" eval FAILOVER="\$FAILOVER_$SOURCE" if [ "$FAILOVER" = "" ]; then FAILOVER=$FAILOVER_DEFAULT fi eval SOURCEFILES="\$SOURCEFILES_$SOURCE" eval MOUNT="\$MOUNT_$SOURCE" eval MOUNTCMD="\$MOUNTCMD_$SOURCE" eval UMOUNTCMD="\$UMOUNT_$SOURCE" eval MOUNTSPEC="\$MOUNTSPEC_$SOURCE" eval DEST="\$DEST_$SOURCE" eval DEST_EXISTS="\$DEST_EXISTS_$SOURCE" AlternativeDestPath $DEST_EXISTS eval DEST2="\$DEST2_$SOURCE" eval SOURCE_EXCLUDE="\$EXCLUDE_$SOURCE" eval SOURCE_INCLUDE="\$INCLUDE_$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_RETRY_DELAY="\$RETRY_DELAY_$SOURCE" eval SOURCE_IGNORE_ERRORS="\$IGNORE_ERRORS_$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_SYNC_OLDEST_FIRST="\$SYNC_OLDEST_FIRST_$SOURCE" eval SOURCE_DEST_CREATE="\$DEST_CREATE_$SOURCE" eval SOURCE_COMPRESS_FILES="\$COMPRESS_FILES_$SOURCE" eval SOURCE_WARN_DAYS="\$WARN_DAYS_$SOURCE" if [ "$SOURCE_WARN_DAYS" = "" ]; then SOURCE_WARN_DAYS=2 fi DECOMPRESS_TIME=0 RSYNC_TIME=0 COMPRESS_TIME=0 ERRORS=0 ERRORS_TITLE="errors" WARNINGS=0 WARNINGS_TITLE="warnings" MISC=0 MISC_TITLE="miscellaneous" # 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 # Inclusions list RSYNC_INCLUDE="" INCLUDE_MODIFIER="" if [ "$SOURCE_INCLUDE" != "" ]; then for INCLUDE in $SOURCE_INCLUDE; do if [ "$INCLUDE" != "" ]; then if [ "$INCLUDE" = "+" ]; then INCLUDE_MODIFIER="+" elif [ "$EXCLUDE" = "-" ]; then INCLUDE_MODIFIER="-" else if [ "$INCLUDE_MODIFIER" = "" ]; then RSYNC_INCLUDE="$RSYNC_INCLUDE --include=$INCLUDE" else RSYNC_INCLUDE="$RSYNC_INCLUDE --include='$INCLUDE_MODIFIER $INCLUDE'" fi INCLUDE_MODIFIER="" fi fi done 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 [ "$SHOW_INFO" != "1" ] && [ "$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 LogMessage "Delay: Waiting until $ORIGINAL_TIME before starting backup..." else sleep 1m WAIT_TIME=`expr $WAIT_TIME + 1` if [ $WAIT_TIME -gt 1500 ]; then NLLogMessageNL "Error: Start time $ORIGINAL_TIME not reached within 25 hours. Backup aborted." ERROR BOTH 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 [ "$SHOW_INFO" != "1" ] && [ $MANUAL_RUN -ne 1 ]; then sleep `expr $RANDOM % 60`s fi # Check for hung process, or killed process leaving lock file behind if [ "$SHOW_INFO" != "1" ] && [ -e "$TMP_NAME.$SOURCE.lock" ]; then LOCK="`cat $TMP_NAME.$SOURCE.lock`" if [ -e "$TMP_NAME.$SOURCE.pid" ]; then _PID="`cat $TMP_NAME.$SOURCE.pid`" if [ "$_PID" != "" ]; then _P_PID="`ps -p ${_PID} |grep ${_PID}`" if [ "$_P_PID" != "" ]; then _P_PID="`ps -p ${_PID} |grep ${_PID} |grep $0`" if [ "$LOCK" = "" ]; then LOCK="0" fi NOW="`date +%s`" if [ `expr $NOW - $LOCK` -ge `expr $LOCK_TIME \* 60 \* 60` ]; then LogMessage "Warning: Active lock file exists and has expired - Killing process ${_PID}." WARNING BOTH kill $_PID _P_PID="`ps l -p ${_PID} |grep ${_PID} |grep $0`" if [ "$_P_PID" != "" ]; then kill -9 $_PID _P_PID="`ps l -p ${_PID} |grep ${_PID} |grep $0`" if [ "$_P_PID" != "" ]; then kill -11 $_PID fi fi # Check process has gone _P_PID="`ps l -p ${_PID} |grep ${_PID} |grep $0`" if [ "$_P_PID" != "" ]; then LogMessage "Error: Process is still present after attempt to kill ${_PID}: ${_P_PID}" ERROR BOTH BACKUP_SOURCE=0 else rm -f $TMP_NAME.$SOURCE.* fi fi else LogMessage "Error: Lock file exists, but no process with PID ${_PID} was found. Removing lock file." ERROR BOTH rm -f $TMP_NAME.$SOURCE.* fi else LogMessage "Error: Lock file exists, but PID is blank. Removing lock file." ERROR BOTH rm -f $TMP_NAME.$SOURCE.* fi else LogMessage "Error: Lock file exists, but there is no PID file. Removing lock file." ERROR BOTH rm -f $TMP_NAME.$SOURCE.* fi fi # Check lock file for active process if [ "$SHOW_INFO" != "1" ] && [ -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 LogMessage "Warning: Active lock file exists - Backup of source skipped." WARNING BOTH NOSTATUS BACKUP_SOURCE=0 fi fi fi # Check that appropriate destination archive directory is mounted if [ "$SHOW_INFO" != "1" ]; then for DIR in $MOUNT_DEST; do if [ "$DIR" != "" ]; then MOUNTED="`mount |grep \" $DIR \"`" if [ "$MOUNTED" = "" ]; then mount $DIR > /dev/null 2>&1 MOUNTED="`mount |grep \" $DIR \"`" if [ "$MOUNTED" = "" ]; then MATCH_DEST="`echo "$DEST" |grep "^\`echo "$DIR/" |sed -e "s~/\+\$~/~g"\`"`" if [ "$MATCH_DEST" != "" ]; then LogMessageNL "Error: Destination archive directory '$DIR' is not mounted. Backup aborted." ERROR BOTH BACKUP_SOURCE=0 fi fi fi fi done fi # Show information if [ "$SHOW_INFO" == "1" ]; then if [ "$SHOW_STATUS" != "1" ]; then if [ $Processed -gt 1 ]; then echo "" fi echo "Source #${Processed}: $SOURCE" echo "" fi if [ -e "$TMP_NAME.$SOURCE.lock" ]; then if [ "$SHOW_STATUS" != "1" ]; then echo "Status: In progress." if [ -e "/tmp/$SOURCE.start" ]; then RUNTIME="`cat /tmp/$SOURCE.start`" if [ "$RUNTIME" != "" ] && [ $RUNTIME -gt 0 ]; then RUNTIME="$[ `date +%s` - $RUNTIME ]" if [ "$RUNTIME" != "" ] && [ $RUNTIME -gt 0 ]; then echo "Run-time: $[ $RUNTIME / 86400 ] days(s), $[ ( $RUNTIME % 86400 ) / 3600 ] hour(s), $[ ($RUNTIME % 86400 % 3600) / 60 ] minute(s)" fi fi fi echo "Lock file: $TMP_NAME.$SOURCE.lock" TIMESTAMP="`cat $TMP_NAME.$SOURCE.lock`" echo "Last refresh: `date -d @$TIMESTAMP`" if [ -e "$TMP_NAME.$SOURCE.pid" ]; then echo "PID: `cat $TMP_NAME.$SOURCE.pid`" fi if [ -e "$TMP_NAME.$SOURCE.status" ]; then echo "" echo "Last message: `cat $TMP_NAME.$SOURCE.status`" fi if [ -e "$TMP_NAME.$SOURCE.rsync" ]; then echo -e "\nRsync progress:\n" tail -n 10 $TMP_NAME.$SOURCE.rsync fi else if [ -e "$TMP_NAME.$SOURCE.status" ]; then if [ -e "/tmp/$SOURCE.start" ]; then RUNTIME="`cat /tmp/$SOURCE.start`" if [ "$RUNTIME" != "" ] && [ $RUNTIME -gt 0 ]; then RUNTIME="$[ `date +%s` - $RUNTIME ]" if [ "$RUNTIME" != "" ] && [ $RUNTIME -gt 0 ]; then echo "$SOURCE: Running ($[ $RUNTIME / 86400 ]d $[ ( $RUNTIME % 86400 ) / 3600 ]h $[ ($RUNTIME % 86400 % 3600) / 60 ]m):" else echo "$SOURCE: Running:" fi else echo "$SOURCE: Running:" fi else echo "$SOURCE: Running:" fi echo "`cat $TMP_NAME.$SOURCE.status`" if [ -e "$TMP_NAME.$SOURCE.rsync" ]; then tail -n 1 $TMP_NAME.$SOURCE.rsync fi else echo "$SOURCE: In progress." fi fi else if [ "$SHOW_STATUS" != "1" ]; then echo "Status: Not running." if [ -e "$TMP_NAME.$SOURCE.status" ]; then echo "" echo "Last message: `cat $TMP_NAME.$SOURCE.status`" fi else if [ -e "$TMP_NAME.$SOURCE.status" ]; then echo "$SOURCE: Not running: `cat $TMP_NAME.$SOURCE.status`" else echo "$SOURCE: Not running." fi fi fi if [ "$SHOW_STATUS" != "1" ]; then echo "" echo "Local source: $SOURCE_LOCAL" echo "Manual run only: $SOURCE_MANUAL" echo "Backup time: $SOURCE_TIME" echo "Backup days: $SOURCE_DAYS" echo "" if [ "$FAILOVER" = "1" ]; then echo "Source path(s): $SOURCEPATH (Failover enabled)" else echo "Source path(s): $SOURCEPATH" fi echo "Source files: $SOURCEFILES" if [ "$DEST_PATHS" != "" ] && [ "$DEST" != "$DEST_PATHS" ]; then echo "Destinations: $DEST_PATHS" echo "Current dest: $DEST" else echo "Destination: $DEST" fi if [ "$DEST2" != "" ]; then echo "Alt. dest: $DEST2" fi echo "" if [ "$MOUNT" != "" ]; then echo "Mount source: $MOUNT" fi echo "Create dest: $SOURCE_DEST_CREATE" if [ "$SOURCE_RECURSE_DIRS" != "" ]; then echo "Recurse dirs: $SOURCE_RECURSE_DIRS" fi echo "Sync oldest first: $SOURCE_SYNC_OLDEST_FIRST" if [ "$SOURCE_EXCLUDE" != "" ]; then echo "Exclusions: $SOURCE_EXCLUDE" fi if [ "$SOURCE_INCLUDE" != "" ]; then echo "Inclusions: $SOURCE_INCLUDE" fi if [ "$SOURCE_IGNORE_ERRORS" != "" ]; then echo "Ignore errors: $SOURCE_IGNORE_ERRORS" fi echo "Max. utilisation: ${SOURCE_UTILISATION}%" echo "" echo "Compress: $SOURCE_COMPRESS" if [ "$SOURCE_BANDWIDTH" != "" ]; then echo "Bandwidth limit: $SOURCE_BANDWIDTH" fi if [ "$SOURCE_TIMEOUT" != "" ]; then echo "Timeout: $SOURCE_TIMEOUT" fi echo "Multiplex: $SOURCE_MULTIPLEX" if [ "$SOURCE_DELAY" != "" ]; then echo "Delay: $SOURCE_DELAY" fi echo "" echo "Keep archives: $SOURCE_KEEP" echo "Incremental: $SOURCE_INCREMENTAL_BACKUPS" echo "" if [ "$SOURCE_INCREMENTAL_BACKUPS" = "1" ]; then echo "Daily archives: $SOURCE_INCREMENTAL_DAILY" echo "Weekly archives: $SOURCE_INCREMENTAL_WEEKLY" echo "Monthly archives: $SOURCE_INCREMENTAL_MONTHLY" echo "" if [ -e "$DEST/incremental/.last.daily" ]; then TIMESTAMP="`cat $DEST/incremental/.last.daily`" echo "Last daily: `date -d @$TIMESTAMP`" else echo "Last daily: Never" fi if [ -e "$DEST/incremental/.last.weekly" ]; then TIMESTAMP="`cat $DEST/incremental/.last.weekly`" echo "Last weekly: `date -d @$TIMESTAMP`" else echo "Last weekly: Never" fi if [ -e "$DEST/incremental/.last.monthly" ]; then TIMESTAMP="`cat $DEST/incremental/.last.monthly`" echo "Last monthly: `date -d @$TIMESTAMP`" else echo "Last monthly: Never" fi echo "" fi if [ "$SOURCE_RSYNC_OPTIONS" != "" ] || [ "$SOURCE_SSH_PORT" != "" ] || [ "$SOURCE_PRE_EXEC" != "" ] || [ "$SOURCE_POST_EXEC" != "" ]; then echo "Rsync options: $SOURCE_RSYNC_OPTIONS" if [ "$SOURCE_SSH_PORT" != "" ]; then echo "Alternative SSH port: $SOURCE_SSH_PORT" fi if [ "$SOURCE_PRE_EXEC" != "" ]; then echo "Pre exec: $SOURCE_PRE_EXEC" fi if [ "$SOURCE_POST_EXEC" != "" ]; then echo "Post exec: $SOURCE_POST_EXEC" fi echo "" fi else echo "" fi BACKUP_SOURCE=0 fi if [ $BACKUP_SOURCE -ne 1 ] || [ "$SHOW_INFO" = "1" ]; then # Don't backup this source BACKUP_SOURCE=0 elif [ $MULTIPLEX -ne 1 ] && [ $SOURCE_MULTIPLEX -eq 1 ]; then # Multiplex this source LogMessage "Initiating multiplexed backup..." $BGSHELL $0 multiplex "$SOURCE" "$ERRORS_ONLY" "$SHOW_SUMMARY" "$SHOW_ERRORS" "$Processed" & else # Backup this source if [ "$SOURCEFILES" = "" ]; then LogMessageNL "Error: No source files/directories (SOURCEFILES_$SOURCE=) specified for source $SOURCE in '$SOURCEDEFS' (Source doesn't exist?) Valid sources: $ORIGINAL_SOURCES" ERROR BOTH elif [ "$DEST" = "" ]; then LogMessageNL "Error: No backup destination path (DEST_$SOURCE=) specified for source $SOURCE in '$SOURCEDEFS'." ERROR BOTH elif [ "$DEST" != "NO_VALID_DESTS" ]; then # Create lock file echo "`date +%s`" > $TMP_NAME.$SOURCE.lock echo "$$" > $TMP_NAME.$SOURCE.pid # Randomise source host in source path if [ "$FAILOVER" != "1" ]; then RandomSourcePath "$SOURCEPATH" 0 else LAST_HOST="" LAST_PATH="" FailoverSourcePath "$SOURCEPATH" fi # 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 LogMessage "Error: Unable to create destination directory '$DIRS' for source $SOURCE." ERROR DirError=1 fi fi done # Create secondary destination directories DIR_LIST="" if [ "$SOURCE_DEST_CREATE" = "1" ] && [ "$DEST2" != "" ]; then DEST2_DIRS="" DEST2_PATH="" for DIRS in `echo "$DEST2" |sed -e "s/[[:space:]]\+/_.-/g" |sed -e "s~/~ ~g"`; do DIRNAME="`echo \"$DIRS\" |sed -e \"s/_.-/ /g\"`" DEST2_PATH="$DEST2_PATH/$DIRNAME" DEST2_DIRS="$DEST2_DIRS $DEST2_PATH" done; DIR_LIST="$DEST2_DIRS $DIR_LIST" fi for DIRS in $DIR_LIST; do if [ ! -e "$DIRS" ]; then mkdir $DIRS if [ ! -e "$DIRS" ]; then LogMessage "Error: Unable to create secondary destination directory '$DIRS' for source $SOURCE." ERROR DirError=1 fi fi done # Update lock file echo "`date +%s`" > $TMP_NAME.$SOURCE.lock if [ $DirError -eq 0 ]; then # 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 "$LOGERRORS" ]; then rm $LOGERRORS; fi if [ -e "$LOGSUMMARY" ]; then rm $LOGSUMMARY; fi TOTAL_FILE_COUNT=0 TOTAL_DIR_COUNT=0 CHANGED_FILE_COUNT=0 CHANGED_DIR_COUNT=0 MODIFIED_FILE_COUNT=0 # Log start time START_TIME="`date`" if [ $MULTIPLEX -eq 1 ]; then LogMessageNL "Multiplexed backup of '$SOURCEFILES' from $RANDOMPATH ($SOURCE) to $DEST started at $START_TIME..." else LogMessageNL "Backup of '$SOURCEFILES' from $RANDOMPATH ($SOURCE) to $DEST started at $START_TIME..." fi echo "`date +%s`" >/tmp/$SOURCE.start # 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 # Execute pre command(s) if [ "$SOURCE_PRE_EXEC" != "" ]; then LogMessage "Executing pre-source command(s): $SOURCE_PRE_EXEC" PrePostExec "$RANDOMPATH" "$SOURCE_PRE_EXEC" fi # Update lock file echo "`date +%s`" > $TMP_NAME.$SOURCE.lock # 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 # 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 LogMessage "Mounting $MOUNT..." 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$MOUNTSPEC 2>&1 |grep -v "No such file or directory"`" if [ "$MOUNTED_VOLUME" = "" ] || [ "$MOUNTED_FILES" = "" ]; then LogMessage "Error: Unable to mount $MOUNT or mounted volume contains no files/directories." ERROR BOTH IsMounted=0 fi fi if [ $IsMounted -eq 1 ]; then # Backup source files SOURCECOUNT=1 SOURCELIST="" for FILE in $SOURCEFILES; do if [ "$FILE" != "" ]; then LAST_FILE="$FILE" fi done for FILE in $SOURCEFILES; do CURRENT_FILE="$FILE" 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 LogMessage "Error: Unknown file type '$FILE_TYPE' - Please specify either dir:$FILE_NAME or file:$FILE_NAME" ERROR 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 [ $RetryCount -gt 1 ]; then LogMessage "(Retry `expr $RSYNC_RETRIES - $RetryCount + 1` of $RSYNC_RETRIES) Unable to backup $FILE: $MOUNTED_FILES_SSH" GENERAL else LogMessage "Error: (Retry `expr $RSYNC_RETRIES - $RetryCount + 1` of $RSYNC_RETRIES) Unable to backup $FILE: $MOUNTED_FILES_SSH" ERROR fi IsMounted=0 elif [ "$MOUNTED_FILES" = "" ]; then LogMessage "Error: $RANDOMPATH_PATH$FILE contains no files/directories." ERROR if [ "$FAILOVER" != "1" ]; then RetryCount=1 fi IsMounted=0 fi fi if [ $IsMounted -eq 0 ]; then # Randomise or failover source host in source path if [ "$FAILOVER" != "1" ]; then RANDOMPATH_LAST="$RANDOMPATH" RandomSourcePath "$SOURCEPATH" 1 else FailoverSourcePath "$SOURCEPATH" fi fi RetryCount=`expr $RetryCount - 1` done; fi if [ $IsMounted -eq 1 ]; then if [ $SOURCECOUNT -gt 1 ]; then NLLogMessageNL "Backing up $FILE via $RANDOMPATH on `date`..." else LogMessageNL "Backing up $FILE via $RANDOMPATH on `date`..." fi SOURCE_START="`date`" # 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 $BGSHELL -c "while [ -e $TMP_NAME.$SOURCE.updatelock ]; do echo \"\`date +%s\`\" > $TMP_NAME.$SOURCE.lock; sleep 1s; done" & # 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 or copy previous backup files into new current backup directory if [ -e "$DEST/incremental/m0-w0-d2" ]; then if [ "$SOURCE_COMPRESS_FILES" = "1" ]; then # Copy files (No hardlinks) cp -a $DEST/incremental/m0-w0-d2/* $DEST/incremental/m0-w0-d1 >>$LOGERRORS 2>&1 else # Hard-link files cp -al $DEST/incremental/m0-w0-d2/* $DEST/incremental/m0-w0-d1 >>$LOGERRORS 2>&1 fi fi fi # 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 # Uncompress files RSYNC_RETRY=1 if [ $SOURCE_INCREMENTAL_BACKUPS -eq 1 ] && [ "$SOURCE_COMPRESS_FILES" = "1" ] && [ -e "$DEST/incremental/m0-w0-d1/$FILE" ]; then LogMessageNL "Uncompressing files: $FILE" UNZIP_ERRORS="`gunzip -r $DEST/incremental/m0-w0-d1/$FILE 2>&1`" if [ "$UNZIP_ERRORS" != "" ]; then LogMessage "Error: Unable to uncompress files: $FILE" ERROR echo "$UNZIP_ERRORS" >> $LOGERRORS RSYNC_RETRY=0 UNZIP_ERRORS="" fi fi # Copy file(s) via rsync Delay $SOURCE_DELAY AvoidBackupHours LogMessageNL "Initiating rsync transfer: $FILE" TIME_START="`date +%s`" RSYNC_RETRY_COUNT=1 while [ $RSYNC_RETRY -eq 1 ]; do # Delay next retry time? if [ "$SOURCE_RETRY_DELAY" != "" ]; then NEXT_RETRY="$[ `date +%s` + $SOURCE_RETRY_DELAY ]" else NEXT_RETRY="`date +%s`" fi # Individually sync oldest files first? if [ "$SOURCE_SYNC_OLDEST_FIRST" != "" ]; then pushd $DEST/incremental/m0-w0-d1 >/dev/null SFILE="`echo \"$FILE\" |sed \"s~^/~~g\"`" RecurseDirOldest $SFILE popd >/dev/null fi # Synchronise 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_INCLUDE$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_INCLUDE$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_INCLUDE$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_INCLUDE$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 [ "$SOURCE_IGNORE_ERRORS" != "" ]; then RETRY_ERRORS="`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 |grep -v -e \"$SOURCE_IGNORE_ERRORS\" |grep -v \"some files could not be transferred\"`" else RETRY_ERRORS="`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`" fi if [ "$RETRY_ERRORS" != "" ]; then if [ $RSYNC_RETRY_COUNT -le $RSYNC_RETRIES ]; then if [ "$FAILOVER" != "1" ]; then RandomSourcePath "$SOURCEPATH" 1 else FailoverSourcePath "$SOURCEPATH" fi LogMessage "Retry: $RSYNC_RETRY_COUNT of $RSYNC_RETRIES - Restarting transfer of $FILE from $SOURCE via $RANDOMPATH at `date`..." RSYNC_RETRY=1 sleep $RSYNC_RETRY_DELAY if [ "$NEXT_RETRY" != "" ]; then DELAY_NOTIFIED=0 while [ `date +%s` -lt $NEXT_RETRY ]; do if [ "$DELAY_NOTIFIED" != "1" ]; then LogMessage "Retry: $RSYNC_RETRY_COUNT of $RSYNC_RETRIES - Next retry delayed until `date --date="@$NEXT_RETRY"`..." DELAY_NOTIFIED=1 fi sleep 1m done fi # Purge previous backups before retry in case of insufficient disk space if [ $SOURCE_INCREMENTAL_BACKUPS -eq 1 ]; then PurgeBackups "$DEST/incremental" $SOURCE_UTILISATION 0 else PurgeBackups "$DEST/previous" $SOURCE_UTILISATION $SOURCE_KEEP fi AvoidBackupHours else LogMessage "Retry: $RSYNC_RETRY_COUNT of $RSYNC_RETRIES - Maximum number of retries exceeded for $FILE from $SOURCE at `date`." ERROR fi fi if [ -e "$TMP_NAME.$SOURCE.rsync" ]; then rm $TMP_NAME.$SOURCE.rsync fi RSYNC_RETRY_COUNT=`expr $RSYNC_RETRY_COUNT + 1` RETRY_ERRORS="" done # Warn if there are no files in backup modified in last 48 hours CHECK_DEST_FILE="`echo \"$FILE\" |sed -e \"s~^.*/~~g\"`" if [ $SOURCE_INCREMENTAL_BACKUPS -eq 1 ]; then CHECK_DEST_DIR="$DEST/incremental/m0-w0-d1/$CHECK_DEST_FILE" else CHECK_DEST_DIR="$DEST/current/$CHECK_DEST_FILE" fi if [ -e "$CHECK_DEST_DIR" ]; then # Total files/dirs DEST_FILE_COUNT="`find $CHECK_DEST_DIR/. -type f |wc -l`" DEST_DIR_COUNT="`find $CHECK_DEST_DIR/. -type d |grep -v \"^.\$\" |wc -l`" TOTAL_FILE_COUNT=$[ $DEST_FILE_COUNT + $TOTAL_FILE_COUNT ] TOTAL_DIR_COUNT=$[ $DEST_DIR_COUNT + $TOTAL_DIR_COUNT ] # Recently changed files/dirs DEST_FILE_COUNT="`find $CHECK_DEST_DIR/. -type f -ctime -$SOURCE_WARN_DAYS |wc -l`" DEST_DIR_COUNT="`find $CHECK_DEST_DIR/. -type d -ctime -$SOURCE_WARN_DAYS |grep -v \"^.\$\" |wc -l`" CHANGED_FILE_COUNT=$[ $DEST_FILE_COUNT + $CHANGED_FILE_COUNT ] CHANGED_DIR_COUNT=$[ $DEST_DIR_COUNT + $CHANGED_DIR_COUNT ] # Recently modified files DEST_FILE_COUNT="`find $CHECK_DEST_DIR/. -type f -mtime -$SOURCE_WARN_DAYS |wc -l`" MODIFIED_FILE_COUNT=$[ $DEST_FILE_COUNT + $MODIFIED_FILE_COUNT ] fi # Compress files if [ $SOURCE_INCREMENTAL_BACKUPS -eq 1 ] && [ "$SOURCE_COMPRESS_FILES" = "1" ] && [ -e "$DEST/incremental/m0-w0-d1/$FILE" ]; then LogMessageNL "Compressing files: $FILE" ZIP_ERRORS="`gzip -r --rsyncable $DEST/incremental/m0-w0-d1/$FILE 2>&1 |grep -v \"Too many levels of symbolic links\"`" if [ "$ZIP_ERRORS" != "" ]; then LogMessage "Error: Unable to compress files: $FILE" ERROR echo "$ZIP_ERRORS" >> $LOGERRORS ZIP_ERRORS="" fi fi # Synchronise to secondary location if [ "$DEST2" != "" ] && [ "$CURRENT_FILE" = "$LAST_FILE" ]; then LogMessageNL "Synchronising to secondary archive $DEST2 on `date`..." rsync $RSYNC_OPTIONS_DEST2 "$DEST/" "$DEST2/" > $TMP_NAME.$SOURCE.rsync 2>&1 # Errors occured? 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 LogMessage "Error: Errors occured synchronising to secondary destination: $DEST2" ERROR fi if [ -e "$TMP_NAME.$SOURCE.rsync" ]; then rm $TMP_NAME.$SOURCE.rsync fi fi # Log end time TIME_END="`date +%s`" RSYNC_TIME=`expr $RSYNC_TIME + \( $TIME_END - $TIME_START \)` if [ -e "/tmp/$SOURCE.start" ]; then rm /tmp/$SOURCE.start; fi 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 # Remove lock and PID files 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` LogMessageNL "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 # Randomise source host in source path if [ "$FAILOVER" != "1" ]; then RandomSourcePath "$SOURCEPATH" 0 fi 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 LogMessage "Compressing $DEST/previous/$BACKUP_DATE..." 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 LogMessage "Executing post-source command(s): $SOURCE_POST_EXEC" 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 # Remove PID file if [ -e "$TMP_NAME.$SOURCE.pid" ]; then rm $TMP_NAME.$SOURCE.pid >/dev/null 2>&1 fi # Remove lock fie update process file if [ -e "$TMP_NAME.$SOURCE.updatelock" ]; then rm $TMP_NAME.$SOURCE.updatelock >/dev/null 2>&1 fi # Remove status file if [ -e "$TMP_NAME.$SOURCE.status" ]; then rm $TMP_NAME.$SOURCE.status >/dev/null 2>&1 fi fi fi fi # Warn if no files changed/modified if [ "$MODIFIED_FILE_COUNT" = "" ] || [ "$MODIFIED_FILE_COUNT" = "0" ]; then LogMessage "Warning: No files have been modified in the past $SOURCE_WARN_DAYS day(s)." WARNING fi # Remove lock file if it still exists (Backup skipped) if [ -e "$TMP_NAME.$SOURCE.lock" ]; then rm $TMP_NAME.$SOURCE.lock >/dev/null 2>&1 fi # Remove PID file if [ -e "$TMP_NAME.$SOURCE.pid" ]; then rm $TMP_NAME.$SOURCE.pid >/dev/null 2>&1 fi # Remove lock fie update process file if [ -e "$TMP_NAME.$SOURCE.updatelock" ]; then rm $TMP_NAME.$SOURCE.updatelock >/dev/null 2>&1 fi # Remove status file if [ -e "$TMP_NAME.$SOURCE.status" ]; then rm $TMP_NAME.$SOURCE.status >/dev/null 2>&1 fi # Remove start time file if [ -e "$TMP_NAME.$SOURCE.start" ]; then rm $TMP_NAME.$SOURCE.start >/dev/null 2>&1 fi # Update next number if [ "$SOURCE" != "" ] && [ -e "/tmp/.mbackuprsync.$SOURCE.next" ]; then ItmNo="`cat /tmp/.mbackuprsync.$SOURCE.next`" else ItmNo="" fi if [ "$ItmNo" = "" ]; then ItmNo="1"; fi echo "$[ $ItmNo + 1 ]" > /tmp/.mbackuprsync.$SOURCE.next # Log finish time END_TIME="`date`" LogMessage "Summary for backup of '$SOURCEFILES' from $RANDOMPATH ($SOURCE) to $DEST..." GENERAL LOG NOSTATUS NOTIMEDATE NOSOURCE NLNL E # Summary: Script start time/date LogMessage "Start time: \t$START_TIME" GENERAL LOG NOSTATUS NOTIMEDATE NOSOURCE NL E # Summary: Time spent rsync'ing files LogMessage "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" GENERAL LOG NOSTATUS NOTIMEDATE NOSOURCE "" E # 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 LogMessage "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" GENERAL LOG NOSTATUS NOTIMEDATE NOSOURCE "" E # Summary: Total overall time LogMessage "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" GENERAL LOG NOSTATUS NOTIMEDATE NOSOURCE NL E # Summary: Finish time/date LogMessage "Finish time:\t$END_TIME" GENERAL LOG NOSTATUS NOTIMEDATE NOSOURCE "" E # Generate summary/errors if [ -e "$LOGFILE" ]; then # Get errors/warnings grep -E -i "((bunzip2?|bzip2?|error|retry|gunzip|gzip|mv|rm|rsync|tar|warning):|failed\\sto\\sopen|file\\shas\\svanished|permission\\sdenied|send_files\\sfailed|unable\\sto\\sbackup|timed\\sout)" $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 echo -e "File count: \t$TOTAL_FILE_COUNT file(s)\t$TOTAL_DIR_COUNT dir(s)" >> $LOGSUMMARY echo -e "Updated: \t$CHANGED_FILE_COUNT file(s)\t$CHANGED_DIR_COUNT dir(s)\t" >> $LOGSUMMARY echo -e "Modified: \t$MODIFIED_FILE_COUNT file(s)" >> $LOGSUMMARY echo -e "Time period: \t$SOURCE_WARN_DAYS day(s)" >> $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 "\n\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 # Errors/warnings/others count if [ -e "$LOGERRORS" ]; then ERRORS_GREP="(warning:|retry:|file\\shas\\svanished)" WARNINGS_GREP="warning:" MISC_GREP="(retry:|file\\shas\\svanished)" ERRORS="`cat $LOGERRORS |grep -v -E -i \"$ERRORS_GREP\" |wc -l |cut -d' ' -f1`" WARNINGS="`cat $LOGERRORS |grep -E -i \"$WARNINGS_GREP\" |wc -l |cut -d' ' -f1`" MISC="`cat $LOGERRORS |grep -E -i \"$MISC_GREP\" |wc -l |cut -d' ' -f1`" if [ "$ERRORS" = "" ]; then ERRORS=0; fi if [ "$WARNINGS" = "" ]; then WARNINGS=0; fi if [ "$MISC" = "" ]; then MISC=0; fi if [ $ERRORS -eq 1 ]; then ERRORS_TITLE="error"; else ERRORS_TITLE="errors"; fi if [ $WARNINGS -eq 1 ]; then WARNINGS_TITLE="warning"; else WARNINGS_TITLE="warnings"; fi if [ $MISC -eq 1 ]; then MISC_TITLE="miscellaneous"; else MISC_TITLE="miscellaneous"; fi fi # Show errors/warnings summary? if [ $SHOW_ERRORS -eq 1 ] && [ -e "$LOGERRORS" ] && [ `wc -l $LOGERRORS |cut -d' ' -f1` -gt 0 ]; then # Errors if [ $ERRORS_ONLY -eq 0 ]; then if [ $ERRORS -gt 0 ]; then echo -e "\nErrors:" echo "~~~~~~~" cat $LOGERRORS |grep -v -E -i "$ERRORS_GREP" echo "" fi else echo -e "$SOURCE errors/warnings:\n" cat $LOGERRORS fi # Warnings if [ $ERRORS_ONLY -eq 0 ] && [ $WARNINGS -gt 0 ]; then echo -e "\nWarnings:" echo "~~~~~~~~~" cat $LOGERRORS |grep -E -i "$WARNINGS_GREP" echo "" fi # Others if [ $ERRORS_ONLY -eq 0 ] && [ $MISC -gt 0 ]; then echo -e "\nMiscellaneous:" echo "~~~~~~~~~~~~~~" cat $LOGERRORS |grep -E -i "$MISC_GREP" echo "" fi else if [ $SHOW_ERRORS -eq 1 ]; then echo ""; fi fi # E-mail summary and/or errors/warnings? 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 [ $ERRORS -gt 0 ]; then if [ $NEWLINE -eq 1 ]; then echo "" >> $TMP_EMAIL; fi echo -e "\nErrors:" >> $TMP_EMAIL echo "~~~~~~~" >> $TMP_EMAIL cat $LOGERRORS |grep -v -E -i "$ERRORS_GREP" >> $TMP_EMAIL NEWLINE=1 fi if [ $WARNINGS -gt 0 ]; then if [ $NEWLINE -eq 1 ]; then echo "" >> $TMP_EMAIL; fi echo -e "\nWarnings:" >> $TMP_EMAIL echo "~~~~~~~~~" >> $TMP_EMAIL cat $LOGERRORS |grep -E -i "$WARNINGS_GREP" >> $TMP_EMAIL NEWLINE=1 fi if [ $MISC -gt 0 ]; then if [ $NEWLINE -eq 1 ]; then echo "" >> $TMP_EMAIL; fi echo -e "\nMiscellaneous:" >> $TMP_EMAIL echo "~~~~~~~~~~~~~~" >> $TMP_EMAIL cat $LOGERRORS |grep -E -i "$MISC_GREP" >> $TMP_EMAIL NEWLINE=1 fi 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