#!/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 <<END_OF_SHORT_HELP
$THIS - backup directories to files using afio

usage:
  $THIS create [options] <backup directory> <archive file>
  $THIS restore [options] <restore directory> <archive file>
  $THIS verify [options] <backup directory> <archive file>
  $THIS list <archive file>

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 bytes>
      Split archive every <split-bytes> bytes
        <split-bytes> goes to the split program, so examples are
        600m for a CD or 1440k for tapes
  -l <filelist generating program> is a program that will output a null-separated
        list of filenames which will be piped to afio.  For example: find -print0
  -y <pattern>
      Only process filename which match <pattern>, which is a filename pattern
      such as etc/program/conf*.  Can be used multiple times.
  -Y <pattern>
      Don't process filenames which match <pattern>.  Overrides -y if both match.
      Can be used multiple times.
  -w <file>
      Treat each line in <file> as a -y pattern.
  -W <file>
      Treat each line in <file> as a -Y pattern.
END_OF_SHORT_HELP
}

function long_help() {
    short_help
    cat <<END_OF_LONG_HELP

See the afio man page for more info on the -w, -W, -y, and -Y options.  Only
one of options -e, -z, or -b may be used.

afio stores "compressed" files with extension ".z" whether the "compression" is
gzip, bzip2, or gpg.  Files are run through the compression filter regardless
of whether they are compressed already or not.  This allows a restore operation
to verify file integrity.

Encryption is only supported for gpg.  To create an encryption archive using the
-e and -r options, you should verify that you can encrypt a file with the same
options (gpg -e -r <recipient>).  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
