1{ config, pkgs, ... }:
2let
3 # Hourly backup: rsync the local working drives up to the NAS, then (only on a
4 # fully clean push) trigger a ZFS snapshot + tiered prune on the NAS. The backup
5 # on the NAS is plain browsable files; history lives in ZFS snapshots.
6 sandwich-backup = pkgs.writeShellApplication {
7 name = "sandwich-backup";
8 runtimeInputs = with pkgs; [
9 rsync
10 openssh
11 coreutils
12 ];
13 text = ''
14 NAS=clo@git.paperclover.net
15 DEST=$NAS:/mnt/storage1/backup/sandwich
16 LOCK=/tmp/sandwich-backup.lock
17
18 # Dedicated passphraseless key so the launchd job runs unattended; the
19 # interactive key keeps its passphrase. IdentitiesOnly stops ssh from
20 # offering the passphrased key first.
21 SSH=(ssh -i "$HOME/.ssh/id_sandwich_backup" -o IdentitiesOnly=yes -o BatchMode=yes
22 -o ConnectTimeout=30 -o ServerAliveInterval=30 -o ServerAliveCountMax=4)
23
24 # Single-instance guard: the initial sync takes hours, far longer than the
25 # hourly interval. mkdir is atomic; recover if a previous run died uncleanly.
26 if ! mkdir "$LOCK" 2>/dev/null; then
27 if [ -f "$LOCK/pid" ] && kill -0 "$(cat "$LOCK/pid")" 2>/dev/null; then
28 echo "already running (pid $(cat "$LOCK/pid"))"
29 exit 0
30 fi
31 echo "clearing stale lock"
32 rm -rf "$LOCK"
33 mkdir "$LOCK"
34 fi
35 echo $$ >"$LOCK/pid"
36 trap 'rm -rf "$LOCK"' EXIT
37
38 # rsync exit 24 (source files vanished mid-transfer) is benign on a live
39 # machine; anything else is a real failure and aborts before the snapshot.
40 rsync_one() {
41 local rc=0
42 rsync -a --delete --partial --timeout=600 --human-readable --stats \
43 -e "''${SSH[*]}" \
44 --exclude '.DS_Store' \
45 --exclude '.Spotlight-V100' \
46 --exclude '.Trashes' \
47 --exclude '.fseventsd' \
48 --exclude '.TemporaryItems' \
49 --exclude '.DocumentRevisions-V100' \
50 "$@" || rc=$?
51 if [ "$rc" -ne 0 ] && [ "$rc" -ne 24 ]; then
52 return "$rc"
53 fi
54 }
55
56 rsync_one /Volumes/Asset/ "$DEST/Asset/"
57 rsync_one /Volumes/Project/ "$DEST/Project/"
58 rsync_one --exclude 'LMStudio/' /Volumes/Documents/ "$DEST/Documents/"
59 rsync_one "$HOME/Desktop/" "$DEST/Desktop/"
60
61 # Snapshot only reached if every push above succeeded.
62 "''${SSH[@]}" "$NAS" /mnt/storage1/apps/home-infra/backup/snapshot-prune.sh storage1/backup/sandwich
63 '';
64 };
65in
66{
67 home.packages = with pkgs; [
68 rsync
69 sandwich-backup
70 ];
71
72 launchd.agents.sandwich-backup = {
73 enable = true;
74 config = {
75 ProgramArguments = [ "${sandwich-backup}/bin/sandwich-backup" ];
76 StartInterval = 3600;
77 RunAtLoad = true;
78 StandardOutPath = "${config.home.homeDirectory}/Library/Logs/sandwich-backup.log";
79 StandardErrorPath = "${config.home.homeDirectory}/Library/Logs/sandwich-backup.log";
80 };
81 };
82}