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:
Waters Church
2026-09-24 14:48:13 -04:00
co-authored by Claude Opus 5
parent 4429ae70b7
commit 2ee1b18dc3
+31 -23
View File
@@ -150,11 +150,24 @@ fi
# out. That is backwards: the whole value of the off-site copy is that it outlives the NAS's # 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 # 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. # genuinely needs trimming, do it by hand, and never below what the NAS still holds.
cd "$DRIVE_DIR" || die "cannot enter $DRIVE_DIR" # Reporting and freshness.
TOTAL="$(ls -1 checklists-????-??-??.json 2>/dev/null | wc -l | tr -d ' ')" #
OLDEST="$(ls -1 checklists-????-??-??.json 2>/dev/null | sort | head -1)" # This deliberately does NOT rely on listing the Drive folder. Under launchd, macOS lets a
NEWEST="$(ls -1 checklists-????-??-??.json 2>/dev/null | sort | tail -1)" # named path be stat'd but blocks enumerating ~/Library/CloudStorage, so a glob quietly
say "✓ done — $TOTAL copies off-site, ${OLDEST:-none} → ${NEWEST:-none}" # 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 # 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. # 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." were skipped. See the log for which ones."
fi fi
# The staleness check is the one that would have caught both of the incidents in the header # The check that would have caught both of the incidents in the header comment. Every
# comment. Every failure path above assumes this script RAN; if the Mac is asleep, or the # failure path above assumes this script RAN; if the Mac is asleep or the LaunchAgent is
# LaunchAgent is unloaded, nothing above ever fires. This at least turns a quietly ageing # unloaded, nothing fires at all.
# off-site copy into mail the next time it does run.
if [ -z "$NEWEST" ]; then 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 else
NEWEST_DATE="$(printf '%s' "$NEWEST" | sed -E 's/^checklists-([0-9]{4}-[0-9]{2}-[0-9]{2})\.json$/\1/')" say "freshness OK — newest off-site copy is $AGE_DAYS day(s) old"
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
fi fi