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