summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLaurent <laurent@pogopixels.com>2013-11-16 06:17:54 -0800
committerLaurent <laurent@pogopixels.com>2013-11-16 06:17:54 -0800
commit12784d1d327cbb1dae622bc96810bd62797f05a1 (patch)
tree705c3cf6bcfed43d41d1c75110781f13e1049cef
parent2ade9de4229d073102644e59cc9bae27982d6ed0 (diff)
parentf5a4275a044111d8d6de010ae88c32e37d66bff5 (diff)
Merge pull request #14 from robru/master
Time Machine style backup pruning.
-rw-r--r--README.md6
-rw-r--r--rsync_tmbackup.sh111
2 files changed, 81 insertions, 36 deletions
diff --git a/README.md b/README.md
index e36e090..b4417a6 100644
--- a/README.md
+++ b/README.md
@@ -40,14 +40,10 @@ An optional exclude file can be provided as a third parameter. It should be comp
# TODO
-* Manage the backups in a way similar to Time Machine - hourly backups for the past 24 hours; daily backups for the past month; weekly backups for the previous months.
-
-* Other minor changes (see TODO comments in the source).
+* Minor changes (see TODO comments in the source).
# LICENSE
[MIT](http://opensource.org/licenses/MIT)
-
[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/laurent22/rsync-time-backup/trend.png)](https://bitdeli.com/free "Bitdeli Badge")
-
diff --git a/rsync_tmbackup.sh b/rsync_tmbackup.sh
index 45f8478..8b2a0e1 100644
--- a/rsync_tmbackup.sh
+++ b/rsync_tmbackup.sh
@@ -28,6 +28,34 @@ fn_terminate_script() {
trap 'fn_terminate_script' SIGINT
# -----------------------------------------------------------------------------
+# Small utility functions for reducing code duplication
+# -----------------------------------------------------------------------------
+
+fn_parse_date() {
+ # Converts YYYY-MM-DD-HHMMSS to YYYY-MM-DD HH:MM:SS and then to Unix Epoch.
+ case "$OSTYPE" in
+ linux*) date -d "${1:0:10} ${1:11:2}:${1:13:2}:${1:15:2}" +%s ;;
+ darwin*) date -j -f "%Y-%m-%d-%H%M%S" "$1" "+%s" ;;
+ esac
+}
+
+fn_find_backups() {
+ find "$DEST_FOLDER" -type d -name "????-??-??-??????" -prune
+}
+
+fn_expire_backup() {
+ # Double-check that we're on a backup destination to be completely
+ # sure we're deleting the right folder
+ if [ -z "$(fn_is_backup_destination "$(dirname -- "$1")")" ]; then
+ fn_log_error "$1 is not on a backup destination - aborting."
+ exit 1
+ fi
+
+ fn_log_info "Expiring $1"
+ rm -rf -- "$1"
+}
+
+# -----------------------------------------------------------------------------
# Source and destination information
# -----------------------------------------------------------------------------
@@ -53,15 +81,10 @@ fn_backup_marker_path() {
}
fn_is_backup_destination() {
- DEST_MARKER_FILE="$(fn_backup_marker_path $1)"
- if [ -f "$DEST_MARKER_FILE" ]; then
- echo "1"
- else
- echo "0"
- fi
+ find "$(fn_backup_marker_path "$1")" 2>/dev/null
}
-if [ "$(fn_is_backup_destination $DEST_FOLDER)" != "1" ]; then
+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 "If it is indeed a backup folder, you may add the marker file by running the following command:"
fn_log_info ""
@@ -74,12 +97,18 @@ fi
# Setup additional variables
# -----------------------------------------------------------------------------
-BACKUP_FOLDER_PATTERN=????-??-??-??????
+# 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.
PROFILE_FOLDER="$HOME/.rsync_tmbackup"
LOG_FILE="$PROFILE_FOLDER/$NOW.log"
DEST=$DEST_FOLDER/$NOW
-PREVIOUS_DEST=$(find "$DEST_FOLDER" -type d -name "$BACKUP_FOLDER_PATTERN" -prune | sort | tail -n 1)
+PREVIOUS_DEST=$(fn_find_backups | sort | tail -n 1)
INPROGRESS_FILE=$DEST_FOLDER/backup.inprogress
# -----------------------------------------------------------------------------
@@ -100,10 +129,10 @@ if [ -f "$INPROGRESS_FILE" ]; then
# - Last backup is moved to current backup folder so that it can be resumed.
# - 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."
- LINE_COUNT=$(find "$DEST_FOLDER" -type d -name "$BACKUP_FOLDER_PATTERN" -prune | sort | tail -n 2 | wc -l)
+ LINE_COUNT=$(fn_find_backups | sort | tail -n 2 | wc -l)
mv -- "$PREVIOUS_DEST" "$DEST"
if [ "$LINE_COUNT" -gt 1 ]; then
- PREVIOUS_PREVIOUS_DEST=$(find "$DEST_FOLDER" -type d -name "$BACKUP_FOLDER_PATTERN" -prune | sort | tail -n 2 | head -n 1)
+ PREVIOUS_PREVIOUS_DEST=$(fn_find_backups | sort | tail -n 2 | head -n 1)
PREVIOUS_DEST=$PREVIOUS_PREVIOUS_DEST
else
PREVIOUS_DEST=""
@@ -139,6 +168,34 @@ while [ "1" ]; do
fi
# -----------------------------------------------------------------------------
+ # Purge certain old backups before beginning new backup.
+ # -----------------------------------------------------------------------------
+
+ # Default value for $prev ensures that the most recent backup is never deleted.
+ prev="0000-00-00-000000"
+ for fname in $(fn_find_backups | sort -r); do
+ date=$(basename "$fname")
+ stamp=$(fn_parse_date $date)
+
+ # Skip if failed to parse date...
+ [ -n "$stamp" ] || continue
+
+ if [ $stamp -ge $KEEP_ALL_DATE ]; then
+ true
+
+ elif [ $stamp -ge $KEEP_DAILIES_DATE ]; then
+ # Delete all but the most recent of each day.
+ [ "${date:0:10}" == "${prev:0:10}" ] && fn_expire_backup "$fname"
+
+ else
+ # Delete all but the most recent of each month.
+ [ "${date:0:7}" == "${prev:0:7}" ] && fn_expire_backup "$fname"
+ fi
+
+ prev=$date
+ done
+
+ # -----------------------------------------------------------------------------
# Start backup
# -----------------------------------------------------------------------------
@@ -187,9 +244,9 @@ while [ "1" ]; do
grep --quiet "Result too large (34)" "$LOG_FILE"
NO_SPACE_LEFT="$?"
fi
-
+
rm -- "$LOG_FILE"
-
+
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
@@ -198,29 +255,21 @@ while [ "1" ]; do
esac
fn_log_warn "No space left on device - removing oldest backup and resuming."
-
- BACKUP_FOLDER_COUNT=$(find "$DEST_FOLDER" -type d -name "$BACKUP_FOLDER_PATTERN" -prune | wc -l)
+
+ 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."
exit 1
fi
-
- OLD_BACKUP_PATH=$(find "$DEST_FOLDER" -type d -name "$BACKUP_FOLDER_PATTERN" -prune | head -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
-
- # Double-check that we're on a backup destination to be completely sure we're deleting the right folder
- OLD_BACKUP_PARENT_PATH=$(dirname -- "$OLD_BACKUP_PATH")
- if [ "$(fn_is_backup_destination $OLD_BACKUP_PARENT_PATH)" != "1" ]; then
- fn_log_error "'$OLD_BACKUP_PATH' is not on a backup destination - aborting."
- exit 1
- fi
-
- fn_log_info "Deleting '$OLD_BACKUP_PATH'..."
- rm -rf -- "$OLD_BACKUP_PATH"
-
+
+ fn_expire_backup "$OLD_BACKUP_PATH"
+
# Resume backup
continue
fi
@@ -229,16 +278,16 @@ while [ "1" ]; do
fn_log_error "Exited with error code $RSYNC_EXIT_CODE"
exit $RSYNC_EXIT_CODE
fi
-
+
# -----------------------------------------------------------------------------
# Add symlink to last successful backup
# -----------------------------------------------------------------------------
-
+
cd "$DEST_FOLDER"
rm -f -- "latest"
ln -s -- $(basename -- "$DEST") "latest"
cd -
-
+
rm -- "$INPROGRESS_FILE"
# TODO: grep for "^rsync error:.*$" in log
fn_log_info "Backup completed without errors."