A path with an apostrophe broke the remote shell quoting and a big file outran the connect timeout, so intact backups were reported corrupt and absent

This commit is contained in:
Gmer4Lfe
2026-08-25 21:30:57 -04:00
parent d42b1e2dda
commit d444fd8036
2 changed files with 50 additions and 10 deletions
Executable → Regular
+46 -10
View File
@@ -122,6 +122,7 @@ source "$SCRIPT_DIR/../load_config.sh"
parse_args "$@"
SSH_TIMEOUT=15
BACKUP_VERIFY_MD5_TIMEOUT_MAX="${BACKUP_VERIFY_MD5_TIMEOUT_MAX:-600}"
# ==============================================================================================
# ━━━ Setup ━━━
@@ -228,6 +229,7 @@ TOTAL_CHECKED=0
TOTAL_MATCH=0
TOTAL_MISMATCH=0
TOTAL_MISSING=0
TOTAL_UNVERIFIED=0
SHARES_WITH_ISSUES=()
for share in "${VERIFY_SHARES[@]}"; do
@@ -265,6 +267,7 @@ for share in "${VERIFY_SHARES[@]}"; do
SHARE_MATCH=0
SHARE_MISMATCH=0
SHARE_MISSING=0
SHARE_UNVERIFIED=0
for local_file in "${SAMPLE_FILES[@]}"; do
[[ -z "$local_file" ]] && continue
@@ -276,19 +279,49 @@ for share in "${VERIFY_SHARES[@]}"; do
continue
fi
# Remote checksum via SSH — timeout protected
remote_md5=$(timeout "$SSH_TIMEOUT" ssh -i "$SSH_KEY" \
# The path is interpolated into a remote shell command, so it must be escaped for
# reuse as one word. A bare '$local_file' inside single quotes breaks on the first
# apostrophe — "Frieren - Beyond Journey's End" ended the quote early, md5sum fell
# back to reading stdin, and the empty-input hash d41d8cd9... was reported as a
# MISMATCH against a file that is byte-identical on the remote.
printf -v remote_q '%q' "$local_file"
# Existence and content are separate questions. Asking them together means a slow
# checksum is indistinguishable from an absent file.
remote_exists=$(timeout "$SSH_TIMEOUT" ssh -i "$SSH_KEY" \
-o ConnectTimeout="$SSH_TIMEOUT" \
-o StrictHostKeyChecking=no \
root@"$REMOTE_SERVER" \
"md5sum '$local_file' 2>/dev/null | awk '{print \$1}'" 2>/dev/null)
"test -f $remote_q && echo yes" 2>/dev/null </dev/null)
(( TOTAL_CHECKED++ ))
if [[ -z "$remote_md5" ]]; then
if [[ "$remote_exists" != "yes" ]]; then
warn "$ICON_ERROR MISSING: $(basename "$local_file")"
(( SHARE_MISSING++ ))
(( TOTAL_MISSING++ ))
continue
fi
# md5sum of a multi-GB file cannot finish inside a connect-sized timeout. Budget by
# size — a 5.9GB file needs ~30s and was being killed at 15s, then counted MISSING
# even though it was present and correct.
local_size=$(stat -c%s "$local_file" 2>/dev/null || echo 0)
md5_timeout=$(( local_size / 52428800 + SSH_TIMEOUT ))
(( md5_timeout > BACKUP_VERIFY_MD5_TIMEOUT_MAX )) && md5_timeout=$BACKUP_VERIFY_MD5_TIMEOUT_MAX
remote_md5=$(timeout "$md5_timeout" ssh -i "$SSH_KEY" \
-o ConnectTimeout="$SSH_TIMEOUT" \
-o StrictHostKeyChecking=no \
root@"$REMOTE_SERVER" \
"md5sum $remote_q 2>/dev/null | awk '{print \$1}'" 2>/dev/null </dev/null)
if [[ -z "$remote_md5" ]]; then
# Present but unreadable within budget. Reporting this as a mismatch or a miss
# would be a claim the run did not earn.
warn "$ICON_WARN UNVERIFIED (checksum timed out after ${md5_timeout}s): $(basename "$local_file")"
(( SHARE_UNVERIFIED++ ))
(( TOTAL_UNVERIFIED++ ))
elif [[ "$local_md5" == "$remote_md5" ]]; then
log "MATCH: $(basename "$local_file")"
(( SHARE_MATCH++ ))
@@ -303,8 +336,8 @@ for share in "${VERIFY_SHARES[@]}"; do
done
# Per-share result — only visible if issues found
if [[ "$SHARE_MISMATCH" -gt 0 || "$SHARE_MISSING" -gt 0 ]]; then
warn "$SHARE_NAME — match: $SHARE_MATCH missing: $SHARE_MISSING mismatch: $SHARE_MISMATCH"
if [[ "$SHARE_MISMATCH" -gt 0 || "$SHARE_MISSING" -gt 0 || "$SHARE_UNVERIFIED" -gt 0 ]]; then
warn "$SHARE_NAME — match: $SHARE_MATCH missing: $SHARE_MISSING mismatch: $SHARE_MISMATCH unverified: $SHARE_UNVERIFIED"
SHARES_WITH_ISSUES+=("$SHARE_NAME")
else
echo "$SHARE_NAME — all $SHARE_MATCH files match ✅"
@@ -325,10 +358,11 @@ echo "$ICON_VERIFY Checked: $TOTAL_CHECKED files"
echo "$ICON_TIME Duration: $(format_duration $(( END - START )))"
echo ""
if [[ "$TOTAL_MISMATCH" -gt 0 || "$TOTAL_MISSING" -gt 0 ]]; then
echo "$ICON_SUCCESS Match: $TOTAL_MATCH"
warn "Missing: $TOTAL_MISSING"
[[ "$TOTAL_MISMATCH" -gt 0 ]] && echo "$ICON_ERROR Mismatch: $TOTAL_MISMATCH"
if [[ "$TOTAL_MISMATCH" -gt 0 || "$TOTAL_MISSING" -gt 0 || "$TOTAL_UNVERIFIED" -gt 0 ]]; then
echo "$ICON_SUCCESS Match: $TOTAL_MATCH"
warn "Missing: $TOTAL_MISSING"
[[ "$TOTAL_MISMATCH" -gt 0 ]] && echo "$ICON_ERROR Mismatch: $TOTAL_MISMATCH"
[[ "$TOTAL_UNVERIFIED" -gt 0 ]] && warn "Unverified: $TOTAL_UNVERIFIED (present, checksum timed out)"
fi
if [[ "$DRY_RUN" == true ]]; then
@@ -337,6 +371,8 @@ elif [[ "$TOTAL_MISMATCH" -gt 0 || "$TOTAL_MISSING" -gt 0 ]]; then
echo "$ICON_ERROR Status: ISSUES FOUND — ${#SHARES_WITH_ISSUES[@]} share(s) need attention: ${SHARES_WITH_ISSUES[*]}"
notify "Backup verify FAILED on $(hostname)$REMOTE_SERVER_NAME — mismatches: $TOTAL_MISMATCH missing: $TOTAL_MISSING — shares: ${SHARES_WITH_ISSUES[*]}" \
"Backup Verify" "warning"
elif [[ "$TOTAL_UNVERIFIED" -gt 0 ]]; then
warn "Status: $TOTAL_MATCH verified, $TOTAL_UNVERIFIED could not be checksummed in time — NOT a clean run"
else
echo "$ICON_DONE Status: all $TOTAL_CHECKED files match across ${#VERIFY_SHARES[@]} shares ✅"
fi