#!/bin/bash # Delbackups bash script -- Delete recursive backups by his label # Based on name format like: "rootDir-%Y-%m-%d_%H:%M" # note that separators `-`, `:` or `_` could by replaced by any non number # character: "rootDir-%Y-%m-%d %Hh%M", "rootDir_%Y.%m.%d %H-%M" # (C) 2000-2024 F-Hauri - http://www.f-hauri.ch # Version: 0.1.12 -- Last update: Thu Feb 27 14:52:56 CET 2025 # Licensed under terms of LGPL v3. www.gnu.org # Goal of this is to keep N backup by kind of consideration: # shellcheck disable=SC2059 # No vars in printf... dtFmt, fmt[ltr], bkNamFmt shopt -s extglob nWeeks=15 nMonths=18 nDays=12 nHalfHours=72 nYears=30 base="base" root="." QUIET=false statbackups=false bkNamFmt='' suf='' slPath='' usage() { cat <<-EOF Usage: ${0##*/} [OPTIONS] [/path/to/root] [baseDir] -Y Int Number of Years to keep [$nYears] -M Int Number of Months to keep [$nMonths] -W Int Number of Weeks to keep [$nWeeks] -D Int Number of Days to keep [$nDays] -H Int Number of Half hours to keep [$nHalfHours] -q Quiet (disabled by -s) -s Disk usage Stats (\`du\` could take lot of time) implie ! -q -r Path Path to root of backups [$root] -b Dir Base dir name [$base] -h show This -x suffiX or eXtension [$suf] -l Path Create symlinks into Path (will be destroyed) -R Show sample Rsync script -[y|n] Answer for ultime question: Do delete unwanted backups? Note: [-r] and [-b] switches are ignored if submited as 1st and 2nd arguments. EOF } readCmdLine(){ local opt OPTARG OPTIND local -A 'iStrs=([Y]=nYears [W]=nWeeks [M]=nMonths [D]=nDays [H]=nHalfHours [l]=slPath [r]=root [b]=base [x]=suf )' while getopts "ynqshRY:M:W:D:H:r:b:x:l:" opt; do case $opt in [YMWDHrbxl] ) # shellcheck disable=SC2154 # Referenced but not assigned. printf -v "${iStrs["$opt"]}" %s "$OPTARG" ;; q ) QUIET=true ;; s ) statbackups=true;QUIET=false ;; h ) usage;exit 0 ;; R ) declare -f backupSample;exit 0 ;; [yn] ) [[ -v doDelete ]] && die '-y and -n are exclusive!' doDelete=$opt;; esac done shift $((OPTIND-1)) [[ $1 ]] && [[ -d $1 ]] && root=$1 [[ $2 ]] && [[ -d $root/$2$suf ]] && base=$2 } backupSample(){ : ' Short sample showing how rsync will maintain $base up to date. ' local base=base root="/path/to/backups/root/" suf='' cpylink local dateFmt="${base}_%Y-%m-%d_%Hh%M" cd "$root" || exit 1 test -d "$base$suf" || exit 1 cpylink=$(date -r "$base$suf" +"$dateFmt") test -d "$cpylink" || cp -al "$base$suf" "$cpylink" : 'Filsystem create and mount snapshot at Source!' ssh source <<< "lvcreate -s -L 10G -n Snap Grp/Vol; mount /dev/Grp/Snap path/to/source/; touch path/to/source/" rsync -ax ... source:path/to/source/. "$base/". ssh source <<<'umount path/to/source/;lvremove -f Grp/Snap' } die() { printf >&2 '%s ERROR: %s\n' "${0##*/}" "$*" exit 1 } human() { local _hu=0 _v=${1}0000 _abr=("${abr[@]}") for ((; ${#_v}>7; _v>>=10,_hu++)) { :;} printf -v "$2" %.3f%s \ "${_v:0:$((${#_v}-4))}.${_v:$((${#_v}-4))}" "${_abr[_hu]}" } doYMReStr() { local Cm C8y i local -a restr=() month=() for i in {1..12}; do TZ=UTC printf -v "month[i]" "%(%b)T" $((29000000+i*86400*32)) done printf -v Cm "%(%Y%m)T" "$now" C8y=${Cm::4} for ((i=nYears;i--;)); do printf -v "descr[Y$C8y]" "%s" "$C8y" restr+=("$C8y") ((C8y-=1)) done restr=("${restr[*]}"); printf -v "reStrs[Y]" "%s" "${restr// /|}" restr=() for ((i=nMonths;i--;)) ;do printf -v "descr[M$Cm]" "%s %s" "${month[10#${Cm:4}]}" "${Cm::4}" restr+=("$Cm") ((Cm=(Cm-1)%100?Cm-1:Cm-89)) done restr=("${restr[*]}"); printf -v "reStrs[M]" "%s" "${restr// /|}" } doReStr() { local ltr lhs rhs refmt dscfmt local -a restr=() IFS=\; read -r ltr lhs rhs refmt dscfmt <<<"$1" for ((i=lhs;i<=now;i+=rhs)) ;do printf -v o "%($refmt)T" $i printf -v "descr[$ltr$o]" "%($dscfmt)T" $i restr+=("$o") done restr=("${restr[*]}"); printf -v "reStrs[$ltr]" "%s" "${restr// /|}" } buildReStrs() { printf -v now "%(%s)T" -1 doYMReStr doReStr 'W;now-604800*nWeeks;604800;%Y%W;#%W %Y' doReStr 'D;now-86400*nDays;86400;%Y%m%d;%d %b %y' doReStr 'H;(now-1800*nHalfHours)/1800*1800;1800;%Y%m%d%H%M;%d %b %H:%M' } selthisfile() { local letter="$1" file="$2" seconds="$3" this local -n expr="reStrs[$letter]" local -A fmt=([Y]='%(%Y)T' [M]='%(%Y%m)T' [W]='%(%Y%W)T' [D]='%(%Y%m%d)T' [H]='%(%Y%m%d%H%M)T'); [ "$letter" = "H" ] && seconds=$(( seconds / 1800 * 1800 + 1800 )) printf -v this "${fmt[$letter]}" "$seconds" [ -z "${this//*($expr)}" ] && selfiles[$letter$this]=$file } rootsSelection() { local sedCmd='\([0-9]\+\)' fileAsSec thisfile kind allRoots dtFmt printf -v sedCmd \ 's/^%s[^0-9]%s[^0-9]%s[^0-9]%s[^0-9]%s[^0-9]%s/\\1\\/\\2\/\\3 \\4:\\5/'\ "$base" "$sedCmd"{,,,,} allRoots=( "$base"[^0-9][0-9][0-9][0-9][0-9][^0-9][0-9][0-9][^0-9][0-9][0-9][^0-9][0-9][0-9][^0-9][0-9][0-9] ) [[ -d ${allRoots[0]} ]] || die "Can't find backups matching '${allRoots[0]}'" dtFmt=${allRoots[0]/$base/%s} dtFmt=${dtFmt/+([0-9])/%(%Y} dtFmt=${dtFmt/+([0-9])/%m} dtFmt=${dtFmt/+([0-9])/%d} dtFmt=${dtFmt/+([0-9])/%H} dtFmt=${dtFmt/+([0-9])/%M)T} bkNamFmt=${dtFmt//%?/%s} bkNamFmt=${bkNamFmt//%s[^-][^%]/%s} while read -r fileAsSec ;do printf -v thisfile '%(%Y %m %d %H %M)T' "$fileAsSec" printf -v "result[${thisfile// }]" "$dtFmt" "$base" "$fileAsSec" timeStamp["$thisfile"]=fileAsSec for kind in Y M W D H; do selthisfile "$kind" "$thisfile" "$fileAsSec" done done < <( printf %s\\n "$base"[^0-9][0-9][0-9][0-9][0-9][^0-9][0-9][0-9][^0-9][0-9][0-9][^0-9][0-9][0-9][^0-9][0-9][0-9] | sed "$sedCmd" | date -f - +%s ) } unwantedRoots(){ local kind for kind in "${!selfiles[@]}"; do unset "result[${selfiles["$kind"]// }]" keeped["${selfiles[$kind]// }"]+="$kind " done $QUIET || printf 'KEEPED: %d \e[34m(%d)\e[0m, DELETE: %d\n' \ "${#keeped[@]}" "${#selfiles[@]}" ${#result[@]} } printOut() { if $statbackups; then declare -i APPEARFD REALFD doRunStatBackup fi local cnt=() asizes=() tasiz='' trsiz='' crtdname tkind fmt selidxs tdsc local -i totrsize=0 totasize=0 selidxs=("${!selfiles[@]}") for tkind in Yearly Monthly Weekly Daily HalfHourly; do order=(${selidxs[@]/#[^${tkind::1}]*}) (( ${#order[@]} )) || continue printf -v fmt '[10#%s]=%%s ' ${order[@]/#${tkind::1}} printf -v fmt "$fmt" ${order[@]} local -a order="($fmt)" local -a cnted=("${!order[@]}") printf '%s (%d):\n' $tkind ${#order[@]} for i in "${!cnted[@]}"; do printf -v sfr %s%02d "${tkind:0:1}" "${cnted[i]}" tdsc=${descr[$sfr]} kpr=${selfiles[$sfr]// } OLANG=$LANG; LANG=C; u8c=${#tdsc}; LANG=$OLANG [ -v "cnt[kpr]" ] && c=34 || c=0 read -ra selfile <<<"${selfiles[$sfr]}" if $statbackups; then printf -v crtdname "$bkNamFmt" "$base" "${selfile[@]}" if [ -v "cnt[kpr]" ]; then rsize=0 trsiz=0b asize=${asizes[kpr]} human "$asize" tasiz else read -ru "$APPEARFD" asize dir1 read -ru "$REALFD" rsize dir2 asizes[kpr]=$asize human "$asize" tasiz human "$rsize" trsiz ((totasize+=asize)) ((totrsize+=rsize)) [[ $dir1 == "$dir2" ]]&&[[ $dir1 == "$crtdname" ]]||printf \ "\e[31mWARNING Crt vs DU don't match '%s'-'%s-'%s''\n"\ "$crtdname" "$dir1" "$dir2" fi printf '\e[%dm %s/%s/%s %s:%s %1s %*s %9s %9s %s%02d [ %s]\e[0m\n' \ "$c" "${selfile[@]}" "${cnt[kpr]:-+}" \ $((u8c-${#tdsc}+12)) "$tdsc" "$tasiz" "$trsiz" \ "${tkind:0:1}" $((${#cnted[@]}-i-1)) "${keeped[$kpr]}" else printf '\e[%dm %s/%s/%s %s:%s %1s %*s %s%02d [ %s]\e[0m\n' \ "$c" "${selfile[@]}" "${cnt[kpr]:-+}" \ $((u8c-${#tdsc}+12)) "$tdsc" "${tkind:0:1}" $((${#cnted[@]}-i-1)) \ "${keeped[$kpr]}" fi cnt[kpr]=' ' done done if $statbackups; then read -ru "$APPEARFD" asize dir1 read -ru "$REALFD" rsize dir2 [[ $dir1 == "$dir2" ]] && [[ $dir2 == "$base$suf" ]] || printf '\e[31mWARNING: "%s" == "%s" == "%s" not match!\e[0m\n' \ "$dir1" "$dir2" "$base$suf" human "$asize" tasiz human "$rsize" trsiz printf '%-14s %30s %9s\n' "$base$suf" "$tasiz" "$trsiz" ((totasize+=asize)) ((totrsize+=rsize)) human "$totasize" tasiz human "$totrsize" trsiz printf 'Total %39s %9s\n' "$tasiz" "$trsiz" fi } doRunStatBackup() { local chk=() cmd1='du -cks ' cmd2='' kind order i fname for kind in Y M W D H; do order=() for i in "${!selfiles[@]}"; do [ "${i:0:1}" = "$kind" ] && { if [ -z "${chk[${selfiles[$i]// }]}" ]; then chk[${selfiles["$i"]// }]='-' order[${selfiles["$i"]// }]=${selfiles["$i"]} fi } done for i in "${order[@]}"; do read -ra fname <<< "$i" printf -v i "$bkNamFmt" "$base" "${fname[@]}" cmd1+="'$i' " cmd2+="du -ks '$i';" done done cmd1+="'$base$suf' " cmd2+="du -ks '$base$suf';" exec {REALFD}< <( eval "$cmd1" ) exec {APPEARFD}< <( eval "$cmd2" ) } ask4delete() { local loop answer echo -n "Delete ${#result[@]} unwanted backups (y/n)? " loop=true while $loop; do read -srn1 answer [ "$answer" ] && [ -z "${answer#[yn]}" ] && loop=false done case $answer in y ) echo Yes; doDelete=y ;; * ) echo No; doDelete=n ;; esac } doSymLinks() { [[ -d $slPath ]] || die "Path to SymLinks '$slPath/' don't exist." rm -fR "${slPath:?}"/* local crtdname tkind i selidxs order linkname fmt selidxs=("${!selfiles[@]}") for tkind in Yearly Monthly Weekly Daily HalfHourly; do mkdir "$slPath/$tkind" order=(${selidxs[@]/#[^${tkind::1}]*}) printf -v fmt '[10#%s]=%%s ' ${order[@]/#${tkind::1}} printf -v fmt "$fmt" ${order[@]} local -a order="($fmt)" order=(${order[@]}) for i in ${!order[@]}; do read -ra selfile <<<"${selfiles["${order[i]}"]}" printf -v crtdname "$bkNamFmt" "$base" "${selfile[@]}" printf -v linkname '%s/%s/%02d-%(%a_%d_%b_%Y-%H%M)T' \ "$slPath" "$tkind" $(( ${#order[@]} -1 - i )) \ ${timeStamp["${selfile[@]}"]} ln -s "$PWD/$crtdname" "$linkname" && touch -d "@${timeStamp[${selfile[@]}]}" -h "$linkname" done done } readCmdLine "$@" cd "$root" || die "Can't cd to '$root'." [[ -d $base$suf ]] || die "Can't find '$base$suf' dir." export -a abr=(K M G T P E Z Y) # shellcheck disable=SC2034 # reStrs appears unused declare -A reStrs=() descr=() buildReStrs declare -A selfiles declare -iA timeStamp declare -a result=() keeped=() rootsSelection unwantedRoots ! $QUIET && printOut [[ $slPath ]] && doSymLinks if [ "${#result[@]}" -eq 0 ]; then $QUIET || echo "Nothing to do!" exit 0 fi $QUIET || fold -s ${COLUMNS+-w} ${COLUMNS} < <(echo "${result[@]#$base-}") ! [[ -v doDelete ]] && ask4delete if [[ $doDelete == y ]]; then $QUIET || echo 'RUN Job: delete' ${#result[@]}, keep ${#keeped[@]}. if ! rm -fr "${result[@]}"; then find "${result[@]}" ! -perm /u+w -exec chmod +w {} + rm -fr "${result[@]}" fi else $QUIET || echo 'Doing nothing.' fi