#!/bin/bash # ================================================================================ # # TRANSFER :: PLUGIN - RESTIC # # this script will be included in ../../transfer.sh # # -------------------------------------------------------------------------------- # ah - Axel Hahn <axel.hahn@iml.unibe.ch> # 2021-05-19 ah v0.1 INIT ... WIP # 2022-01-06 ah v0.2 added support for Repository with REST and authentication # 2022-02-09 ah v0.3 show diff to last backup; update pruning # 2022-02-09 ah v0.3 update pruning; more keep-params # 2022-03-07 ah v0.4 add verify in post task # 2022-05-10 ah v0.5 fix handling with nocache flag (use globally as default param - not in backup only) # 2022-05-16 ah v0.6 added restic prune # 2022-10-21 ah v0.7 simplify restic exec commands in _restic; remove --prune in check # 2022-11-04 ah v1.0 one command for forget and prune; rename hooks # 2023-03-20 ah v1.1 use vss for backup on MS Windows # 2024-01-23 ah v1.2 prune uses --max-unused unlimited # 2024-01-31 ah v1.3 fix replace of fqdn # ================================================================================ # -------------------------------------------------------------------------------- # INIT # -------------------------------------------------------------------------------- function t_checkRequirements(){ j_requireBinary "restic" j_requireUser "root" } function t_setVars(){ export RESTIC_PASSWORD=$PASSPHRASE # if we set RESTIC_REPOSITORY then "-r TARGET" is not # needed in restic commands # TODO: for restic with https and auth - remove the host in the path local _target=$( j_getFullTarget ) if grep "^rest:http.*@" <<< "${_target}" >/dev/null; then local _regex=${FQDN//\./\\.} _target=$( echo $_target | sed "s#${_regex}/##" ) fi export RESTIC_REPOSITORY=$_target # WORKAROUND for bug while writing on a SMB target export GODEBUG="asyncpreemptoff=1" RESTORE_ITEM=latest RESTIC_MOUNTPOINT=$( _j_getvar ${STORAGEFILE} "${CFGPREFIX}mountpoint" ) } # -------------------------------------------------------------------------------- # GENERATE PARAMS :: ALL DIRS # -------------------------------------------------------------------------------- # return a string with default params # param string param1 of transfer.sh; one of full|inc|auto # param string param2 of transfer.sh; for auto: date i.e. 3M for 3 monthes function t_getParamDefault(){ # verbose to see more details echo -n --verbose=$( _j_getvar ${STORAGEFILE} "${CFGPREFIX}verbose" ) # no cache ... to create no local cache dirs, what saves space but backup + verify is much slower _nocacheFlag=$( _j_getvar ${STORAGEFILE} "${CFGPREFIX}nocache" ) if [ "$_nocacheFlag" != "" ] && [ "$_nocacheFlag" != "0" ] && [ "$_nocacheFlag" != "false" ]; then echo -n "--no-cache " fi } # return a string with backup parameters that will be added to defaults function t_getParamBackup(){ local _tag local _nocacheFlag # tagging _tag=$( _j_getvar ${STORAGEFILE} "${CFGPREFIX}tag" ) if [ "$_tag" != "" ]; then echo -n "--tag $_tag " fi # Use VSS on MS Windows if _isMswindows; then echo -n "--use-fs-snapshot " fi } # return a cli parameter for a single exlude directory # param string cache directory for local index files function t_getParamCacheDir(){ if [ ! -z "$1" ]; then local sCacheDir="$1" if [ ! -d $sCacheDir ]; then mkdir -p $sCacheDir chmod 750 $sCacheDir fi echo --cache-dir $sCacheDir fi } # return a cli parameter for a single exlude directory # param string exlude pattern function t_getParamExlude(){ test -z "$1" || echo --exclude "'"$*"'" } # return a cli parameter for a single exlude directory # param string exlude pattern function t_getParamInlude(){ test -z "$1" || echo --include "'"$*"'" } # return a cli parameter to use an ssh keyfile # param string filename if ssh private key file function t_getParamSshKey(){ if [ ! -z "$1" ]; then echo $STORAGE_BASEDIR | grep -E "^(scp|sftp|rsync):" >/dev/null if [ $? -eq 0 ]; then # scp://backup@storage.example.com//netshare/backup/one # ^^^^^^^^^^^^^^^^^^^^^^^^^^ # sshtarget fetches user@host ... if protocol matches local sshtarget=$( echo $STORAGE_BASEDIR | cut -f 3 -d '/' ) echo -o sftp.command="'"ssh -i ${1} ${sshtarget} -s sftp"'" fi fi } # execute a restic command # param string options and subcommand function _restic(){ local _mycmd="restic $* ${ARGS_DEFAULT}" echo "$_mycmd" sleep 3 color cmd eval "$_mycmd" local _myrc=$? color reset return $_myrc } # -------------------------------------------------------------------------------- # BACKUP ACTIONS :: TRANSFER # -------------------------------------------------------------------------------- # pre backup actions # uses global vars from ../../transfer.sh # function t_backupDoPreTasks(){ function t_backupDoPreTasks(){ # if eval restic snapshots ${ARGS_DEFAULT} >/dev/null 2>&1 if _restic list keys >/dev/null 2>&1 then echo "__REPO__ OK, Backup repository already exists." echo "--- UNLOCK ... just in case :-)" _restic unlock echo else echo "Backup repository needs to be created." _restic init local _myrc=$? # detect return code ... and abort on any error. t_rcCheckInit $_myrc fi echo } # post backup actions # uses global vars from ../../transfer.sh function t_backupDoPostTasks(){ # -------------------- echo "--- UNLOCK" _restic unlock echo } # prune old data # uses global vars from ../../transfer.sh # return exitcode of restic prune function t_backupDoPrune(){ # -------------------- echo "--- FORGET AND PRUNE (in all pathes of repository)" local _tag=$( _j_getvar ${STORAGEFILE} "${CFGPREFIX}tag") local _mycmd="forget \ --tag $_tag \ --group-by paths,tags \ --prune " local _keep for mykeep in last hourly daily weekly monthly yearly within within-hourly within-daily within-weekly within-monthly within-yearly tag do _keep=$( _j_getvar "${STORAGEFILE}" "${CFGPREFIX}keep-${mykeep}") if [ -n "$_keep" ]; then _mycmd+="--keep-${mykeep} ${_keep} " fi done _mycmd+=$( _j_getvar "${STORAGEFILE}" "restic_prune-params") _restic "$_mycmd" _myrc=$? t_rcCheckCleanup $_myrc echo _j_runHooks "330-after-prune" "$_myrc" echo return $_myrc } # verify backup data # uses global vars from ../../transfer.sh # return exitcode of restic check function t_backupDoVerify(){ # -------------------- echo "--- VERIFY (whole repository)" # param --read-data takes a long time. Maybe use an extra job with it. # _mycmd="time restic check ${ARGS_DEFAULT} --with-cache --read-data" _restic check --with-cache _myrc=$? t_rcCheckVerify $_myrc _j_runHooks "340-after-verify" "$_myrc" echo return $_myrc } # -------------------------------------------------------------------------------- # BACKUP ACTIONS :: SINGLE DIR # -------------------------------------------------------------------------------- # get target url/ directory # param string directory to backup function t_backupDirGetTarget(){ # directory based target # j_getFullTarget "$1" # for host based backup target - remove param: j_getFullTarget "" } function t_backupDirGetCmdBackup(){ echo eval restic backup ${ARGS_DEFAULT} ${ARGS_BACKUP} ${RESTIC_PARAMS} ${mydir} } # pre backup actions # uses global vars from ../../transfer.sh function t_backupDirDoPreTasks(){ echo "Nothing to do." } # post backup actions # uses global vars from ../../transfer.sh function t_backupDirDoPostTasks(){ echo "--- SHOW CHANGES between last 2 snapshots" local _data local _snapshotLast local _snapshotNow # get list of snapshots and filter the lines with a date YYYY-MM-DD _data=$( t_restoreDoShowVolumes | grep "[12][0-9][0-9][0-9]-[0-2][0-9]-[0-3][0-9]" | tail -5 ) # echo "..." # echo "$_data" _snapshotLast=$( echo "$_data" | tail -2 | head -1 | cut -f 1 -d " ") _snapshotNow=$( echo "$_data" | tail -1 | cut -f 1 -d " ") if [ "${_snapshotLast}" = "${_snapshotNow}" ]; then echo "This was the initial (full) Backup" else _restic diff "${_snapshotLast}" "${_snapshotNow}" fi echo } # -------------------------------------------------------------------------------- # RESTORE # -------------------------------------------------------------------------------- # show stored volumes on backup repository # used in restore; directory param is checked before # param string name of backup dir, i.e. /etc function t_restoreDoShowVolumes(){ eval restic snapshots ${ARGS_DEFAULT} --path ${BACKUP_DIR} } # select a snapshot to restore from function t_restoreDoSelect(){ local _selid= echo "--- Existing snapshots:" color cmd t_restoreDoShowVolumes color reset showPrompt "ID of the snapshot or \"latest\" to restore from [${RESTORE_ITEM}] >" read _selid test -z "$_selid" && _selid=${RESTORE_ITEM} RESTORE_ITEM=${_selid} test "$RESTORE_ITEM" = "latest" && RESTORE_ITEMINFO="automatic value" test "$RESTORE_ITEM" = "latest" || RESTORE_ITEMINFO=$( t_restoreDoShowVolumes | grep "^${RESTORE_ITEM} " | awk '{ print $2 " " $3} ' ) if [ -z "${RESTORE_ITEMINFO}" ]; then color error echo ERROR: A napshot ID \"${_selid}\" does not exist. RESTORE_ITEM=latest color reset fi echo using \"${RESTORE_ITEM}\" echo } # set a filter to reduce count of files to restore function t_restoreDoSetIncludeFilter(){ local _inc= echo "You can enter ..." echo " - a single directory name anywhere in the folderstructure" echo " - a filename without path" echo " - a filemask" showPrompt "Include >" read _inc RESTORE_FILTER="$( t_getParamInlude "${_inc}" )" echo using parameter \"${RESTORE_FILTER}\" echo } # show stored volumes on backup repository # used in restore; directory param is checked before # param string name of backup dir, i.e. /etc function t_restoreDoRestore(){ echo "eval restic restore ${ARGS_DEFAULT} --path ${BACKUP_DIR} --target ${RESTORE_TARGETPATH} ${RESTORE_FILTER} ${RESTORE_ITEM}" } # Mount backup data # see "restic help mount" function t_restoreDoMountBackupdata(){ local _cmd= echo "HINT: This feature requires fuse. It works with root on UNIX/ LINUX platforms - not on MS Windows." echo " It can mount a single directory and shows all snapshots (not only [$RESTORE_ITEM])." echo if [ -z "$RESTIC_MOUNTPOINT" ]; then color error echo "ERROR: no mountpoint was set in ${STORAGEFILE}; example: restic_mountpoint = /mnt/restore" color reset else j_requireUser "root" test -d "$RESTIC_MOUNTPOINT" || mkdir -p $RESTIC_MOUNTPOINT _cmd="restic mount ${ARGS_DEFAULT} $RESTIC_MOUNTPOINT" test -z "${BACKUP_DIR}" || _cmd="restic mount ${ARGS_DEFAULT} --path ${BACKUP_DIR} $RESTIC_MOUNTPOINT" echo $_cmd color cmd eval $_cmd color reset fi } # search a file in the given snapshot and backup dir # param string regex to filter function t_restoreDoSearchFile(){ eval restic ls ${ARGS_DEFAULT} --path "${BACKUP_DIR}" ${RESTORE_ITEM} | grep -E "$1" } # -------------------------------------------------------------------------------- # VERIFY RETURNCODES # -------------------------------------------------------------------------------- # init repository # param integer exitcode of command function t_rcCheckInit(){ echo -n "__REPO__ " case $1 in 0) color ok; echo "OK - the repository was created." ;; 1) color warning; echo "FAILED - You can ignore the error if the repository was initialized already." ;; *) color error; echo "FAILED - Verify output above - returncode of restic init was $1" ;; esac color reset } # backup files # param integer exitcode of command # param string directory that was backed up function t_rcCheckBackup(){ echo -n "__BACKUP__ " case $1 in 0) color ok; echo "OK - DIR ${2}" ;; 1) color error; echo "FAILED - DIR ${2} - Unable to connect with restic repository." ;; *) color error; echo "FAILED - DIR ${2} - Backup error - returncode was $1" ;; esac color reset } # repository cleanup # param integer exitcode of command function t_rcCheckCleanup(){ echo -n "__CLEANUP__ " case $1 in 0) color ok; echo "OK" ;; *) color error; echo "FAILED ${2} - Cleanup error - returncode was $1" ;; esac color reset } # verify backup data # param integer exitcode of command function t_rcCheckVerify(){ echo -n "__VERIFY__ " case $1 in 0) color ok; echo "OK" ;; *) color error; echo "FAILED - returncode was $1" ;; esac color reset } # restore files # param integer exitcode of command function t_rcCheckRestore(){ echo -n "__RESTORE__ " case $1 in 0) color ok; echo "OK" ;; *) color error; echo "Restore error - returncode was $1" ;; esac color reset } # --------------------------------------------------------------------------------