From b9152369ef4bb42bc51105a6430bfcf97a53e6fc Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Aug 2026 22:09:25 +0000 Subject: [PATCH] Fix DR-spare path reset on reinstall and tilde-quoting in remote commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two stacked bugs, found together when re-running the backup installer to add an SFTP mirror silently reverted a previously-set absolute DR_SYNC_PATH back to the script's tilde-based default, which then failed outright: 1. services/backup.sh never read DR_SYNC_HOST/DR_SYNC_PATH back from an existing backup.conf before prompting (every other setting in this file does — passwords, mirrors). Accepting the prompt defaults on a rerun silently reset both to blank/"~/docker/backup" instead of keeping what was already configured. Fixed by reading them back the same way DEST_*_PASSWORD already does. 2. extras/backup_kopia.sh's DR-spare sync wraps the remote path in single quotes for its `ssh host "mkdir -p '...'"` / `"chmod 600 '.../...'"` commands. Single-quoting a leading ~ stops the remote shell from expanding it at all, so it looked for a literal directory named "~" instead of the home directory — breaking the script's own DEFAULT DR_SYNC_PATH ("~/docker/backup") for anyone who actually used it. rsync's own transfer step has separate, correct tilde handling, which is why the sync itself "succeeded" while the follow-up chmod couldn't find the file. Fixed with a small _dr_remote_quote() helper that keeps a leading ~/ outside the quotes while still safely quoting the rest of the path. Verified the quoting fix by parsing the exact constructed command string in bash directly — a plain '~/docker/backup' stays literal (the bug), ~/'docker/backup' correctly expands to $HOME/docker/backup (the fix). --- extras/backup_kopia.sh | 24 ++++++++++++++++++++++-- services/backup.sh | 8 ++++++-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/extras/backup_kopia.sh b/extras/backup_kopia.sh index 252eb98..73150bc 100644 --- a/extras/backup_kopia.sh +++ b/extras/backup_kopia.sh @@ -229,6 +229,26 @@ done # yet. dr_bringup.sh on the spare only needs these two small files — the # repo data itself already lives wherever REMOTE_TYPE mirrored it (or is # local, if the spare IS that target). +# Wraps a remote path for use inside a `ssh host "command '...'"` string so +# it's still safely quoted (spaces etc.) while a leading ~/ stays OUTSIDE +# the quotes — single-quoting a leading tilde stops the remote shell from +# expanding it at all, so it goes looking for a literal directory named +# "~" instead of the actual home directory. Confirmed live: this is exactly +# what broke `mkdir -p '~/docker/backup'` / `chmod 600 '~/docker/backup/...'` +# — the DEFAULT DR_SYNC_PATH — while rsync's own transfer step (which has +# its own tilde-aware remote-path handling, unrelated to shell quoting) +# succeeded against the exact same path. +_dr_remote_quote() { + local p="$1" + if [[ "$p" == "~/"* ]]; then + printf "~/'%s'" "${p#\~/}" + elif [[ "$p" == "~" ]]; then + printf '~' + else + printf "'%s'" "$p" + fi +} + if [ -n "${DR_SYNC_HOST:-}" ]; then _dr_path="${DR_SYNC_PATH:-~/docker/backup}" log "Syncing backup.conf + README to spare ($DR_SYNC_HOST:$_dr_path)..." @@ -240,9 +260,9 @@ if [ -n "${DR_SYNC_HOST:-}" ]; then # closed" while plain ssh exec and rsync's own protocol both still work # fine over the same connection. Confirmed live: scp failing this way # while `ssh "$DR_SYNC_HOST" true` succeeded, rsync doesn't hit it. - if ssh -o BatchMode=yes -o ConnectTimeout=10 "$DR_SYNC_HOST" "mkdir -p '$_dr_path'" 2>"$_ERR" \ + if ssh -o BatchMode=yes -o ConnectTimeout=10 "$DR_SYNC_HOST" "mkdir -p $(_dr_remote_quote "$_dr_path")" 2>"$_ERR" \ && rsync -a -e 'ssh -o BatchMode=yes -o ConnectTimeout=10' "${_dr_files[@]}" "$DR_SYNC_HOST:$_dr_path/" 2>>"$_ERR" \ - && ssh -o BatchMode=yes -o ConnectTimeout=10 "$DR_SYNC_HOST" "chmod 600 '$_dr_path/backup.conf'" 2>>"$_ERR"; then + && ssh -o BatchMode=yes -o ConnectTimeout=10 "$DR_SYNC_HOST" "chmod 600 $(_dr_remote_quote "$_dr_path/backup.conf")" 2>>"$_ERR"; then log "OK spare sync ($DR_SYNC_HOST)" else _err_text="$(cat "$_ERR" 2>/dev/null)" diff --git a/services/backup.sh b/services/backup.sh index 657ed23..b3a18b7 100644 --- a/services/backup.sh +++ b/services/backup.sh @@ -563,9 +563,13 @@ install_backup() { echo " up first if you haven't (ssh-keygen, then ssh-copy-id to the spare)." echo "" local DR_SYNC_HOST="" DR_SYNC_PATH="" - prompt_text " Spare box SSH destination, user@host (blank to skip):" "" DR_SYNC_HOST + if [ -f "$CONF_FILE" ]; then + DR_SYNC_HOST="$(grep '^DR_SYNC_HOST=' "$CONF_FILE" 2>/dev/null | sed -E "s/^DR_SYNC_HOST='(.*)'\$/\1/")" + DR_SYNC_PATH="$(grep '^DR_SYNC_PATH=' "$CONF_FILE" 2>/dev/null | sed -E "s/^DR_SYNC_PATH='(.*)'\$/\1/")" + fi + prompt_text " Spare box SSH destination, user@host (blank to skip):" "$DR_SYNC_HOST" DR_SYNC_HOST if [ -n "$DR_SYNC_HOST" ]; then - prompt_text " Path for backup.conf/README on the spare:" "~/docker/backup" DR_SYNC_PATH + prompt_text " Path for backup.conf/README on the spare:" "${DR_SYNC_PATH:-~/docker/backup}" DR_SYNC_PATH DR_SYNC_PATH="${DR_SYNC_PATH:-~/docker/backup}" # Catch a missing/unauthorized key now, not at 2am during the first