From 55f9fe9a0ce6ee86b27a2101350f674b26650bd2 Mon Sep 17 00:00:00 2001
From: Jens Bannmann <jens.b@web.de>
Date: Thu, 26 Mar 2020 17:39:59 +0100
Subject: [PATCH] Add option to create full backups instead of per-target

---
 acts             | 122 ++++++++++++++++++++++++++++++++---------------
 acts.conf.sample |   5 ++
 2 files changed, 88 insertions(+), 39 deletions(-)

diff --git a/acts b/acts
index 43b24e9..cb58901 100755
--- a/acts
+++ b/acts
@@ -92,6 +92,7 @@ verbose="${verbose=0}"
 hostname="${hostname=$(hostname -s)}"
 uselocaltime="${uselocaltime=0}"
 backuptargets="${backuptargets=}"
+fullbackups="${fullbackups=0}"
 prebackupscript="${prebackupscript=}"
 postbackupscript="${postbackupscript=}"
 tarsnap="${tarsnap:=tarsnap}"
@@ -109,6 +110,10 @@ if [ -z "$backuptargets" ]; then
     die "config-error message=\"\$backuptargets is not set in acts.conf.\""
 fi
 
+if [ "$fullbackups" -ne 0 ] && [ "$fullbackups" -ne 1 ]; then
+    die "config-error message=\"invalid \$fullbackups in acts.conf; valid options are 0 (one archive per target) or 1 (one archive).\""
+fi
+
 if [ "$uselocaltime" = 0 ]; then
     utc="-u"
 elif [ "$uselocaltime" = 1 ]; then
@@ -127,7 +132,7 @@ if [ -d "$lockfile" ]; then
 else
     if output=$(mkdir "$lockfile" 2>&1); then
         echo $$ >"$lockfile/pid"
-        log_debug "message=\"Created $lockfile/ with 'pid' referencing $$\""
+        log_debug "message=\"Created $lockfile with 'pid' referencing $$\""
     else
         die "acts-error message=\"Can't create lock $lockfile: $output\""
     fi
@@ -191,36 +196,39 @@ if [ -n "$prebackupscript" ]; then
 fi
 
 # PART 3: Backup
-backuprc=0 # Notice any failed backups
-backuplist="" # Maintain list of successfully created archives
-for dir in $backuptargets; do
-    log_debug "message=\"Starting backup of $dir\""
-    archive_starttime=$(date +%s)
-    nicedirname=$(prettyprint archive_dirname "$dir")
-    logdirname=$(prettyprint log_dirname "$dir")
 
-    dailyarchive="$hostname-daily-$today$nicedirname"
-    monthlyarchive="$hostname-monthly-$today$nicedirname"
-    yearlyarchive="$hostname-yearly-$today$nicedirname"
+# create_backup [archivesuffix] [logdirpath] [logarchivelabel] [dirarguments]
+# create_backup "etcopt" "/etc/opt" "/etc/opt" "etc/opt"
+create_backup() {
+    cb_archivesuffix="$1"
+    cb_dirlogging="$2"
+    cb_logarchivelabel="$3"
+    cb_dirarguments="$4"
+
+    dailyarchive="$hostname-daily-$today$cb_archivesuffix"
+    monthlyarchive="$hostname-monthly-$today$cb_archivesuffix"
+    yearlyarchive="$hostname-yearly-$today$cb_archivesuffix"
+
+    log_verbose "backup-start  type=daily ${cb_dirlogging}name=$dailyarchive"
+    archive_starttime=$(date +%s)
 
-    log_verbose "backup-start  type=daily dir=/$dir name=$dailyarchive"
     # Uncontrolled expansion is bad, but we have little choice. See https://github.com/koalaman/shellcheck/wiki/Sc2086
     # shellcheck disable=SC2086
-    if output="$(calltarsnap -c -f "$dailyarchive" -C / $tarsnapbackupoptions "$dir" 2>&1)"; then
+    if output="$(calltarsnap -c -f "$dailyarchive" -C / $tarsnapbackupoptions $cb_dirarguments 2>&1)"; then
         backuplist="$backuplist $dailyarchive"
     else
         log_output=$(prettyprint log_output "$output")
-        log_message "backup-error type=daily dir=$logdirname output=\"$log_output\""
+        log_message "backup-error type=daily ${cb_dirlogging}output=\"$log_output\""
         backuprc=1
     fi
     archive_endtime=$(date +%s)
-    log_verbose "backup-finish type=daily dir=$logdirname duration=$((archive_endtime - archive_starttime))s"
+    log_verbose "backup-finish type=daily ${cb_dirlogging}duration=$((archive_endtime - archive_starttime))s"
 
     # Determine whether other backup levels need to be created too
     # If so, copy them from $dailyarchive
-    if ! echo "$archives" | grep -E -q "^$hostname-yearly-$year-.+$nicedirname$"; then
+    if ! echo "$archives" | grep -E -q "^$hostname-yearly-$year-.+${cb_archivesuffix}$"; then
         # No yearly backup
-        log_debug "message=\"Copying $logdirname daily archive to yearly archive\""
+        log_debug "message=\"Copying ${cb_logarchivelabel}daily archive to yearly archive\""
         archive_starttime=$(date +%s)
         # shellcheck disable=SC2086
         if output="$(calltarsnap -c -f "$yearlyarchive" $tarsnapbackupoptions "@@$dailyarchive" 2>&1)"; then
@@ -231,12 +239,12 @@ for dir in $backuptargets; do
             backuprc=1
         fi
         archive_endtime=$(date +%s)
-        log_verbose "copy-finish type=yearly dir=$logdirname name=$yearlyarchive duration=$((archive_endtime - archive_starttime))s"
+        log_verbose "copy-finish type=yearly ${cb_dirlogging}name=$yearlyarchive duration=$((archive_endtime - archive_starttime))s"
     fi
 
-    if ! echo "$archives" | grep -E -q "^$hostname-monthly-$year-$month-.+$nicedirname$"; then
+    if ! echo "$archives" | grep -E -q "^$hostname-monthly-$year-$month-.+${cb_archivesuffix}$"; then
         # No monthly backup
-        log_debug "message=\"Copying $logdirname daily archive to monthly archive\""
+        log_debug "message=\"Copying ${cb_logarchivelabel}daily archive to monthly archive\""
         archive_starttime=$(date +%s)
         # shellcheck disable=SC2086
         if output="$(calltarsnap -c -f "$monthlyarchive" $tarsnapbackupoptions "@@$dailyarchive" 2>&1)"; then
@@ -247,9 +255,30 @@ for dir in $backuptargets; do
             backuprc=1
         fi
         archive_endtime=$(date +%s)
-        log_verbose "copy-finish type=monthly dir=$logdirname name=$monthlyarchive duration=$((archive_endtime - archive_starttime))s"
+        log_verbose "copy-finish type=monthly ${cb_dirlogging}name=$monthlyarchive duration=$((archive_endtime - archive_starttime))s"
     fi
-done
+}
+
+backuprc=0 # Notice any failed backups
+backuplist="" # Maintain list of successfully created archives
+
+if [ "$fullbackups" = 0 ]; then
+    for dir in $backuptargets; do
+        log_debug "message=\"Starting backup of $dir\""
+
+        archivesuffix=$(prettyprint archive_dirname "$dir")
+        logdirname=$(prettyprint log_dirname "$dir")
+        dirlogging="dir=$logdirname "
+        logarchivelabel="$logdirname "
+
+        create_backup "$archivesuffix" "$dirlogging" "$logarchivelabel" "$dir"
+    done
+elif [ "$fullbackups" = 1 ]; then
+    log_debug "message=\"Starting full backup of $backuptargets\""
+
+    create_backup "--full" "" "" "$backuptargets"
+fi
+
 # Update the archive listing
 log_debug "Listing tarsnap archives before deleting old backups"
 archives=$(calltarsnap --list-archives | sort -n)
@@ -259,40 +288,55 @@ if [ "$backuprc" != "0" ]; then
     die "acts-tarsnap-error One of the backups failed -- not deleting old backups"
 fi
 
+# prune_backup_levels [archivesuffix]
+# prune_backup_levels etcopt
+prune_backup_levels() {
+    pbl_archivesuffix=$1
+
+    prune_backups $pbl_archivesuffix yearly  $yearlybackups
+    prune_backups $pbl_archivesuffix monthly $monthlybackups
+    prune_backups $pbl_archivesuffix daily   $dailybackups
+}
+
+# prune_backups [archivesuffix] [backuplevel] [max]
+# prune_backups etcopt monthly 12
 prune_backups() {
-    backuplevel=$1 max=$2
+    pb_archivesuffix=$1
+    pb_backuplevel=$2
+    pb_max=$3
 
-    if [ "$max" -gt 0 ]; then
-        backupslist=$(echo "$archives" | grep -E "$hostname-$backuplevel-.+$nicedirname$" | sort -rn)
+    if [ "$pb_max" -gt 0 ]; then
+        backupslist=$(echo "$archives" | grep -E "$hostname-$pb_backuplevel-.+${pb_archivesuffix}$" | sort -rn)
         numberofbackups=$(printf '%s' "$backupslist" | awk 'END{print NR}')
-        if [ "$numberofbackups" -gt "$max" ]; then
-            log_debug "message=\"More than $max $backuplevel backups, deleting the oldest\""
-            echo "$backupslist" | tail -n +"$((max + 1))" | while read -r archiveprefixtodel; do
+        if [ "$numberofbackups" -gt "$pb_max" ]; then
+            log_debug "message=\"More than $pb_max $pb_backuplevel backups, deleting the oldest\""
+            echo "$backupslist" | tail -n +"$((pb_max + 1))" | while read -r archiveprefixtodel; do
                 log_verbose "message=\"Deleting backup prefix $archiveprefixtodel*\""
                 echo "$archives" | grep -E "^$archiveprefixtodel" | while read -r archivetodel; do
                     log_debug "message=\"Deleting backup $archivetodel\""
                     if ! output="$(calltarsnap -d -f "$archivetodel" 2>&1)"; then
                         log_output=$(prettyprint log_output "$output")
-                        log_message "delete-error type=$backuplevel output=\"$log_output\""
+                        log_message "delete-error type=$pb_backuplevel output=\"$log_output\""
                     fi
                 done
             done
         else
-            log_debug "message=\"Found $numberofbackups $backuplevel backups; not deleting any\""
+            log_debug "message=\"Found $numberofbackups $pb_backuplevel backups; not deleting any\""
         fi
     else
-        log_debug "message=\"Keeping all $backuplevel backups indefinitely; not deleting any\""
+        log_debug "message=\"Keeping all $pb_backuplevel backups indefinitely; not deleting any\""
     fi
 }
 
-for dir in $backuptargets; do
-    log_debug "message=\"Checking $dir for old backups to delete\""
-    nicedirname=$(prettyprint archive_dirname "$dir")
-
-    prune_backups yearly  $yearlybackups
-    prune_backups monthly $monthlybackups
-    prune_backups daily   $dailybackups
-done
+if [ "$fullbackups" = 0 ]; then
+    for dir in $backuptargets; do
+        log_debug "message=\"Checking $dir for old backups to delete\""
+        prune_backup_levels $(prettyprint archive_dirname "$dir")
+    done
+elif [ "$fullbackups" = 1 ]; then
+    log_debug "message=\"Checking for old full backups to delete\""
+    prune_backup_levels "--full"
+fi
 
 # Run the post-backup script
 if [ -n "$postbackupscript" ]; then
diff --git a/acts.conf.sample b/acts.conf.sample
index 03bd296..d8bd61d 100644
--- a/acts.conf.sample
+++ b/acts.conf.sample
@@ -7,6 +7,11 @@
 # Default: unset
 #backuptargets="var etc home root"
 
+# fullbackups
+# Create only one archive per run that includes all backup directories.
+# Default: 0 (i.e. create one archive per target)
+#fullbackups=1
+
 # tarsnap
 # What command to call for 'tarsnap'.
 # Anything that should be used every time (like --configfile)