#!/bin/sh # backup-afio version 0.92 - backup stuff in a robust way using afio # This software is copyright 2005 Daniel M. Webb, and distributed under the # terms of the GNU GPL license, available at http://www.gnu.org/licenses/gpl.html THIS=backup-afio PATH=$PATH:/bin:/sbin:/usr/bin:/usr/sbin DATE_STRING=$(date +%F.%H-%M) LOG_DIR=$(pwd) LOG_FILE=$LOG_DIR/backup-afio.$DATE_STRING.log STDERR_LOG=$LOG_DIR/backup-afio.$DATE_STRING.stderr SPLIT_SUFFIX_LENGTH=3 function die () { echo "$THIS: Fatal error: $*" exit 1 } function short_help() { cat < $THIS restore [options] $THIS verify [options] $THIS list Options: -h Full help -e Filter files through GPG public key encryption -r Set GPG recipient (same as gpg -r option) -z Filter files through gzip compression -b Filter files through bzip2 compression -k Allow starting in the middle of an archive This allows restoring from a broken archive or from a split archive without joining it. -x Cross filesystem boundaries (default to not cross). Ignored if -l used. -s Split archive every bytes goes to the split program, so examples are 600m for a CD or 1440k for tapes -l is a program that will output a null-separated list of filenames which will be piped to afio. For example: find -print0 -y Only process filename which match , which is a filename pattern such as etc/program/conf*. Can be used multiple times. -Y Don't process filenames which match . Overrides -y if both match. Can be used multiple times. -w Treat each line in as a -y pattern. -W Treat each line in as a -Y pattern. END_OF_SHORT_HELP } function long_help() { short_help cat <). To decrypt (all commands except create), you should verify that you can decrypt a file created with the command above without entering a password. To accomplish this you must set up the gpg-agent daemon. Examples: $ $THIS create -b -s 600m /home/me /tmp/mb.bz2.afio create a set of archives in /tmp/: mb.bz2.afio.000, mb.bz2.afio.001, etc. I like to name my files to indicate the options I used (bz2 to indicate bzip2 compression), because the afio file itself doesn't save this information. $ $THIS create -e -r files@bob.net . ./f.gpg.afio Create an archive of the current directory with encrypted files. $ $THIS restore -e /tmp/test f.gpg.afio Restore (extract) the encrypted archive f.gpg.afio into /tmp/test. END_OF_LONG_HELP } if [ "$1" = -h ]; then long_help exit 1 elif [ -z "$2" ]; then short_help exit 1 fi COMMAND=$1 shift # ================ Options processing ================ GPG_RECIPIENT="" COMPRESS=false K_OPTION="" CROSS_FILESYSTEM_BOUNDARIES=" -xdev " # Tell find not to cross filesystem boundaries INCLUDE_PATTERN_FILE="" IGNORE_PATTERN_FILE="" INCLUDE_PATTERN="" IGNORE_PATTERN="" SPLIT_SIZE="" FIND_PIPE="" while [ "${1:0:1}" = '-' ]; do [ $1 = '-e' ] && { COMPRESS=gpg; shift; continue; } [ $1 = '-r' ] && { shift; GPG_RECIPIENT="$1"; shift; continue; } [ $1 = '-z' ] && { COMPRESS=gzip; shift; continue; } [ $1 = '-b' ] && { COMPRESS=bzip2; shift; continue; } [ $1 = '-k' ] && { K_OPTION="-k"; shift; continue; } [ $1 = '-x' ] && { CROSS_FILESYSTEM_BOUNDARIES=""; shift; continue; } [ $1 = '-l' ] && { shift; FIND_PIPE="$1"; shift; continue; } [ $1 = '-s' ] && { shift; SPLIT_SIZE="$1"; shift; continue; } [ $1 = '-w' ] && { shift; INCLUDE_PATTERN_FILE="-w $1"; shift; continue; } [ $1 = '-W' ] && { shift; IGNORE_PATTERN_FILE="-W $1"; shift; continue; } [ $1 = '-y' ] && { shift; INCLUDE_PATTERN="$INCLUDE_PATTERN -y $1"; shift; continue; } [ $1 = '-Y' ] && { shift; IGNORE_PATTERN="$IGNORE_PATTERN -Y $1"; shift; continue; } die "unknown option: $1" done # Set filelist generator to default if not specified if [ -z "$FIND_PIPE" ]; then FIND_PIPE="find . $CROSS_FILESYSTEM_BOUNDARIES -print0" fi # ================ Compression options ================ FILTER_BASE="" FILTER_OPTIONS="" if [ $COMPRESS = gzip ]; then if [ -z "$(which gzip)" ]; then die "compression program gzip not found" fi FILTER_BASE="-Z -U" # -Z = compress files # -U = force compression of all files elif [ $COMPRESS = bzip2 ]; then if [ -z "$(which bzip2)" ]; then die "compression program bzip2 not found" fi FILTER_BASE="-Z -U -P bzip2" # -Z = compress files # -U = force compression of all files # -P bzip2 = use compression program bzip2 if [ $COMMAND = "create" ]; then FILTER_OPTIONS="-Q -1" # Pass -1 option to bzip2 (use 100k blocks) else FILTER_OPTIONS="-Q -d" # Pass -d option to bzip2 (decompress) fi elif [ $COMPRESS = gpg ]; then [ -z "$(which gpg)" ] && die "encryption program gpg not found" FILTER_BASE="-Z -3 0 -U -P gpg -Q --batch -Q --quiet" # -Z = "compress" files (really encrypt) # -U = force compression of all files # -P gpg = use "compression" program gpg # --batch = non-interactive # --quiet = be as quiet as possible if [ $COMMAND = "create" ]; then [ -z "$GPG_RECIPIENT" ] && die "GPG recipient must be set with -r option" FILTER_OPTIONS="-Q --encrypt -Q --recipient -Q $GPG_RECIPIENT" else FILTER_OPTIONS="-Q --decrypt -Q --use-agent" fi fi # ================ File and directory ================ if [ $COMMAND = "create" -o $COMMAND = "restore" -o $COMMAND = "verify" ]; then BACKUP_DIR="$1" BACKUP_FILE="$2" [ -d "$BACKUP_DIR" ] || die "$BACKUP_DIR not found (or not a directory)" # Fix BACKUP_FILE if it is not absolute if [ ! "$(echo $BACKUP_FILE | cut --characters=1)" = "/" ]; then BACKUP_FILE="$(pwd)/$BACKUP_FILE" fi [ -z "$BACKUP_FILE" ] && die "not enough arguments (BACKUP_FILE = \"$BACKUP_FILE\")" BACKUP_FILE_LIST="$BACKUP_FILE.list" elif [ $COMMAND = list ]; then BACKUP_FILE="$1" [ $COMPRESS = gpg ] && die "list and -e are incompatible" else die "unknown command: $COMMAND" fi if [ $COMMAND != "create" -a ! -f "$BACKUP_FILE" ]; then die "$BACKUP_FILE is not a file" fi # ================ Setup output ================ OUTPUT_OPTION=">$BACKUP_FILE" if [ ! -z "$SPLIT_SIZE" ]; then which split >/dev/null || die "split program not found" OUTPUT_OPTION="| split --numeric-suffixes --suffix-length=$SPLIT_SUFFIX_LENGTH \ --bytes=$SPLIT_SIZE - $BACKUP_FILE." fi # ================ Create options ================ ALL_COMMON="-x -z -L $LOG_FILE \ $INCLUDE_PATTERN_FILE $IGNORE_PATTERN_FILE $K_OPTION \ $INCLUDE_PATTERN $IGNORE_PATTERN" # -x = Retain file ownership and setuid/setgid permissions # -z = Print execution statistics # -L x = Log to file x CREATE_COMMON="afio -o $ALL_COMMON -0 -v -B" # -0 = Assume input filenames to be terminated with a '\0' instead of a '\n' # -v = Report pathnames (to stderr) as they are processed (ls -l style) # -B = Report byte offset of files in report LIST_COMMON="afio -t $ALL_COMMON" INSTALL_COMMON="afio -i $ALL_COMMON -v" VERIFY_COMMON="afio -r $ALL_COMMON" # ================================ case $COMMAND in create) cd $BACKUP_DIR RUNME="$CREATE_COMMON $FILTER_BASE $FILTER_OPTIONS - 2>$BACKUP_FILE_LIST $OUTPUT_OPTION" echo "command: $FIND_PIPE | $RUNME" eval "$FIND_PIPE | $RUNME" ;; restore) cd $BACKUP_DIR RUNME="$INSTALL_COMMON $FILTER_BASE $FILTER_OPTIONS $BACKUP_FILE" echo "command: $RUNME 2>$STDERR_LOG" eval "$RUNME 2>$STDERR_LOG" # Check for bad files BAD_FILES=$(cat $STDERR_LOG | egrep --before-context=1 --after-context=1 "^afio") if [ ! -z "$BAD_FILES" ]; then echo "$THIS: -------------------------------------------------------------------------" echo "$THIS: Possible bad files during afio restore. Each broken pipe or nonzero exit" echo "$THIS: message below means the gzip, bzip2, or gpg programs failed to extract the" echo "$THIS: file. Even if it says \"-- uncompressed\", it's not true." echo "$THIS: Full afio sterr output is in:" echo "$THIS: $STDERR_LOG" echo "$THIS: Normal log file is:" echo "$THIS: $LOG_FILE" echo "$THIS: -------------------------------------------------------------------------" echo "$BAD_FILES" fi ;; verify) cd $BACKUP_DIR RUNME="$VERIFY_COMMON $FILTER_BASE $FILTER_OPTIONS $BACKUP_FILE" echo "command: $RUNME" eval "$RUNME" ;; list) RUNME="$LIST_COMMON $BACKUP_FILE" echo "command: $RUNME" eval "$RUNME" ;; esac