system settings
git clone https://git.lucas.co/cce-system-interface.git
scripts/restic-backup.sh (3.7K)
1 #!/bin/sh
2 # Nightly restic backup of $HOME, driven by restic-backup.timer.
3 #
4 # Runs as the USER, deliberately: the old backup-system.sh needed pkexec (so it
5 # could never run unattended) and, because it used `tar --one-file-system` from
6 # /, it silently archived only the root btrfs subvolume — /home has a distinct
7 # st_dev, so none of the user's data was ever in it. This backs up exactly the
8 # data that one missed.
9 #
10 # Writes the SAME status file the settings app's Storage page reads
11 # (last_backup_time / backup_size / error_message), so that page reports these
12 # runs with no change to it.
13 #
14 # Credentials live in ~/.config/restic/env (mode 600, NOT versioned): the
15 # repository URL, the backend's keys, and RESTIC_PASSWORD_FILE. Nothing secret
16 # belongs in this file. That env file may also set RESTIC_OPTS with backend
17 # tuning (`-o s3.region=auto` for Cloudflare R2, say) — kept generic here so a
18 # backend quirk never needs a code change.
19
20 set -eu
21
22 STATUS_FILE="${1:-$HOME/.config/cce/backup_status.txt}"
23 ENV_FILE="${RESTIC_ENV_FILE:-$HOME/.config/restic/env}"
24
25 PREV_TIME="Never"
26 PREV_SIZE="0 B"
27 read_previous() {
28 [ -f "$STATUS_FILE" ] || return 0
29 PREV_TIME=$(sed -n 's/^last_backup_time *= *//p' "$STATUS_FILE" | head -1)
30 PREV_SIZE=$(sed -n 's/^backup_size *= *//p' "$STATUS_FILE" | head -1)
31 [ -n "$PREV_TIME" ] || PREV_TIME="Never"
32 [ -n "$PREV_SIZE" ] || PREV_SIZE="0 B"
33 }
34
35 write_status() {
36 mkdir -p "$(dirname "$STATUS_FILE")"
37 cat > "$STATUS_FILE" <<EOF
38 last_backup_time = $1
39 backup_size = $2
40 error_message = ${3:-}
41 EOF
42 }
43
44 # Preserve the last good run's time and size on failure, so one bad night does
45 # not blank a good backup's record (the same rule backup-system.sh follows).
46 fail() {
47 read_previous
48 write_status "$PREV_TIME" "$PREV_SIZE" "$1"
49 echo "Error: $1" >&2
50 exit 1
51 }
52
53 [ -f "$ENV_FILE" ] || fail "No restic env file at $ENV_FILE"
54 # shellcheck disable=SC1090
55 . "$ENV_FILE"
56 [ -n "${RESTIC_REPOSITORY:-}" ] || fail "RESTIC_REPOSITORY not set in $ENV_FILE"
57 [ -n "${RESTIC_PASSWORD_FILE:-}${RESTIC_PASSWORD:-}" ] || \
58 fail "Neither RESTIC_PASSWORD_FILE nor RESTIC_PASSWORD set in $ENV_FILE"
59
60 # Deliberately unquoted: RESTIC_OPTS carries multiple whitespace-separated
61 # flags and must word-split.
62 # shellcheck disable=SC2086
63 restic ${RESTIC_OPTS:-} snapshots --no-lock >/dev/null 2>&1 || \
64 fail "Cannot reach or unlock the restic repository ($RESTIC_REPOSITORY)"
65
66 # --exclude-caches honours CACHEDIR.TAG, which cargo writes into every target/
67 # dir — that alone drops the ~432k build-artifact files. The explicit excludes
68 # cover the churny paths that carry no tag.
69 # shellcheck disable=SC2086
70 restic ${RESTIC_OPTS:-} backup "$HOME" \
71 --tag nightly \
72 --exclude-caches \
73 --exclude "$HOME/.cache" \
74 --exclude "$HOME/.dropbox" \
75 --exclude "$HOME/.dropbox-dist" \
76 --exclude "$HOME/.local/state/cce-shadow" \
77 --exclude "$HOME/.local/share/Trash" \
78 --exclude "**/target/debug" \
79 --exclude "**/target/release" \
80 --exclude "**/node_modules" \
81 --exclude "**/__pycache__" \
82 --exclude "**/.venv" \
83 || fail "restic backup failed (see journalctl --user -u restic-backup)"
84
85 # Retention. Runs after a successful backup only, so a failed night never
86 # prunes anything.
87 # shellcheck disable=SC2086
88 restic ${RESTIC_OPTS:-} forget --tag nightly \
89 --keep-daily 7 --keep-weekly 4 --keep-monthly 12 \
90 --prune >/dev/null 2>&1 || echo "Warning: forget/prune failed" >&2
91
92 # shellcheck disable=SC2086
93 SIZE_STR=$(restic ${RESTIC_OPTS:-} stats --mode raw-data 2>/dev/null \
94 | sed -n 's/.*Total Size: *//p' | head -1)
95 [ -n "$SIZE_STR" ] || SIZE_STR="unknown"
96
97 write_status "$(date '+%Y-%m-%d %H:%M:%S')" "$SIZE_STR" ""
98 echo "Backup completed successfully ($SIZE_STR in repo)"