summaryrefslogtreecommitdiff
path: root/push-btrfs
diff options
context:
space:
mode:
Diffstat (limited to 'push-btrfs')
-rwxr-xr-xpush-btrfs113
1 files changed, 113 insertions, 0 deletions
diff --git a/push-btrfs b/push-btrfs
new file mode 100755
index 0000000..5e9b537
--- /dev/null
+++ b/push-btrfs
@@ -0,0 +1,113 @@
1#!/bin/bash
2MAX_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 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
34get_age()
35{
36 now=$(date +%s)
37 then=$(stat -L -c %Y "$1")
38 echo $((now - then))
39}
40
41btrfs_receive()
42{
43 ssh -- "${1%%:*}" btrfs receive -- "${1#*:}"
44}
45
46set -e
47check_dependencies
48check_user_is_root
49
50if [ $# = 1 ]
51then
52 case "$1" in
53 '' ) die 'config file name is blank' ;;
54 */*) die 'config file name must not contain "/"' ;;
55 *.json) ;;
56 *) die "config file name must end in '.json'" ;;
57 esac
58 config_file=$CONFIG_DIR/$1
59 config_file_temp=$CONFIG_DIR/.$1~tmp
60else
61 echo "Usage: $0 <config.json>" >&2
62 exit 1
63fi
64
65[ -r "$config_file" ] || die "config file does not exist: $config_file"
66exec 3<>"$config_file"
67flock -n 3
68declare -A config
69read_config <&3
70
71src=${config[source]}
72dst=${config[destination]}
73remote_head=${config[head]}
74
75case "$dst" in
76 *:/*) ;;
77 /*) dst=localhost:$dst ;;
78 *) die "Invalid destination: $dst" ;;
79esac
80
81is_subvolume "$src"
82
83if [ "$(get_age "$remote_head")" -le "$MAX_AGE_SECONDS" ]
84then
85 echo "Up-to-date." >&2
86 exit
87else
88 new_snapshot=${src%/}/.snapshot~$(date -Ins)
89 btrfs subvolume snapshot -r -- "$src" "$new_snapshot"
90fi
91
92btrfs send ${remote_head:+ -p "$remote_head"} -- "$new_snapshot" | pv | btrfs_receive "$dst"
93jq --arg h "$new_snapshot" '. | .head=$h' <"$config_file" >"$config_file_temp"
94mv -T -- "$config_file_temp" "$config_file"
95
96if [ "$remote_head" ]
97then
98 btrfs subvolume delete "$remote_head"
99fi
100
101### OK, basic idea:
102#
103## the system has a list of source subvolumes
104## each source subvolume has a snapshot period
105## each source subvolume has a list of subscribers
106## each source subvolume has an ordered list of its snapshots
107## each subscriber has a last-received snapshot (if any)
108
109# when a subvolume's last snapshot is older than the configured period, a new snapshot is created
110# when a subvolume has subscribers whose last snapshot is out-of-date, a snapshot is pushed (using the common ancestor)
111# the subscriber's last snapshot is then updated
112# if there are no subscriber's holding open a snapshot which is not the latest snapshot, then it is deleted
113