Support multiple simultaneous offsite mirrors, not just one

Requested: mirror to Backblaze B2 AND directly to the IONOS spare box
over Tailscale, at the same time, not one or the other. REMOTE_TYPE/
REMOTE_ARGS was hardcoded to a single mirror target — extending it to a
list would have meant redesigning the one thing that already works and
was already verified against real B2 credentials, so this adds a
separate, additive mechanism instead: EXTRA_MIRROR_NAMES, a space-
separated list, with per-entry MIRROR_<name>_TYPE/_ARGS (same argument
shape as REMOTE_ARGS). An existing B2-only backup.conf keeps working
completely unchanged if this new section is skipped.

install_backup() gets a new "ADDITIONAL MIRROR" prompt after the
existing B2 section: offers a direct SFTP mirror (Kopia's sync-to sftp,
not the deprecated b2 provider — same reasoning as the S3/B2 choice
already made), defaults the destination to whatever was typed at the
DR-spare prompt above (same box, same purpose, no reason to ask twice),
checks passwordless SSH and an SSH key exist first, then verifies with a
--dry-run against the just-created 'default' repo before saving it —
same "don't save something broken" discipline as the B2 flow. Verified
against a mock backup.conf that install-side writes and worker-side
reads agree on the exact format, and that reusing an existing mirror
name reconfigures it instead of duplicating it in the name list.

One correction while researching sync-to sftp's flags: unlike plain ssh,
Kopia doesn't shell out to the system SSH client, so it needs an
explicit --keyfile and --known-hosts path rather than picking up
whatever `ssh` already trusts automatically — checked Kopia's own docs
for the exact flags before writing this, same as the earlier S3 case.

extras/backup_kopia.sh's worker loops through EXTRA_MIRROR_NAMES after
the existing REMOTE_TYPE mirror step, running sync-to for each
destination against each additional mirror and folding failures into
the same FAILED_SVCS/notification reporting the primary mirror already
uses. Verified end-to-end against a mock backup.conf and a stubbed
kp_for: both the B2 and the new SFTP mirror get called in sequence with
the correct arguments.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H4k6J1qXXyYxhGEgnJaMvn
This commit is contained in:
Claude
2026-08-14 03:20:49 +00:00
parent f36cb2c394
commit b99ca32438
2 changed files with 124 additions and 0 deletions
+21
View File
@@ -170,6 +170,27 @@ if [ "${REMOTE_TYPE:-none}" != "none" ] && [ -n "${REMOTE_TYPE:-}" ]; then
done
fi
# Additional mirrors (EXTRA_MIRROR_NAMES) — run alongside REMOTE_TYPE above,
# not instead of it, so a box can mirror to e.g. Backblaze B2 AND directly
# to a spare over SSH/SFTP at the same time.
for mirror_name in ${EXTRA_MIRROR_NAMES:-}; do
_mtype_var="MIRROR_${mirror_name}_TYPE"
_margs_var="MIRROR_${mirror_name}_ARGS"
_mtype="${!_mtype_var:-}"
_margs="${!_margs_var:-}"
[ -n "$_mtype" ] || continue
for dest in ${DEST_NAMES:-default}; do
log "Mirroring '$dest' to '$mirror_name' ($_mtype)..."
# shellcheck disable=SC2086
if ! kp_for "$dest" repository sync-to "$_mtype" $_margs 2>"$_ERR"; then
_reason="$(categorize_error "$(cat "$_ERR")")"
log "WARNING: mirror '$mirror_name' failed for '$dest' — $_reason"
FAILED_SVCS+=("mirror[$mirror_name/$dest]: $_reason")
rc=1
fi
done
done
# ── Keep a spare box's copy of backup.conf + README current ─────────────────
# Runs after the data itself is backed up (and mirrored, if configured) so a
# sync never ships config pointing at a repo state that isn't actually there