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