#!/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
    }

# --------------------------------------------------------------------------------