refactor(storage): route elevation tests and the WebUI tree through paths.sh

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>
This commit is contained in:
librelad 2026-08-24 04:04:19 +01:00
parent db04f8f000
commit 2d24a764a8
65 changed files with 114 additions and 83 deletions

View File

@ -5,7 +5,7 @@
# mount set takes effect.
crowdsecToggleLibrePortalLogMounts() {
local mode="$1"
local compose="${containers_dir}libreportal/docker-compose.yml"
local compose="$(webuiDir)/docker-compose.yml"
[[ -f "$compose" ]] || return 0
case "$mode" in
@ -26,7 +26,7 @@ crowdsecToggleLibrePortalLogMounts() {
if runFileOp docker ps --format '{{.Names}}' 2>/dev/null | grep -q '^libreportal-service$'; then
isNotice "Recreating libreportal so log mount toggle takes effect..."
( cd "${containers_dir}libreportal" && runAsManager docker compose up -d >/dev/null 2>&1 ) || true
( cd "$(webuiDir)" && runAsManager docker compose up -d >/dev/null 2>&1 ) || true
fi
}

View File

@ -9,8 +9,8 @@
appDashyUpdateConf()
{
local conf_file="${containers_dir}dashy/etc/conf.yml"
local services_json="${containers_dir}libreportal/frontend/data/apps/generated/apps-services.json"
local icons_src_dir="${containers_dir}libreportal/frontend/icons/apps"
local services_json="$(webuiDir)/frontend/data/apps/generated/apps-services.json"
local icons_src_dir="$(webuiDir)/frontend/icons/apps"
# Don't check the apps DB here — appUpdateSpecifics calls us at
# install step 6, BEFORE databaseInstallApp (step 8) writes the

View File

@ -10,7 +10,7 @@
# 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="${containers_dir}libreportal/frontend/data/apps/generated/gluetun-providers.json"
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.$$"

View File

@ -5,7 +5,7 @@
# override flag. Skips itself, Traefik, LibrePortal, fail2ban.
gluetunRouteExistingAppsPrompt()
{
local categories_file="${containers_dir}libreportal/frontend/data/apps/gluetun-eligible-categories.json"
local categories_file="$(webuiDir)/frontend/data/apps/gluetun-eligible-categories.json"
local override="${CFG_REQUIREMENT_GLUETUN_FOR_ALL:-false}"
if ! command -v sqlite3 >/dev/null 2>&1 || [[ ! -f "$docker_dir/$db_file" ]]; then

View File

@ -25,7 +25,7 @@ libreportal_catalog_install_post_setup()
# Bundle the App Center's category icons so the site's sidebar matches the
# WebUI (same per-category glyphs). Copied from the live frontend so they
# stay in step; best-effort (the site falls back to misc.svg if absent).
local cat_src="$containers_dir/libreportal/frontend/core/icons/categories"
local cat_src="$(webuiDir)/frontend/core/icons/categories"
if [[ -d "$cat_src" ]]; then
runFileOp mkdir -p "$dest/categories" || true
runFileOp cp -f "$cat_src"/*.svg "$dest/categories/" 2>/dev/null || true

View File

@ -12,7 +12,7 @@
cliAppToolList()
{
local filter_app="$1"
local manifest="$containers_dir/libreportal/frontend/data/apps/generated/apps-tools.json"
local manifest="$(webuiDir)/frontend/data/apps/generated/apps-tools.json"
if [[ ! -f "$manifest" ]]; then
isError "apps-tools.json not found at $manifest. Run the WebUI updater to generate it."

View File

@ -194,7 +194,7 @@ updaterSetAnchorRef()
# — i.e. the roll-back target. Read from history.json's most recent update/ok.
updaterLastUpdateFrom()
{
local app="$1" hist="$containers_dir/libreportal/frontend/data/updater/generated/history.json"
local app="$1" hist="$(webuiDir)/frontend/data/updater/generated/history.json"
[ -f "$hist" ] && command -v jq >/dev/null 2>&1 || return 0
jq -r --arg a "$app" 'first(.entries[]? | select(.app==$a and .action=="update" and .result=="ok") | .from) // ""' "$hist" 2>/dev/null
}
@ -336,7 +336,7 @@ updaterRecordHistory()
{
local app="$1" action="$2" from="$3" to="$4" result="$5"
local artifact_id="${6:-}" serial="${7:-}" undo_id="${8:-}" trigger="${9:-manual}"
local f="$containers_dir/libreportal/frontend/data/updater/generated/history.json"
local f="$(webuiDir)/frontend/data/updater/generated/history.json"
local ts; ts="$(date -Iseconds 2>/dev/null || date)"
[ -f "$f" ] || printf '{ "entries": [] }\n' | runFileWrite "$f"

View File

@ -71,7 +71,7 @@ cliHandleWebuiCommands()
cliWebuiLoginReset()
{
local auth_file="$containers_dir/libreportal/frontend/.auth.json"
local auth_file="$(webuiDir)/frontend/.auth.json"
local webui_logins_file="$configs_dir/webui/webui_logins"
isNotice "Resetting WebUI credentials..."

View File

@ -21,7 +21,7 @@
# crontab_task_processor.sh and webui_task_create.sh use; if those move, both
# this file and they have to update together.
_taskDir() {
echo "${containers_dir}libreportal/frontend/data/tasks"
echo "$(webuiDir)/frontend/data/tasks"
}
_genTaskId() {

View File

@ -142,7 +142,7 @@ reconcileConfigFile()
if [[ -s "$tmp" ]] && grep -q '^CFG_' "$tmp" && ! runInstallOp cmp -s "$tmp" "$live"; then
local bak_file="${live%/*}/.${live##*/}.bak"
runCfgOp cp -a "$live" "$bak_file"
{ [[ -n "$containers_dir" && "$live" == "$containers_dir"* ]] && runFileWrite "$live" < "$tmp" || runInstallOp cp "$tmp" "$live"; }
{ pathIsContainerData "$live" && runFileWrite "$live" < "$tmp" || runInstallOp cp "$tmp" "$live"; }
# A hidden .<file>.bak sibling is still written above for safety; the
# per-file result is intentionally silent — the intro notice and the
# per-section "...completed." lines are the only reconcile output.

View File

@ -51,6 +51,6 @@ tagsManagerUpdateUniversalTag()
# manager-owned configs/ + install templates use runInstallOp. The read
# (awk above) needs no escalation — config/compose files are world-readable.
local op="runInstallOp"
[[ "$file_path" == "$containers_dir"* || "$file_path" == "${LP_CONTAINERS_DIR:-/libreportal-containers}"/* ]] && op="runFileOp"
pathIsContainerData "$file_path" && op="runFileOp"
$op sed -i "/#LIBREPORTAL|${tag_name}|/s|${esc_placeholder}|${esc_new}|g" "$file_path"
}

View File

@ -89,7 +89,7 @@ dockerUninstallApp()
# scan happened to run. Surgical delete rather than a rescan: a full
# updater scan re-runs CVE checks against every image and has no place
# inside an uninstall.
local _upd_gen="${containers_dir}libreportal/frontend/data/updater/generated"
local _upd_gen="$(webuiDir)/frontend/data/updater/generated"
local _upd_f
for _upd_f in updates.json cves.json; do
if [[ -f "$_upd_gen/$_upd_f" ]] && command -v jq >/dev/null 2>&1; then
@ -114,7 +114,7 @@ dockerUninstallApp()
echo ""
echo "---- $menu_number. Removing related task history"
echo ""
local _tasks_dir="${containers_dir}libreportal/frontend/data/tasks"
local _tasks_dir="$(webuiDir)/frontend/data/tasks"
local _removed=0
if [[ -d "$_tasks_dir" ]]; then
for _tf in "$_tasks_dir"/task_*.json; do

View File

@ -91,13 +91,33 @@ runInstallWrite() {
# file" bug — the substitution silently failed, leaving the placeholder).
runCfgOp() {
local _file="${!#}"
if [[ -n "$containers_dir" && "$_file" == "$containers_dir"* ]]; then
if _runCfgIsContainerPath "$_file"; then
runFileOp "$@"
else
runInstallOp "$@"
fi
}
# paths.sh owns pathIsContainerData (it knows every registered storage root).
# init.sh sources THIS file directly at install time WITHOUT paths.sh, so fall
# back to the primary root alone — at install no extra locations exist yet, so
# the two answers agree.
#
# Deliberately not named pathIsContainerData: a same-named top-level definition
# here gets indexed by generate_function_manifest.sh, and the resulting autoload
# stub would shadow the real multi-root implementation with this primary-only
# one — silently classifying every file on a second disk as manager-owned.
_runCfgIsContainerPath() {
local p="$1"
if declare -F pathIsContainerData >/dev/null 2>&1; then
pathIsContainerData "$p"
return $?
fi
local root="${LP_CONTAINERS_DIR:-/libreportal-containers}"
root="${root%/}"
[[ -n "$p" && ( "$p" == "$root" || "$p" == "$root/"* ) ]]
}
# Backup-engine command (borg/restic/kopia) run AS the dedicated backup user
# ($docker_install_user), with the repo password and BORG_/RESTIC_/KOPIA_ env
# vars carried across the privilege drop. Never root — the scoped sudoers lets

View File

@ -43,7 +43,7 @@ dockerRemoveStrandedContainers()
[[ -z "$name" || -z "$work_dir" ]] && continue
# Only LibrePortal's own container tree, and only when the project
# directory is provably gone.
[[ "$work_dir" == "$containers_dir"* ]] || continue
pathIsContainerData "$work_dir" || continue
[[ -d "$work_dir" ]] && continue
if runFileOp docker rm -f "$name" >/dev/null 2>&1; then

View File

@ -67,7 +67,7 @@ dockerHealthScan() {
# $state_file, written ONLY by the throttled caller (webuiSystemHealthCheck)
# via HEALTH_RESTART_SNAPSHOT, so the heal's rapid re-scans don't disturb the
# baseline. One inspect over all containers (name|count|status|restarting).
local state_file="${containers_dir}/libreportal/frontend/data/system/.health_restart_counts"
local state_file="$(webuiDir)/frontend/data/system/.health_restart_counts"
HEALTH_RESTART_SNAPSHOT=""
declare -A _prev_rc=()

View File

@ -17,7 +17,7 @@ copyFile()
# the manager-owned control plane (configs/logs/etc.) is runInstallOp.
# Mirrors createTouch's path-based ownership; $user_name is now advisory.
local op="runInstallOp"
[[ "$save_dir" == "$containers_dir"* || "$save_dir" == "${LP_CONTAINERS_DIR:-/libreportal-containers}"/* ]] && op="runFileOp"
pathIsContainerData "$save_dir" && op="runFileOp"
if [ "$silent_flag" == "loud" ]; then
local result; result=$($op cp $flags_full "$file" "$save_dir")
@ -43,7 +43,7 @@ repairStubDirForFile()
[[ -d "$target" ]] || return 0
local op="runInstallOp"
[[ "$target" == "$containers_dir"* || "$target" == "${LP_CONTAINERS_DIR:-/libreportal-containers}"/* ]] && op="runFileOp"
pathIsContainerData "$target" && op="runFileOp"
local name; name=$(basename "$target")
local staged="$target.stub-repair.$$"

View File

@ -9,7 +9,7 @@ copyFiles()
# Write as the destination's owner (see copyFile).
local op="runInstallOp"
[[ "$save_dir" == "$containers_dir"* || "$save_dir" == "${LP_CONTAINERS_DIR:-/libreportal-containers}"/* ]] && op="runFileOp"
pathIsContainerData "$save_dir" && op="runFileOp"
local files=($($op find "$source" -type f))
if [ ${#files[@]} -eq 0 ]; then

View File

@ -21,7 +21,7 @@ createTouch()
local file_dir=$(dirname "$clean_file")
local op="runInstallOp"
if [[ "$clean_file" == "$containers_dir"* || "$clean_file" == "${LP_CONTAINERS_DIR:-/libreportal-containers}"/* ]]; then
if pathIsContainerData "$clean_file"; then
op="runFileOp"
fi

View File

@ -10,7 +10,7 @@ moveFile()
if [ -e "$file" ]; then
# Move as the destination's owner — no root, no chown (see copyFile).
local op="runInstallOp"
[[ "$save_dir" == "$containers_dir"* || "$save_dir" == "${LP_CONTAINERS_DIR:-/libreportal-containers}"/* ]] && op="runFileOp"
pathIsContainerData "$save_dir" && op="runFileOp"
local result; result=$($op mv "$file" "$save_dir")
checkSuccess "Moving $file_name to $save_dir"
else

View File

@ -8,8 +8,8 @@ copyFolder()
local user_name="$3" # advisory — the destination path determines the owner
# Write as the destination's owner — no root, no chown (see copyFile).
if [[ "$save_dir" == "$containers_dir"* || "$save_dir" == "${LP_CONTAINERS_DIR:-/libreportal-containers}"/* ]]; then
if [[ "$folder" == "$containers_dir"* || "$folder" == "${LP_CONTAINERS_DIR:-/libreportal-containers}"/* ]]; then
if pathIsContainerData "$save_dir"; then
if pathIsContainerData "$folder"; then
# container -> container: same owner (dockerinstall), a plain cp works.
local result; result=$(runFileOp cp -rf "$folder" "$save_dir")
else

View File

@ -8,7 +8,7 @@ copyFolders()
# Write as the destination's owner — no root, no chown (see copyFile).
local op="runInstallOp"
[[ "$save_dir" == "$containers_dir"* || "$save_dir" == "${LP_CONTAINERS_DIR:-/libreportal-containers}"/* ]] && op="runFileOp"
pathIsContainerData "$save_dir" && op="runFileOp"
local subdirs=($(find "$source" -mindepth 1 -maxdepth 1 -type d))
if [ ${#subdirs[@]} -eq 0 ]; then

View File

@ -14,7 +14,7 @@ createFolders()
# AS that user via runFileOp — creating it as the right owner avoids a
# chown-to-another-user the unprivileged runtime can't do. Mirrors
# createTouch; the $user_name hint is advisory for these paths.
if [[ "$clean_dir" == "$containers_dir"* || "$clean_dir" == "${LP_CONTAINERS_DIR:-/libreportal-containers}"/* ]]; then
if pathIsContainerData "$clean_dir"; then
if [ ! -d "$dir_path" ]; then
local result; result=$(runFileOp mkdir -p "$dir_path")
[ "$silent_flag" == "loud" ] && checkSuccess "Creating $folder_name directory"

View File

@ -125,7 +125,7 @@ setupApplyFinalize()
# failed app's own task row is already red; the WebUI watcher is what gates
# the "your install is ready" hand-off on this same group-level result.
if [[ -n "$setup_group" ]]; then
local tasks_dir="${containers_dir}libreportal/frontend/data/tasks"
local tasks_dir="$(webuiDir)/frontend/data/tasks"
local total=0 failed=0 failed_names="" f
if [[ -d "$tasks_dir" ]]; then
for f in "$tasks_dir"/task_*.json; do

View File

@ -1,21 +1,27 @@
#!/bin/bash
SETUP_LOCK_FILE="${containers_dir}libreportal/frontend/data/.setup_complete"
# Resolved on call, not at source time: this file can be sourced by the lazy
# loader before paths.sh has run, and a source-time expansion would bake an
# empty root into an absolute-looking path.
setupLockFile()
{
printf '%s' "$(webuiDir)/frontend/data/.setup_complete"
}
isSetupWizardComplete()
{
[[ -f "$SETUP_LOCK_FILE" ]]
[[ -f "$(setupLockFile)" ]]
}
setupWizardMarkComplete()
{
# .setup_complete lives in the docker-install-owned frontend/data -> runFileOp
# (created as that user; no root, no chown).
runFileOp touch "$SETUP_LOCK_FILE" 2>/dev/null
runFileOp chmod 644 "$SETUP_LOCK_FILE" 2>/dev/null
runFileOp touch "$(setupLockFile)" 2>/dev/null
runFileOp chmod 644 "$(setupLockFile)" 2>/dev/null
}
setupWizardReset()
{
runFileOp rm -f "$SETUP_LOCK_FILE" 2>/dev/null
runFileOp rm -f "$(setupLockFile)" 2>/dev/null
}

View File

@ -915,6 +915,7 @@ declare -gA LP_FN_MAP=(
[runAsManager]="docker/command/run_privileged.sh"
[runBackupOp]="docker/command/run_privileged.sh"
[runBinInstall]="docker/command/run_privileged.sh"
[_runCfgIsContainerPath]="docker/command/run_privileged.sh"
[runCfgOp]="docker/command/run_privileged.sh"
[runCrowdsec]="docker/command/run_privileged.sh"
[runFileOp]="docker/command/run_privileged.sh"
@ -951,6 +952,7 @@ declare -gA LP_FN_MAP=(
[setupHeadscaleUser]="headscale/scripts/headscale_user.sh"
[setupHeadscaleVariables]="network/variables/headscale_variables.sh"
[setupLocalDnsRewrites]="network/dns/setup_local_dns.sh"
[setupLockFile]="setup/setup_lock.sh"
[setupTaskDir]="task/crontab_task_processor.sh"
[setupWizardMarkComplete]="setup/setup_lock.sh"
[setupWizardReset]="setup/setup_lock.sh"
@ -2097,6 +2099,7 @@ declare -gA LP_FN_ROOT=(
[runAsManager]="scripts"
[runBackupOp]="scripts"
[runBinInstall]="scripts"
[_runCfgIsContainerPath]="scripts"
[runCfgOp]="scripts"
[runCrowdsec]="scripts"
[runFileOp]="scripts"
@ -2133,6 +2136,7 @@ declare -gA LP_FN_ROOT=(
[setupHeadscaleUser]="containers"
[setupHeadscaleVariables]="scripts"
[setupLocalDnsRewrites]="scripts"
[setupLockFile]="scripts"
[setupTaskDir]="scripts"
[setupWizardMarkComplete]="scripts"
[setupWizardReset]="scripts"
@ -2387,7 +2391,6 @@ LP_EAGER_FILES=(
"containers:matrix/scripts/matrix_auth.sh"
"scripts:migrate/migrate_url_rewrite.sh"
"containers:nextcloud/scripts/nextcloud_upgrade_hooks.sh"
"scripts:setup/setup_lock.sh"
"scripts:source/artifacts.sh"
"scripts:task/crontab_check_processor.sh"
"scripts:task/crontab_task_processor.sh"
@ -3316,6 +3319,7 @@ runAppCfg() { unset -f runAppCfg; __lpAutoload "${install_scripts_dir}docker/com
runAsManager() { unset -f runAsManager; __lpAutoload "${install_scripts_dir}docker/command/run_privileged.sh"; runAsManager "$@"; }
runBackupOp() { unset -f runBackupOp; __lpAutoload "${install_scripts_dir}docker/command/run_privileged.sh"; runBackupOp "$@"; }
runBinInstall() { unset -f runBinInstall; __lpAutoload "${install_scripts_dir}docker/command/run_privileged.sh"; runBinInstall "$@"; }
_runCfgIsContainerPath() { unset -f _runCfgIsContainerPath; __lpAutoload "${install_scripts_dir}docker/command/run_privileged.sh"; _runCfgIsContainerPath "$@"; }
runCfgOp() { unset -f runCfgOp; __lpAutoload "${install_scripts_dir}docker/command/run_privileged.sh"; runCfgOp "$@"; }
runCrowdsec() { unset -f runCrowdsec; __lpAutoload "${install_scripts_dir}docker/command/run_privileged.sh"; runCrowdsec "$@"; }
runFileOp() { unset -f runFileOp; __lpAutoload "${install_scripts_dir}docker/command/run_privileged.sh"; runFileOp "$@"; }
@ -3352,6 +3356,7 @@ setupHeadscaleRemote() { unset -f setupHeadscaleRemote; __lpAutoload "${install_
setupHeadscaleUser() { unset -f setupHeadscaleUser; __lpAutoload "${install_containers_dir}headscale/scripts/headscale_user.sh"; setupHeadscaleUser "$@"; }
setupHeadscaleVariables() { unset -f setupHeadscaleVariables; __lpAutoload "${install_scripts_dir}network/variables/headscale_variables.sh"; setupHeadscaleVariables "$@"; }
setupLocalDnsRewrites() { unset -f setupLocalDnsRewrites; __lpAutoload "${install_scripts_dir}network/dns/setup_local_dns.sh"; setupLocalDnsRewrites "$@"; }
setupLockFile() { unset -f setupLockFile; __lpAutoload "${install_scripts_dir}setup/setup_lock.sh"; setupLockFile "$@"; }
setupTaskDir() { unset -f setupTaskDir; __lpAutoload "${install_scripts_dir}task/crontab_task_processor.sh"; setupTaskDir "$@"; }
setupWizardMarkComplete() { unset -f setupWizardMarkComplete; __lpAutoload "${install_scripts_dir}setup/setup_lock.sh"; setupWizardMarkComplete "$@"; }
setupWizardReset() { unset -f setupWizardReset; __lpAutoload "${install_scripts_dir}setup/setup_lock.sh"; setupWizardReset "$@"; }

View File

@ -3,7 +3,7 @@
crontabSetupCheckProcessor()
{
local task_check_script="$install_scripts_dir/task/crontab_check_processor.sh"
local task_dir="$containers_dir/libreportal/frontend/data/tasks"
local task_dir="$(webuiDir)/frontend/data/tasks"
# Update TASK_DIR in the task processor script
if [ -f "$task_check_script" ]; then

View File

@ -3,7 +3,7 @@
crontabSetupTaskProcessor()
{
local task_processor_script="$install_scripts_dir/task/crontab_task_processor.sh"
local task_dir="$containers_dir/libreportal/frontend/data/tasks"
local task_dir="$(webuiDir)/frontend/data/tasks"
# Update TASK_DIR in the task processor script
if [ -f "$task_processor_script" ]; then

View File

@ -3,7 +3,7 @@
# Update a specific app's status in apps.json (used for install/uninstall operations)
webuiUpdateAppStatus() {
local app_name="$1"
local output_file="${containers_dir}libreportal/frontend/data/apps/generated/apps.json"
local output_file="$(webuiDir)/frontend/data/apps/generated/apps.json"
if [[ -z "$app_name" ]]; then
isError "app_name parameter required for webuiUpdateAppStatus"

View File

@ -33,7 +33,7 @@ _webuiReadServiceTags() {
webuiGenerateLibrePortalConfig() {
local testing_mode="$1"
local specific_app="$2"
local output_file="${containers_dir}libreportal/frontend/data/apps/generated/apps.json"
local output_file="$(webuiDir)/frontend/data/apps/generated/apps.json"
local first_app=true
local app_count=0
@ -162,7 +162,7 @@ EOF
icon_file="$app_name.png"
fi
if [[ -n "$icon_file" ]]; then
local icons_apps_dir="${containers_dir}libreportal/frontend/core/icons/apps"
local icons_apps_dir="$(webuiDir)/frontend/core/icons/apps"
runFileOp mkdir -p "$icons_apps_dir"
runFileWrite "$icons_apps_dir/$icon_file" < "$dir/$icon_file" 2>/dev/null
fi

View File

@ -7,7 +7,7 @@ webuiPatchAppConfigJson() {
local app_name="$1"
[[ -z "$app_name" ]] && return 1
local apps_json="${containers_dir}libreportal/frontend/data/apps/generated/apps.json"
local apps_json="$(webuiDir)/frontend/data/apps/generated/apps.json"
[[ ! -f "$apps_json" ]] && return 1
local deployed_cfg="${containers_dir}${app_name}/${app_name}.config"

View File

@ -4,7 +4,7 @@
# Generates apps-services.json from database
webuiGenerateAppsServicesConfig() {
local testing_mode="$1"
local output_file="${containers_dir}libreportal/frontend/data/apps/generated/apps-services.json"
local output_file="$(webuiDir)/frontend/data/apps/generated/apps-services.json"
local first_service=true
local service_count=0
@ -81,8 +81,8 @@ EOF
# source IP of the default route) is what LAN/VPN clients dial,
# and matches the host APP_URL is stamped with.
local server_ip=""
if [[ -f "${containers_dir}libreportal/config/generated/configs.json" ]]; then
server_ip=$(grep -o '"CFG_SERVER_IP":[[:space:]]*"[^"]*"' "${containers_dir}libreportal/config/generated/configs.json" | cut -d'"' -f4)
if [[ -f "$(webuiDir)/config/generated/configs.json" ]]; then
server_ip=$(grep -o '"CFG_SERVER_IP":[[:space:]]*"[^"]*"' "$(webuiDir)/config/generated/configs.json" | cut -d'"' -f4)
fi
[[ -z "$server_ip" ]] && server_ip="${local_ip_v4:-localhost}"
[[ -z "$server_ip" ]] && server_ip="localhost"

View File

@ -47,7 +47,7 @@ webuiGenerateAppsToolsConfig() {
return $?
fi
local output_file="${containers_dir}libreportal/frontend/data/apps/generated/apps-tools.json"
local output_file="$(webuiDir)/frontend/data/apps/generated/apps-tools.json"
local tmp="$(mktemp)"
runFileOp mkdir -p "$(dirname "$output_file")"

View File

@ -3,7 +3,7 @@
webuiGenerateBackupAppStatus()
{
local app_name="${1:-}"
local output_dir="$containers_dir/libreportal/frontend/data/backup/generated/apps"
local output_dir="$(webuiDir)/frontend/data/backup/generated/apps"
runFileOp mkdir -p "$output_dir"
# Resolve the app list: a single app (direct call — e.g. right after that

View File

@ -2,7 +2,7 @@
webuiGenerateBackupDashboard()
{
local output_dir="$containers_dir/libreportal/frontend/data/backup/generated"
local output_dir="$(webuiDir)/frontend/data/backup/generated"
local output_file="$output_dir/dashboard.json"
local temp_file="${output_file}.tmp.$$"

View File

@ -7,7 +7,7 @@
webuiGenerateBackupEngines()
{
local src_dir="$install_scripts_dir/backup/engines"
local out_dir="$containers_dir/libreportal/frontend/data/backup/generated/engines"
local out_dir="$(webuiDir)/frontend/data/backup/generated/engines"
runFileOp mkdir -p "$out_dir"
if [[ ! -d "$src_dir" ]]; then

View File

@ -2,7 +2,7 @@
webuiGenerateBackupLocations()
{
local output_dir="$containers_dir/libreportal/frontend/data/backup/generated"
local output_dir="$(webuiDir)/frontend/data/backup/generated"
local output_file="$output_dir/locations.json"
local temp_file="${output_file}.tmp.$$"
runFileOp mkdir -p "$output_dir"

View File

@ -11,7 +11,7 @@
webuiGenerateBackupMigrate()
{
local output_dir="$containers_dir/libreportal/frontend/data/backup/generated"
local output_dir="$(webuiDir)/frontend/data/backup/generated"
local output_file="$output_dir/migrate.json"
runFileOp mkdir -p "$output_dir"

View File

@ -2,7 +2,7 @@
webuiGenerateBackupPasswords()
{
local output_dir="$containers_dir/libreportal/frontend/data/backup/generated"
local output_dir="$(webuiDir)/frontend/data/backup/generated"
local output_file="$output_dir/passwords.txt"
local temp_file="${output_file}.tmp.$$"

View File

@ -10,7 +10,7 @@
webuiGenerateBackupSchema()
{
local out_dir="$containers_dir/libreportal/frontend/data/backup/generated"
local out_dir="$(webuiDir)/frontend/data/backup/generated"
local out_file="$out_dir/schema.json"
runFileOp mkdir -p "$out_dir"

View File

@ -3,7 +3,7 @@
webuiGenerateBackupSnapshots()
{
local scope="${1:-all}"
local output_dir="$containers_dir/libreportal/frontend/data/backup/generated"
local output_dir="$(webuiDir)/frontend/data/backup/generated"
runFileOp mkdir -p "$output_dir"
local indices=()

View File

@ -14,7 +14,7 @@ createTaskFile() {
fi
# Set task directory
local task_dir="${containers_dir}libreportal/frontend/data/tasks"
local task_dir="$(webuiDir)/frontend/data/tasks"
# Ensure task directory exists (container-owned WebUI data tree -> runFileOp)
if [ ! -d "$task_dir" ]; then

View File

@ -78,7 +78,7 @@ webuiGenerateSystemConfigs() {
fi
}
local output_file="${containers_dir}libreportal/frontend/data/config/generated/configs.json"
local output_file="$(webuiDir)/frontend/data/config/generated/configs.json"
local temp_file="$(mktemp)"
runFileOp mkdir -p "$(dirname "$output_file")"

View File

@ -8,7 +8,7 @@
webuiGeneratePeers()
{
local output_dir="$containers_dir/libreportal/frontend/data/peers/generated"
local output_dir="$(webuiDir)/frontend/data/peers/generated"
local output_file="$output_dir/peers.json"
runFileOp mkdir -p "$output_dir"

View File

@ -6,7 +6,7 @@
webuiGenerateSshAccess()
{
local out_dir="$containers_dir/libreportal/frontend/data/ssh"
local out_dir="$(webuiDir)/frontend/data/ssh"
local out_file="$out_dir/access.json"
runFileOp mkdir -p "$out_dir"

View File

@ -27,7 +27,7 @@ webuiSystemDisk() {
fi
# Create system info directory if it doesn't exist
local system_dir="$containers_dir/libreportal/frontend/data/system"
local system_dir="$(webuiDir)/frontend/data/system"
createFolders "quiet" $sudo_user_name "$system_dir"
# Create temp file first, then atomic move

View File

@ -20,7 +20,7 @@
webuiSystemHealthCheck() {
local force_flag="$1"
local system_dir="$containers_dir/libreportal/frontend/data/system"
local system_dir="$(webuiDir)/frontend/data/system"
local final_file="${system_dir}/health_status.json"
local stamp_file="${system_dir}/.health_check_stamp"
local heal_stamp="${system_dir}/.health_heal_stamp"

View File

@ -31,7 +31,7 @@ webuiSystemInfo() {
local kernel_version=$(uname -r)
# Create system info directory if it doesn't exist
local system_dir="$containers_dir/libreportal/frontend/data/system"
local system_dir="$(webuiDir)/frontend/data/system"
createFolders "quiet" $sudo_user_name "$system_dir"
# Create temp file first, then atomic move

View File

@ -23,7 +23,7 @@ webuiSystemMemory() {
local mem_text="$mem_used_gb GB / $mem_total_gb GB - $mem_percent%"
# Create system info directory if it doesn't exist
local system_dir="${containers_dir}libreportal/frontend/data/system"
local system_dir="$(webuiDir)/frontend/data/system"
createFolders "quiet" $sudo_user_name "$system_dir"
# Create temp file first, then atomic move

View File

@ -27,7 +27,7 @@ _metricsReadCpu() {
}
webuiSystemMetrics() {
local system_dir="${containers_dir}libreportal/frontend/data/system"
local system_dir="$(webuiDir)/frontend/data/system"
createFolders "quiet" "$sudo_user_name" "$system_dir"
local now_epoch; now_epoch=$(date +%s)
@ -209,7 +209,7 @@ EOF
# run per CFG_APP_STORAGE_INTERVAL (default 10 min) via the .stamp idiom used by
# the update/verify checks above. Pass "force" to bypass it.
webuiSystemAppStorage() {
local system_dir="${containers_dir}libreportal/frontend/data/system"
local system_dir="$(webuiDir)/frontend/data/system"
createFolders "quiet" "$sudo_user_name" "$system_dir"
local final_file="$system_dir/app_storage.json"
@ -304,7 +304,7 @@ webuiSystemAppStorage() {
# compose project label fall back to their container name so nothing is lost.
webuiSystemApps() {
local now_epoch="$1" now_iso="$2" system_dir="$3"
[[ -z "$system_dir" ]] && system_dir="${containers_dir}libreportal/frontend/data/system"
[[ -z "$system_dir" ]] && system_dir="$(webuiDir)/frontend/data/system"
command -v docker &>/dev/null || return 0
# name|project|state|status for every container (running or not)

View File

@ -16,7 +16,7 @@
webuiSystemNetworkCheck() {
local force_flag="$1"
local system_dir="$containers_dir/libreportal/frontend/data/system"
local system_dir="$(webuiDir)/frontend/data/system"
local final_file="${system_dir}/network_status.json"
local stamp_file="${system_dir}/.network_check_stamp"
local interval="${CFG_NETWORK_CHECK_INTERVAL:-300}"

View File

@ -52,7 +52,7 @@ webuiSystemUpdateCheck() {
local force_flag="$1"
local repo_dir="${script_dir}"
local system_dir="$containers_dir/libreportal/frontend/data/system"
local system_dir="$(webuiDir)/frontend/data/system"
local final_file="${system_dir}/update_status.json"
local stamp_file="${system_dir}/.update_check_stamp"
# How long (seconds) a fetch result stays "fresh" before we hit the network
@ -267,7 +267,7 @@ EOF
webuiSystemVerify() {
local force_flag="$1"
local system_dir="$containers_dir/libreportal/frontend/data/system"
local system_dir="$(webuiDir)/frontend/data/system"
local final_file="${system_dir}/verify_status.json"
local stamp_file="${system_dir}/.verify_check_stamp"
local interval="${CFG_VERIFY_CHECK_INTERVAL:-86400}"

View File

@ -355,7 +355,7 @@ updaterDisplayVersion() {
}
webuiUpdaterScan() {
local out_dir="$containers_dir/libreportal/frontend/data/updater/generated"
local out_dir="$(webuiDir)/frontend/data/updater/generated"
runFileOp mkdir -p "$out_dir" 2>/dev/null || mkdir -p "$out_dir" 2>/dev/null
local now; now="$(date -Iseconds 2>/dev/null || date)"

View File

@ -15,7 +15,7 @@
# the leftover lock would otherwise wedge EVERY future refresh. No single refresh
# runs anywhere near this long, so a lock older than the threshold is a leftover.
webuiCheckUpdateLock() {
local lock_file="$containers_dir/libreportal/frontend/data/updater.lock"
local lock_file="$(webuiDir)/frontend/data/updater.lock"
local stale_after=900 # seconds (15 min); far longer than any real refresh
if [ ! -f "$lock_file" ]; then

View File

@ -4,7 +4,7 @@
# Creates update lock file to prevent concurrent updates
webuiCreateUpdateLock() {
local lock_file="$containers_dir/libreportal/frontend/data/updater.lock"
local lock_file="$(webuiDir)/frontend/data/updater.lock"
# Create lock file with timestamp and process ID. The lock lives in the
# dockerinstall-owned WebUI data dir, so write it as the path owner.

View File

@ -4,7 +4,7 @@
# Removes setup lock file after update completion
webuiRemoveSetupLock() {
local lock_file="$containers_dir/libreportal/frontend/data/setup.lock"
local lock_file="$(webuiDir)/frontend/data/setup.lock"
if [ -f "$lock_file" ]; then
runFileOp rm -f "$lock_file"

View File

@ -4,7 +4,7 @@
# Removes update lock file after update completion
webuiRemoveUpdateLock() {
local lock_file="$containers_dir/libreportal/frontend/data/updater.lock"
local lock_file="$(webuiDir)/frontend/data/updater.lock"
if [ -f "$lock_file" ]; then
runFileOp rm -f "$lock_file"

View File

@ -4,7 +4,7 @@
webuiCreateLogsFolders()
{
local log_dir="${containers_dir}libreportal/frontend/logs"
local log_dir="$(webuiDir)/frontend/logs"
local apps_dir="${log_dir}/apps"
if [ ! -d "$apps_dir" ]; then
local result; result=$(createFolders "loud" $sudo_user_name $log_dir $apps_dir)

View File

@ -4,7 +4,7 @@
# Ensures essential task system files exist for LibrePortal task management
webuiEnsureTaskFiles() {
local task_dir="${containers_dir}libreportal/frontend/data/tasks"
local task_dir="$(webuiDir)/frontend/data/tasks"
# Create tasks directory if it doesn't exist
if [ ! -d "$task_dir" ]; then

View File

@ -13,7 +13,7 @@ webuiSyncAppIcon() {
local app_name="$1"
[[ -z "$app_name" ]] && return 1
local icons_dir="${containers_dir}libreportal/frontend/core/icons/apps"
local icons_dir="$(webuiDir)/frontend/core/icons/apps"
runFileOp mkdir -p "$icons_dir"
# Icons live in the install template dir — copy_build_context.sh
@ -65,7 +65,7 @@ webuiSyncAppIcons() {
# Only ever removes icons it can match to a missing template, never anything
# else in the directory.
webuiPruneAppIcons() {
local icons_dir="${containers_dir}libreportal/frontend/core/icons/apps"
local icons_dir="$(webuiDir)/frontend/core/icons/apps"
[[ -d "$icons_dir" ]] || return 0
[[ -d "$install_containers_dir" ]] || return 0

View File

@ -6,7 +6,7 @@ webuiUpdateAppLog()
local type=$2 # install or uninstall
# Create logs directory if it doesn't exist
local log_dir="${containers_dir}libreportal/frontend/logs"
local log_dir="$(webuiDir)/frontend/logs"
local apps_dir="${log_dir}/apps"
if [ ! -d "$apps_dir" ]; then
local result; result=$(createFolders "loud" $sudo_user_name $log_dir $apps_dir)

View File

@ -13,7 +13,7 @@ atomicWriteWebUI() {
# can write the dockerinstall-owned WebUI/app files. Temp + rename share the
# target's directory, so the mv stays atomic (same filesystem, same owner).
local op="runInstallOp" wop="runInstallWrite"
if [[ "$target_file" == "$containers_dir"* || "$target_file" == "${LP_CONTAINERS_DIR:-/libreportal-containers}"/* ]]; then
if pathIsContainerData "$target_file"; then
op="runFileOp"; wop="runFileWrite"
fi

View File

@ -17,7 +17,7 @@ installLibrePortalImageWebUI()
# libreportal.config as a Docker-created directory. The tar copy below can't
# extract a file over a directory ("Cannot open: File exists") and aborts the
# whole source copy, so clear the stub first.
repairStubDirForFile "$containers_dir/libreportal/libreportal.config" "loud"
repairStubDirForFile "$(webuiDir)/libreportal.config" "loud"
local result; result=$(copyFolder "$install_containers_dir/libreportal" "$containers_dir" "$sudo_user_name")
checkSuccess "Copy the LibrePortal to the containers folder"
@ -40,6 +40,6 @@ installLibrePortalImageWebUI()
fi
isNotice "Building libreportal-service image, this may take a while..."
local result; result=$(runFileOp docker build -t libreportal-service -f $containers_dir/libreportal/Dockerfile $containers_dir/libreportal >/dev/null 2>&1)
local result; result=$(runFileOp docker build -t libreportal-service -f $(webuiDir)/Dockerfile $(webuiDir) >/dev/null 2>&1)
checkSuccess "Built LibrePortal WebUI Docker image"
}

View File

@ -14,7 +14,7 @@ installLibrePortalWebUITaskService()
[[ "$CFG_REQUIREMENT_WEBUI_SERVICE" == "true" ]] || return 0
local task_processor_script="$install_scripts_dir/task/crontab_task_processor.sh"
local task_dir="$containers_dir/libreportal/frontend/data/tasks"
local task_dir="$(webuiDir)/frontend/data/tasks"
# Point the processor at the task dir (idempotent). This edits the
# manager-owned install tree, so no privilege is needed.

View File

@ -49,7 +49,7 @@ _lpRegenOrphanedApp() {
lpRegenWebui() {
local force="$1"
local gen="${containers_dir}libreportal/frontend/data/apps/generated"
local gen="$(webuiDir)/frontend/data/apps/generated"
local apps_json="$gen/apps.json"
local tools_json="$gen/apps-tools.json"

View File

@ -59,7 +59,7 @@ webuiLibrePortalUpdate() {
checkSuccess "Generated system configurations..."
# Generate categories
local result; result=$(webuiCreateCategories $containers_dir/libreportal/frontend/data)
local result; result=$(webuiCreateCategories $(webuiDir)/frontend/data)
checkSuccess "Generated app and config categories..."
# Generate LibrePortal app configuration