#!/bin/bash # # Eloy backup system. # # Copyright (c) 2007-2008 Maximilian Antoni. All rights reserved. # # This software is licensed as described in the file LICENSE.txt, which you # should have received as part of this distribution. The terms are also # available at http://svn.evaserver.com/eloy/trunk/LICENSE.txt. # # the Eloy version number: VERSION="0.3.4-SNAPSHOT" # exit values: E_NO_ARGS=65 E_NOFILE=66 LINE="----------------------------------------------------------------------------" # the Eloy config file name: CONFIG_FILE="eloy.cf" # the default number of backups for the list subcommand: DEFAULT_LIST_COUNT=5 # the prefix used by the out function. This is used to prefix plugin # output with the name of the plugin. OUT_PREFIX="" # prints the version number. version() { echo "Eloy backup system, version $VERSION." } # prints general usage. usage() { version NAME=`basename $0` echo "Usage: $NAME [path/to/eloy.cf] Type '$NAME help ' for help on a specific subcommand. Available subcommands: backup compress diff (d) help (?, h) info (i) list (l) restore (r) Project homepage: http://evaserver.com/projects/eloy/ " } # prints usage for the "info" subcommand. usageInfo() { version echo "info (i): print information about the Eloy setup. usage: info [path/to/eloy.cf] " } # prints usage for the "backup" subcommand. usageBackup() { version echo "backup: create a new backup. usage: backup [path/to/eloy.cf] Creates an initial backup, if no current backup is available. Otherwise a partial backup is created automatically. It is recommended to setup a cron job to create at least one backup per day. If you would like to backup your system every hour, you should use the compress subcommand to reduce the hourly backups after 24 hours. Valid options: -l : write output to logfile " } # prints usage for the "list" subcommand. usageList() { version echo "list (l): list backups. usage: list [path/to/eloy.cf] Lists all files that have changed in the backup history. Valid options: -c count : list backups (defaults to $DEFAULT_LIST_COUNT) -f filename : filter by " } # prints usage for the "restore" subcommand. usageRestore() { version echo "restore (r): restore a file from the backup. usage: restore [path/to/eloy.cf] Restores the file with in the current directory with the last change that is available in the backup. Use the diff command to see the changes made between the current version and the backup. Valid options: -b backup : restore the file from this backup " } # prints usage for the "diff" subcommand. usageDiff() { version echo "diff (d): compare a file with a backup. usage: diff [path/to/eloy.cf] Compares the file with in the current directory with the last change that is available in the backup. Valid options: -b backup : compare the file with this backup " } # prints usage for the "compress" subcommand. usageCompress() { version echo "compress: delete older backups. usage: compress [path/to/eloy.cf] Deletes all but one backup of each day. If COMPRESS_KEEP_BACKUPS is not zero, Eloy also deletes old backups if there are more than COMPRESS_KEEP_BACKUPS backups, todays backups not included. It is recommended to use this command in a cron job which is executed every day in the hour before midnight. That way, all backups of at least the last 24 hours are kept and older backups are reduced to one per day. Valid options: -l : write output to logfile -m recipient : email logfile to recipient and archive logfile " } out() { if [ "$WRITE_LOG" == "yes" ]; then if [ "$1" != "" ]; then echo "[$(date +%Y-%m-%d\ %H:%M:%S)] $OUT_PREFIX$1" >> "$LOG_FILENAME" fi else echo $OUT_PREFIX$1 fi } # prints an error message stating that a property is missing in the # configuration and exits. missingConfigVariable() { echo "Missing $1 in \"$CONFIG_FILE\"." echo exit 1 } # prints an error message station that a directory does not exist # and exits. directoryDoesNotExist() { echo "Directory \"$1\" does not exist." echo exit 1 } # searches for the configuration file. findConfigFile() { if [ -z $1 ]; then CONFIG_FILE="$HOME/.eloy" if [ ! -f "$CONFIG_FILE" ]; then THIS_DIR=`dirname $0` CONFIG_FILE="$THIS_DIR/eloy.cf" if [ ! -f "$CONFIG_FILE" ]; then echo "Cannot find eloy.cf" exit $E_NOFILE fi fi else if [ -f "$1" ]; then CONFIG_FILE=$1 else echo "\"$1\" does not exist." exit $E_NOFILE fi fi } # searches for the configuration file and includes it. readConfig() { findConfigFile $1 # Read configuration: . "$CONFIG_FILE" # Check configuration: if [ -z "$SOURCE_DIR" ]; then missingConfigVariable "SOURCE_DIR" fi for DIR in $SOURCE_DIR do if [ ! -d "$DIR" ]; then directoryDoesNotExist "$DIR" fi done if [ -z "$TARGET_DIR" ]; then missingConfigVariable "TARGET_DIR" fi if [ ! -d "$TARGET_DIR" ]; then directoryDoesNotExist "$TARGET_DIR" fi if [ -z "$COMPRESS_KEEP_BACKUPS" ]; then missingConfigVariable "COMPRESS_KEEP_BACKUPS" fi if [ -z "$LOG_FILENAME" ]; then missingConfigVariable "LOG_FILENAME" fi if [ -z "$PLUGINS" ]; then missingConfigVariable "PLUGINS" fi } # prints out information about the Eloy configuration and state. performInfo() { version echo findConfigFile $1 echo "Config file: \"$CONFIG_FILE\"" readConfig $1 echo "Backup target: $TARGET_DIR" echo "Backup source: $SOURCE_DIR" if [ -z "$EXCLUDES" ]; then echo "Excludes: no excludes defined" else echo "Excludes: $EXCLUDES" fi echo "Keep backups: $COMPRESS_KEEP_BACKUPS" echo "Logfile: $LOG_FILENAME" echo } # performs the actual backup operation. performBackup() { TIME_ALL=$(date +%s) out "" out "Eloy backup - $(date)" out $LINE CURRENT="$TARGET_DIR/current" BACKUP_DIR="$TARGET_DIR/$(date +%y-%m-%d-%H%M%S)" THIS_DIR=`dirname $0` for PLUGIN in $PLUGINS do out "Running plugin \"$PLUGIN\"." OUT_PREFIX="> $PLUGIN: " . "$THIS_DIR/eloy_$PLUGIN.sh" OUT_PREFIX="" if [ $? -ne 0 ]; then out "Plugin \"$PLUGIN\" FAILED" fi out $LINE done if [ -d "$CURRENT" ]; then out "Creating PARTIAL backup in \"$BACKUP_DIR\"." else out "Creating INITIAL backup in \"$BACKUP_DIR\"." fi out $LINE for SOURCE in $SOURCE_DIR do out "$SOURCE" ARGS="" if [ ! -z "$EXCLUDES" ]; then for EXCLUDE in $EXCLUDES do if [[ "$EXCLUDE" == "$SOURCE"* ]]; then ARGS="$ARGS --exclude=/`basename $SOURCE`${EXCLUDE:${#SOURCE}}" fi done fi BACKUP_PATH="$BACKUP_DIR.inProgress$SOURCE/.." if [ "$WRITE_LOG" == "yes" ]; then mkdir -p $BACKUP_PATH >> "$LOG_FILENAME" else mkdir -p $BACKUP_PATH fi if [ $? -ne 0 ]; then out $LINE out "Backup FAILED" out $LINE out "" exit 1 fi if [ -d "$CURRENT" ]; then # Partial backup: CURRENT_PATH="$CURRENT$SOURCE/.." if [ "$WRITE_LOG" == "yes" ]; then rsync -a $ARGS --link-dest=$CURRENT_PATH $SOURCE $BACKUP_PATH >> "$LOG_FILENAME" else rsync -a $ARGS --link-dest=$CURRENT_PATH $SOURCE $BACKUP_PATH fi else # Initial backup: if [ "$WRITE_LOG" == "yes" ]; then rsync -a $ARGS $SOURCE $BACKUP_PATH >> "$LOG_FILENAME" else rsync -a $ARGS $SOURCE $BACKUP_PATH fi fi if [ $? -ne 0 ]; then out $LINE out "Backup FAILED" out $LINE rm -rf "$BACKUP_DIR.inProgress" out "" exit 1 fi done # Remove "current" soft link if available: if [ -e "$CURRENT" ]; then rm "$CURRENT" fi # Rename inProgress directory to final name: if [ "$WRITE_LOG" == "yes" ]; then mv "$BACKUP_DIR.inProgress" "$BACKUP_DIR" >> "$LOG_FILENAME" else mv "$BACKUP_DIR.inProgress" "$BACKUP_DIR" fi if [ $? -ne 0 ]; then out $LINE out "Backup FAILED" out $LINE out "" exit 1 fi # Create new "current" soft link: ln -s "$BACKUP_DIR" "$CURRENT" TIME_ALL=`expr $(date +%s) - $TIME_ALL` out $LINE out "Backup finished: $TIME_ALL seconds" out $LINE out "" } findInArray() { for X in $1 do if [ "$X" == "$2" ]; then return 1 fi done return 0 } # ensures, that the current directory is part of the backup sources. validateCurrentDir() { CURRENT_DIR=`pwd` for SOURCE in $SOURCE_DIR do if [[ "$CURRENT_DIR" == "$SOURCE"* ]]; then FOUND=1 break fi done if [ ! $FOUND ]; then echo " \"$CURRENT_DIR\" is not in the backup sources. " exit 1 fi } # requires COUNT, FILENAME and TARGET_DIR performList() { validateCurrentDir BACKUP_DIRS=`cd "$TARGET_DIR"; ls -r` BACKUP_COUNT=0 BACKUPS=0 LATEST_BACKUP="" HASH=( ) DIRS=( ) for BACKUP_DIR in $BACKUP_DIRS do if [ "$BACKUP_DIR" == "current" ]; then continue fi CURRENT_BACKUP_DIR="$TARGET_DIR/$BACKUP_DIR$CURRENT_DIR" if [ ! -d "$CURRENT_BACKUP_DIR" ]; then continue fi if [ "$LATEST_BACKUP" == "" ]; then LATEST_BACKUP=$BACKUP_DIR fi let "BACKUPS+=1" FILE_COUNT=0 FILES=$(cd "$CURRENT_BACKUP_DIR"; ls -A1 | grep "$FILENAME") FILES=${FILES// /\/} # escape spaces with slashes for FILE in $FILES do FILE=${FILE//\// } # undo escaping if [ -d "$CURRENT_BACKUP_DIR/$FILE" ]; then if [ -d "$CURRENT_DIR/$FILE" ]; then continue fi findInArray $DIRS $FILE if [ $? -eq 1 ]; then continue fi DIRS[${#DIRS[*]}]=$FILE else cmp "$CURRENT_BACKUP_DIR/$FILE" "$CURRENT_DIR/$FILE" &> /dev/null if [ $? -eq 0 ]; then continue fi INDEX=`ls -i "$CURRENT_BACKUP_DIR/$FILE" | awk '{ print $1 }'` if [ "${HASH[$INDEX]}" == "$FILE" ]; then continue fi HASH[$INDEX]=$FILE fi if [ $FILE_COUNT -eq 0 ]; then echo echo $LINE echo "$BACKUP_DIR" echo $LINE fi INFO=`cd $CURRENT_BACKUP_DIR; ls -Alh | grep -E " ([0-9]{2}:[0-9]{2}|[0-9]{4}) $FILE( -> .*)?$"` echo " ${INFO}" let "FILE_COUNT+=1" done if [ $FILE_COUNT -ne 0 ]; then let "BACKUP_COUNT+=1" if [ $BACKUP_COUNT -eq $COUNT ]; then echo return fi fi done if [ $BACKUP_COUNT -eq 0 ]; then echo if [ $BACKUPS -eq 0 ]; then echo " No backups available for \"$CURRENT_DIR\"." else echo " Latest backup: $LATEST_BACKUP" echo " No changes found." fi fi echo } lookupBackup() { if [ -z $LOOKUP_BACKUP ]; then BACKUP_DIRS=`cd "$TARGET_DIR"; ls -r` for BACKUP_DIR in $BACKUP_DIRS do if [ "$BACKUP_DIR" == "current" ]; then continue fi CURRENT_BACKUP_DIR="$TARGET_DIR/$BACKUP_DIR$CURRENT_DIR" if [ ! -d "$CURRENT_BACKUP_DIR" ]; then continue fi FILE="$CURRENT_BACKUP_DIR/$LOOKUP_FILE" if [ ! -e "$FILE" ]; then continue fi if [ -d "$FILE" ]; then LOOKUP_BACKUP=$BACKUP_DIR break else cmp "$FILE" "$CURRENT_DIR/$LOOKUP_FILE" &> /dev/null if [ $? -ne 0 ]; then LOOKUP_BACKUP=$BACKUP_DIR break fi fi done if [ -z $LOOKUP_BACKUP ]; then echo " No backup found for \"$LOOKUP_FILE\"." echo exit 1 fi else if [ ! -d "$TARGET_DIR/$LOOKUP_BACKUP" ]; then echo " Backup \"$LOOKUP_BACKUP\" does not exist." echo exit 1 fi fi } performRestore() { validateCurrentDir echo lookupBackup echo "Restoring \"$LOOKUP_FILE\" from backup \"$LOOKUP_BACKUP\"" echo $LINE BACKUP_DIR="$TARGET_DIR/$LOOKUP_BACKUP" FILE="$BACKUP_DIR$CURRENT_DIR/$LOOKUP_FILE" if [ -d "$FILE" ]; then cp -p -R -v "$FILE" "$CURRENT_DIR" else cp -p -v "$FILE" "$CURRENT_DIR" fi echo exit 0 } performDiff() { validateCurrentDir echo if [ ! -e "$CURRENT_DIR/$LOOKUP_FILE" ]; then echo " \"$LOOKUP_FILE\" does not exist." echo exit 1 fi lookupBackup echo "Comparing \"$LOOKUP_FILE\" with backup \"$LOOKUP_BACKUP\"" echo $LINE FILE="$TARGET_DIR/$LOOKUP_BACKUP$CURRENT_DIR/$LOOKUP_FILE" if [ -d "$FILE" ]; then echo echo " \"$LOOKUP_FILE\" is a directory." echo else diff "$CURRENT_DIR/$LOOKUP_FILE" "$FILE" fi echo exit 0 } performCompress() { out "" out "Compressing backups in $TARGET_DIR" out $LINE DID_SOMETHING=0 COUNT=0 BACKUP_DIRS=`cd "$TARGET_DIR"; ls` CURRENT_BACKUP_DATE="" TODAY_BACKUP_DATE=$(date +%y-%m-%d) LS_OUT=$(ls -l "$TARGET_DIR/current") CURRENT_BACKUP_DIR=${LS_OUT#*-> } for BACKUP_DIR in $BACKUP_DIRS do # Ignore the symlink to the current backup: if [ "$BACKUP_DIR" == "current" ]; then continue fi # Ignore everything that is not a directory: if [ ! -d "$TARGET_DIR/$BACKUP_DIR" ]; then continue fi # Make sure the current backup is not touched: if [ "$TARGET_DIR/$BACKUP_DIR" == "$CURRENT_BACKUP_DIR" ]; then continue fi # Check whether the backup was created today: BACKUP_DATE=${BACKUP_DIR:0:8} if [ "$BACKUP_DATE" == "$TODAY_BACKUP_DATE" ]; then continue fi if [ "$BACKUP_DATE" == "$CURRENT_BACKUP_DATE" ]; then out " Removing $BACKUP_DIR" DID_SOMETHING=1 rm -rf "$TARGET_DIR/$BACKUP_DIR" continue fi let "COUNT+=1" CURRENT_BACKUP_DATE=$BACKUP_DATE done if [ $COMPRESS_KEEP_BACKUPS -ne 0 -a $COUNT -gt $COMPRESS_KEEP_BACKUPS ]; then BACKUP_DIRS=`cd "$TARGET_DIR"; ls` for BACKUP_DIR in $BACKUP_DIRS do # Ignore the symlink to the current backup: if [ "$BACKUP_DIR" == "current" ]; then continue fi # Ignore everything that is not a directory: if [ ! -d "$TARGET_DIR/$BACKUP_DIR" ]; then continue fi # Make sure the current backup is not touched: if [ "$TARGET_DIR/$BACKUP_DIR" == "$CURRENT_BACKUP_DIR" ]; then continue fi # Check whether the backup was created today: BACKUP_DATE=${BACKUP_DIR:0:8} if [ "$BACKUP_DATE" == "$TODAY_BACKUP_DATE" ]; then continue fi out " Removing $BACKUP_DIR" DID_SOMETHING=1 rm -rf "$TARGET_DIR/$BACKUP_DIR" let "COUNT-=1" if [ $COUNT -eq $COMPRESS_KEEP_BACKUPS ]; then break fi done fi if [ $DID_SOMETHING == 0 ]; then out " Nothing to do." fi out "" if [ ! -z "$MAILTO" ]; then cat "$LOG_FILENAME" | mail -s "Eloy backup log" $MAILTO if [ $? -ne 0 ]; then out "Error emailing logfile \"$LOG_FILENAME\" to $MAILTO." else mv "$LOG_FILENAME" "$LOG_FILENAME.$(date +%y%m%d%H%M%S)" fi fi exit 0 } # Check arguments: if [ -z $1 ]; then NAME=`basename $0` echo "Type '$NAME help' for usage." exit $E_NO_ARGS fi case "$1" in "help" | "h" | "?" ) if [ -z $2 ]; then usage else case "$2" in "info" | "i" ) usageInfo ;; "backup" ) usageBackup ;; "list" | "l" ) usageList ;; "restore" | "r" ) usageRestore ;; "diff" | "d" ) usageDiff ;; "compress" ) usageCompress ;; * ) usage ;; esac fi ;; "info" | "i" ) performInfo $2 ;; "backup" ) WRITE_LOG="no" while [ $# -ge 2 ]; do case "$2" in "-l" ) WRITE_LOG="yes" shift ;; * ) break ;; esac done readConfig $2 performBackup ;; "list" | "l" ) COUNT=$DEFAULT_LIST_COUNT FILENAME="" while [ $# -ge 2 ]; do case "$2" in "-c" ) COUNT=$3 shift 2 ;; "-f" ) FILENAME=$3 shift 2 ;; * ) break ;; esac done readConfig $2 performList ;; "diff" | "d" ) while [ $# -ge 2 ]; do case "$2" in "-b" ) LOOKUP_BACKUP=$3 shift 2 ;; * ) if [ -z $LOOKUP_FILE ]; then LOOKUP_FILE=$2 shift else break fi ;; esac done if [ -z $LOOKUP_FILE ]; then usageDiff exit 1 fi readConfig $2 performDiff ;; "restore" | "r" ) while [ $# -ge 2 ]; do case "$2" in "-b" ) LOOKUP_BACKUP=$3 shift 2 ;; * ) if [ -z $LOOKUP_FILE ]; then LOOKUP_FILE=$2 shift else break fi ;; esac done if [ -z $LOOKUP_FILE ]; then usageRestore exit 1 fi readConfig $2 performRestore ;; "compress" ) WRITE_LOG="no" while [ $# -ge 2 ]; do case "$2" in "-l" ) WRITE_LOG="yes" shift ;; "-m" ) MAILTO=$3 shift 2 ;; * ) break ;; esac done readConfig $2 performCompress ;; * ) usage exit 1 ;; esac exit 0