+#!/bin/bash
+#
+# Duplicity Control Tool -- description at far below (after "exec duplicity")
+
+if [ $# -lt 2 ] ; then
+ cat <<EOF >&2
+** Usage: dupltool [options] source url
+** Source directory and store url are required, and the url
+** MUST be of form "pexpect+scp:/\$HOST/\$REMOTEDIR"
+EOF
+ exit 1
+fi
+
+#-- Grab command line arguments
+DUPL=( $* )
+STORE="${DUPL[-1]}"
+SOURCE="${DUPL[-2]}"
+
+#-- Confirm that the STORE url was of expected format
+
+if [ -n "${STORE##pexpect+scp:/*/*}" ] ; then
+ echo "** Bad store URL: $STORE" >&2
+ echo "** Required format: pexpect+scp:/$HOST/$REMOTEDIR" >&2
+ exit 1
+fi
+
+## Helper function to read duplicity pathnames and prefix them with
+## their from and to months, as in ${FROM}${TO}:${FILENAME}
+## both $FROM and $TO are 6 digits (yyyymm).
+remote_from_to_month_prefix() {
+ sftp $SFTP <<< "ls" | while read F ; do
+ case "$F" in
+ duplicity-full-signatures.*) echo "${F:26:6}${F:26:6}:$F" ; ;;
+ duplicity-full.*) echo "${F:15:6}${F:15:6}:$F" ; ;;
+ duplicity-inc.*) echo "${F:14:6}${F:34:6}:$F" ; ;;
+ duplicity-new-signatures.*) echo "${F:25:6}${F:45:6}:$F" ; ;;
+ esac
+ done | sort -r
+}
+
+THISMONTH="$(date -u "+%Y%m")" # UTC date
+
+SFTP="${STORE##pexpect+scp://}"
+HOST="${SFTP%%/*}"
+REMOTEDIR="${SFTP#*/}"
+SFTP="sftp://$SFTP"
+
+# Obtain remote pathnames with from-to prefix in reverse date order,
+# i.e., newest first.
+FILES=( $(remote_from_to_month_prefix) )
+
+if [ -n "$FILES" ] ; then
+ THATMONTH="${FILES[0]:6:6}" # to-month of newest
+ if [ "$THISMONTH" != "$THATMONTH" ] ; then
+ echo "** New monthly snapshot; stashing daily into .$THATMONTH" >&2
+ { # Generate command stream for sftp
+ echo "mkdir .$THATMONTH"
+ FX=
+ for F in ${FILES[@]} ; do
+ [ "${F:0:6}" != "$THATMONTH" ] && break
+ [ -n "$FX" ] && echo "rename $FX .$THATMONTH/$FX"
+ FX="${F#*:}"
+ [ -z "${FX%duplicity-full*}" ] && break
+ done
+ } | sftp $SFTP
+ echo "** Stashing into .$THATMONTH completed" >&2
+ fi
+fi
+
+exec duplicity ${DUPL[@]}
+exit 1 # jic
+
+# Duplicity Control Tool
+# ======================
+#
+# This Duplicity Control Tool should be run as a daily anacron job (or
+# daily as a cron job) so as to make regular snapshots of a directory
+# tree using duplicity with the pexpect+scp backend.
+#
+# Ideally the backup store should be set up with password-less scp
+# access, preferrably as a non-root remote user, or at the very least
+# be accessed by means of a local ssh-agent.
+#
+# The store is then managed by this Duplicity Control Tool to keep a
+# multi-level history, which ends up as a succession of monthly
+# snapshots with a most recent tail of daily snapshots. Specifically,
+# at the first snapshot event each month, the store is revised by
+# stashing away the snapshots of the most recent previous month, to
+# leave the first one (or the most recent full) as the start point for
+# a new snapshot, which thus becomes a "monthly" snapshot.
+#
+# The chain of daily snapshots from that first (monthly or full)
+# snapshot remain stashed in the ".%Y%m" directory.
+#
+# Note that the Duplicity Control Tool considers "the month of the
+# last snapshot or full" to be "the previous month" regardless of how
+# many months back from today that might be, i.e., it will stash
+# snapshots just as if that month was the prior month. In other words,
+# if you would happen to have a huge temporal hole in the snapshot
+# series you might consider running a plain duplicity snapshot first
+# and then let the subsequent, regular runs of the Duplicity Control
+# Tool build its multi-level snapshot history henceforth.
+#
+# === These are the examples of the various duplicity snapshot files:
+# duplicity-full.20230128T010719Z.vol1.difftar.gz
+# duplicity-full-signatures.20230128T010719Z.sigtar.gz
+# duplicity-new-signatures.20221216T230345Z.to.20221217T230403Z.sigtar.gz
+# duplicity-inc.20230128T010719Z.to.20230130T082241Z.manifest
+# duplicity-inc.20230127T061825Z.to.20230130T081002Z.vol1.difftar.gz