#!/bin/bash
# Duplicity backup script
# (C) Murty Rompalli
# Last updated: 2/10/2013

# Backup: dupy.sh config-file
# Restore: dupy.sh config-file file1 file2 ..
# Verify: dupy.sh config-file -v

# Config file example:
# TYPE=ftp
# PASSPHRASE="passphrase"
# SERVER=server.com
# USERNAME="username"
# PASSWORD="password"
# LABEL=nickname
# DIRS='/dir1 /dir2 /dir3'

trap 'echo Exiting on Ctrl-C / Ctrl-\\; exit' SIGINT SIGQUIT

# Preliminary checks
[ $# -gt 0 ] || {
  cat <<EOF
  Usage:
  $0 config-file
  $0 config-file restore-file1 [ restore-file2 restore-file3 .. ]
EOF
  exit
}

id | grep -q ^uid=0\( || {
  echo $0: Must be run as root
  exit
}

[ $HOME = / -o $HOME = /root ] || {
  echo $0: HOME is not correctly
  exit
}

# Default variables
set -e
CMD=duplicity
DUPLOAD="$CMD --full-if-older-than 1M --asynchronous-upload"
DCLEANUP="$CMD cleanup --force"
DREMOVEOLD=
#DREMOVEOLD="$CMD remove-older-than 6M --force"
#DREMOVEOLD="$CMD remove-all-but-n-full 1 --force"
DRESTORE="$CMD --file-to-restore"
HOST=`hostname -s`
MYPWD="`pwd`"

# Exclude list
# NOTE: If an exclude entry does not begin with /,
# then it cannot contain slash anywhere else in its name

EXCLUDELIST='
/proc
/sys
/mnt
/media
/dev
/var/lock
/var/run
/var/log
/var/named/chroot/proc
/var/named/chroot/dev
/home/*/.gvfs
/home/*/.thumbnails
/home/*/.local/share/Trash
cache
Cache
.cache
.Cache
.Trash
.Junk
dovecot.index.log
'

getexclude() {

cd "$SRC" || exit
BASE="`pwd`"
echo $BASE | grep -q /$ || BASE="$BASE/"

for i in $EXCLUDELIST $@
do

  if echo $i | grep -q ^/
  then
    echo $i | grep -q ^$BASE || continue
    DIR=`echo $i | sed "s|^$BASE||"`
    [ -d $DIR -o -f $DIR ] || continue
    EXCLUDE="$EXCLUDE --exclude $i"
    continue
  fi

  if echo $i | grep -q /
  then
    echo $i: bad exclude entry
    exit
  fi

  while read j
  do
    EXCLUDE="$EXCLUDE --exclude $j"
  done < <(find "$SRC" -name "$i" 2> /dev/null)
done  
}

restore() {
LIST=''
for file in "$@"
do
  echo $file | grep -q ^/ || {
    echo $file: not absolute name
    exit
  }
  SET=''
  TMP="`echo $file | tr -s / | sed 's|\([^/]\)/$|\1|'`"
  echo "$LIST" | grep -q " $TMP " && {
    echo $CONFIG: $TMP: Already restored, skipping
    continue
  }
  LIST="$LIST $TMP "
  while [ -z "$SET" ]
  do
    for dir in $DIRS
    do
      DIR="`echo $dir | tr -s / | sed 's|\([^/]\)/$|\1|'`"
      if [ "$DIR" == "$TMP" ]
      then
        SET="$DIR"
        echo Backup set: $SET
        break
      fi
    done

    if [ -z "$SET" ]
    then
      if [ "$TMP" == / ]
      then
        break
      else
         TMP="`dirname \"$TMP\"`"
      fi
    fi
  done

  if [ -z "$SET" ]
  then
    echo $CONFIG: $file: not backed up
    exit
  fi

  TMP="`echo $file | tr -s / | sed 's|\([^/]\)/$|\1|'`"
  TMP2="`echo $SET | tr -s / | sed 's|\([^/]\)/$|\1|'`"
  FLAG="`date | tr -d ' '`"
  NAME="`echo $LABEL$SET | sed 's|/$||' | tr / -`"
  TARGET="$DST$SET"

  NEWFILE="$MYPWD/`basename $TMP`$FLAG"
  NEWFILE="`echo $NEWFILE | tr -s /`"

  if [ "$TMP" == "$TMP2" ]
  then
    FILE=/
  else
    TMP2="`echo $TMP2 | sed 's|/$||'`"
    FILE="`echo $TMP | sed s#^$TMP2/##`"
    FILE="`echo $FILE/ | tr -s / | sed 's|/$||'`"
  fi

  echo File: $FILE
  echo ---\> Restoring $file as $NEWFILE
  PASSPHRASE="$PASSPHRASE" FTP_PASSWORD="$PASSWORD" \
  $DRESTORE $FILE $OPTS --name="$NAME" $TARGET $NEWFILE
  echo
  sleep 1
done
echo $CONFIG: Restored into directory $MYPWD: $LIST
}

backup() {

# Start the backup
echo
echo ===\> BEGIN Backup to $LABEL
echo

for SRC in $DIRS
do
  TMP="`echo $SRC | tr -s /`"
  NAME="`echo $LABEL$TMP | sed 's|/$||' | tr / -`"
  TARGET="$DST$TMP"

# Delete empty files on target first
  echo -----\> Deleting empty files on "$TARGET"
  if [[ $DST =~ ftp: ]]
  then
    TMPCMDS=`mktemp`
    ncftpls -l -u "$USERNAME" -p "$PASSWORD" -t 30 $DST1/"$SRC" |
    awk '{print $5,$NF}' | grep ^'0 duplicity-' |
    cut -d\  -f2 > $TMPCMDS
    if [ -s $TMPCMDS ]
    then
      echo Deleting empty files on destination:
      cat $TMPCMDS
      sed -i 's/^/rm /' $TMPCMDS
      echo quit >> $TMPCMDS
      ncftp -u "$USERNAME" -p "$PASSWORD" $DST1/"$SRC" < $TMPCMDS
    fi
    \rm -f $TMPCMDS
  elif [[ $DST =~ file: ]]
  then
    set +e
    ls $MNT/"$SRC"/duplicity-*.gpg > /dev/null 2>&1
    if [ $? -eq 0 ]
    then
      for i in $MNT/"$SRC"/duplicity-*.gpg
      do
        [ -s $i ] || \rm -v $i
      done
    fi
    set -e
  fi
  
  [ "$DREMOVEOLD" ] &&
  echo -----\> Checking for old backups &&
  PASSPHRASE="$PASSPHRASE" FTP_PASSWORD="$PASSWORD" \
  $DREMOVEOLD $OPTS --name="$NAME" "$TARGET"
  
  echo -----\> Cleaning up orphan files on "$TARGET"
  PASSPHRASE="$PASSPHRASE" FTP_PASSWORD="$PASSWORD" \
  $DCLEANUP $OPTS --name="$NAME" "$TARGET"
  
  echo -----\> Cleaning up orphan files in local cache
  for i in `find /root/.cache/duplicity/"$NAME" -type f -name '*.manifest.part'`
  do
    \rm -v $i
  done

  EXCLUDE=''
  getexclude
  cd $MYPWD || exit
  echo ---\> $DUPLOAD $OPTS --name="$NAME" "$SRC" "$TARGET"

  # For native webdav targets, duplcity frequently fails with 501 notfound error
  # and due to some bug, duplicity never retries upon such failures

  set +e
  RC=30
  COUNT=0
  while [ $RC -eq 30 -a $COUNT -lt 5 ]
  do
    PASSPHRASE="$PASSPHRASE" FTP_PASSWORD="$PASSWORD" \
    $DUPLOAD $OPTS --name="$NAME" $EXCLUDE "$SRC" "$TARGET"

    RC=$?
    let COUNT=COUNT+1
  done
  set -e

done

echo
echo ===\> END Backup to $LABEL
echo
}

verify() {

set +e
echo ===\> $LABEL: Verifying backup ----------------------------------

for SRC in $DIRS
do
  TMP="`echo $SRC | tr -s /`"
  NAME="`echo $LABEL$TMP | sed 's|/$||' | tr / -`"
  TARGET="$DST$TMP"
  EXCLUDE=''
  getexclude
  echo ---\> $CMD verify $OPTS --name="$NAME" "$TARGET" "$SRC"
  PASSPHRASE="$PASSPHRASE" FTP_PASSWORD="$PASSWORD" \
  $CMD verify $OPTS --name="$NAME" $EXCLUDE "$TARGET" "$SRC"
done

set -e
}

status() {

set +e
echo ===\> $LABEL: Status ----------------------------------

for SRC in $DIRS
do
  TMP="`echo $SRC | tr -s /`"
  NAME="`echo $LABEL$TMP | sed 's|/$||' | tr / -`"
  TARGET="$DST$TMP"
  echo ---\> $CMD collection-status $OPTS --name="$NAME" "$TARGET"
  PASSPHRASE="$PASSPHRASE" FTP_PASSWORD="$PASSWORD" \
  $CMD collection-status $OPTS --name="$NAME" "$TARGET"
done

set -e
}

# Main

# Read config file
CONFIG=$1
shift
[ -f "$CONFIG" -a -x "$CONFIG" ] || {
  echo $CONFIG: config file missing or not executable
  exit
}
. $CONFIG

if [ "$TYPE" == ftp ]
then
    for var in PASSPHRASE LABEL DIRS USERNAME PASSWORD
    do
        [ "${!var}" ] || {
            echo $CONFIG: $var not defined
            exit
        }
    done

    DST1=ftp://$SERVER//$HOST
    #DST=ftp://"$USERNAME"@$SERVER//$HOST
    DST=ftp://"$USERNAME"@$SERVER/%2F$HOST
elif [ "$TYPE" == webdav -o "$TYPE" == webdavs ]
then
    for var in PASSPHRASE LABEL DIRS USERNAME PASSWORD
    do
        [ "${!var}" ] || {
            echo $CONFIG: $var not defined
            exit
        }
    done

    if [ "$WORKAROUND" ]
    then
        [ "$WORKAROUND" == 1 -o "$WORKAROUND" == 0 ] || {
            echo $CONFIG: WORKAROUND: invalid, expecting 1 or 0
            exit
        }
    else
       WORKAROUND=0
    fi

    if [ $WORKAROUND -ne 0 ]
    then
        [ "$MNT" ] || {
            echo $CONFIG: MNT must defined when WORKAROUND is set
            exit
        }
    fi

    echo $MNT | grep -q ^/ || {
         echo $CONFIG: $MNT: Not absolute
         exit
    }
    [ -d "$MNT" ] || {
        echo $CONFIG: $MNT: No such directory
        exit
    }

    if [ $WORKAROUND -eq 0 ]
    then
        DST=$TYPE://$USERNAME@$SERVER/$HOST
    else
        # Enable workaround because webdav support is broken
        [ $TYPE == webdavs ] && S=s
        DAV=http$S://$SERVER/$HOST
        if grep -q "($DAV) $MNT " /etc/mtab
        then
            echo $DAV mounted on $MNT
            # echo Unmounting $DAV on $MNT
            # umount /mnt || exit
        else
            echo Trying to mount $DAV on $MNT ...
            wdfs $DAV $MNT -o username="$USERNAME",password="$PASSWORD"
        fi
        grep -q "($DAV) $MNT " /etc/mtab || exit
        DST=file://$MNT
    fi
elif [ "$TYPE" == local ]
then
    for var in PASSPHRASE LABEL DIRS MNT
    do
        [ "${!var}" ] || {
            echo $CONFIG: $var not defined
            exit
        }
    done

    echo $MNT | grep -q ^/ || {
         echo $CONFIG: $MNT: Not absolute
         exit
    }
    [ -d "$MNT" ] || {
        echo $CONFIG: $MNT: No such directory
        exit
    }

    mkdir -p $MNT/$HOST
    DST=file://$MNT/$HOST
else
  echo $CONFIG: $TYPE: undefined or unsupported
  exit
fi

# check command line arguments
for SRC in $DIRS
do
  echo "$SRC" | grep -q ^/ || {
    echo $0: "$SRC": directory not absolute
    exit
  }

  [ -d "$SRC" ] || {
    echo $0: "$SRC": directory not found
    exit
  }

  [ -r "$SRC" -a -x "$SRC" ] || {
    echo $0: "$SRC": directory not accessible
    exit
  }

  for F in `echo "$SRC" | tr '/' ' '`
  do
    [ "$F" = . -o "$F" = .. ] || continue
    echo $0: "$SRC": invalid source
    exit
  done
done

case "$1" in
  "") backup
      status ;;
  -v|verify) verify ;;
  -s|status) status ;;
   *) restore "$@" ;;
esac
