#!/bin/bash
# ======================================================================
#
# RESTIC REST PRUNER
#
# ----------------------------------------------------------------------
# 2024-02-01  v0.1  <axel.hahn@unibe.ch>  first lines
# 2024-02-02  v0.2  <axel.hahn@unibe.ch>  add: timer, skip file, skin N days, limit process time, stats
# 2024-02-03  v0.3  <axel.hahn@unibe.ch>  enable cache dir; unlock before pruning
# ======================================================================


cd "$( dirname $0 )" || exit
_version=0.3
logdir=_last_prune

prune_basedir=
prune_params=
prune_skipdays=7
prune_cachedir=

# stop pruning more repositories when running longer N seconds
prune_timeout=7200

# flag file inside repository to mark an archived backup
sSkipfile=inactive.txt

bOptAll=0
bOptDebug=0
bOptStats=0
bOptForce=0

typeset -i iTimerStart; iTimerStart=$( date +%s )

typeset -i iCountDirs=0
typeset -i iCountMatch=0
typeset -i iCountPrune=0
typeset -i iCountPruneError=0

typeset -i rcAll=0

. "inc_config.sh" || exit 1
cfgfile=rest_pruner.cfg || exit 1

test -z "$prune_cachedir" && prune_cachedir="${prune_basedir}/.cache_for_pruning"


# ----------------------------------------------------------------------
# FUNCTIONS
# ----------------------------------------------------------------------

# Show help text
function _showHelp(){
    local _self; _self=$( basename $0 )
    cat <<EOH
Pruner for restic rest server with append only option.
This script prunes all repositories on server side.

The config file [$cfgfile] contains <USER>:<RESTIC_PASSWORD>
If a directory matches ${prune_basedir}/<USER> then it will be pruned.

Institute for Medical Education * University of Bern
GNU GPL 3.0


SYNTAX:
  $_self [OPTIONS] [FILTER]


OPTIONS:
  -h, --help           show help and exit.
  -a, --all            process all repositories, default: rest only + archives
  -d, --debug          show more infos
  -f, --force          force pruning; do not wait $prune_skipdays days
  -s, --status         show pruning status with last prunes


PARAMETERS:
  FILTER               regex to filter directory list in
                       ${prune_basedir}/*

EXAMPLES:
  $_self
                       Start pruning of all matching repositories

  $_self mail
                       Prune servers that match "mail",
                       eg. my-mailhub.example.com

  $_self --force mail
                       Prune servers that match "mail",
                       eg. my-mailhub.example.com

  $_self --status
                       Show statistics with last prunes, if it is archive or
                       running or on error.
                       It shows repositories with passwords and archives.
                       Other repositories are not visible until you
                       specify --all.

EOH
}

# get age of a file in sec
# https://www.axel-hahn.de/blog/2020/03/09/bash-snippet-alter-einer-datei/
# param  string  filename to test
#
function _getFileAge(){
    echo $(($(date +%s) - $(date +%s -r "$1")))
}

# get execution time in seconds
# global  integer  iTimerStart  starting time in sec
function _getTimer(){
    local _iTimer
    _iTimer=$( date +%s )
    echo $(( _iTimer - iTimerStart ))
}

# start prune of a given repository
# global  string   cfgfile  name of the config file
# global  integer  rcAll    sum of all prune exit status
#
# param   string            directory to prune
function _prune(){
        local _dir="$1"

        local mybase; mybase=$( basename "${_dir}" )
        local mypw; mypw=$( grep "^${mybase}:" "${cfgfile}" | cut -f2 -d ':')
        local logfile="${logdir}/${mybase}.log"

        iCountDirs+=1

        if [ -n "$mypw" ]; then

            iCountMatch+=1
            bDoRun=1

            if [ -f "$_dir/${sSkipfile}" ]; then
                echo "SKIP: $_dir - inactive backup"
                bDoRun=0
            elif ps -eo command | grep "restic forget.*${_dir}" | grep -v "grep" | grep . ; then
                echo "SKIP: $_dir - a process is still running..."
                bDoRun=0
            else
                rm -f "${logfile}.running"

                if [ "$bOptForce" = "0" ] && [ -f "${logfile}" ]; then
                    local _iAge; _iAge=$( _getFileAge "${logfile}" )
                    local _iDays; typeset -i _iDays=$(( _iAge/60/60/24 ))
                    if [ $_iDays -le $prune_skipdays ]; then
                        echo "SKIP: $_dir - Last run $_iDays days ($_iAge sec) ago - a prune will be started after $prune_skipdays days."
                        bDoRun=0
                    fi
                fi
            fi

            if [ "$bDoRun" -eq "1" ]; then
                echo ">>>>> $( _getTimer ) sec >>>>> $_dir"

                local _user; 
                _user=$( stat -c "%U" "$_dir" )
                
                local _cachedir; 
                export _cachedir=$prune_cachedir/${mybase}
                echo "Creating cache $_cachedir ..."
                test -d "$_cachedir" || mkdir -p "$_cachedir"
                chown ${_user}:${_user} "$_cachedir"
                
                echo "Starting prune as user $_user ..."
                su - $_user - /bin/bash -c "
                        echo START $( date ) $_dir
                        export RESTIC_PASSWORD=$mypw
                        set -vx
                        restic unlock -r $_dir --cache-dir=$_cachedir 2>&1
                        restic forget -r $_dir --cache-dir=$_cachedir $prune_params 2>&1
                " | tee "${logfile}.running"
                rc=${PIPESTATUS[0]}
                rcAll+=$rc
                echo END $( date ) exitcode $rc | tee -a "${logfile}.running"
                if [ "$rc" -eq "0" ]; then
                        mv "${logfile}.running" "${logfile}"
                        iCountPrune+=1
                else
                        iCountPruneError+=1
                        mv "${logfile}.running" "${logfile}.error"
                        echo "!!! ERROR !!! pruning failed."
                fi
                echo "Removing cache $_cachedir ..."
                rm -rf "$_cachedir"
            fi
        else
            test "$bOptAll" -eq "1" && echo "SKIP: $_dir - no password for this repo"
        fi
}


# start prune of a given repository
# global  string   cfgfile  name of the config file
# global  integer  rcAll    sum of all prune exit status
#
# param   string            directory to prune
function _status(){

        local _processes; 
        _processes="$( ps -eo command )"
        
        echo "Prune status:"
        for _dir in $( find ${prune_basedir} -maxdepth 1 -type d | grep -E "$filter")
        do
            local mybase; mybase=$( basename "${_dir}" )
            local mypw; mypw=$( grep "^${mybase}:" "${cfgfile}" | cut -f2 -d ':')
            local logfile="${logdir}/${mybase}.log"


            local _flagPw="."
            local _flagArchive="."
            local _flagRunning="."
            local _flagError="."
            local _sLastPrune="?"

            iCountDirs+=1

            test -n "$mypw" && _flagPw="Y"
            test -f "$_dir/${sSkipfile}" && _flagArchive="A"
            test -f "${logfile}.error" && _flagError="E"
            grep "restic forget.*${_dir}" <<< "$_processes" | grep -v "grep" | grep . >/dev/null && _flagRunning="R"

            if [ -f "${logfile}" ]; then
                local _iAge; _iAge=$( _getFileAge "${logfile}" )
                local _iDays; typeset -i _iDays=$(( _iAge/60/60/24 ))
                _sLastPrune="$_iDays days ago"
            fi

            if [ -n "$mypw" ] ||  [ "$_flagArchive" = "A" ] || [ "$bOptAll" -eq "1" ]; then
                iCountMatch+=1
                printf "%1s %1s %1s %1s  %-12s %s\n" "$_flagPw"  "$_flagArchive" "$_flagError" "$_flagRunning" "$_sLastPrune" "$_dir"
            fi

        done
        if [ "$iCountMatch" -gt "0" ]; then
            echo
            echo ": : : :  :            :"
            echo ": : : :  :            +-- repository dir"
            echo ": : : :  +-- last successful prune"
            echo ": : : +-- is it currently running?"
            echo ": : +-- Last prune on error?"
            echo ": +-- Repository is archived?"
            echo "+-- has password?"
        else
            echo "INFO: No (matching) Repository was found."
        fi
        echo
}
# ----------------------------------------------------------------------
# MAIN
# ----------------------------------------------------------------------

cat << ENDHEADER
------------------------------------------------------------------------------
  --------========###|    RESTIC REST PRUNER :: v${_version}    |###=======--------
------------------------------------------------------------------------------

ENDHEADER

# ----- check parameters
allparams="$*"
while [[ "$#" -gt 0 ]]; do case $1 in
    -h|--help)        _showHelp; exit 0;;
    -a|--all)         bOptAll=1; shift;;
    -d|--debug)       bOptDebug=1; shift;;
    -f|--force)       bOptForce=1; shift;;
    -s|--status)      bOptStats=1; shift;;
    *) if grep "^-" <<< "$1" >/dev/null ; then
        echo; echo "ERROR: Unknown parameter: $1"; echo; _showHelp; exit 2
       fi
       break;
       ;;
esac; done

test "$bOptDebug" -eq "1" && cat << ENDDEBUGINFO
----- 8< ----- DEBUG ------------------------------------------------ 8< -----

Config:
$( ls -l inc_config.sh && echo )
$( grep "prune_.*=" inc_config.sh | grep -v "#")

$( ls -l ${cfgfile} 2>&1 && echo)

Parameters: $allparams

----- 8< ----- /DEBUG ----------------------------------------------- 8< -----

ENDDEBUGINFO


# ----- verify needed settings

if [ ! -f "$cfgfile" ]; then
    echo "ERROR: The configuration [$cfgfile] does not exist (yet)."
    exit 3
fi

if [ "$USER" != "root" ]; then
        echo "ERROR: This script must be started as root."
        exit 1
fi

cfgpermissions=$( stat -c '%A:%U:%G' "$cfgfile" )
if [ "$cfgpermissions" != "-r--------:root:root" ]; then
    echo "WARNING: The configuration file must be owned by root:root with permission 0400."
    ls -l "$cfgfile"
    echo "Try to fix it..."
    chown root:root "$cfgfile"
    chmod 0400 "$cfgfile"
    ls -l "$cfgfile"
    cfgpermissions=$( stat -c '%A:%U:%G' "$cfgfile" )
    if [ "$cfgpermissions" != "-r--------:root:root" ]; then
        echo "Fix failed. Aborting."
        exit 3
    fi
    echo "OK"
fi

if [ -z "${prune_basedir}" ]; then
    echo "ERROR: variable [prune_basedir] was not set in [inc_config.sh]"
    exit 3
fi

if [ ! -d "${prune_basedir}" ]; then
    echo "ERROR: variable [prune_basedir] in [inc_config.sh] points to a directory"
    echo "that does not exist: ${prune_basedir}"
    exit 3
fi

if [ -z "${prune_params}" ]; then
    echo "ERROR: variable [prune_params] was not set in [inc_config.sh]"
    exit 3
fi

if ! grep "\-\-keep\-" <<< "${prune_params}" >/dev/null;  then
    echo "ERROR: variable [prune_params] does not contain a --keep-* prameter."
    exit 3
fi

if ! which restic >/dev/null; then
    echo "ERROR: The restic client was not found."
    exit 3
fi

# ----- Go

test -d "${logdir}" || mkdir -p "${logdir}"
filter=${1:-.}

test "$bOptStats" -eq "1" && _status "${mydir}"

test "$bOptStats" -eq "0" && for mydir in $( find ${prune_basedir} -maxdepth 1 -type d | grep -E "$filter")
do
        _iTimer=$( _getTimer )
        if [ "$_iTimer" -gt "$prune_timeout" ]; then
            echo "ABORT: Processing time reached $_iTimer sec - limit is $prune_timeout sec."
            echo "Stopping before processing ${mydir}."
            echo
            break;
        fi
        _prune "${mydir}"
done

test "$bOptStats" -eq "0" && cat <<ENDSTATS

Statistics:

Elapsed time : $( _getTimer ) sec
Directories  : $iCountDirs
Matching     : $iCountMatch
Pruned       : $iCountPrune
Prune erors  : $iCountPruneError
------------------------------------------------------------------------------
ENDSTATS

echo "done - exitcode $rcAll"
exit $rcAll

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