Two mechanical sweeps, no behaviour change on a single-root install. The 14 `[[ "$p" == "$containers_dir"* ]]` prefix tests that decide manager-vs-container-user elevation become pathIsContainerData, so a file on a second storage root is no longer misclassified as manager-owned — which would have written it with the wrong owner and failed later, far from the cause. The 65 references to the WebUI's own tree become webuiDir(), which is pinned to the primary root by design. Two traps found while doing it: run_privileged.sh is sourced directly by init.sh without paths.sh, so it needs a fallback. Defining one named pathIsContainerData was wrong: generate_function_manifest.sh indexes top-level definitions, and the resulting autoload stub would have shadowed the real multi-root implementation with the primary-only fallback — silently classifying every file on a second disk as manager-owned, which is exactly the bug this sweep exists to prevent. Renamed to _runCfgIsContainerPath, which delegates when the real one is loaded. setup_lock.sh built its path in a top-level assignment, so it was evaluated at source time and needed the file flagged eager. Made it a function instead: the path resolves on call, and the file drops off LP_EAGER_FILES entirely. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
123 lines
5.3 KiB
Bash
123 lines
5.3 KiB
Bash
#!/bin/bash
|
|
|
|
# Per-app routine WebUI-refresh hook (appWebuiRefresh_<app>): webui_updater calls
|
|
# it on every WebUI update while gluetun is installed; the installer (gluetun.sh)
|
|
# and the 'gluetun_refresh_providers' tool call it directly too.
|
|
#
|
|
# Fetches gluetun's upstream servers.json, slims it down to a
|
|
# { providers: { <name>: { vpnTypes, countries } } } shape, and writes it
|
|
# to the WebUI data dir so the per-app config dropdowns stay honest as
|
|
# gluetun adds/removes providers and protocols. Falls back silently to the
|
|
# previous snapshot (or the bundled default) on network failure.
|
|
appWebuiRefresh_gluetun() {
|
|
local output_file="$(webuiDir)/frontend/data/apps/generated/gluetun-providers.json"
|
|
local upstream="https://raw.githubusercontent.com/qdm12/gluetun/master/internal/storage/servers.json"
|
|
local tmp="$(mktemp)"
|
|
local raw="${output_file}.raw.$$"
|
|
|
|
runFileOp mkdir -p "$(dirname "$output_file")"
|
|
|
|
if ! command -v jq >/dev/null 2>&1; then
|
|
isNotice "jq not installed; skipping gluetun provider refresh."
|
|
return 0
|
|
fi
|
|
|
|
# Refresh throttle. The WebUI updater calls this hook on every routine
|
|
# update (the task processor fires it repeatedly), but the upstream server
|
|
# list is a few MB and changes rarely — re-fetching it each time adds a
|
|
# silent, slow-on-a-bad-link stall to every update. So only reach upstream
|
|
# once per refresh window. Direct callers that want a guaranteed pull (the
|
|
# Tools "refresh" button, the install hook) set GLUETUN_PROVIDERS_FORCE=1.
|
|
# CFG_GLUETUN_PROVIDERS_REFRESH_HOURS=0 disables auto-fetch (manual only).
|
|
local refresh_hours="${CFG_GLUETUN_PROVIDERS_REFRESH_HOURS:-24}"
|
|
local stamp="/tmp/libreportal_gluetun_providers_checked"
|
|
if [[ "${GLUETUN_PROVIDERS_FORCE:-0}" != "1" ]]; then
|
|
if [[ "$refresh_hours" == "0" ]]; then
|
|
return 0
|
|
fi
|
|
if [[ -s "$output_file" ]]; then
|
|
local _now _last
|
|
_now=$(date +%s)
|
|
_last=$(stat -c '%Y' "$stamp" 2>/dev/null || echo 0)
|
|
if (( _now - _last < refresh_hours * 3600 )); then
|
|
return 0
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# GitHub raw only sends ETag (no Last-Modified), so use If-None-Match
|
|
# via a sidecar to skip the multi-MB body when nothing has changed
|
|
# upstream. --compressed asks for gzip too (this JSON shrinks ~7x), and
|
|
# --connect-timeout fails fast on a dead link instead of hanging.
|
|
local etag_file="${output_file}.etag"
|
|
local etag=""
|
|
[[ -s "$etag_file" ]] && etag=$(<"$etag_file")
|
|
local headers="${output_file}.hdr.$$"
|
|
local http_code
|
|
http_code=$(curl -sSL --compressed \
|
|
${etag:+-H "If-None-Match: $etag"} \
|
|
--connect-timeout 15 \
|
|
--speed-limit 1000 --speed-time 30 \
|
|
--retry 2 --retry-delay 2 --retry-all-errors \
|
|
-D "$headers" -o "$raw" \
|
|
-w '%{http_code}' "$upstream") || http_code=""
|
|
|
|
# Whatever the outcome (fresh copy, 304, or a failed/slow link), record
|
|
# that we just checked so the throttle window restarts — a persistent
|
|
# outage then can't re-stall every subsequent update.
|
|
touch "$stamp" 2>/dev/null || true
|
|
|
|
# $raw and $headers live next to $output_file (under containers_dir/
|
|
# libreportal/frontend/data/, dockerinstall-owned in rootless). The
|
|
# manager can't `rm` them directly without a Permission denied — same
|
|
# class of bug as the updateConfigOption sed-i issue. runFileOp routes
|
|
# the rm through the right user. $tmp is from mktemp (/tmp), so
|
|
# `rm -f $tmp` stays unwrapped.
|
|
if [[ "$http_code" == "304" ]]; then
|
|
runFileOp rm -f "$raw" "$headers"
|
|
return 0
|
|
fi
|
|
if [[ "$http_code" != "200" ]]; then
|
|
isNotice "Upstream fetch failed (${http_code:-no response}); keeping existing snapshot."
|
|
runFileOp rm -f "$raw" "$headers"
|
|
return 0
|
|
fi
|
|
local new_etag
|
|
new_etag=$(awk 'tolower($1)=="etag:"{print $2}' "$headers" | tr -d '\r')
|
|
runFileOp rm -f "$headers"
|
|
|
|
# servers.json is a top-level object keyed by provider; each provider
|
|
# entry has a `servers` array whose items have `vpn` (wireguard|openvpn),
|
|
# `country`, `city`, etc. We collapse that into per-provider unique
|
|
# vpn-type and country lists. Drop the `version` key (it's not a provider).
|
|
if ! jq '
|
|
[ to_entries[]
|
|
| select(.key != "version")
|
|
| { key: .key,
|
|
value: {
|
|
vpnTypes: ((.value.servers // []) | map(.vpn) | unique | map(select(. != null and . != ""))),
|
|
countries: ((.value.servers // []) | map(.country) | unique | map(select(. != null and . != "")))
|
|
}
|
|
}
|
|
]
|
|
| from_entries
|
|
| { providers: . }
|
|
' "$raw" > "$tmp" 2>/dev/null; then
|
|
isNotice "Failed to parse gluetun servers.json; keeping existing provider snapshot."
|
|
runFileOp rm -f "$raw"
|
|
rm -f "$tmp"
|
|
return 0
|
|
fi
|
|
|
|
runFileOp rm -f "$raw"
|
|
|
|
if [ -s "$tmp" ]; then
|
|
runFileWrite "$output_file" < "$tmp"; rm -f "$tmp"
|
|
[[ -n "$new_etag" ]] && echo "$new_etag" | runFileWrite "$etag_file"
|
|
isSuccessful "Refreshed gluetun provider snapshot ($(jq '.providers | length' "$output_file") providers)."
|
|
else
|
|
rm -f "$tmp"
|
|
isNotice "Empty gluetun snapshot generated; ignoring."
|
|
fi
|
|
}
|