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>
181 lines
9.1 KiB
Bash
Executable File
181 lines
9.1 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
# LibrePortal WebUI Main Updater
|
|
# Coordinates all webui data updates including system info and app configurations
|
|
|
|
webuiLibrePortalUpdate() {
|
|
# Debounce safety net: if this function gets called twice in quick
|
|
# succession (e.g. two app installs in a row), skip the second run when the
|
|
# last successful one is within the debounce window. The fresh-install
|
|
# duplicate — installLibrePortal followed by startScan — is handled
|
|
# deterministically upstream (installLibrePortal defers to startScan during a
|
|
# bootstrap install), so this timer is only a backstop. Callers that
|
|
# genuinely need a forced refresh can set WEBUI_UPDATER_FORCE=1.
|
|
local stamp_file="/tmp/libreportal_webui_updater_last"
|
|
local debounce_seconds="${WEBUI_UPDATER_DEBOUNCE:-30}"
|
|
if [[ -z "$WEBUI_UPDATER_FORCE" && -f "$stamp_file" ]]; then
|
|
local _now=$(date +%s)
|
|
local _last=$(stat -c '%Y' "$stamp_file" 2>/dev/null)
|
|
if [[ -n "$_last" ]] && (( _now - _last < debounce_seconds )); then
|
|
isNotice "WebUI updater ran $((_now - _last))s ago — skipping (debounced, within ${debounce_seconds}s)."
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
local status=$(dockerCheckAppInstalled "libreportal" "docker")
|
|
|
|
# Main process: generate config if app is installed
|
|
if [ "$status" == "installed" ]; then
|
|
isHeader "LibrePortal WebUI Updater"
|
|
|
|
# Check for update lock file first. webuiCheckUpdateLock echoes its
|
|
# verdict on stdout (and auto-clears a stale lock); capture it directly.
|
|
# A $(...) subshell means any global the function sets never reaches us
|
|
# here, so the verdict MUST come back via the echo, not a shared var.
|
|
local lock_file_found; lock_file_found=$(webuiCheckUpdateLock)
|
|
checkSuccess "Checked for update lock file."
|
|
|
|
if [[ "$lock_file_found" == "true" ]]; then
|
|
echo ""
|
|
isNotice "Update already in progress. Please wait for current update to complete."
|
|
isNotice "Status: Locked"
|
|
isNotice "WebUI update skipped due to concurrent update"
|
|
echo ""
|
|
else
|
|
# Create update lock file
|
|
local result; result=$(webuiCreateUpdateLock)
|
|
checkSuccess "Created update lock file..."
|
|
|
|
# Update system information first
|
|
local result; result=$(webuiSystemUpdate)
|
|
checkSuccess "Updated system information..."
|
|
|
|
# Ensure task system files exist (failsafe)
|
|
local result; result=$(webuiEnsureTaskFiles)
|
|
checkSuccess "Ensured task system files exist..."
|
|
|
|
# Generate system configuration
|
|
local result; result=$(webuiGenerateSystemConfigs)
|
|
checkSuccess "Generated system configurations..."
|
|
|
|
# Generate categories
|
|
local result; result=$(webuiCreateCategories $(webuiDir)/frontend/data)
|
|
checkSuccess "Generated app and config categories..."
|
|
|
|
# Generate LibrePortal app configuration
|
|
local result; result=$(webuiGenerateLibrePortalConfig)
|
|
checkSuccess "Generated LibrePortal app configuration..."
|
|
|
|
# Generate apps-services.json
|
|
local result; result=$(webuiGenerateAppsServicesConfig)
|
|
checkSuccess "Generated apps-services.json..."
|
|
|
|
# Generate apps-tools.json (aggregate of per-app *.tools.json)
|
|
local result; result=$(webuiGenerateAppsToolsConfig)
|
|
checkSuccess "Generated apps-tools.json..."
|
|
|
|
# Per-app routine refresh hooks. An installed app may define
|
|
# appWebuiRefresh_<app> (in containers/<app>/scripts/) for data it
|
|
# wants refreshed on every WebUI update — e.g. gluetun's provider
|
|
# snapshot. Gated on the app being installed (its live compose
|
|
# exists, tested directly so it works without list perm on the
|
|
# container-user-owned data dir), so non-users never pay for it.
|
|
local _app _dir _hook
|
|
for _dir in "${install_containers_dir}"*/; do
|
|
_app="$(basename "$_dir")"
|
|
[[ -f "${containers_dir}${_app}/docker-compose.yml" ]] || continue
|
|
_hook="appWebuiRefresh_${_app}"
|
|
declare -F "$_hook" >/dev/null 2>&1 || continue
|
|
# Announce before running: a hook may reach upstream (e.g.
|
|
# gluetun's provider list), and its own output is captured
|
|
# below, so without this the update looks frozen mid-fetch.
|
|
isNotice "Refreshing ${_app} WebUI data..."
|
|
local result; result=$($_hook)
|
|
checkSuccess "Refreshed ${_app} WebUI data..."
|
|
done
|
|
|
|
# Backup dashboard data, split by cost:
|
|
# * local-only (engines/schema/passwords) — cheap file + config
|
|
# reads, no remote I/O; always run so engine/password/config
|
|
# edits surface on the next pass.
|
|
# * remote (locations stats, dashboard, snapshots, app-status,
|
|
# migrate) — each spawns restic against every enabled location
|
|
# (over SSH for remote repos). Backups and location changes
|
|
# already regen this live at the moment they happen
|
|
# (backup_app_start.sh / location_*.sh), so this routine pass is
|
|
# only a drift catch-up. Throttle it: pull at most once per
|
|
# CFG_BACKUP_DASHBOARD_REFRESH_INTERVAL minutes unless a backup
|
|
# marked the data dirty since, or the caller forced a full
|
|
# refresh (WEBUI_UPDATER_FORCE). 0 disables the throttle.
|
|
local result
|
|
result=$(webuiGenerateBackupEngines && webuiGenerateBackupSchema && webuiGenerateBackupPasswords)
|
|
checkSuccess "Refreshed backup engine/schema/password data..."
|
|
|
|
local backup_refresh_min="${CFG_BACKUP_DASHBOARD_REFRESH_INTERVAL:-30}"
|
|
local backup_stamp="/tmp/libreportal_webui_backup_refreshed"
|
|
# Touched by backup_app_start.sh when a backup completes; a value
|
|
# newer than the stamp forces the next routine pass to pull.
|
|
local backup_dirty="/tmp/libreportal_webui_backup_dirty"
|
|
local do_remote=1
|
|
if [[ -z "$WEBUI_UPDATER_FORCE" && "$backup_refresh_min" != "0" && -f "$backup_stamp" ]]; then
|
|
local _bnow _blast _bdirty
|
|
_bnow=$(date +%s)
|
|
_blast=$(stat -c '%Y' "$backup_stamp" 2>/dev/null || echo 0)
|
|
_bdirty=$(stat -c '%Y' "$backup_dirty" 2>/dev/null || echo 0)
|
|
if (( _bnow - _blast < backup_refresh_min * 60 )) && (( _bdirty <= _blast )); then
|
|
do_remote=0
|
|
fi
|
|
fi
|
|
|
|
if (( do_remote )); then
|
|
# Announce before running: the chain reaches remote backup
|
|
# locations (restic over SSH) and its output is captured below,
|
|
# so without this the update looks frozen mid-fetch on slow links.
|
|
isNotice "Refreshing backup data (may be slow on poor connections)..."
|
|
# One unfiltered restic snapshots pull per location, shared across
|
|
# the generators below instead of one each — see engineSnapshotsJson.
|
|
export LP_SNAP_CACHE_DIR; LP_SNAP_CACHE_DIR="$(mktemp -d 2>/dev/null)"
|
|
result=$(webuiGenerateBackupLocations && webuiGenerateBackupDashboard && webuiGenerateBackupSnapshots all && webuiGenerateBackupAppStatus && webuiGenerateBackupMigrate)
|
|
checkSuccess "Refreshed backup dashboard data..."
|
|
[[ -n "$LP_SNAP_CACHE_DIR" ]] && rm -rf "$LP_SNAP_CACHE_DIR"
|
|
unset LP_SNAP_CACHE_DIR
|
|
touch "$backup_stamp" 2>/dev/null || true
|
|
else
|
|
isNotice "Backup data current — remote refresh throttled (last pull <${backup_refresh_min}m ago)."
|
|
fi
|
|
|
|
# Peers (named other LibrePortal instances) — small, cheap; lives
|
|
# in its own data/peers/generated/peers.json file consumed by
|
|
# /peers and overlay-read by the migrate tab.
|
|
local result; result=$(webuiGeneratePeers)
|
|
checkSuccess "Refreshed peers data..."
|
|
|
|
# SSH access snapshot (authorized keys + password-login state)
|
|
local result; result=$(webuiGenerateSshAccess)
|
|
checkSuccess "Refreshed SSH access data..."
|
|
|
|
# Sync app icons
|
|
local result; result=$(webuiSyncAppIcons)
|
|
checkSuccess "Synced app icons..."
|
|
|
|
# Generate log files for installed apps
|
|
local result; result=$(webuiGenerateAppLogs)
|
|
checkSuccess "Generated log files for installed apps..."
|
|
|
|
# Remove update lock file
|
|
local result; result=$(webuiRemoveUpdateLock)
|
|
checkSuccess "Removed update lock file..."
|
|
|
|
# Remove setup lock file
|
|
local result; result=$(webuiRemoveSetupLock)
|
|
checkSuccess "Removed setup lock file..."
|
|
|
|
isSuccessful "WebUI update completed successfully!"
|
|
touch "$stamp_file" 2>/dev/null || true
|
|
fi
|
|
else
|
|
isNotice "LibrePortal not installed - skipping WebUI update"
|
|
fi
|
|
}
|
|
|