diff options
author | Andrew Cady <d@jerkface.net> | 2023-05-24 11:36:35 -0400 |
---|---|---|
committer | Andrew Cady <d@jerkface.net> | 2023-05-24 11:36:35 -0400 |
commit | c8daee10060554d1692e808c040c1f4263fab361 (patch) | |
tree | bed0ead6d939c6c501c9feac5eacf7c6374f7521 /src | |
parent | a002492cc53c77e0435e957f86aa546f4ce2d1b0 (diff) |
rename
Diffstat (limited to 'src')
-rwxr-xr-x | src/push-btrfs | 127 | ||||
-rw-r--r-- | src/push-btrfs@.service | 2 |
2 files changed, 128 insertions, 1 deletions
diff --git a/src/push-btrfs b/src/push-btrfs new file mode 100755 index 0000000..3284b69 --- /dev/null +++ b/src/push-btrfs | |||
@@ -0,0 +1,127 @@ | |||
1 | #!/bin/bash | ||
2 | MIN_AGE_SECONDS=60 | ||
3 | CONFIG_DIR=/etc/btrfs-backup/remotes | ||
4 | |||
5 | die() | ||
6 | { | ||
7 | printf 'Error: %s\n' "$*" >&2 | ||
8 | exit 1 | ||
9 | } | ||
10 | |||
11 | read_config() | ||
12 | { | ||
13 | eval config="($(jq -r '. | to_entries | .[] | "[\"" + .key + "\"]=" + (.value | @sh)'))" | ||
14 | } | ||
15 | |||
16 | check_dependencies() | ||
17 | { | ||
18 | for c in flock jq btrfs pv realpath stat egrep date mv ln rm | ||
19 | do | ||
20 | command -v $c >/dev/null | ||
21 | done | ||
22 | } | ||
23 | |||
24 | check_user_is_root() | ||
25 | { | ||
26 | [ "$UID" = 0 ] | ||
27 | } | ||
28 | |||
29 | is_subvolume() | ||
30 | { | ||
31 | btrfs subvolume show -- "$1" >/dev/null 2>&1 | ||
32 | } | ||
33 | |||
34 | is_readonly_subvolume() | ||
35 | { | ||
36 | btrfs subvolume show -- "$1" | egrep -q '^ Flags:.*\breadonly\b' | ||
37 | } | ||
38 | |||
39 | get_age() | ||
40 | { | ||
41 | now=$(date +%s) | ||
42 | then=$(date +%s -d "${1##*.snapshot~}") | ||
43 | echo $((now - then)) | ||
44 | } | ||
45 | |||
46 | btrfs_receive() | ||
47 | { | ||
48 | ssh -- "${1%%:*}" btrfs receive -- "${1#*:}" | ||
49 | } | ||
50 | |||
51 | set -e | ||
52 | check_dependencies | ||
53 | check_user_is_root | ||
54 | |||
55 | if [ $# = 1 ] | ||
56 | then | ||
57 | case "$1" in | ||
58 | '' ) die 'config file name is blank' ;; | ||
59 | */*) die 'config file name must not contain "/"' ;; | ||
60 | *.json) ;; | ||
61 | *) die "config file name must end in '.json'" ;; | ||
62 | esac | ||
63 | config_file=$CONFIG_DIR/$1 | ||
64 | config_file_temp=$CONFIG_DIR/.$1~tmp | ||
65 | else | ||
66 | echo "Usage: $0 <config.json>" >&2 | ||
67 | exit 1 | ||
68 | fi | ||
69 | |||
70 | [ -r "$config_file" ] || die "config file does not exist: $config_file" | ||
71 | exec 3<>"$config_file" | ||
72 | flock -n 3 | ||
73 | declare -A config | ||
74 | read_config <&3 | ||
75 | |||
76 | src=${config[source]} | ||
77 | dst=${config[destination]} | ||
78 | remote_head=${config[head]} | ||
79 | |||
80 | case "$dst" in | ||
81 | *:/*) ;; | ||
82 | /*) dst=localhost:$dst ;; | ||
83 | *) die "Invalid destination: $dst" ;; | ||
84 | esac | ||
85 | |||
86 | is_subvolume "$src" | ||
87 | |||
88 | if [ "$remote_head" ] | ||
89 | then | ||
90 | is_readonly_subvolume "$remote_head" | ||
91 | |||
92 | AGE=$(get_age "$remote_head") | ||
93 | if [ "$AGE" -le "$MIN_AGE_SECONDS" ] | ||
94 | then | ||
95 | echo "Up-to-date." >&2 | ||
96 | exit | ||
97 | fi | ||
98 | fi | ||
99 | |||
100 | new_snapshot=${src%/}/.snapshot~$(date -Ins) | ||
101 | btrfs subvolume snapshot -r -- "$src" "$new_snapshot" | ||
102 | if ! btrfs send ${remote_head:+ -p "$remote_head"} -- "$new_snapshot" | pv | btrfs_receive "$dst" | ||
103 | then | ||
104 | btrfs subvolume delete "$new_snapshot" | ||
105 | exit 1 | ||
106 | fi | ||
107 | jq --arg h "$new_snapshot" '. | .head=$h' <"$config_file" >"$config_file_temp" | ||
108 | mv -T -- "$config_file_temp" "$config_file" | ||
109 | |||
110 | if [ "$remote_head" ] | ||
111 | then | ||
112 | btrfs subvolume delete "$remote_head" | ||
113 | fi | ||
114 | |||
115 | ### OK, basic idea: | ||
116 | # | ||
117 | ## the system has a list of source subvolumes | ||
118 | ## each source subvolume has a snapshot period | ||
119 | ## each source subvolume has a list of subscribers | ||
120 | ## each source subvolume has an ordered list of its snapshots | ||
121 | ## each subscriber has a last-received snapshot (if any) | ||
122 | |||
123 | # when a subvolume's last snapshot is older than the configured period, a new snapshot is created | ||
124 | # when a subvolume has subscribers whose last snapshot is out-of-date, a snapshot is pushed (using the common ancestor) | ||
125 | # the subscriber's last snapshot is then updated | ||
126 | # if there are no subscriber's holding open a snapshot which is not the latest snapshot, then it is deleted | ||
127 | |||
diff --git a/src/push-btrfs@.service b/src/push-btrfs@.service index 9eb1713..c62febe 100644 --- a/src/push-btrfs@.service +++ b/src/push-btrfs@.service | |||
@@ -4,7 +4,7 @@ ConditionUser = root | |||
4 | ConditionFileNotEmpty = /etc/btrfs-backup/remotes/%I.json | 4 | ConditionFileNotEmpty = /etc/btrfs-backup/remotes/%I.json |
5 | 5 | ||
6 | [Service] | 6 | [Service] |
7 | ExecStart = /root/src/local-btrfs-backup/push-btrfs %I.json | 7 | ExecStart = /root/src/local-btrfs-backup/src/push-btrfs %I.json |
8 | 8 | ||
9 | [Install] | 9 | [Install] |
10 | WantedBy=default.target | 10 | WantedBy=default.target |