lib/update.sh

10.5 KB · 380 lines · bash

1#!/usr/bin/env bash
2
3BRT_BASE_URL="${BRT_BASE_URL:-https://brt.sushii.dev}"
4BRT_INSTALL_DIR="${BRT_DIR:-${BRT_INSTALL_DIR:-${HOME}/.local/share/brt}}"
5BRT_BIN_DIR="${BRT_BIN_DIR:-${HOME}/.local/bin}"
6BRT_UPDATE_CHECK_MARKER='# brt auto-update check'
7BRT_UPDATE_CHECK_INTERVAL="${BRT_UPDATE_CHECK_INTERVAL:-86400}"
8
9brt_shell_rc_file() {
10 case "${SHELL##*/}" in
11 zsh) printf '%s' "${HOME}/.zshrc" ;;
12 bash)
13 if [[ "$(uname -s)" == "Darwin" ]]; then
14 printf '%s' "${HOME}/.bash_profile"
15 else
16 printf '%s' "${HOME}/.bashrc"
17 fi
18 ;;
19 *) printf '%s' "${HOME}/.profile" ;;
20 esac
21}
22
23brt_ensure_startup_check_hook() {
24 brt_remove_startup_check_hook
25}
26
27brt_remove_startup_check_hook() {
28 local rc tmp
29 rc=$(brt_shell_rc_file)
30 [[ -f "$rc" ]] || return 0
31 grep -qE 'brt -update-check|# brt auto-update check|e\[3J|\033\[3J|\[3J.*\[2J' "$rc" 2>/dev/null || return 0
32
33 tmp=$(mktemp)
34 awk -v marker="$BRT_UPDATE_CHECK_MARKER" '
35 $0 == marker { skip=1; next }
36 /brt -update-check/ { skip=1; next }
37 /e\[3J|print -n.*3J|printf.*\\033\[3J/ { skip=1; next }
38 skip {
39 if (/disown/) { skip=0; next }
40 if (/&!/) { skip=0; next }
41 if (/nonotify|nomonitor/) next
42 if (/set \+m/) next
43 if (/setopt localoptions/) next
44 if (/\/dev\/tty/) next
45 if (/sleep 0\./) next
46 if (/print -n|printf/) next
47 if (/^[[:space:]]*[\{\(][[:space:]]*$/) next
48 if (/^[[:space:]]*\}[[:space:]]*$/) { skip=0; next }
49 if (/^[[:space:]]*$/) { skip=0; next }
50 skip=0
51 }
52 { print }
53 ' "$rc" > "$tmp" && mv "$tmp" "$rc"
54}
55
56brt_update_resolve_install_dir() {
57 local brt_bin install_dir
58 brt_bin=$(command -v brt 2>/dev/null || true)
59 [[ -n "$brt_bin" ]] || return 1
60 install_dir=$(brt_realpath "$brt_bin" 2>/dev/null || printf '%s' "$brt_bin")
61 dirname "$install_dir"
62}
63
64brt_update_check_startup() {
65 local cache_dir="${HOME}/.brt"
66 local cache_file="${cache_dir}/last-update-check"
67 local install_dir local_version remote_version newest now last=0
68
69 [[ "${BRT_AUTO_UPDATE:-1}" == "0" ]] && return 0
70 command -v curl &>/dev/null || return 0
71
72 install_dir="${BRT_DIR:-}"
73 if [[ -z "$install_dir" || ! -f "${install_dir}/brt" ]]; then
74 install_dir=$(brt_update_resolve_install_dir 2>/dev/null || true)
75 fi
76 [[ -n "$install_dir" && -f "${install_dir}/brt" ]] || return 0
77
78 now=$(date +%s)
79 if [[ -f "$cache_file" ]]; then
80 last=$(tr -d '[:space:]' < "$cache_file" 2>/dev/null || echo 0)
81 [[ "$last" =~ ^[0-9]+$ ]] || last=0
82 if (( now - last < BRT_UPDATE_CHECK_INTERVAL )); then
83 return 0
84 fi
85 fi
86
87 remote_version=$(
88 curl -fsSL --max-time 4 \
89 -H 'Cache-Control: no-cache' \
90 -H 'Pragma: no-cache' \
91 "${BRT_BASE_URL}/version" 2>/dev/null \
92 | tr -d '[:space:]'
93 ) || return 0
94 [[ -n "$remote_version" ]] || return 0
95
96 mkdir -p "$cache_dir"
97 printf '%s\n' "$now" > "$cache_file"
98
99 local_version=$(brt_read_version "$install_dir")
100 local_version="${local_version:-unknown}"
101 [[ "$local_version" == "unknown" ]] && return 0
102
103 if [[ "$local_version" == "$remote_version" ]]; then
104 return 0
105 fi
106
107 newest=$(printf '%s\n' "$local_version" "$remote_version" | sort -V | tail -1)
108 if [[ "$newest" != "$remote_version" ]]; then
109 return 0
110 fi
111
112 printf '%s → %s\n' "$local_version" "$remote_version" > "${cache_dir}/update-available"
113 return 0
114}
115
116brt_update_check_manual() {
117 brt_require curl
118
119 local local_version remote_version newest
120 local_version=$(brt_read_version "$BRT_DIR")
121 local_version="${local_version:-${BRT_VERSION}}"
122
123 brt_info "Checking for updates at ${BRT_BASE_URL}..."
124
125 remote_version=$(
126 curl -fsSL --max-time 10 \
127 -H 'Cache-Control: no-cache' \
128 -H 'Pragma: no-cache' \
129 "${BRT_BASE_URL}/version" 2>/dev/null \
130 | tr -d '[:space:]'
131 ) || {
132 brt_error "Could not fetch version from ${BRT_BASE_URL}/version"
133 exit 1
134 }
135
136 if [[ -z "$remote_version" ]]; then
137 brt_error "Remote version file is empty"
138 exit 1
139 fi
140
141 if [[ "$local_version" == "$remote_version" ]]; then
142 brt_ok "brt ${local_version} is up to date"
143 return 0
144 fi
145
146 newest=$(printf '%s\n' "$local_version" "$remote_version" | sort -V | tail -1)
147 if [[ "$newest" != "$remote_version" ]]; then
148 brt_ok "brt ${local_version} is up to date (remote: ${remote_version})"
149 return 0
150 fi
151
152 brt_warn "Update available: ${local_version} → ${remote_version}"
153 brt_info "Run: brt -update"
154}
155
156brt_update_check() {
157 case "${1:-}" in
158 --startup)
159 brt_update_check_startup
160 ;;
161 --install-hook)
162 brt_remove_startup_check_hook
163 ;;
164 *)
165 brt_update_check_manual "$@"
166 ;;
167 esac
168}
169
170BRT_LIB_MODULES="common progress deps ip dns portscan ping whois trace curl post headers ssl reverse edit json encode hash url grep uuid pass run serve watch listen killport du extract sysinfo publickey timestamp timer calc env which speed weather qr image video doom browse config access update uninstall"
171
172UPDATE_FILES=()
173UPDATE_DOWNLOAD=()
174UPDATE_SKIPPED=0
175
176brt_update_download_files() {
177 UPDATE_FILES=(brt)
178 local f
179 for f in ${BRT_LIB_MODULES}; do
180 UPDATE_FILES+=("lib/${f}.sh")
181 done
182 UPDATE_FILES+=(lib/access.py lib/media.py lib/browse.py version)
183}
184
185brt_update_sha256_file() {
186 local file="$1"
187 [[ -f "$file" ]] || return 0
188 if command -v sha256sum &>/dev/null; then
189 sha256sum "$file" | awk '{print $1}'
190 else
191 shasum -a 256 "$file" | awk '{print $1}'
192 fi
193}
194
195brt_update_manifest_hash() {
196 local manifest="$1"
197 local path="$2"
198 awk -v p="$path" '$0 !~ /^#/ && $2 == p { print $1; exit }' "$manifest"
199}
200
201brt_update_fetch_checksums() {
202 local dest="$1"
203 curl -fsSL \
204 -H 'Cache-Control: no-cache' \
205 -H 'Pragma: no-cache' \
206 "${BRT_BASE_URL}/checksums" \
207 -o "$dest" 2>/dev/null
208}
209
210brt_update_remove_stale_files() {
211 local f rel keep entry
212 for f in "${BRT_INSTALL_DIR}/lib/"*.sh "${BRT_INSTALL_DIR}/lib/"*.py; do
213 [[ -e "$f" ]] || continue
214 rel="lib/$(basename "$f")"
215 keep=0
216 for entry in "${UPDATE_FILES[@]}"; do
217 [[ "$entry" == "$rel" ]] && keep=1 && break
218 done
219 [[ "$keep" -eq 1 ]] || rm -f "$f"
220 done
221}
222
223brt_update_plan_downloads() {
224 local manifest="$1"
225 local path remote_hash local_hash
226 UPDATE_DOWNLOAD=()
227 UPDATE_SKIPPED=0
228
229 for path in "${UPDATE_FILES[@]}"; do
230 remote_hash=$(brt_update_manifest_hash "$manifest" "$path")
231 if [[ -n "$remote_hash" && -f "${BRT_INSTALL_DIR}/${path}" ]]; then
232 local_hash=$(brt_update_sha256_file "${BRT_INSTALL_DIR}/${path}")
233 if [[ "$remote_hash" == "$local_hash" ]]; then
234 ((UPDATE_SKIPPED++)) || true
235 continue
236 fi
237 fi
238 UPDATE_DOWNLOAD+=("$path")
239 done
240}
241
242brt_update_fetch() {
243 local path="$1"
244 local dest="${BRT_INSTALL_DIR}/${path}"
245 mkdir -p "$(dirname "$dest")"
246 curl -fsSL \
247 -H 'Cache-Control: no-cache' \
248 -H 'Pragma: no-cache' \
249 "${BRT_BASE_URL}/${path}" \
250 -o "$dest" || {
251 brt_error "Failed to download ${BRT_BASE_URL}/${path}"
252 exit 1
253 }
254}
255
256brt_update_remote_brt_version() {
257 curl -fsSL "${BRT_BASE_URL}/brt" 2>/dev/null \
258 | grep '^_BRT_EMBEDDED_VERSION=' \
259 | head -1 \
260 | cut -d'"' -f2
261}
262
263brt_update_install() {
264 local path total i=0
265 local manifest use_manifest=0
266
267 brt_update_download_files
268 mkdir -p "${BRT_INSTALL_DIR}/lib" "${BRT_BIN_DIR}"
269
270 manifest=$(mktemp)
271 if brt_update_fetch_checksums "$manifest"; then
272 use_manifest=1
273 brt_update_plan_downloads "$manifest"
274 else
275 UPDATE_DOWNLOAD=("${UPDATE_FILES[@]}")
276 UPDATE_SKIPPED=0
277 brt_warn "Remote checksums unavailable — downloading all files"
278 fi
279 rm -f "$manifest"
280
281 total=${#UPDATE_DOWNLOAD[@]}
282 if [[ "$total" -eq 0 ]]; then
283 brt_ok "All ${#UPDATE_FILES[@]} files already up to date"
284 else
285 if [[ "$use_manifest" -eq 1 && "$UPDATE_SKIPPED" -gt 0 ]]; then
286 brt_info "Downloading ${total} changed file(s), skipped ${UPDATE_SKIPPED} unchanged"
287 else
288 brt_info "Downloading brt from ${BRT_BASE_URL}..."
289 fi
290
291 for path in "${UPDATE_DOWNLOAD[@]}"; do
292 ((i++)) || true
293 brt_progress_bar "$i" "$total" "$path"
294 brt_update_fetch "$path"
295 done
296 fi
297
298 brt_update_remove_stale_files
299
300 chmod +x "${BRT_INSTALL_DIR}/brt" "${BRT_INSTALL_DIR}/lib/"*.sh "${BRT_INSTALL_DIR}/lib/access.py" "${BRT_INSTALL_DIR}/lib/media.py" "${BRT_INSTALL_DIR}/lib/browse.py" 2>/dev/null || true
301 ln -sf "${BRT_INSTALL_DIR}/brt" "${BRT_BIN_DIR}/brt"
302}
303
304brt_update_confirm() {
305 printf "Install update? [Y/n] "
306 read -r ans
307 [[ -z "$ans" || "$ans" =~ ^[Yy]$ ]]
308}
309
310brt_update() {
311 brt_require curl
312
313 local local_version remote_version remote_brt_version installed_version
314 local_version=$(brt_read_version "$BRT_DIR")
315 local_version="${local_version:-${BRT_VERSION}}"
316
317 brt_info "Checking for updates at ${BRT_BASE_URL}..."
318
319 remote_version=$(curl -fsSL "${BRT_BASE_URL}/version" 2>/dev/null | tr -d '[:space:]') || {
320 brt_error "Could not fetch version from ${BRT_BASE_URL}/version"
321 exit 1
322 }
323
324 if [[ -z "$remote_version" ]]; then
325 brt_error "Remote version file is empty"
326 exit 1
327 fi
328
329 remote_brt_version=$(brt_update_remote_brt_version)
330 if [[ -z "$remote_brt_version" ]]; then
331 brt_error "Could not read version from ${BRT_BASE_URL}/brt"
332 exit 1
333 fi
334
335 if [[ "$remote_version" != "$remote_brt_version" ]]; then
336 brt_error "Site files are out of sync (version=${remote_version}, brt=${remote_brt_version})"
337 brt_info "Try again after the site redeploys, or reinstall:"
338 brt_info "$(brt_install_hint_primary)"
339 exit 1
340 fi
341
342 if [[ "$local_version" == "$remote_version" ]]; then
343 brt_ok "brt ${local_version} is up to date"
344 exec "${BRT_DIR}/brt" -version
345 fi
346
347 local newest
348 newest=$(printf '%s\n' "$local_version" "$remote_version" | sort -V | tail -1)
349
350 if [[ "$newest" != "$remote_version" ]]; then
351 brt_ok "brt ${local_version} is up to date (remote: ${remote_version})"
352 exec "${BRT_DIR}/brt" -version
353 fi
354
355 brt_info "Update available: ${local_version} → ${remote_version}"
356 if ! brt_update_confirm; then
357 brt_info "Update cancelled"
358 return 0
359 fi
360
361 brt_update_install
362
363 installed_version=$(brt_read_version "$BRT_INSTALL_DIR")
364 if [[ "$installed_version" != "$remote_version" ]]; then
365 brt_error "Update failed: expected ${remote_version}, got ${installed_version:-unknown}"
366 exit 1
367 fi
368
369 hash -r 2>/dev/null || rehash 2>/dev/null || true
370
371 # shellcheck source=/dev/null
372 source "${BRT_INSTALL_DIR}/lib/deps.sh"
373 brt_install_media_dependencies || true
374
375 brt_ok "Updated to brt ${installed_version}"
376 brt_info "Installed at: ${BRT_INSTALL_DIR}"
377 brt_ensure_startup_check_hook || true
378 exec "${BRT_INSTALL_DIR}/brt" -version
379}
380