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
+30 -22
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" \
else "No checklists-<date>.json for any of the last 60 days was found in
NEWEST_DATE="$(printf '%s' "$NEWEST" | sed -E 's/^checklists-([0-9]{4}-[0-9]{2}-[0-9]{2})\.json$/\1/')" $DRIVE_DIR
NEWEST_EPOCH="$(date -j -f "%Y-%m-%d" "$NEWEST_DATE" +%s 2>/dev/null || echo 0)" Either copying has stopped, or this machine cannot read that folder."
if [ "$NEWEST_EPOCH" -gt 0 ]; then elif [ "$AGE_DAYS" -gt "$STALE_DAYS" ]; then
AGE_DAYS=$(( ( $(date +%s) - NEWEST_EPOCH ) / 86400 ))
if [ "$AGE_DAYS" -gt "$STALE_DAYS" ]; then
alert "off-site copy is $AGE_DAYS days old" \ alert "off-site copy is $AGE_DAYS days old" \
"Newest file off-site is $NEWEST ($AGE_DAYS days old); anything more than "Newest file off-site is $NEWEST ($AGE_DAYS days old); more than $STALE_DAYS days
$STALE_DAYS days means the chain from the NAS to Drive is not working, even though means the chain from the NAS to Drive is not working, even though this run did
this run did not error." not error."
else else
say "freshness OK — newest off-site copy is $AGE_DAYS day(s) old" say "freshness OK — newest off-site copy is $AGE_DAYS day(s) old"
fi
fi
fi fi