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: