2 Commits

Author SHA1 Message Date
Laurent Cozic cca0c1ac56 Removed test code 2013-11-15 15:34:17 +08:00
Laurent Cozic 78003f6b94 Support for Mac OS X 2013-11-15 14:59:59 +08:00
2 changed files with 105 additions and 70 deletions
+2 -8
View File
@@ -1,6 +1,6 @@
# Rsync time backup # Rsync time backup
Time Machine style backup with rsync. Should work on Linux (tested on some distros), Mac OS X (fully tested) and Windows with Cygwin (not tested yet but feeback would be welcome). Time Machine style backup with rsync. Should work on Linux, Mac OS X and Windows with Cygwin.
# Installation # Installation
@@ -36,18 +36,12 @@ An optional exclude file can be provided as a third parameter. It should be comp
* Exclude file - support for pattern-based exclusion via the `--exclude-from` rsync parameter. * Exclude file - support for pattern-based exclusion via the `--exclude-from` rsync parameter.
* Automatically purge old backups - within 24 hours, all backups are kept. Within one month, the most recent backup for each day is kept. For all previous backups, the most recent of each month is kept. * The application is one bash script that can be easily edited.
* "latest" symlink that points to the latest successful backup.
* The application is just one bash script that can be easily edited.
# TODO # TODO
* Minor changes (see TODO comments in the source). * Minor changes (see TODO comments in the source).
* Backup to remote drive?
# LICENSE # LICENSE
[MIT](http://opensource.org/licenses/MIT) [MIT](http://opensource.org/licenses/MIT)
+103 -62
View File
@@ -1,21 +1,27 @@
#!/usr/bin/env bash #!/usr/bin/env bash
APPNAME=$(basename $0 | sed "s/\.sh$//")
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Log functions # Log functions
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
fn_log_info() { echo "$APPNAME: $1"; } fn_log_info() {
fn_log_warn() { echo "$APPNAME: [WARNING] $1"; } echo "rsync_tmbackup: $1"
fn_log_error() { echo "$APPNAME: [ERROR] $1"; } }
fn_log_warn() {
echo "rsync_tmbackup: [WARNING] $1"
}
fn_log_error() {
echo "rsync_tmbackup: [ERROR] $1"
}
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Make sure everything really stops when CTRL+C is pressed # Make sure everything really stops when CTRL+C is pressed
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
fn_terminate_script() { fn_terminate_script() {
fn_log_info "SIGINT caught." echo "rsync_tmbackup: SIGINT caught."
exit 1 exit 1
} }
@@ -34,13 +40,13 @@ fn_parse_date() {
} }
fn_find_backups() { fn_find_backups() {
find "$DEST_FOLDER" -type d -name "????-??-??-??????" -prune | sort -r find "$DEST_FOLDER" -type d -name "????-??-??-??????" -prune
} }
fn_expire_backup() { fn_expire_backup() {
# Double-check that we're on a backup destination to be completely # Double-check that we're on a backup destination to be completely
# sure we're deleting the right folder # sure we're deleting the right folder
if [ -z "$(fn_find_backup_marker "$(dirname -- "$1")")" ]; then if [ -z "$(fn_is_backup_destination "$(dirname -- "$1")")" ]; then
fn_log_error "$1 is not on a backup destination - aborting." fn_log_error "$1 is not on a backup destination - aborting."
exit 1 exit 1
fi fi
@@ -53,12 +59,12 @@ fn_expire_backup() {
# Source and destination information # Source and destination information
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
SRC_FOLDER="${1%/}" SRC_FOLDER=${1%/}
DEST_FOLDER="${2%/}" DEST_FOLDER=${2%/}
EXCLUSION_FILE="$3" EXCLUSION_FILE=$3
for ARG in "$SRC_FOLDER" "$DEST_FOLDER" "$EXCLUSION_FILE"; do for arg in "$SRC_FOLDER" "$DEST_FOLDER" "$EXCLUSION_FILE"; do
if [[ "$ARG" == *"'"* ]]; then if [[ "$arg" == *"'"* ]]; then
fn_log_error 'Arguments may not have any single quote characters.' fn_log_error 'Arguments may not have any single quote characters.'
exit 1 exit 1
fi fi
@@ -70,14 +76,19 @@ done
# TODO: check that the destination supports hard links # TODO: check that the destination supports hard links
fn_backup_marker_path() { echo "$1/backup.marker"; } fn_backup_marker_path() {
fn_find_backup_marker() { find "$(fn_backup_marker_path "$1")" 2>/dev/null; } echo "$1/backup.marker"
}
if [ -z "$(fn_find_backup_marker "$DEST_FOLDER")" ]; then fn_is_backup_destination() {
find "$(fn_backup_marker_path "$1")" 2>/dev/null
}
if [ -z "$(fn_is_backup_destination $DEST_FOLDER)" ]; then
fn_log_info "Safety check failed - the destination does not appear to be a backup folder or drive (marker file not found)." fn_log_info "Safety check failed - the destination does not appear to be a backup folder or drive (marker file not found)."
fn_log_info "If it is indeed a backup folder, you may add the marker file by running the following command:" fn_log_info "If it is indeed a backup folder, you may add the marker file by running the following command:"
fn_log_info "" fn_log_info ""
fn_log_info "touch \"$(fn_backup_marker_path "$DEST_FOLDER")\"" fn_log_info "touch \"$(fn_backup_marker_path $DEST_FOLDER)\""
fn_log_info "" fn_log_info ""
exit 1 exit 1
fi fi
@@ -86,17 +97,24 @@ fi
# Setup additional variables # Setup additional variables
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Date logic
NOW=$(date +"%Y-%m-%d-%H%M%S")
EPOCH=$(date "+%s")
KEEP_ALL_DATE=$(($EPOCH - 86400)) # 1 day ago
KEEP_DAILIES_DATE=$(($EPOCH - 2678400)) # 31 days ago
export IFS=$'\n' # Better for handling spaces in filenames. export IFS=$'\n' # Better for handling spaces in filenames.
PROFILE_FOLDER="$HOME/.$APPNAME" NOW=$(date +"%Y-%m-%d-%H%M%S")
DEST="$DEST_FOLDER/$NOW" PROFILE_FOLDER="$HOME/.rsync_tmbackup"
PREVIOUS_DEST="$(fn_find_backups | head -n 1)" LOG_FILE="$PROFILE_FOLDER/$NOW.log"
INPROGRESS_FILE="$DEST_FOLDER/backup.inprogress" DEST=$DEST_FOLDER/$NOW
PREVIOUS_DEST=$(fn_find_backups | sort | tail -n 1)
INPROGRESS_FILE=$DEST_FOLDER/backup.inprogress
case "$OSTYPE" in
linux*)
KEEP_ALL_DATE=$(date -d '-1 day' +%s)
KEEP_DAILIES_DATE=$(date -d '-1 month' +%s)
;;
darwin*)
KEEP_ALL_DATE=$(date -j -f "%a %b %d %T %Z %Y" "`date -v -1d`" "+%s")
KEEP_DAILIES_DATE=$(date -j -f "%a %b %d %T %Z %Y" "`date -v -1m`" "+%s")
;;
esac
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Create profile folder if it doesn't exist # Create profile folder if it doesn't exist
@@ -112,13 +130,15 @@ fi
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
if [ -f "$INPROGRESS_FILE" ]; then if [ -f "$INPROGRESS_FILE" ]; then
if [ -n "$PREVIOUS_DEST" ]; then if [ "$PREVIOUS_DEST" != "" ]; then
# - Last backup is moved to current backup folder so that it can be resumed. # - Last backup is moved to current backup folder so that it can be resumed.
# - 2nd to last backup becomes last backup. # - 2nd to last backup becomes last backup.
fn_log_info "$INPROGRESS_FILE already exists - the previous backup failed or was interrupted. Backup will resume from there." fn_log_info "$INPROGRESS_FILE already exists - the previous backup failed or was interrupted. Backup will resume from there."
LINE_COUNT=$(fn_find_backups | sort | tail -n 2 | wc -l)
mv -- "$PREVIOUS_DEST" "$DEST" mv -- "$PREVIOUS_DEST" "$DEST"
if [ "$(fn_find_backups | wc -l)" -gt 1 ]; then if [ "$LINE_COUNT" -gt 1 ]; then
PREVIOUS_DEST="$(fn_find_backups | sed -n '2p')" PREVIOUS_PREVIOUS_DEST=$(fn_find_backups | sort | tail -n 2 | head -n 1)
PREVIOUS_DEST=$PREVIOUS_PREVIOUS_DEST
else else
PREVIOUS_DEST="" PREVIOUS_DEST=""
fi fi
@@ -126,21 +146,21 @@ if [ -f "$INPROGRESS_FILE" ]; then
fi fi
# Run in a loop to handle the "No space left on device" logic. # Run in a loop to handle the "No space left on device" logic.
while : ; do while [ "1" ]; do
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Check if we are doing an incremental backup (if previous backup exists). # Check if we are doing an incremental backup (if previous backup exists) or not
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
LINK_DEST_OPTION="" LINK_DEST_OPTION=""
if [ -z "$PREVIOUS_DEST" ]; then if [ "$PREVIOUS_DEST" == "" ]; then
fn_log_info "No previous backup - creating new one." fn_log_info "No previous backup - creating new one."
else else
# If the path is relative, it needs to be relative to the destination. To keep # If the path is relative, it needs to be relative to the destination. To keep
# it simple, just use an absolute path. See http://serverfault.com/a/210058/118679 # it simple, just use an absolute path. See http://serverfault.com/a/210058/118679
PREVIOUS_DEST="$(cd "$PREVIOUS_DEST"; pwd)" PREVIOUS_DEST=`cd \`dirname -- "$PREVIOUS_DEST"\`; pwd`"/"`basename -- "$PREVIOUS_DEST"`
fn_log_info "Previous backup found - doing incremental backup from $PREVIOUS_DEST" fn_log_info "Previous backup found - doing incremental backup from $PREVIOUS_DEST"
LINK_DEST_OPTION="--link-dest='$PREVIOUS_DEST'" LINK_DEST_OPTION="--link-dest=$PREVIOUS_DEST"
fi fi
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -156,29 +176,36 @@ while : ; do
# Purge certain old backups before beginning new backup. # Purge certain old backups before beginning new backup.
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Default value for $PREV ensures that the most recent backup is never deleted. # Default value for $prev ensures that the most recent backup is never deleted.
PREV="0000-00-00-000000" prev="0000-00-00-000000"
for FILENAME in $(fn_find_backups | sort -r); do for fname in $(fn_find_backups | sort -r); do
BACKUP_DATE=$(basename "$FILENAME") date=$(basename "$fname")
TIMESTAMP=$(fn_parse_date $BACKUP_DATE) stamp=$(fn_parse_date $date)
# Skip if failed to parse date... # Skip if failed to parse date...
if [ -z "$TIMESTAMP" ]; then # TODO: display warning
fn_log_warn "Could not parse date: $FILENAME" [ -n "$stamp" ] || continue
continue
fi
if [ $TIMESTAMP -ge $KEEP_ALL_DATE ]; then if [ $stamp -ge $KEEP_ALL_DATE ]; then
true true
elif [ $TIMESTAMP -ge $KEEP_DAILIES_DATE ]; then
elif [ $stamp -ge $KEEP_DAILIES_DATE ]; then
# Delete all but the most recent of each day. # Delete all but the most recent of each day.
[ "${BACKUP_DATE:0:10}" == "${PREV:0:10}" ] && fn_expire_backup "$FILENAME" if [ ${date:0:7} == ${prev:0:7} ]; then
[ ${date:8:2} -eq ${prev:8:2} ] && fn_expire_backup "$fname"
fi
else else
# Delete all but the most recent of each month. # Delete all but the most recent of each month.
[ "${BACKUP_DATE:0:7}" == "${PREV:0:7}" ] && fn_expire_backup "$FILENAME" if [ ${date:0:4} == ${prev:0:4} ]; then
[ ${date:5:2} -eq ${prev:5:2} ] && fn_expire_backup "$fname"
fi
fi fi
PREV=$BACKUP_DATE prev=$date
done done
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -196,12 +223,14 @@ while : ; do
CMD="$CMD --numeric-ids" CMD="$CMD --numeric-ids"
CMD="$CMD --links" CMD="$CMD --links"
CMD="$CMD --hard-links" CMD="$CMD --hard-links"
CMD="$CMD --delete"
CMD="$CMD --delete-excluded"
CMD="$CMD --one-file-system" CMD="$CMD --one-file-system"
CMD="$CMD --archive" CMD="$CMD --archive"
CMD="$CMD --itemize-changes" CMD="$CMD --itemize-changes"
CMD="$CMD --verbose" CMD="$CMD --verbose"
CMD="$CMD --log-file '$LOG_FILE'" CMD="$CMD --log-file '$LOG_FILE'"
if [ -n "$EXCLUSION_FILE" ]; then if [ "$EXCLUSION_FILE" != "" ]; then
# We've already checked that $EXCLUSION_FILE doesn't contain a single quote # We've already checked that $EXCLUSION_FILE doesn't contain a single quote
CMD="$CMD --exclude-from '$EXCLUSION_FILE'" CMD="$CMD --exclude-from '$EXCLUSION_FILE'"
fi fi
@@ -221,19 +250,38 @@ while : ; do
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# TODO: find better way to check for out of space condition without parsing log. # TODO: find better way to check for out of space condition without parsing log.
NO_SPACE_LEFT="$(grep "No space left on device (28)\|Result too large (34)" "$LOG_FILE")" grep --quiet "No space left on device (28)" "$LOG_FILE"
NO_SPACE_LEFT="$?"
if [ "$NO_SPACE_LEFT" != "0" ]; then
# This error might also happen if there is no space left
grep --quiet "Result too large (34)" "$LOG_FILE"
NO_SPACE_LEFT="$?"
fi
rm -- "$LOG_FILE" rm -- "$LOG_FILE"
if [ -n "$NO_SPACE_LEFT" ]; then if [ "$NO_SPACE_LEFT" == "0" ]; then
# TODO: -y flag
read -p "It looks like there is no space left on the destination. Delete old backup? (Y/n) " yn
case $yn in
[Nn]* ) exit 0;;
esac
fn_log_warn "No space left on device - removing oldest backup and resuming." fn_log_warn "No space left on device - removing oldest backup and resuming."
if [[ "$(fn_find_backups | wc -l)" -lt "2" ]]; then BACKUP_FOLDER_COUNT=$(fn_find_backups | wc -l)
if [ "$BACKUP_FOLDER_COUNT" -lt "2" ]; then
fn_log_error "No space left on device, and no old backup to delete." fn_log_error "No space left on device, and no old backup to delete."
exit 1 exit 1
fi fi
fn_expire_backup "$(fn_find_backups | tail -n 1)" OLD_BACKUP_PATH=$(fn_find_backups | head -n 1)
if [ "$OLD_BACKUP_PATH" == "" ]; then
fn_log_error "No space left on device, and cannot get path to oldest backup to delete."
exit 1
fi
fn_expire_backup "$OLD_BACKUP_PATH"
# Resume backup # Resume backup
continue continue
@@ -244,14 +292,7 @@ while : ; do
exit $RSYNC_EXIT_CODE exit $RSYNC_EXIT_CODE
fi fi
# ----------------------------------------------------------------------------- rm -- "$INPROGRESS_FILE"
# Add symlink to last successful backup
# -----------------------------------------------------------------------------
rm -rf -- "$DEST_FOLDER/latest"
ln -vs -- "$(basename -- "$DEST")" "$DEST_FOLDER/latest"
rm -f -- "$INPROGRESS_FILE"
# TODO: grep for "^rsync error:.*$" in log # TODO: grep for "^rsync error:.*$" in log
fn_log_info "Backup completed without errors." fn_log_info "Backup completed without errors."
exit 0 exit 0