summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorAndrew Cady <d@jerkface.net>2023-05-24 11:36:35 -0400
committerAndrew Cady <d@jerkface.net>2023-05-24 11:36:35 -0400
commitc8daee10060554d1692e808c040c1f4263fab361 (patch)
treebed0ead6d939c6c501c9feac5eacf7c6374f7521 /src
parenta002492cc53c77e0435e957f86aa546f4ce2d1b0 (diff)
rename
Diffstat (limited to 'src')
-rwxr-xr-xsrc/push-btrfs127
-rw-r--r--src/push-btrfs@.service2
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
2MIN_AGE_SECONDS=60
3CONFIG_DIR=/etc/btrfs-backup/remotes
4
5die()
6{
7 printf 'Error: %s\n' "$*" >&2
8 exit 1
9}
10
11read_config()
12{
13 eval config="($(jq -r '. | to_entries | .[] | "[\"" + .key + "\"]=" + (.value | @sh)'))"
14}
15
16check_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
24check_user_is_root()
25{
26 [ "$UID" = 0 ]
27}
28
29is_subvolume()
30{
31 btrfs subvolume show -- "$1" >/dev/null 2>&1
32}
33
34is_readonly_subvolume()
35{
36 btrfs subvolume show -- "$1" | egrep -q '^ Flags:.*\breadonly\b'
37}
38
39get_age()
40{
41 now=$(date +%s)
42 then=$(date +%s -d "${1##*.snapshot~}")
43 echo $((now - then))
44}
45
46btrfs_receive()
47{
48 ssh -- "${1%%:*}" btrfs receive -- "${1#*:}"
49}
50
51set -e
52check_dependencies
53check_user_is_root
54
55if [ $# = 1 ]
56then
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
65else
66 echo "Usage: $0 <config.json>" >&2
67 exit 1
68fi
69
70[ -r "$config_file" ] || die "config file does not exist: $config_file"
71exec 3<>"$config_file"
72flock -n 3
73declare -A config
74read_config <&3
75
76src=${config[source]}
77dst=${config[destination]}
78remote_head=${config[head]}
79
80case "$dst" in
81 *:/*) ;;
82 /*) dst=localhost:$dst ;;
83 *) die "Invalid destination: $dst" ;;
84esac
85
86is_subvolume "$src"
87
88if [ "$remote_head" ]
89then
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
98fi
99
100new_snapshot=${src%/}/.snapshot~$(date -Ins)
101btrfs subvolume snapshot -r -- "$src" "$new_snapshot"
102if ! btrfs send ${remote_head:+ -p "$remote_head"} -- "$new_snapshot" | pv | btrfs_receive "$dst"
103then
104 btrfs subvolume delete "$new_snapshot"
105 exit 1
106fi
107jq --arg h "$new_snapshot" '. | .head=$h' <"$config_file" >"$config_file_temp"
108mv -T -- "$config_file_temp" "$config_file"
109
110if [ "$remote_head" ]
111then
112 btrfs subvolume delete "$remote_head"
113fi
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
4ConditionFileNotEmpty = /etc/btrfs-backup/remotes/%I.json 4ConditionFileNotEmpty = /etc/btrfs-backup/remotes/%I.json
5 5
6[Service] 6[Service]
7ExecStart = /root/src/local-btrfs-backup/push-btrfs %I.json 7ExecStart = /root/src/local-btrfs-backup/src/push-btrfs %I.json
8 8
9[Install] 9[Install]
10WantedBy=default.target 10WantedBy=default.target