Stop the backup report depending on listing the Drive folder
Under launchd, macOS allows a named path on ~/Library/CloudStorage to be stat'd but blocks enumerating the directory. A glob therefore expands to nothing, and the reporting block read that as zero: the first real LaunchAgent run after the TCC fix logged "0 copies off-site" and emailed "no off-site copies at all" while 57 files sat in the folder. The copy loop was unaffected, because it uses filenames taken from the NAS rather than a glob. Freshness is now derived by testing explicit filenames for the last 60 days, which works in both contexts. Enumeration is still used for the total count, but a count of zero is reported as unknown rather than alerted on — an unreadable folder and an empty one are different things, and only one of them is an emergency. Preferred over granting Full Disk Access: a privacy grant is exactly what an OS update silently reset here in the first place. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
4429ae70b7
commit
2ee1b18dc3
+31
-23
@@ -150,11 +150,24 @@ fi
|
||||
# out. That is backwards: the whole value of the off-site copy is that it outlives the NAS's
|
||||
# 30-day window. Each file is ~260 KB, so a decade of them is under a gigabyte. If it ever
|
||||
# genuinely needs trimming, do it by hand, and never below what the NAS still holds.
|
||||
cd "$DRIVE_DIR" || die "cannot enter $DRIVE_DIR"
|
||||
TOTAL="$(ls -1 checklists-????-??-??.json 2>/dev/null | wc -l | tr -d ' ')"
|
||||
OLDEST="$(ls -1 checklists-????-??-??.json 2>/dev/null | sort | head -1)"
|
||||
NEWEST="$(ls -1 checklists-????-??-??.json 2>/dev/null | sort | tail -1)"
|
||||
say "✓ done — $TOTAL copies off-site, ${OLDEST:-none} → ${NEWEST:-none}"
|
||||
# Reporting and freshness.
|
||||
#
|
||||
# This deliberately does NOT rely on listing the Drive folder. Under launchd, macOS lets a
|
||||
# named path be stat'd but blocks enumerating ~/Library/CloudStorage, so a glob quietly
|
||||
# expands to nothing — which on 2026-09-24 made this script report "0 copies off-site" and
|
||||
# email an alert while 57 files sat there. Checking explicit filenames works in both
|
||||
# contexts, so freshness is derived that way and enumeration is treated as a bonus.
|
||||
NEWEST=""
|
||||
for i in $(seq 0 60); do
|
||||
d="$(date -v-${i}d +%Y-%m-%d 2>/dev/null)" || break
|
||||
if [ -s "$DRIVE_DIR/checklists-$d.json" ]; then NEWEST="checklists-$d.json"; NEWEST_DATE="$d"; AGE_DAYS=$i; break; fi
|
||||
done
|
||||
|
||||
# Enumeration is allowed when run by hand; when it is blocked the count is unknown rather
|
||||
# than zero, and an unknown count is never treated as a missing backup.
|
||||
TOTAL="$(ls -1 "$DRIVE_DIR"/checklists-????-??-??.json 2>/dev/null | wc -l | tr -d ' ')"
|
||||
if [ "$TOTAL" = "0" ]; then TOTAL="unknown (cannot list the folder from here)"; fi
|
||||
say "✓ done — $TOTAL copies off-site, newest ${NEWEST:-none}"
|
||||
|
||||
# Some files were fetched but were not usable. The run "succeeded" in the sense that it did
|
||||
# not crash, which is exactly the kind of success that hides a broken backup.
|
||||
@@ -164,24 +177,19 @@ if [ "${FAILED:-0}" -gt 0 ]; then
|
||||
were skipped. See the log for which ones."
|
||||
fi
|
||||
|
||||
# The staleness check is the one that would have caught both of the incidents in the header
|
||||
# comment. Every failure path above assumes this script RAN; if the Mac is asleep, or the
|
||||
# LaunchAgent is unloaded, nothing above ever fires. This at least turns a quietly ageing
|
||||
# off-site copy into mail the next time it does run.
|
||||
# The check that would have caught both of the incidents in the header comment. Every
|
||||
# failure path above assumes this script RAN; if the Mac is asleep or the LaunchAgent is
|
||||
# unloaded, nothing fires at all.
|
||||
if [ -z "$NEWEST" ]; then
|
||||
alert "no off-site copies at all" "$DRIVE_DIR contains no checklists-<date>.json files."
|
||||
alert "no recent off-site copy" \
|
||||
"No checklists-<date>.json for any of the last 60 days was found in
|
||||
$DRIVE_DIR
|
||||
Either copying has stopped, or this machine cannot read that folder."
|
||||
elif [ "$AGE_DAYS" -gt "$STALE_DAYS" ]; then
|
||||
alert "off-site copy is $AGE_DAYS days old" \
|
||||
"Newest file off-site is $NEWEST ($AGE_DAYS days old); more than $STALE_DAYS days
|
||||
means the chain from the NAS to Drive is not working, even though this run did
|
||||
not error."
|
||||
else
|
||||
NEWEST_DATE="$(printf '%s' "$NEWEST" | sed -E 's/^checklists-([0-9]{4}-[0-9]{2}-[0-9]{2})\.json$/\1/')"
|
||||
NEWEST_EPOCH="$(date -j -f "%Y-%m-%d" "$NEWEST_DATE" +%s 2>/dev/null || echo 0)"
|
||||
if [ "$NEWEST_EPOCH" -gt 0 ]; then
|
||||
AGE_DAYS=$(( ( $(date +%s) - NEWEST_EPOCH ) / 86400 ))
|
||||
if [ "$AGE_DAYS" -gt "$STALE_DAYS" ]; then
|
||||
alert "off-site copy is $AGE_DAYS days old" \
|
||||
"Newest file off-site is $NEWEST ($AGE_DAYS days old); anything more than
|
||||
$STALE_DAYS days means the chain from the NAS to Drive is not working, even though
|
||||
this run did not error."
|
||||
else
|
||||
say "freshness OK — newest off-site copy is $AGE_DAYS day(s) old"
|
||||
fi
|
||||
fi
|
||||
say "freshness OK — newest off-site copy is $AGE_DAYS day(s) old"
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user