-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathexport_and_send.sh
More file actions
executable file
·197 lines (184 loc) · 7.78 KB
/
Copy pathexport_and_send.sh
File metadata and controls
executable file
·197 lines (184 loc) · 7.78 KB
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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
#!/usr/bin/env bash
# export_and_send.sh — export the phone snapshot, push it over Taildrop from
# a user-owned relay, and delete the local copy once the push succeeds.
#
# Usage: ./export_and_send.sh [tailnet-device] (default: ios-app)
#
# Rocky is a tagged Tailscale node, and Taildrop cannot send from tagged nodes.
# The relay is a separate, user-owned Tailscale node in a short-lived rootless
# Podman container. Its state volume survives each transfer, so it reconnects
# as the same node without interactive authentication; the container and its
# copy of the export are removed after every run. The relay receives the export
# through `podman cp`, never a mount of this checkout.
#
# Bootstrap the relay once by generating an untagged, non-ephemeral auth key
# in the Tailscale admin console, then enter it without placing it in shell
# history:
#
# read -rs -p 'Tailscale auth key: ' TAILSCALE_AUTHKEY
# printf '\n'
# export TAILSCALE_AUTHKEY
# ./export_and_send.sh
# unset TAILSCALE_AUTHKEY
#
# Disable key expiry for the resulting `wordle-taildrop` machine in the
# Tailscale admin console. Subsequent runs need no auth key. Do not put an auth
# key in this repository or in the persistent state volume. That volume holds
# the relay node's durable, user-level Tailnet credential. Protect it as you
# would any other standing credential; restrict its ACL grants to the receiving
# device where possible. To revoke it, remove `wordle-taildrop` in the
# Tailscale admin console and delete its Podman state volume.
#
# The export snapshot is transitory: its job ends when the bytes reach the
# phone. `tailscale file cp` blocks until the peer has received the file and
# exits nonzero otherwise, so its exit status is the handoff signal — the local
# copy is deleted only after a successful push. The phone deletes its copy
# after a successful merge (import_cache.py's default), which ends the cycle
# with no snapshot left on either device.
#
# The watermark file (next to this script) holds the unix time of the start
# of the last successfully pushed delta. Its absence means bootstrap: a full
# export, unchanged from the original whole-file behavior. Every export
# after that carries only rows updated since the watermark, via
# export_cache.py --since.
#
# next_watermark is captured *before* exporting, so a row that commits
# during this run's WAL-snapshot read is re-sent (not skipped) by the next
# delta — an overlap window absorbs clock skew and in-flight writes at the
# cost of some harmlessly re-sent rows (import_cache.py's merge is
# idempotent). The watermark file is updated only after the push succeeds:
# if the export or the push fails, the watermark is left untouched so the
# next run re-covers the same span.
set -euo pipefail
if [ $# -gt 1 ]; then
echo "Usage: $0 [tailnet-device] (default: ios-app)" >&2
exit 2
fi
tailnet_device="${1:-ios-app}"
cd "$(dirname "$0")"
export_file="wordle_erd_export.sqlite3"
watermark_file="wordle_export_watermark"
overlap_seconds=3600
taildrop_state_volume="${WORDLE_TAILDROP_STATE_VOLUME:-wordle-taildrop-state}"
taildrop_hostname="${WORDLE_TAILDROP_HOSTNAME:-wordle-taildrop}"
taildrop_image="${WORDLE_TAILDROP_IMAGE:-docker.io/tailscale/tailscale:stable}"
taildrop_container="wordle-taildrop"
taildrop_lock_file="${WORDLE_TAILDROP_LOCK_FILE:-${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}/wordle-taildrop-export.lock}"
taildrop_started=false
taildrop_temporary_output=""
if ! command -v flock >/dev/null; then
echo "flock is required to serialize Taildrop exports." >&2
exit 1
fi
exec {taildrop_lock_fd}>"$taildrop_lock_file"
if ! flock -n "$taildrop_lock_fd"; then
echo "Another Taildrop export is already running." >&2
exit 1
fi
cleanup_taildrop_container() {
if [ -n "$taildrop_temporary_output" ]; then
rm -f "$taildrop_temporary_output"
fi
if "$taildrop_started"; then
podman rm --force "$taildrop_container" >/dev/null 2>&1 || true
fi
}
trap cleanup_taildrop_container EXIT
trap 'exit 130' INT TERM
start_taildrop_relay() {
local -a podman_args=(
run --detach
--name "$taildrop_container"
--hostname "$taildrop_hostname"
--security-opt no-new-privileges
--cap-drop=ALL
--volume "$taildrop_state_volume:/var/lib/tailscale"
--tmpfs /exports:rw,noexec,nosuid,nodev
--env TS_AUTH_ONCE=true
--env TS_STATE_DIR=/var/lib/tailscale
--env TS_USERSPACE=true
)
if [ -n "${TAILSCALE_AUTHKEY:-}" ]; then
podman_args+=(--env TS_AUTHKEY)
fi
if podman container exists "$taildrop_container"; then
if [ "$(podman container inspect --format '{{.State.Running}}' \
"$taildrop_container" 2>/dev/null)" = true ]; then
echo "A Taildrop relay is already running: $taildrop_container." >&2
return 1
fi
echo "Removing a leftover Taildrop relay from an interrupted run:" >&2
podman logs "$taildrop_container" >&2 || true
podman rm --force "$taildrop_container" >/dev/null
fi
podman_args+=("$taildrop_image")
if [ -n "${TAILSCALE_AUTHKEY:-}" ]; then
TS_AUTHKEY="$TAILSCALE_AUTHKEY" podman "${podman_args[@]}" >/dev/null
else
podman "${podman_args[@]}" >/dev/null
fi
taildrop_started=true
local attempt status_json relay_running
for attempt in {1..30}; do
status_json="$(podman exec "$taildrop_container" \
tailscale status --json --peers=false 2>&1 || true)"
if [[ "$status_json" =~ \"BackendState\"[[:space:]]*:[[:space:]]*\"Running\" ]]; then
return
fi
relay_running="$(podman container inspect --format '{{.State.Running}}' \
"$taildrop_container" 2>/dev/null || true)"
if [ "$relay_running" != true ]; then
echo "The Taildrop relay stopped before it connected." >&2
podman logs "$taildrop_container" >&2 || true
return 1
fi
sleep 1
done
echo "The Taildrop relay did not connect within 30 seconds." >&2
if [ -z "${TAILSCALE_AUTHKEY:-}" ]; then
echo "For first-time setup, run with TAILSCALE_AUTHKEY=tskey-auth-...." >&2
fi
podman logs "$taildrop_container" >&2 || true
return 1
}
run_taildrop_command() {
local headline="$1" retry_instructions="$2" command_status
shift 2
taildrop_temporary_output="$(mktemp)"
if "$@" \
>"$taildrop_temporary_output" 2>&1; then
rm -f "$taildrop_temporary_output"
taildrop_temporary_output=""
return
else
command_status=$?
fi
echo "$headline" >&2
echo "The export was kept at $export_file; retrying will resend it." >&2
if "$retry_instructions"; then
echo "On the receiving device, open Tailscale and wait for it to report synchronized, then retry:" >&2
echo " ./export_and_send.sh" >&2
fi
if [ -s "$taildrop_temporary_output" ]; then
printf 'Details: %s\n' "$(<"$taildrop_temporary_output")" >&2
fi
rm -f "$taildrop_temporary_output"
taildrop_temporary_output=""
return "$command_status"
}
since_args=()
if [ -f "$watermark_file" ]; then
watermark="$(cat "$watermark_file")"
since_args=(--since "$watermark")
fi
next_watermark=$(( $(date +%s) - overlap_seconds ))
start_taildrop_relay
python3.13 export_cache.py "${since_args[@]}"
run_taildrop_command "The Taildrop relay could not prepare the export." false \
podman cp "$export_file" "$taildrop_container:/exports/$export_file"
run_taildrop_command "Taildrop could not reach $tailnet_device." true \
podman exec "$taildrop_container" \
tailscale file cp "/exports/$export_file" "${tailnet_device}:"
echo "$next_watermark" > "$watermark_file"
rm -f "$export_file" "$export_file-wal" "$export_file-shm"
echo "Sent $export_file to $tailnet_device through the Taildrop relay and deleted the local copy."