git.lucas.co / cce-system-interface
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)"