Parent directory

duplicity-backup.sh

27954 bytes
  1#!/usr/bin/env bash
  2#
  3# Copyright (c) 2008-2010 Damon Timm.
  4# Copyright (c) 2010 Mario Santagiuliana.
  5# Copyright (c) 2012-2018 Marc Gallet.
  6# Copyright (c) 2021 TheEdgeOfRage
  7#
  8# This program is free software: you can redistribute it and/or modify it under
  9# the terms of the GNU General Public License as published by the Free Software
 10# Foundation, either version 3 of the License, or (at your option) any later
 11# version.
 12#
 13# This program is distributed in the hope that it will be useful, but WITHOUT
 14# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 15# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
 16# details.
 17#
 18# You should have received a copy of the GNU General Public License along with
 19# this program.  If not, see <http://www.gnu.org/licenses/>.
 20#
 21# ---------------------------------------------------------------------------- #
 22
 23DBSH_VERSION="v1.7.2"
 24
 25# make a backup of stdout and stderr for later
 26exec 6>&1
 27exec 7>&2
 28
 29# ------------------------------------------------------------
 30
 31usage(){
 32echo "USAGE:
 33  $(basename "$0") [options]
 34
 35  Options:
 36    -c, --config CONFIG_FILE   specify the config file to use
 37
 38    -b, --backup               runs an incremental backup
 39    -f, --full                 forces a full backup
 40    -v, --verify               verifies the backup
 41    -e, --cleanup              cleanup the backup (eg. broken sessions), by default using
 42                               duplicity --force flag, use --dry-run to actually log what
 43                               will be cleaned up without removing (see man duplicity
 44                               > ACTIONS > cleanup for details)
 45    -l, --list-current-files   lists the files currently backed up in the archive
 46    -s, --collection-status    show all the backup sets in the archive
 47
 48        --restore [PATH]       restores the entire backup to [path]
 49        --restore-file [FILE_TO_RESTORE] [DESTINATION]
 50                               restore a specific file
 51        --restore-dir [DIR_TO_RESTORE] [DESTINATION]
 52                               restore a specific directory
 53
 54    -t, --time TIME            specify the time from which to restore or list files
 55                               (see duplicity man page for the format)
 56
 57    --backup-script            automatically backup the script and secret key(s) to
 58                               the current working directory
 59
 60    -q, --quiet                silence most of output messages, except errors and output
 61                               that are intended for interactive usage. Silenced output
 62                               is still logged in the logfile.
 63
 64    -n, --dry-run              perform a trial run with no changes made
 65    -d, --debug                echo duplicity commands to logfile
 66    -V, --version              print version information about this script and duplicity
 67    -h, --help                 print this help and exit
 68
 69  CURRENT SCRIPT VARIABLES:
 70  ========================
 71    DEST (backup destination)       = ${DEST}
 72    INCLIST (directories included)  = ${INCLIST[*]:0}
 73    EXCLIST (directories excluded)  = ${EXCLIST[*]:0}
 74    ROOT (root directory of backup) = ${ROOT}
 75    LOGFILE (log file path)         = ${LOGFILE}
 76" >&6
 77USAGE=1
 78}
 79
 80DUPLICITY="$(command -v duplicity)"
 81
 82if [ ! -x "${DUPLICITY}" ]; then
 83  echo "ERROR: duplicity not installed, that's gotta happen first!" >&2
 84  exit 1
 85fi
 86
 87DUPLICITY_VERSION=$(${DUPLICITY} --version)
 88DUPLICITY_VERSION=${DUPLICITY_VERSION//[^0-9\.]/}
 89
 90version(){
 91  echo "duplicity-backup.sh ${DBSH_VERSION}"
 92  echo "duplicity ${DUPLICITY_VERSION}"
 93  exit 0
 94}
 95
 96# Some expensive argument parsing that allows the script to
 97# be insensitive to the order of appearance of the options
 98# and to handle correctly option parameters that are optional
 99while getopts ":c:t:bfvelsqndhV-:" opt; do
100  case $opt in
101    # parse long options (a bit tricky because builtin getopts does not
102    # manage long options and I don't want to impose GNU getopt dependancy)
103    -)
104      case "${OPTARG}" in
105        # --restore [restore dest]
106        restore)
107          COMMAND=${OPTARG}
108          # We try to find the optional value [restore dest]
109          if [ -n "${!OPTIND:0:1}" ] && [ ! "${!OPTIND:0:1}" = "-" ]; then
110            RESTORE_DEST=${!OPTIND}
111            OPTIND=$(( OPTIND + 1 )) # we found it, move forward in arg parsing
112          fi
113        ;;
114        # --restore-file [file to restore] [restore dest]
115        # --restore-dir [path to restore] [restore dest]
116        restore-file|restore-dir)
117          COMMAND=${OPTARG}
118          # We try to find the first optional value [file to restore]
119          if [ -n "${!OPTIND:0:1}" ] && [ ! "${!OPTIND:0:1}" = "-" ]; then
120            FILE_TO_RESTORE=${!OPTIND}
121            OPTIND=$(( OPTIND + 1 )) # we found it, move forward in arg parsing
122          else
123            continue # no value for the restore-file option, skip the rest
124          fi
125          # We try to find the second optional value [restore dest]
126          if [ -n "${!OPTIND:0:1}" ] && [ ! "${!OPTIND:0:1}" = "-" ]; then
127            RESTORE_DEST=${!OPTIND}
128            OPTIND=$(( OPTIND + 1 )) # we found it, move forward in arg parsing
129          fi
130        ;;
131        config) # set the config file from the command line
132          # We try to find the config file
133          if [ -n "${!OPTIND:0:1}" ] && [ ! "${!OPTIND:0:1}" = "-" ]; then
134            CONFIG=${!OPTIND}
135            OPTIND=$(( OPTIND + 1 )) # we found it, move forward in arg parsing
136          fi
137        ;;
138        time) # set the restore time from the command line
139          # We try to find the restore time
140          if [ -n "${!OPTIND:0:1}" ] && [ ! "${!OPTIND:0:1}" = "-" ]; then
141            TIME=${!OPTIND}
142            OPTIND=$(( OPTIND + 1 )) # we found it, move forward in arg parsing
143          fi
144        ;;
145        quiet)
146          QUIET=1
147        ;;
148        dry-run)
149          DRY_RUN="--dry-run"
150        ;;
151        debug)
152          ECHO=$(command -v echo)
153        ;;
154        help)
155          usage
156          exit 0
157        ;;
158        version)
159          version
160        ;;
161        *)
162          COMMAND=${OPTARG}
163        ;;
164        esac
165    ;;
166    # here are parsed the short options
167    c) CONFIG=${OPTARG};; # set the config file from the command line
168    t) TIME=${OPTARG};; # set the restore time from the command line
169    b) COMMAND="backup";;
170    f) COMMAND="full";;
171    v) COMMAND="verify";;
172    e) COMMAND="cleanup";;
173    l) COMMAND="list-current-files";;
174    s) COMMAND="collection-status";;
175    q) QUIET=1;;
176    n) DRY_RUN="--dry-run";; # dry run
177    d) ECHO=$(command -v echo);; # debug
178    h)
179      usage
180      exit 0
181    ;;
182    V) version;;
183    :)
184      echo "Option -${OPTARG} requires an argument." >&2
185      COMMAND=""
186    ;;
187    \?)
188      echo "Invalid option: -${OPTARG}" >&2
189      COMMAND=""
190    ;;
191  esac
192done
193#echo "Options parsed. COMMAND=${COMMAND}" # for debugging
194
195
196# ----------------  Read config file if specified -----------------
197
198if [ -z "${CONFIG}" ];
199then
200  echo "ERROR: set config file destination with CONFIG env var or -c file flag" >&2
201  usage
202  exit 1
203fi
204
205if [ -n "${CONFIG}" ] && [ -f "${CONFIG}" ];
206then
207  # shellcheck disable=SC1090
208  . "${CONFIG}"
209else
210  echo "ERROR: can't find config file! (${CONFIG})" >&2
211  usage
212  exit 1
213fi
214
215# ----------------------- Setup logging ---------------------------
216
217# Setup logging as soon as possible, in order to be able to perform i/o redirection
218
219LOGDIR=${LOGDIR:-"${HOME}/.local/log/duplicity/"}
220LOG_FILE=${LOG_FILE:-"duplicity-$(date +%Y-%m-%dT%H:%M:%S).log"}
221
222# Ensure a trailing slash always exists in the log directory name
223LOGDIR="${LOGDIR%/}/"
224LOGFILE="${LOGDIR}${LOG_FILE}"
225
226if [ ! -d "${LOGDIR}" ]; then
227  echo "Attempting to create log directory ${LOGDIR} ..."
228  if ! mkdir -p "${LOGDIR}"; then
229    echo "Log directory ${LOGDIR} could not be created by this user: ${USER}" >&2
230    echo "Aborting..." >&2
231    exit 1
232  else
233    echo "Directory ${LOGDIR} successfully created."
234  fi
235elif [ ! -w "${LOGDIR}" ]; then
236  echo "Log directory ${LOGDIR} is not writeable by this user: ${USER}" >&2
237  echo "Aborting..." >&2
238  exit 1
239fi
240
241# -------------------- Setup I/O redirections --------------------
242# Magic from
243# http://superuser.com/questions/86915/force-bash-script-to-use-tee-without-piping-from-the-command-line
244#
245#  ##### Redirection matrix in the case when quiet mode is ON #####
246#
247#  QUIET mode ON  | shown on screen | not shown on screen
248#  ---------------+-----------------+----------------------
249#  logged         |    fd2, fd3     |      fd1, fd5
250#  not logged     |      fd4        |         -
251#
252#  ##### Redirection matrix in the case when quiet mode is OFF #####
253#
254#  QUIET mode OFF | shown on screen | not shown on screen
255#  ---------------+-----------------+----------------------
256#  logged         | fd1, fd2, fd3   |        fd5
257#  not logged     |      fd4        |         -
258#
259# fd1 is stdout and is always logged but only shown if not QUIET
260# fd2 is stderr and is always shown on screen and logged
261# fd3 is like stdout but always shown on screen (for interactive prompts)
262# fd4 is always shown on screen but never logged (for the usage text)
263# fd5 is never shown on screen but always logged (for delimiters in the log)
264#
265
266# fd2 and fd3 are always logged and shown on screen via tee
267# for fd2 (original stderr) the output of tee needs to be redirected to stderr
268exec 2> >(tee -ia "${LOGFILE}" >&2)
269# create fd3 as a redirection to stdout and the logfile via tee
270exec 3> >(tee -ia "${LOGFILE}")
271
272# create fd4 as a copy of stdout, but that won't be redirected to tee
273# so that it is always shown and never logged
274exec 4>&1
275
276# create fd5 as a direct redirection to the logfile
277# so that the content is never shown on screen but always logged
278exec 5>> "${LOGFILE}"
279
280# finally we modify stdout (fd1) to always being logged (like fd3 and fd5)
281# but only being shown on screen if quiet mode is not active
282if [[ ${QUIET} == 1 ]]; then
283  # Quiet mode: stdout not shown on screen but still logged via fd5
284  exec 1>&5
285else
286  # Normal mode: stdout shown on screen and logged via fd3
287  exec 1>&3
288fi
289
290# tests for debugging the magic
291#echo "redirected to fd1"
292#echo "redirected to fd2" >&2
293#echo "redirected to fd3" >&3
294#echo "redirected to fd4" >&4
295#echo "redirected to fd5" >&5
296
297# ------------------------- Setting up variables ------------------------
298
299if [ -n "${DRY_RUN}" ]; then
300  STATIC_OPTIONS="${DRY_RUN} ${STATIC_OPTIONS}"
301fi
302
303if [ -n "${STORAGECLASS}" ]; then
304  STATIC_OPTIONS="${STATIC_OPTIONS} ${STORAGECLASS}"
305fi
306
307SIGN_PASSPHRASE=${PASSPHRASE}
308
309export AWS_ACCESS_KEY_ID
310export AWS_SECRET_ACCESS_KEY
311export AWS_PROFILE
312export PASSPHRASE
313export SIGN_PASSPHRASE
314
315if [[ -n "${BACKEND_PASSWORD}" ]]; then
316  export BACKEND_PASSWORD
317fi
318
319if [[ -n "${TMPDIR}" ]]; then
320  export TMPDIR
321fi
322
323# File to use as a lock. The lock is used to insure that only one instance of
324# the script is running at a time.
325LOCKFILE=${LOGDIR}backup.lock
326
327ENCRYPT="--gpg-options \"${GPG_OPTIONS}\""
328if [ -n "${GPG_ENC_KEY}" ] && [ -n "${GPG_SIGN_KEY}" ]; then
329  if [ "${HIDE_KEY_ID}" = "yes" ]; then
330    ENCRYPT="${ENCRYPT} --hidden-encrypt-key=${GPG_ENC_KEY}"
331    if [ "${COMMAND}" != "restore" ] && [ "${COMMAND}" != "restore-file" ] && [ "${COMMAND}" != "restore-dir" ]; then
332      ENCRYPT="${ENCRYPT} --sign-key=${GPG_SIGN_KEY}"
333    fi
334  else
335    ENCRYPT="${ENCRYPT} --encrypt-key=${GPG_ENC_KEY} --sign-key=${GPG_SIGN_KEY}"
336  fi
337elif [ -n "${PASSPHRASE}" ]; then
338  ENCRYPT=""
339fi
340
341NO_S3CMD="WARNING: s3cmd not found in PATH, remote file \
342size information unavailable."
343NO_S3CMD_CFG="WARNING: s3cmd is not configured, run 's3cmd --configure' \
344in order to retrieve remote file size information. Remote file \
345size information unavailable."
346
347NO_B2CMD="WARNING: b2 not found in PATH, remote file size information \
348unavailable. Is the python-b2 package installed?"
349
350if  [ "$(echo "${DEST}" | cut -c 1,2)" = "s3" ] || [ "$(echo "${DEST}" | cut -c 1,2)" = "bo" ]; then
351  DEST_IS_S3=true
352  S3CMD="$(command -v s3cmd)"
353  if [ ! -x "${S3CMD}" ]; then
354    echo "${NO_S3CMD}"; S3CMD_AVAIL=false
355  elif [ -z "${S3CMD_CONF_FILE}" ] && [ ! -f "${HOME}/.s3cfg" ]; then
356    S3CMD_CONF_FOUND=false
357    echo "${NO_S3CMD_CFG}"; S3CMD_AVAIL=false
358  elif [ -n "${S3CMD_CONF_FILE}" ] && [ ! -f "${S3CMD_CONF_FILE}" ]; then
359    S3CMD_CONF_FOUND=false
360    echo "${S3CMD_CONF_FILE} not found, check S3CMD_CONF_FILE variable in duplicity-backup's configuration!";
361    echo "${NO_S3CMD_CFG}";
362    S3CMD_AVAIL=false
363  else
364    # shellcheck disable=SC2034
365    S3CMD_AVAIL=true
366    # shellcheck disable=SC2034
367    S3CMD_CONF_FOUND=true
368    if [ -n "${S3CMD_CONF_FILE}" ] && [ -f "${S3CMD_CONF_FILE}" ]; then
369      # if conf file specified and it exists then add it to the command line for s3cmd
370      S3CMD="${S3CMD} -c ${S3CMD_CONF_FILE}"
371    fi
372  fi
373else
374  DEST_IS_S3=false
375fi
376
377if  [ "$(echo "${DEST}" | cut -c 1,2)" = "b2" ]; then
378  DEST_IS_B2=true
379  B2CMD="$(command -v b2)"
380  if [ ! -x "${B2CMD}" ]; then
381    echo "${NO_B2CMD}"
382    # shellcheck disable=SC2034
383    B2CMD_AVAIL=false
384  fi
385else
386  # shellcheck disable=SC2034
387  DEST_IS_B2=false
388fi
389
390config_sanity_fail()
391{
392  EXPLANATION=$1
393  CONFIG_VAR_MSG="Oops!! ${0} was unable to run!\nWe are missing one or more important variables in the configuration file.\nCheck your configuration because it appears that something has not been set yet."
394  echo -e "${CONFIG_VAR_MSG}\n  ${EXPLANATION}." >&2
395  echo -e "---------------------    END    ---------------------\n" >&5
396  exit 1
397}
398
399check_variables ()
400{
401  [[ ${ROOT} = "" ]] && config_sanity_fail "ROOT must be configured"
402  [[ ${DEST} = "" || ${DEST} = "s3+http://backup-foobar-bucket/backup-folder/" ]] && config_sanity_fail "DEST must be configured"
403  [[ ${INCLIST[0]} = "/home/foobar_user_name/Documents/" ]] && config_sanity_fail "INCLIST must be configured"
404  [[ ${EXCLIST[0]} = "/home/foobar_user_name/Documents/foobar-to-exclude" ]] && config_sanity_fail "EXCLIST must be configured"
405  [[ ( ${ENCRYPTION} = "yes" && (${GPG_ENC_KEY} = "foobar_gpg_key" || \
406       ${GPG_SIGN_KEY} = "foobar_gpg_key" || \
407       ${PASSPHRASE} = "foobar_gpg_passphrase")) ]] && \
408  config_sanity_fail "ENCRYPTION is set to 'yes', but GPG_ENC_KEY, GPG_SIGN_KEY, or PASSPHRASE have not been configured"
409  [[ ( ${DEST_IS_S3} = true && (${AWS_ACCESS_KEY_ID} = "foobar_aws_key_id" || ${AWS_SECRET_ACCESS_KEY} = "foobar_aws_access_key" )) ]] && \
410  config_sanity_fail "An s3 DEST has been specified, but AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY have not been configured"
411  [[ -n "${INCEXCFILE}" && ! -f ${INCEXCFILE} ]] && config_sanity_fail "The specified INCEXCFILE ${INCEXCFILE} does not exists"
412}
413
414mailcmd_sendmail() {
415  # based on http://linux.die.net/man/8/sendmail.sendmail
416  echo -e "From: ${EMAIL_FROM}\nSubject: ${EMAIL_SUBJECT}\n" | cat - "${LOGFILE}" | ${MAILCMD} "${EMAIL_TO}"
417}
418mailcmd_ssmtp() {
419  # based on http://linux.die.net/man/8/ssmtp
420  echo -e "From: ${EMAIL_FROM}\nSubject: ${EMAIL_SUBJECT}\n" | cat - "${LOGFILE}" | ${MAILCMD} "${EMAIL_TO}"
421}
422mailcmd_msmtp() {
423  # based on http://manpages.ubuntu.com/manpages/precise/en/man1/msmtp.1.html
424  echo -e "Subject: ${EMAIL_SUBJECT}\n" | cat - "${LOGFILE}" | ${MAILCMD} -f "${EMAIL_FROM}" -- "${EMAIL_TO}"
425}
426mailcmd_bsd_mailx() {
427  # based on http://man.he.net/man1/bsd-mailx
428  ${MAILCMD} -s "${EMAIL_SUBJECT}" -a "From: ${EMAIL_FROM}" "${EMAIL_TO}" < "${LOGFILE}"
429}
430mailcmd_heirloom_mailx() {
431  # based on http://heirloom.sourceforge.net/mailx/mailx.1.html
432  ${MAILCMD} -s "${EMAIL_SUBJECT}" -S from="${EMAIL_FROM}" "${EMAIL_TO}" < "${LOGFILE}"
433}
434mailcmd_nail() {
435  # based on http://linux.die.net/man/1/nail
436  ${MAILCMD} -s "${EMAIL_SUBJECT}" -r "${EMAIL_FROM}" "${EMAIL_TO}" < "${LOGFILE}"
437}
438mailcmd_else() {
439  ${MAILCMD} "${EMAIL_SUBJECT}" "${EMAIL_FROM}" "${EMAIL_TO}" < "${LOGFILE}"
440}
441
442email_logfile()
443{
444  if [ -n "${EMAIL_TO}" ]; then
445
446      MAILCMD=$(command -v "${MAIL}")
447      MAILCMD_REALPATH=$(readlink -e "${MAILCMD}")
448      MAILCMD_BASENAME=${MAILCMD_REALPATH##*/}
449
450      if [ ! -x "${MAILCMD}" ]; then
451          echo -e "Email couldn't be sent. ${MAIL} not available." >&2
452      else
453          EMAIL_SUBJECT=${EMAIL_SUBJECT:="duplicity-backup ${BACKUP_STATUS:-"ERROR"} (${HOSTNAME}) ${LOG_FILE}"}
454          case ${MAIL} in
455            ssmtp)
456              mailcmd_ssmtp;;
457            msmtp)
458              mailcmd_msmtp;;
459            mail|mailx)
460              case ${MAILCMD_BASENAME} in
461                bsd-mailx|mail.mailutils)
462                  mailcmd_bsd_mailx;;
463                heirloom-mailx)
464                  mailcmd_heirloom_mailx;;
465                s-nail)
466                  mailcmd_nail;;
467                *)
468                  mailcmd_else;;
469              esac
470              ;;
471            sendmail)
472              mailcmd_sendmail;;
473            nail)
474              mailcmd_nail;;
475            *)
476              mailcmd_else;;
477          esac
478
479          echo -e "Email notification sent to ${EMAIL_TO} using ${MAIL}"
480      fi
481  fi
482}
483
484send_notification()
485{
486  if [ -n "${NOTIFICATION_SERVICE}" ]; then
487    echo "-----------[ Notification Request ]-----------"
488    NOTIFICATION_CONTENT="duplicity-backup ${BACKUP_STATUS:-"ERROR"} [${HOSTNAME}] - \`${LOGFILE}\`"
489
490    if [ "${NOTIFICATION_SERVICE}" = "telegram" ]; then
491      curl -s --max-time 10 -d "chat_id=${TELEGRAM_CHATID}&disable_web_page_preview=1&text=${NOTIFICATION_CONTENT}" "https://api.telegram.org/bot${TELEGRAM_KEY}/sendMessage" >/dev/null
492      echo -e "Telegram notification sent"
493    else
494      echo -e "Unsupported notification service: ${NOTIFICATION_SERVICE}" >&2
495    fi
496
497    echo -e "\n----------------------------------------------\n"
498  fi
499}
500
501get_lock()
502{
503  echo "Attempting to acquire lock ${LOCKFILE}" >&5
504  if ( set -o noclobber; echo "$$" > "${LOCKFILE}" ) 2> /dev/null; then
505      # The lock succeeded. Create a signal handler to remove the lock file when the process terminates.
506      trap 'EXITCODE=$?; echo "Removing lock. Exit code: ${EXITCODE}" >> ${LOGFILE}; rm -f "${LOCKFILE}"' EXIT
507      echo "successfully acquired lock." >&5
508  else
509      # Write lock acquisition errors to log file and stderr
510      echo "lock failed, could not acquire ${LOCKFILE}" >&2
511      echo "lock held by $(cat "${LOCKFILE}")" >&2
512      email_logfile
513      send_notification
514      exit 2
515  fi
516}
517
518include_exclude()
519{
520  # Changes to handle spaces in directory names and filenames
521  # and wrapping the files to include and exclude in quotes.
522  OLDIFS=$IFS
523  IFS=$(echo -en "\t\n")
524
525  # Exclude device files?
526  if [ -n "${EXDEVICEFILES}" ] && [ "${EXDEVICEFILES}" -ne 0 ]; then
527    TMP=" --exclude-device-files"
528    EXCLUDE=${EXCLUDE}${TMP}
529  fi
530
531  for include in "${INCLIST[@]}"
532  do
533    if [[ -n "$include" ]]; then
534      TMP=" --include='$include'"
535      INCLUDE=${INCLUDE}${TMP}
536    fi
537  done
538
539  for exclude in "${EXCLIST[@]}"
540  do
541    if [[ -n "$exclude" ]]; then
542      TMP=" --exclude '$exclude'"
543      EXCLUDE=${EXCLUDE}${TMP}
544    fi
545  done
546
547  # Include/Exclude globbing filelist
548  if [ "${INCEXCFILE}" != '' ]; then
549    TMP=" --include-filelist '${INCEXCFILE}'"
550    INCLUDE=${INCLUDE}${TMP}
551  fi
552
553  # INCLIST and globbing filelist is empty so every file needs to be saved
554  if [ ${#INCLIST[@]} -eq 0 ] && [ "${INCEXCFILE}" == '' ]; then
555    EXCLUDEROOT=''
556  else
557    EXCLUDEROOT="--exclude=**"
558  fi
559
560
561  # Restore IFS
562  IFS=$OLDIFS
563}
564
565duplicity_cleanup()
566{
567  echo "----------------[ Duplicity Cleanup ]----------------"
568  if [[ "${CLEAN_UP_TYPE}" != "none" && -n ${CLEAN_UP_TYPE} && -n ${CLEAN_UP_VARIABLE} ]]; then
569    {
570      eval "${ECHO}" "${DUPLICITY}" "${CLEAN_UP_TYPE}" "${CLEAN_UP_VARIABLE}" "${STATIC_OPTIONS}" --force \
571        "${ENCRYPT}" \
572        "${DEST}"
573    } || {
574      BACKUP_ERROR=1
575    }
576    echo
577  fi
578  if [ -n "${REMOVE_INCREMENTALS_OLDER_THAN}" ] && [[ ${REMOVE_INCREMENTALS_OLDER_THAN} =~ ^[0-9]+$ ]]; then
579    {
580      eval "${ECHO}" "${DUPLICITY}" remove-all-inc-of-but-n-full "${REMOVE_INCREMENTALS_OLDER_THAN}" \
581        "${STATIC_OPTIONS}" --force \
582        "${ENCRYPT}" \
583        "${DEST}"
584    } || {
585      BACKUP_ERROR=1
586    }
587    echo
588  fi
589}
590
591duplicity_backup()
592{
593  {
594    eval "${ECHO}" "${DUPLICITY}" "${OPTION}" "${VERBOSITY}" "${STATIC_OPTIONS}" \
595    "${ENCRYPT}" \
596    "${EXCLUDE}" \
597    "${INCLUDE}" \
598    "${EXCLUDEROOT}" \
599    "${ROOT}" "${DEST}"
600  } || {
601    BACKUP_ERROR=1
602  }
603}
604
605duplicity_cleanup_failed()
606{
607  {
608    eval "${ECHO}" "${DUPLICITY}" "${OPTION}" "${VERBOSITY}" "${STATIC_OPTIONS}" \
609    "${ENCRYPT}" \
610    "${DEST}"
611  } || {
612    BACKUP_ERROR=1
613  }
614}
615
616setup_passphrase()
617{
618  if [ -n "${GPG_ENC_KEY}" ] && [ -n "${GPG_SIGN_KEY}" ] && [ "${GPG_ENC_KEY}" != "${GPG_SIGN_KEY}" ]; then
619    echo -n "Please provide the passphrase for decryption (GPG key 0x${GPG_ENC_KEY}): " >&3
620    builtin read -s -r ENCPASSPHRASE
621    echo -ne "\n" >&3
622    PASSPHRASE=${ENCPASSPHRASE}
623    export PASSPHRASE
624  fi
625}
626
627backup_this_script()
628{
629  if [ "$(echo "${0}" | cut -c 1)" = "." ]; then
630    SCRIPTFILE=$(echo "${0}" | cut -c 2-)
631    SCRIPTPATH=$(pwd)${SCRIPTFILE}
632  else
633    SCRIPTPATH=$(command -v "${0}")
634  fi
635  TMPDIR=duplicity-backup-$(date +%Y-%m-%d)
636  TMPFILENAME=${TMPDIR}.tar.gpg
637
638  echo "You are backing up: " >&3
639  echo "      1. ${SCRIPTPATH}" >&3
640
641  if [ -n "${GPG_ENC_KEY}" ] && [ -n "${GPG_SIGN_KEY}" ]; then
642    if [ "${GPG_ENC_KEY}" = "${GPG_SIGN_KEY}" ]; then
643      echo "      2. GPG Secret encryption and sign key: ${GPG_ENC_KEY}" >&3
644    else
645      echo "      2. GPG Secret encryption key: ${GPG_ENC_KEY} and GPG secret sign key: ${GPG_SIGN_KEY}" >&3
646    fi
647  else
648    echo "      2. GPG Secret encryption and sign key: none (symmetric encryption)" >&3
649  fi
650
651  if [ -n "${CONFIG}" ] && [ -f "${CONFIG}" ];
652  then
653    echo "      3. Config file: ${CONFIG}" >&3
654  fi
655
656  if [ -n "${INCEXCFILE}" ] && [ -f "${INCEXCFILE}" ];
657  then
658    echo "      4. Include/Exclude globbing file: ${INCEXCFILE}" >&3
659  fi
660
661  echo "Backup tarball will be encrypted and saved to: $(pwd)/${TMPFILENAME}" >&3
662  echo >&3
663  echo ">> Are you sure you want to do that ('yes' to continue)?" >&3
664  read -r ANSWER
665  if [ "${ANSWER}" != "yes" ]; then
666    echo "You said << ${ANSWER} >> so I am exiting now." >&3
667    echo -e "---------------------    END    ---------------------\n" >&5
668    exit 1
669  fi
670
671  mkdir -p "${TMPDIR}"
672  cp "${SCRIPTPATH}" "${TMPDIR}"/
673
674  if [ -n "${CONFIG}" ] && [ -f "${CONFIG}" ];
675  then
676    cp "${CONFIG}" "${TMPDIR}"/
677  fi
678
679  if [ -n "${INCEXCFILE}" ] && [ -f "${INCEXCFILE}" ];
680  then
681    cp "${INCEXCFILE}" "${TMPDIR}"/
682  fi
683
684  echo "Encrypting tarball, choose a password you'll remember..." >&3
685  tar -cf - "${TMPDIR}" | gpg -aco "${TMPFILENAME}"
686  rm -Rf "${TMPDIR}"
687  echo -e "\nIMPORTANT!!" >&3
688  echo ">> To restore these files, run the following (remember your password):" >&3
689  echo "gpg -d ${TMPFILENAME} | tar -xf -" >&3
690  echo -e "\nYou may want to write the above down and save it with the file." >&3
691}
692
693# ##################################################
694# ####        end of functions definition       ####
695# ##################################################
696
697check_variables
698
699echo -e "--------    START DUPLICITY-BACKUP SCRIPT for ${HOSTNAME}   --------\n" >&5
700
701echo -e "-------[ Program versions ]-------"
702echo -e "duplicity-backup.sh ${DBSH_VERSION}"
703echo -e "duplicity ${DUPLICITY_VERSION}"
704echo -e "----------------------------------\n"
705
706get_lock
707
708INCLUDE=
709EXCLUDE=
710EXCLUDEROOT=
711
712case "${COMMAND}" in
713  "backup-script")
714    backup_this_script
715    exit 0
716  ;;
717
718  "full")
719    OPTION="full"
720    include_exclude
721    duplicity_backup
722    duplicity_cleanup
723  ;;
724
725  "verify")
726    OLDROOT=${ROOT}
727    ROOT=${DEST}
728    DEST=${OLDROOT}
729    OPTION="verify"
730
731    echo -e "-------[ Verifying Source & Destination ]-------\n"
732    include_exclude
733    setup_passphrase
734    echo -e "Attempting to verify now ...\n" >&3
735    duplicity_backup
736    echo
737
738    OLDROOT=${ROOT}
739    ROOT=${DEST}
740    DEST=${OLDROOT}
741
742    echo -e "Verify complete.\n" >&3
743  ;;
744
745  "cleanup")
746    OPTION="cleanup"
747
748    if [ -z "${DRY_RUN}" ]; then
749      STATIC_OPTIONS="${STATIC_OPTIONS} --force"
750    fi
751
752    echo -e "-------[ Cleaning up Destination ]-------\n"
753    setup_passphrase
754    duplicity_cleanup_failed
755
756    echo -e "Cleanup complete."
757  ;;
758
759  "restore")
760    ROOT=${DEST}
761    OPTION="restore"
762    if [ -n "${TIME}" ]; then
763      STATIC_OPTIONS="${STATIC_OPTIONS} --time ${TIME}"
764    fi
765
766    if [[ ! "${RESTORE_DEST}" ]]; then
767      echo "Please provide a destination path (eg, /home/user/dir):" >&3
768      read -r -e NEWDESTINATION
769      DEST=${NEWDESTINATION}
770      echo ">> You will restore from ${ROOT} to ${DEST}" >&3
771      echo "Are you sure you want to do that ('yes' to continue)?" >&3
772      read -r ANSWER
773      if [[ "${ANSWER}" != "yes" ]]; then
774        echo "You said << ${ANSWER} >> so I am exiting now." >&3
775        echo -e "User aborted restore process ...\n" >&2
776        echo -e "---------------------    END    ---------------------\n" >&5
777        exit 1
778      fi
779    else
780      DEST=${RESTORE_DEST}
781    fi
782
783    setup_passphrase
784    echo "Attempting to restore now ..." >&3
785    duplicity_backup
786  ;;
787
788  "restore-file"|"restore-dir")
789    ROOT=${DEST}
790    OPTION="restore"
791
792    if [ -n "${TIME}" ]; then
793      STATIC_OPTIONS="${STATIC_OPTIONS} --time ${TIME}"
794    fi
795
796    if [[ ! "${FILE_TO_RESTORE}" ]]; then
797      echo "Which file or directory do you want to restore?" >&3
798      echo "(give the path relative to the root of the backup eg, mail/letter.txt):" >&3
799      read -r -e FILE_TO_RESTORE
800      echo
801    fi
802
803    if [[ "${RESTORE_DEST}" ]]; then
804      DEST=${RESTORE_DEST}
805    else
806      DEST=$(basename "${FILE_TO_RESTORE}")
807    fi
808
809    echo -e "YOU ARE ABOUT TO..." >&3
810    echo -e ">> RESTORE: ${FILE_TO_RESTORE}" >&3
811    echo -e ">> TO: ${DEST}" >&3
812    echo -e "\nAre you sure you want to do that ('yes' to continue)?" >&3
813    read -r ANSWER
814    if [ "${ANSWER}" != "yes" ]; then
815      echo "You said << ${ANSWER} >> so I am exiting now." >&3
816      echo -e "User aborted restore process ...\n" >&2
817      echo -e "---------------------    END    ---------------------\n" >&5
818      exit 1
819    fi
820
821    FILE_TO_RESTORE="'${FILE_TO_RESTORE}'"
822    DEST="'${DEST}'"
823
824    setup_passphrase
825    echo "Restoring now ..." >&3
826    #use INCLUDE variable without creating another one
827    INCLUDE="--file-to-restore ${FILE_TO_RESTORE}"
828    duplicity_backup
829  ;;
830
831  "list-current-files")
832    OPTION="list-current-files"
833
834    if [ -n "${TIME}" ]; then
835      STATIC_OPTIONS="${STATIC_OPTIONS} --time ${TIME}"
836    fi
837
838    # shellcheck disable=SC2086
839    eval \
840    "${DUPLICITY}" "${OPTION}" "${VERBOSITY}" "${STATIC_OPTIONS}" \
841    ${ENCRYPT} \
842    "${DEST}"
843  ;;
844
845  "collection-status")
846    OPTION="collection-status"
847
848    # shellcheck disable=SC2086
849    eval \
850    "${DUPLICITY}" "${OPTION}" "${VERBOSITY}" "${STATIC_OPTIONS}" \
851    ${ENCRYPT} \
852    "${DEST}"
853  ;;
854
855  "backup")
856    include_exclude
857    duplicity_backup
858    duplicity_cleanup
859  ;;
860
861  *)
862    echo -e "[Only show $(basename "$0") usage options]\n"
863    usage
864  ;;
865esac
866
867echo -e "---------    END DUPLICITY-BACKUP SCRIPT    ---------\n" >&5
868
869if [ "${USAGE}" ]; then
870  exit 0
871fi
872
873if [ "${BACKUP_ERROR}" ]; then
874  BACKUP_STATUS="ERROR"
875else
876  BACKUP_STATUS="OK"
877fi
878
879# send email
880[[ ${BACKUP_ERROR} || ! "$EMAIL_FAILURE_ONLY" = "yes" ]] && email_logfile
881
882# send notification
883[[ ${BACKUP_ERROR} || ! "$NOTIFICATION_FAILURE_ONLY" = "yes" ]] && send_notification
884
885# remove old logfiles
886# stops them from piling up infinitely
887[[ -n "${REMOVE_LOGS_OLDER_THAN}" ]] && find "${LOGDIR}" -type f -mtime +"${REMOVE_LOGS_OLDER_THAN}" -delete
888
889if [ "${ECHO}" ]; then
890  echo "TEST RUN ONLY: Check the logfile for command output."
891fi
892
893unset AWS_ACCESS_KEY_ID
894unset AWS_SECRET_ACCESS_KEY
895unset AWS_PROFILE
896unset PASSPHRASE
897unset SIGN_PASSPHRASE
898unset BACKEND_PASSWORD
899
900# restore stdout and stderr to their original values
901# and close the other fd
902exec 1>&6 2>&7 3>&- 4>&- 5>&- 6>&- 7>&-
903
904# vim: set tabstop=2 shiftwidth=2 sts=2 autoindent smartindent: