From 6df27e0f709b4ab3e58983bd1d18720442714311 Mon Sep 17 00:00:00 2001
From: Guillaume Subiron <maethor@subiron.org>
Date: Thu, 13 Feb 2025 08:52:08 +0100
Subject: [PATCH 1/5] replace borg source/ignore_missing by filter_warnings,
 and allow to disable warnings on file changed during backup

---
 examples/example.borg | 29 +++++++++++++++--------
 handlers/borg.in      | 54 +++++++++++++++++++++++++++++++------------
 test/borg.bats        |  4 ++--
 3 files changed, 61 insertions(+), 26 deletions(-)

diff --git a/examples/example.borg b/examples/example.borg
index 8f536cb..4fced66 100644
--- a/examples/example.borg
+++ b/examples/example.borg
@@ -124,21 +124,32 @@ exclude = /var/lib/mysql
 ## Default:
 # prune_options =
 
-## by default borg emits a warning when a source file or directory
-## vanishes during the backup operations
-## set to yes to ignore such warnings
+## Path to the directory that will hold borg's cache files. By default this is
+## empty, which will let borg use its default path of "~/.cache/borg".
 ##
-## Example:
-## ignore_missing = yes
+## Default:
+# cache_directory =
+
+## by default borg emits various warnings that are impossible to check on large
+## infrastructures.
+## - when some files/repositories included in borg create does not exists
+## - when some files have changed during the backup (happens a lot on log files)
+## This option allows to disable these warning.
 ##
 ## Default:
-# ignore_missing =
+# filter_warnings = yes
 
-## Path to the directory that will hold borg's cache files. By default this is
-## empty, which will let borg use its default path of "~/.cache/borg".
+## when filter_warning == yes, allows to choose to disable warning if
+## file changed during backup
 ##
 ## Default:
-# cache_directory =
+# warning_if_file_changed_during_backup = yes
+
+## when warning_if_file_changed_during_backup == yes, allows to ignore some
+## paths or filenames.
+##
+## Default:
+# file_changes_to_ignore = /
 
 ######################################################
 ## destination section
diff --git a/handlers/borg.in b/handlers/borg.in
index d31d844..aaf658e 100644
--- a/handlers/borg.in
+++ b/handlers/borg.in
@@ -37,7 +37,9 @@ getconf prune yes
 getconf keep 30d
 getconf prune_options
 getconf cache_directory
-getconf ignore_missing
+getconf filter_warnings yes
+getconf warning_if_file_changed_during_backup yes
+getconf file_changes_to_ignore /
 
 setsection dest
 getconf user
@@ -180,24 +182,46 @@ execstr="${execstr} ${excludes} $execstr_repository::$execstr_archive ${includes
 debug "executing borg create"
 debug "$nice $execstr"
 
-if [ $test = 0 ]; then
-   output=`$nice su -c "$execstr" 2>&1`
+if [ "$test" = 0 ]; then
+   output=$($nice su -c "$execstr" 2>&1)
    ret=$?
-   if [ $ret = 0 ]; then
-      debug $output
-      info "Successfully finished backing up source."
-   elif [ $ret = 1 ]; then
-      warnmsg=$(echo "$output" | @SED@ -n '1,/^-\+$/{x;p;d;}; x' | @SED@ '/^$/d')
-      if [ "$ignore_missing" = "yes" ] && ! echo "$warnmsg" | grep -qv '\[Errno 2\] No such file or directory:'; then
-         debug $output
-         info "Backing up source finished with missing file warnings."
+   if [ $ret = 0 ]; then # borg ok
+      debug "$output"
+      info "Successfully finished backing up source"
+   elif [ $ret = 1 ]; then # borg warning
+      # Borg can return 1 for warnings that are impossible to manually check on large infrastructures :
+      # - when some files/repositories included in borg create does not exist
+      # - when some files have changed during the backup (happens a lot on log files)
+      # So we need to filter the output line by line to print these warnings as debug.
+
+      # Warnings are always printed by borg before a long "---…" line.
+      # So we break the output in two along this line.
+      warning_output=$(echo "$output" | sed '/----------------------------/Q') # Before -----…
+      debug_output=$(echo "$output" | sed -n '/----------------------------/,$p') # After -----…
+
+      if [ -n "$warning_output" ] && [ "$filter_warnings" == "yes" ]; then
+         if [ "$warning_if_file_changed_during_backup" == "yes" ]; then
+            file_changes_to_ignore="($file_changes_to_ignore"'|/var/lib/apt/.*|/var/backups/atop/.*|/var/lib/postfix/.*|.*prometheus.*/wal/.*|.*/pg_wal/.*|.*/upload(s)?/.*|.*/(page_)?cache/.*|/var/mail/.*|.*/log(s)?/.*|.*/var/log/.*|.*(log\.json|\.log|\.err|\.wal|\.rrd(4j)?|\.wsp|\.part|\.seq|\.out|\.aof|\.rdb))'
+         else
+            file_changes_to_ignore=""
+         fi
+         non_warning_regex="(Attempting to access a previously unknown unencrypted repository|The repository at location.*was previously located at|Do you want to continue?|No such file or directory|$file_changes_to_ignore: file changed while we backed it up|Using a pure-python msgpack! This will result in lower performance.|/var/backups/drbd/.*scandir.)"
+         echo "$warning_output" | while IFS= read -r line; do
+            if echo "$line" | grep -Eq "$non_warning_regex"; then
+               debug "$line"
+            else
+               warning "$line"
+            fi
+         done
+         debug "$debug_output"
+         info "Successfully finished backing up source"
       else
-         warning $output
+         warning "$output"
          warning "Backing up source finished with warnings."
       fi
-   else
-      error $output
-      fatal "Failed backing up source."
+   else # borg error
+      error "$output"
+      fatal "Failed backuping up source. Borg returned exit code ${ret}."
    fi
 fi
 
diff --git a/test/borg.bats b/test/borg.bats
index f04874b..e16cf02 100644
--- a/test/borg.bats
+++ b/test/borg.bats
@@ -218,10 +218,10 @@ finish_borg() {
     greplog 'Debug: export BORG_CACHE_DIR="/var/cache/borg"$'
 }
 
-@test "check config parameter source/ignore_missing" {
+@test "check config parameter source/filter_warnings" {
     delconfig source include
     setconfig_repeat source include "$BN_SRCDIR/boot" "$BN_SRCDIR/etc" "$BN_SRCDIR/lib" "$BN_SRCDIR/var" "$BN_SRCDIR/foo"
-    setconfig source ignore_missing yes
+    setconfig source filter_warnings yes
     setconfig dest archive testarchive
     setconfig dest encryption none
     setconfig dest compression zstd,16
-- 
GitLab


From 0bce7106095e50a5ef7ff4d2cb3c8d8c035c94ea Mon Sep 17 00:00:00 2001
From: Guillaume Subiron <maethor@subiron.org>
Date: Thu, 13 Feb 2025 08:53:24 +0100
Subject: [PATCH 2/5] shellcheck borg handler

---
 handlers/borg.in | 35 ++++++++++++++++++++---------------
 1 file changed, 20 insertions(+), 15 deletions(-)

diff --git a/handlers/borg.in b/handlers/borg.in
index aaf658e..fa309c7 100644
--- a/handlers/borg.in
+++ b/handlers/borg.in
@@ -1,3 +1,6 @@
+#!/bin/bash
+# shellcheck shell=bash
+# shellcheck disable=SC2154
 # -*- mode: sh; sh-basic-offset: 3; indent-tabs-mode: nil; -*-
 # vim: set filetype=sh sw=3 sts=3 expandtab autoindent:
 #
@@ -48,7 +51,7 @@ getconf port
 getconf directory
 # strip trailing /
 directory=${directory%/}
-getconf archive {now:%Y-%m-%dT%H:%M:%S}
+getconf archive "{now:%Y-%m-%dT%H:%M:%S}"
 getconf compression lz4
 getconf encryption none
 getconf passphrase
@@ -106,21 +109,23 @@ fi
 
 # check the connection at the source and destination
 [ -n "$test" ] || test=0
+# shellcheck disable=SC2235
 if [ "$host" != "localhost" ] && ([ "$testconnect" = "yes" ] || [ "${test}" -eq 1 ]); then
-   debug "ssh $sshoptions -o PasswordAuthentication=no ${host}${port:+ -p ${port}} -l $user 'echo -n 1'"
-   local ret=`ssh $sshoptions -o PasswordAuthentication=no ${host}${port:+ -p ${port}} -l $user 'echo -n 1'`
+   teststr="ssh $sshoptions -o PasswordAuthentication=no ${host}${port:+ -p ${port}} -l $user 'echo -n 1'"
+   debug "$teststr"
+   ret=$(su -c "$teststr")
    if [ "$ret" = 1 ]; then
       debug "Connected to $host as $user successfully"
    else
       teststr="borg list $options --show-rc -v $execstr_repository"
       debug "$teststr"
-      output=`su -c "$teststr" 2>&1`
+      output=$(su -c "$teststr" 2>&1)
       if echo "$output" | grep "terminating with success status" \
          || echo "$output" | grep "^\S\+ is not a valid repository." \
          || echo "$output" | grep "^Repository \S\+ does not exist."; then
          debug "Connected to $host as $user successfully (forced command)"
       else
-         error $output
+         error "$output"
          fatal "Can't connect to $host as $user."
       fi
    fi
@@ -132,13 +137,13 @@ if [ "$init" == "yes" ]; then
    initstr="borg init $options --encryption=$encryption $execstr_repository"
    debug "executing borg init"
    debug "$initstr"
-   if [ $test = 0 ]; then
-      output="`su -c "$initstr" 2>&1`"
+   if [ "$test" = 0 ]; then
+      output="$(su -c "$initstr" 2>&1)"
       if [ $? = 2 ]; then
-         debug $output
+         debug "$output"
          info "Repository was already initialized"
       else
-         warning $output
+         warning "$output"
          warning "Repository has been initialized"
       fi
    fi
@@ -168,11 +173,11 @@ IFS=$SAVEIFS
 
 set +o noglob
 
-if [ ! -z $bwlimit ]; then
+if [ -n "$bwlimit" ]; then
    execstr="${execstr} --remote-ratelimit=${bwlimit}"
 fi
 
-if [ ! -z "$create_options" ]; then
+if [ -n "$create_options" ]; then
    execstr="${execstr} ${create_options}"
 fi
 
@@ -235,14 +240,14 @@ if [ "$prune" == "yes" ]; then
    prunestr="borg prune $options $prune_options $execstr_repository"
    debug "executing borg prune"
    debug "$prunestr"
-   if [ $test = 0 ]; then
-      output="`su -c "$prunestr" 2>&1`"
+   if [ "$test" = 0 ]; then
+      output="$(su -c "$prunestr" 2>&1)"
       ret=$?
       if [ $ret = 0 ]; then
-         debug $output
+         debug "$output"
          info "Removing old backups succeeded."
       elif [ $ret = 1 ]; then
-         warning $output
+         warning "$output"
          warning "Removing old backups finished with warnings."
       else
          error $output
-- 
GitLab


From 1d7e00fac63bdca523aef9cfa5901b2076c4db1b Mon Sep 17 00:00:00 2001
From: Guillaume Subiron <maethor@subiron.org>
Date: Thu, 13 Feb 2025 08:55:41 +0100
Subject: [PATCH 3/5] borg: allow to export borg list and borg info for
 monitoring

---
 examples/example.borg | 16 +++++++++++++++
 handlers/borg.in      | 47 +++++++++++++++++++++++++++++++++++++++++--
 2 files changed, 61 insertions(+), 2 deletions(-)

diff --git a/examples/example.borg b/examples/example.borg
index 4fced66..9544a94 100644
--- a/examples/example.borg
+++ b/examples/example.borg
@@ -46,6 +46,22 @@
 ## Default:
 # bwlimit = 0
 
+## export "borg info last_archive" to a given file
+## this is usefull for monitoring without using borg
+##
+## Example:
+# borginfo = /var/backups/borginfo.json
+## Default:
+# borginfo =
+
+## export "borg list repository" to a given file
+## this is usefull for monitoring without using borg
+##
+## Example
+# borglist = /var/backups/borglist.json
+## Default:
+# borglist =
+
 ######################################################
 ## source section
 ## (where the files to be backed up are coming from)
diff --git a/handlers/borg.in b/handlers/borg.in
index fa309c7..0fc72b6 100644
--- a/handlers/borg.in
+++ b/handlers/borg.in
@@ -31,6 +31,9 @@ getconf nicelevel 0
 getconf ionicelevel
 getconf bwlimit
 
+getconf borginfo
+getconf borglist
+
 setsection source
 getconf init yes
 getconf include
@@ -250,8 +253,48 @@ if [ "$prune" == "yes" ]; then
          warning "$output"
          warning "Removing old backups finished with warnings."
       else
-         error $output
-         fatal "Failed removing old backups."
+         info "$output"
+         warning "Failed removing old backups. Borg returned exit code ${ret}."
+      fi
+   fi
+fi
+
+### WRITE STATS FILES ###
+
+if [ -n "$borginfo" ]; then
+   mkdir -p "$(dirname "$borginfo")"
+
+   infostr="borg info $execstr_repository --last 1 --json > $borginfo"
+
+   debug "$infostr"
+   if [ "$test" = 0 ]; then
+      output=$(su -c "$infostr" 2>&1)
+      ret=$?
+      if [ $ret = 0 ]; then
+         debug "$output"
+         info "Successfully writing borg info to $borginfo"
+      else
+         info "$output"
+         error "Failed to write borg info to $borginfo"
+      fi
+   fi
+fi
+
+if [ -n "$borglist" ]; then
+   mkdir -p "$(dirname "$borglist")"
+
+   infostr="borg list $execstr_repository --json > $borglist"
+
+   debug "$infostr"
+   if [ "$test" = 0 ]; then
+      output=$(su -c "$infostr" 2>&1)
+      ret=$?
+      if [ $ret = 0 ]; then
+         debug "$output"
+         info "Successfully writing borg list to $borglist"
+      else
+         info "$output"
+         error "Failed to write borg list to $borglist"
       fi
    fi
 fi
-- 
GitLab


From 1cc50d051ae9eec39134243977716e50f4c6a442 Mon Sep 17 00:00:00 2001
From: Guillaume Subiron <maethor@subiron.org>
Date: Thu, 13 Feb 2025 08:58:50 +0100
Subject: [PATCH 4/5] borg: add keephourly, keepdaily, keepweekly and
 keepmonthly variables

---
 examples/example.borg | 10 ++++++++++
 handlers/borg.in      | 16 ++++++++++++++++
 2 files changed, 26 insertions(+)

diff --git a/examples/example.borg b/examples/example.borg
index 9544a94..e7425ec 100644
--- a/examples/example.borg
+++ b/examples/example.borg
@@ -130,6 +130,16 @@ exclude = /var/lib/mysql
 ## Default:
 # keep = 30d
 
+## define hourly, daily, weekly and monthly retention for the "borg prune" operation.
+##
+## theses options will be ignored if set to 0
+##
+## Default:
+## keephourly = 0
+## keepdaily = 0
+## keepweekly = 0
+## keepmonthly = 0
+
 ## define extra command-line options for the "borg prune" operation.
 ##
 ## Example:
diff --git a/handlers/borg.in b/handlers/borg.in
index 0fc72b6..06bde87 100644
--- a/handlers/borg.in
+++ b/handlers/borg.in
@@ -41,6 +41,10 @@ getconf exclude
 getconf create_options
 getconf prune yes
 getconf keep 30d
+getconf keephourly 0
+getconf keepdaily 0
+getconf keepweekly 0
+getconf keepmonthly 0
 getconf prune_options
 getconf cache_directory
 getconf filter_warnings yes
@@ -240,6 +244,18 @@ if [ "$prune" == "yes" ]; then
    if [ ! "$keep" == "0" ]; then
       prune_options="${prune_options} --keep-within=${keep}"
    fi
+   if [ ! "$keephourly" == "0" ]; then
+      prune_options="${prune_options} --keep-hourly=${keephourly}"
+   fi
+   if [ ! "$keepdaily" == "0" ]; then
+      prune_options="${prune_options} --keep-daily=${keepdaily}"
+   fi
+   if [ ! "$keepweekly" == "0" ]; then
+      prune_options="${prune_options} --keep-weekly=${keepweekly}"
+   fi
+   if [ ! "$keepmonthly" == "0" ]; then
+      prune_options="${prune_options} --keep-monthly=${keepmonthly}"
+   fi
    prunestr="borg prune $options $prune_options $execstr_repository"
    debug "executing borg prune"
    debug "$prunestr"
-- 
GitLab


From b37edd26ab246a73f79d0dc40651ff84f62dc7c0 Mon Sep 17 00:00:00 2001
From: Guillaume Subiron <maethor@subiron.org>
Date: Thu, 13 Feb 2025 08:59:33 +0100
Subject: [PATCH 5/5] borg: run borg compact after borg prune when borg version
 > 1.2

---
 handlers/borg.in | 15 +++++++++++++++
 1 file changed, 15 insertions(+)

diff --git a/handlers/borg.in b/handlers/borg.in
index 06bde87..d4de31e 100644
--- a/handlers/borg.in
+++ b/handlers/borg.in
@@ -265,6 +265,21 @@ if [ "$prune" == "yes" ]; then
       if [ $ret = 0 ]; then
          debug "$output"
          info "Removing old backups succeeded."
+
+         if [[ "$(borg --version)" > "borg 1.2" ]] ; then
+            compactstr="borg compact $execstr_repository"
+
+            debug "$compactstr"
+            output="$(su -c "$compactstr" 2>&1)"
+            ret=$?
+            if [ $ret = 0 ]; then
+               debug "$output"
+               info "Compacting borg repository succeeded."
+            else
+               info "$output"
+               warning "Compacting borg repository failed. Borg returned exit code ${ret}."
+            fi
+         fi
       elif [ $ret = 1 ]; then
          warning "$output"
          warning "Removing old backups finished with warnings."
-- 
GitLab