Backup

These are the scripts I use to backup my laptop. They live in the directory /live on an external hard drive, along with backup directories with names like 20090903-195223. Each contains a directory called data (mirroring the system's / directory) and an rsync.log. I run push.sh every day or so.

The code is inspired by Mike Rubel's snapshot research page. Essentially, the script works by creating a new backup directory, then running rsync with the --link-dest option. This very cool feature will use the next oldest backup directory as the baseline for determining whether a file has been modified. If a file in the rsync source is newer, it will be copied over, otherwise it is hardlinked to the link-dest's copy. This prevents unchanged files from taking up room in each new backup.

Setup

  1. Copy the provided code into a directory on your external drive.
  2. Personalize the path /media/ExternalDrive/live to match your setup.
  3. Enter the backup directory and run ./init.sh.
  4. Modify the last few lines of update.sh to reflect your system's partitions. (One rsync per filesystem.)

I have this command in a sidebar launcher to add a backup:

gksudo "gnome-terminal --execute /media/ExternalDrive/live/push.sh"

Code

init.sh

#!/bin/bash
# Update the most recent backup location with the contents of the system.
set -o errexit
set -o nounset

## Fake link to previous
ln -s ./DELETE_ME-previous ./previous

## Fake current backup
mkdir ./DELETE_ME-current
touch ./DELETE_ME-current/rsync.log
mkdir ./DELETE_ME-current/data
ln -s ./DELETE_ME-current ./current

echo 'Created fake backup directory and necessary symlinks.'
echo 'Once there is at least one real backup, you may "rm -r ./DELETE_ME-current"'

push.sh

#!/bin/bash
# Push a new backup location that is identical to the last one, then update it.
set -o errexit
set -o nounset


## Environment
cd /media/ExternalDrive/live
set -o verbose

## Create the new backup location
STAMP=`date '+%Y%m%d-%H%M%S'`
mkdir "./$STAMP"

## Roll the symlinks
unlink "./previous"
mv "./current" "./previous"
ln -s "./$STAMP" "./current"

## Update the current backup
./update.sh

## Wait for approval before closing
read -n 1 -p "Press any key to continue . . ."
echo

update.sh

#!/bin/bash
# Update the most recent backup location with the contents of the system.
set -o errexit
set -o nounset

## Environment
cd /media/ExternalDrive/live
set -o verbose


re_BASE='^/((\.\.[^/]|\.[^./]|[^./])[^/]*/)*$'

## do_rsync $BASE $EXCLUDES
## BASE: absolute path ending in a slash
## EXCLUDES: space-delimited series of --exclude=/foo
function do_rsync {
    BASE=$1
    EXCLUDES=$2
    
    # Using a regex instead of realpath because we don't want to expand symbolic links
    if [[ ! ( "$BASE" =~ $re_BASE ) ]]
    then
        echo "BASE must be a fully absolute, normalized path to a directory. (Must start and end with a slash, contain no '.' or '..' components, and contain no double slashes.)"
        exit 1
    fi
    
    # Make "../"**(number of slashes in BASE + 1)
    BACK=../`echo "$BASE" | perl -ne 'while(/\//g){$count.="../"}; print "$count\n"'`
        
    rsync -avEHAXx --numeric-ids --delete ${EXCLUDES} --link-dest=${BACK}previous/data${BASE} ${BASE} current/data${BASE} 2>&1 | tee -a current/rsync.log
}

## Ensure log file (we can now use append on the first `tee`)
touch current/rsync.log

## Sync each filesystem, descending the tree
## Each BASE must begin and end with a slash.
do_rsync / '--exclude=/proc --exclude=/sys --exclude=/dev --exclude=/mnt --exclude=/media --exclude=/tmp'
do_rsync /home/ '--exclude=/*/.gvfs'