diff --git a/containers/adguard/scripts/adguard_auth.sh b/containers/adguard/scripts/adguard_auth.sh index 4679324..449ba06 100644 --- a/containers/adguard/scripts/adguard_auth.sh +++ b/containers/adguard/scripts/adguard_auth.sh @@ -5,7 +5,7 @@ authAdapter_adguard_setPassword() { user="${user:-${CFG_ADGUARD_ADMIN_USER:-admin}}" [[ -z "$password" ]] && password=$(generateRandomPassword) - local yaml="${containers_dir}adguard/conf/AdGuardHome.yaml" + local yaml="$(appDir adguard)/conf/AdGuardHome.yaml" [[ ! -f "$yaml" ]] && { isError "AdGuardHome.yaml not found at $yaml."; return 1; } if ! command -v htpasswd >/dev/null 2>&1; then isError "htpasswd is required to bcrypt the new password." diff --git a/containers/adguard/scripts/adguard_install_hooks.sh b/containers/adguard/scripts/adguard_install_hooks.sh index a266b47..a6240a5 100644 --- a/containers/adguard/scripts/adguard_install_hooks.sh +++ b/containers/adguard/scripts/adguard_install_hooks.sh @@ -18,7 +18,7 @@ adguard_install_post_start() # install pipeline; the resolved host port is stored in the PORTS_TAG_1 # docker-compose tag (format `external:internal`). Pull it from there # so the curl + URL printout actually point somewhere real. - local adguard_compose_file="$containers_dir$app_name/docker-compose.yml" + local adguard_compose_file="$(appDir "$app_name")/docker-compose.yml" local adguard_port_pair adguard_port_pair=$(tagsManagerGetTagContent "$adguard_compose_file" "PORTS_TAG_1") local adguard_admin_port="${adguard_port_pair%%:*}" @@ -89,11 +89,11 @@ JSON local result if [[ "$public" == "true" ]]; then - result=$(runFileOp sed -i "s|allow_unencrypted_doh: false|allow_unencrypted_doh: true|g" "$containers_dir$app_name/conf/AdGuardHome.yaml") + result=$(runFileOp sed -i "s|allow_unencrypted_doh: false|allow_unencrypted_doh: true|g" "$(appDir "$app_name")/conf/AdGuardHome.yaml") checkSuccess "Setting allow_unencrypted_doh to false for Traefik" fi - result=$(runFileOp sed -i "s|anonymize_client_ip: false: false|anonymize_client_ip: true|g" "$containers_dir$app_name/conf/AdGuardHome.yaml") + result=$(runFileOp sed -i "s|anonymize_client_ip: false: false|anonymize_client_ip: true|g" "$(appDir "$app_name")/conf/AdGuardHome.yaml") checkSuccess "Setting anonymize_client_ip to true for privacy reasons" # Force the admin web bind back to 0.0.0.0:3000 inside the container. @@ -102,7 +102,7 @@ JSON # the install API call AdGuardHome sometimes ends up bound to # 0.0.0.0:80 (its build-time default) — exactly what causes "unable # to connect" on the host port. - local adguard_yaml="$containers_dir$app_name/conf/AdGuardHome.yaml" + local adguard_yaml="$(appDir "$app_name")/conf/AdGuardHome.yaml" if [[ -f "$adguard_yaml" ]]; then runFileOp sed -i 's|^\(\s*address:\s*\)0\.0\.0\.0:[0-9]\+|\10.0.0.0:3000|' "$adguard_yaml" runFileOp sed -i 's|^\(\s*bind_host:\s*\).*|\10.0.0.0|' "$adguard_yaml" diff --git a/containers/authelia/scripts/authelia_install_hooks.sh b/containers/authelia/scripts/authelia_install_hooks.sh index b5ff3cb..7679e16 100644 --- a/containers/authelia/scripts/authelia_install_hooks.sh +++ b/containers/authelia/scripts/authelia_install_hooks.sh @@ -18,18 +18,18 @@ authelia_install_post_compose() local result result=$(copyResource "$app_name" "configuration.yml" "config" | runInstallWrite -a "$logs_dir/$docker_log_file" 2>&1) - checkSuccess "Copying configuration.yml to $containers_dir$app_name/config" + checkSuccess "Copying configuration.yml to $(appDir "$app_name")/config" result=$(copyResource "$app_name" "users_database.yml" "config" | runInstallWrite -a "$logs_dir/$docker_log_file" 2>&1) - checkSuccess "Copying users_database.yml to $containers_dir$app_name/config" + checkSuccess "Copying users_database.yml to $(appDir "$app_name")/config" - local authelia_config_file="$containers_dir$app_name/config/configuration.yml" + local authelia_config_file="$(appDir "$app_name")/config/configuration.yml" runFileOp sed -i "s|AUTHELIA_THEME_PLACEHOLDER|$CFG_AUTHELIA_THEME|g" "$authelia_config_file" runFileOp sed -i "s|AUTHELIA_DOMAIN_PLACEHOLDER|$domain_full|g" "$authelia_config_file" runFileOp sed -i "s|AUTHELIA_HOST_PLACEHOLDER|$host_setup|g" "$authelia_config_file" checkSuccess "Substituting Authelia configuration values (theme=$CFG_AUTHELIA_THEME domain=$domain_full host=$host_setup)" - local authelia_secrets_dir="$containers_dir$app_name/secrets" + local authelia_secrets_dir="$(appDir "$app_name")/secrets" runFileOp mkdir -p "$authelia_secrets_dir" local secret_name secret_file for secret_name in JWT_SECRET SESSION_SECRET STORAGE_ENCRYPTION_KEY; do @@ -58,7 +58,7 @@ authelia_install_post_start() local authelia_admin_user="${CFG_AUTHELIA_ADMIN_USERNAME:-admin}" local authelia_admin_pass="${CFG_AUTHELIA_ADMIN_PASSWORD_1:-authelia}" - local authelia_users_file="$containers_dir$app_name/config/users_database.yml" + local authelia_users_file="$(appDir "$app_name")/config/users_database.yml" local authelia_attempts=0 while ((authelia_attempts < 30)); do if runFileOp docker exec authelia-service authelia --version >/dev/null 2>&1; then diff --git a/containers/bookstack/scripts/bookstack_install_hooks.sh b/containers/bookstack/scripts/bookstack_install_hooks.sh index a178146..16683a2 100644 --- a/containers/bookstack/scripts/bookstack_install_hooks.sh +++ b/containers/bookstack/scripts/bookstack_install_hooks.sh @@ -12,7 +12,7 @@ bookstack_install_post_start() local bookstack_target_email="${CFG_BOOKSTACK_ADMIN_EMAIL:-admin@admin.com}" local bookstack_target_pass="${CFG_BOOKSTACK_ADMIN_PASSWORD_1:-password}" - local bookstack_compose_file="$containers_dir$app_name/docker-compose.yml" + local bookstack_compose_file="$(appDir "$app_name")/docker-compose.yml" local bookstack_port_pair bookstack_port_pair=$(tagsManagerGetTagContent "$bookstack_compose_file" "PORTS_TAG_1") local bookstack_host_port="${bookstack_port_pair%%:*}" diff --git a/containers/crowdsec/scripts/crowdsec_install_host.sh b/containers/crowdsec/scripts/crowdsec_install_host.sh index b151d2d..8082791 100644 --- a/containers/crowdsec/scripts/crowdsec_install_host.sh +++ b/containers/crowdsec/scripts/crowdsec_install_host.sh @@ -175,7 +175,7 @@ installCrowdsecHost() # -f guard always failed, and the key was never mirrored. The key # is declared in crowdsec.config, so that is where it belongs. # The helper handles cscli + tee + chown + chmod atomically. - local cfg_file="${containers_dir}crowdsec/crowdsec.config" + local cfg_file="$(appDir crowdsec)/crowdsec.config" local key_file="/etc/crowdsec/traefik_bouncer.key" # What the config currently holds, if anything. Quotes and whitespace diff --git a/containers/crowdsec/tools/crowdsec_rotate_bouncer_key.sh b/containers/crowdsec/tools/crowdsec_rotate_bouncer_key.sh index 72deb16..f51611b 100644 --- a/containers/crowdsec/tools/crowdsec_rotate_bouncer_key.sh +++ b/containers/crowdsec/tools/crowdsec_rotate_bouncer_key.sh @@ -31,7 +31,7 @@ appCrowdsecRotateBouncerKey() # Mirror it the same way the installer does, so the config page and the key # file agree. updateConfigOption escapes the value, writes as the owner of # the containers tree, and re-sources. - local cfg_file="${containers_dir}${app_name}/${app_name}.config" + local cfg_file="$(appDir "$app_name")/${app_name}.config" if [[ -f "$cfg_file" ]]; then updateConfigOption "CFG_CROWDSEC_TRAEFIK_LAPI_KEY" "$bouncer_key" "$cfg_file" else @@ -41,7 +41,7 @@ appCrowdsecRotateBouncerKey() # Traefik reads the key from the bind-mounted file at startup, so it keeps # presenting the revoked key until it restarts. Without this the rotation # looks successful while every bouncer check fails with 403. - if [[ -d "${containers_dir}traefik" ]]; then + if [[ -d "$(appDir traefik)" ]]; then dockerComposeRestart traefik checkSuccess "Restarting Traefik to load the new bouncer key" else diff --git a/containers/dashy/scripts/dashy_update_conf.sh b/containers/dashy/scripts/dashy_update_conf.sh index 8a5e454..e37c6b8 100755 --- a/containers/dashy/scripts/dashy_update_conf.sh +++ b/containers/dashy/scripts/dashy_update_conf.sh @@ -8,7 +8,7 @@ # (read from the app's `# Category :` header). appDashyUpdateConf() { - local conf_file="${containers_dir}dashy/etc/conf.yml" + local conf_file="$(appDir dashy)/etc/conf.yml" local services_json="$(webuiDir)/frontend/data/apps/generated/apps-services.json" local icons_src_dir="$(webuiDir)/frontend/icons/apps" @@ -18,7 +18,7 @@ appDashyUpdateConf() # Look at the actual docker container instead — if the container # exists or the install dir is present, generate the conf. if ! runFileOp docker ps -a --format '{{.Names}}' 2>/dev/null | grep -qE '^(dashy|dashy-service)$' \ - && [[ ! -d "${containers_dir}dashy" ]]; then + && [[ ! -d "$(appDir dashy)" ]]; then return 0 fi diff --git a/containers/dashy/tools/dashy_manage_shortcuts.sh b/containers/dashy/tools/dashy_manage_shortcuts.sh index deb5870..ab7d05d 100644 --- a/containers/dashy/tools/dashy_manage_shortcuts.sh +++ b/containers/dashy/tools/dashy_manage_shortcuts.sh @@ -16,7 +16,7 @@ appDashyManageShortcuts() selected=$(toolArgsGet "$tool_args" "selected") selected="${selected//%7C/|}" - local cfg_file="${containers_dir}dashy/dashy.config" + local cfg_file="$(appDir dashy)/dashy.config" updateConfigOption "CFG_DASHY_SHORTCUTS" "$selected" "$cfg_file" export CFG_DASHY_SHORTCUTS="$selected" isSuccessful "Saved CFG_DASHY_SHORTCUTS=$selected" diff --git a/containers/gitea/scripts/gitea_install_hooks.sh b/containers/gitea/scripts/gitea_install_hooks.sh index 319f4d3..32c48cb 100644 --- a/containers/gitea/scripts/gitea_install_hooks.sh +++ b/containers/gitea/scripts/gitea_install_hooks.sh @@ -16,7 +16,7 @@ gitea_install_post_compose() if [[ -n "$CFG_GITEA_METRICS_TOKEN_1" ]]; then local result result=$(runFileOp sed -i "s|GITEA_METRICS_TOKEN_PLACEHOLDER|${CFG_GITEA_METRICS_TOKEN_1}|g" \ - "$containers_dir$app_name/resources/monitoring/prometheus-scrape.yml") + "$(appDir "$app_name")/resources/monitoring/prometheus-scrape.yml") checkSuccess "Synced Gitea /metrics token to the Prometheus scrape config" else isNotice "CFG_GITEA_METRICS_TOKEN_1 is empty — Gitea /metrics scrape may 401." diff --git a/containers/gluetun/scripts/gluetun_network.sh b/containers/gluetun/scripts/gluetun_network.sh index b58f4f1..2c0586f 100644 --- a/containers/gluetun/scripts/gluetun_network.sh +++ b/containers/gluetun/scripts/gluetun_network.sh @@ -52,7 +52,7 @@ appNetworkApplyMode_gluetun() # apps drop out automatically. Self-skips if gluetun isn't installed. appNetworkRegisterPorts_gluetun() { - local gluetun_compose="${containers_dir}gluetun/docker-compose.yml" + local gluetun_compose="$(appDir gluetun)/docker-compose.yml" if [[ ! -f "$gluetun_compose" ]]; then return 0; fi if ! command -v sqlite3 >/dev/null 2>&1; then return 0; fi if [[ ! -f "$docker_dir/$db_file" ]]; then return 0; fi @@ -66,7 +66,7 @@ appNetworkRegisterPorts_gluetun() # leaves an undeclared name EMPTY in the CALLER's scope at EOF. while IFS= read -r app; do [[ -z "$app" || "$app" == "gluetun" ]] && continue - local app_config_file="${containers_dir}${app}/${app}.config" + local app_config_file="$(appDir "$app")/${app}.config" [[ -f "$app_config_file" ]] || continue local net net=$(grep -E "^CFG_${app^^}_NETWORK=" "$app_config_file" 2>/dev/null | cut -d'=' -f2 | tr -d '"') @@ -104,7 +104,7 @@ appNetworkRegisterPorts_gluetun() runFileOp mv "$tmp" "$gluetun_compose" if runFileOp docker ps --format '{{.Names}}' 2>/dev/null | grep -q '^gluetun-service$'; then isNotice "Gluetun forwarded ports changed; recreating gluetun-service to apply." - (cd "${containers_dir}gluetun" && runFileOp docker compose up -d --force-recreate gluetun-service >/dev/null 2>&1) || true + (cd "$(appDir gluetun)" && runFileOp docker compose up -d --force-recreate gluetun-service >/dev/null 2>&1) || true # Recreating gluetun gives it a new container ID, which orphans every # `network_mode: container:gluetun-service` reference. Re-attach all # routed apps so they share the new netns instead of getting their own. diff --git a/containers/gluetun/scripts/gluetun_recreate_routed.sh b/containers/gluetun/scripts/gluetun_recreate_routed.sh index 799615f..0ce9536 100644 --- a/containers/gluetun/scripts/gluetun_recreate_routed.sh +++ b/containers/gluetun/scripts/gluetun_recreate_routed.sh @@ -37,7 +37,7 @@ appGluetunRecreateRouted() # leaves an undeclared name EMPTY in the CALLER's scope at EOF. while IFS= read -r app; do [[ -z "$app" || "$app" == "gluetun" ]] && continue - local app_config_file="${containers_dir}${app}/${app}.config" + local app_config_file="$(appDir "$app")/${app}.config" [[ -f "$app_config_file" ]] || continue local net @@ -45,7 +45,7 @@ appGluetunRecreateRouted() | cut -d'=' -f2 | tr -d '"') [[ "$net" != "gluetun" ]] && continue - local app_compose="${containers_dir}${app}/docker-compose.yml" + local app_compose="$(appDir "$app")/docker-compose.yml" [[ -f "$app_compose" ]] || continue # Skip apps with no running/created container — recreate would @@ -56,7 +56,7 @@ appGluetunRecreateRouted() fi isNotice "Re-attaching ${app} to gluetun's namespace (force-recreate)..." - dockerCommandRun "cd ${containers_dir}${app} && docker compose up -d --force-recreate ${app}-service" >/dev/null 2>&1 || true + dockerCommandRun "cd $(appDir "$app") && docker compose up -d --force-recreate ${app}-service" >/dev/null 2>&1 || true ((recreated++)) done <<< "$installed_apps" diff --git a/containers/gluetun/scripts/gluetun_route_apps.sh b/containers/gluetun/scripts/gluetun_route_apps.sh index 1ae5d81..df61655 100644 --- a/containers/gluetun/scripts/gluetun_route_apps.sh +++ b/containers/gluetun/scripts/gluetun_route_apps.sh @@ -32,7 +32,7 @@ gluetunRouteExistingAppsPrompt() # leaves an undeclared name EMPTY in the CALLER's scope at EOF. while IFS= read -r app; do [[ -z "$app" ]] && continue - local cfg_file="${containers_dir}${app}/${app}.config" + local cfg_file="$(appDir "$app")/${app}.config" [[ -f "$cfg_file" ]] || continue local category category=$(grep -E "^CFG_${app^^}_CATEGORY=" "$cfg_file" 2>/dev/null \ @@ -56,7 +56,7 @@ gluetunRouteExistingAppsPrompt() echo " The following installed apps can be routed through Gluetun:" for app in "${eligible[@]}"; do - local cfg_file="${containers_dir}${app}/${app}.config" + local cfg_file="$(appDir "$app")/${app}.config" local current current=$(grep -E "^CFG_${app^^}_NETWORK=" "$cfg_file" 2>/dev/null | head -1 | cut -d'=' -f2) [[ -z "$current" ]] && current="default" @@ -73,7 +73,7 @@ gluetunRouteExistingAppsPrompt() esac for app in "${eligible[@]}"; do - local cfg_file="${containers_dir}${app}/${app}.config" + local cfg_file="$(appDir "$app")/${app}.config" local current current=$(grep -E "^CFG_${app^^}_NETWORK=" "$cfg_file" 2>/dev/null | head -1 | cut -d'=' -f2) [[ -z "$current" ]] && current="default" diff --git a/containers/grafana/scripts/grafana_install_hooks.sh b/containers/grafana/scripts/grafana_install_hooks.sh index cb1f097..55be2c4 100644 --- a/containers/grafana/scripts/grafana_install_hooks.sh +++ b/containers/grafana/scripts/grafana_install_hooks.sh @@ -22,9 +22,9 @@ grafana_install_post_start() # install user and failed per file with "Operation not permitted", failing the # step on every reinstall — while a fresh install passed, because the dir was # still empty. Grafana's files must keep Grafana's ownership regardless. - if [ -d "${containers_dir}grafana/grafana_storage" ]; then + if [ -d "$(appDir grafana)/grafana_storage" ]; then local result - result=$(runFileOp chmod 0777 "${containers_dir}grafana/grafana_storage") + result=$(runFileOp chmod 0777 "$(appDir grafana)/grafana_storage") checkSuccess "Set permissions to grafana_storage folder." fi } diff --git a/containers/headscale/scripts/headscale_commands.sh b/containers/headscale/scripts/headscale_commands.sh index 47d8446..80e334d 100755 --- a/containers/headscale/scripts/headscale_commands.sh +++ b/containers/headscale/scripts/headscale_commands.sh @@ -15,13 +15,13 @@ headscaleCommands() isHeader "Install Headscale Apps List" # Find all subdirectories under the directory where your apps are installed - for app_dir in "$containers_dir"/*/; do + while IFS= read -r app_dir; do if [[ -d "$app_dir" ]]; then # Extract the app name (folder name) local app_name=$(basename "$app_dir") local app_names+=("$app_name") fi - done + done < <(storageAppDirs) # Check if any apps were found if [ ${#app_names[@]} -eq 0 ]; then diff --git a/containers/headscale/scripts/headscale_edit_config.sh b/containers/headscale/scripts/headscale_edit_config.sh index 33f962d..e6c0282 100755 --- a/containers/headscale/scripts/headscale_edit_config.sh +++ b/containers/headscale/scripts/headscale_edit_config.sh @@ -2,7 +2,7 @@ headscaleEditConfig() { - local config_file="${containers_dir}headscale/config/config.yaml" + local config_file="$(appDir headscale)/config/config.yaml" local previous_md5=$(md5sum "$config_file" | awk '{print $1}') $CFG_TEXT_EDITOR "$config_file" local current_md5=$(md5sum "$config_file" | awk '{print $1}') diff --git a/containers/headscale/scripts/headscale_get_hostname.sh b/containers/headscale/scripts/headscale_get_hostname.sh index dd34307..1fd44f5 100755 --- a/containers/headscale/scripts/headscale_get_hostname.sh +++ b/containers/headscale/scripts/headscale_get_hostname.sh @@ -2,7 +2,7 @@ setupHeadscaleGetHostname() { - local config_file="${containers_dir}headscale/config/config.yaml" + local config_file="$(appDir headscale)/config/config.yaml" if [ -f "$config_file" ]; then # Read the line with "server_url" and extract the hostname headscale_live_hostname=$(grep "server_url:" "$config_file" | awk -F'server_url: ' '{print $2}') diff --git a/containers/headscale/scripts/headscale_install_hooks.sh b/containers/headscale/scripts/headscale_install_hooks.sh index c11fcfe..f93c048 100644 --- a/containers/headscale/scripts/headscale_install_hooks.sh +++ b/containers/headscale/scripts/headscale_install_hooks.sh @@ -9,7 +9,7 @@ headscale_install_post_compose() local app_name="$1" local result - result=$(createFolders "loud" $docker_install_user $containers_dir$app_name/config) + result=$(createFolders "loud" $docker_install_user $(appDir "$app_name")/config) checkSuccess "Create config folder" result=$(copyResource "$app_name" "config.yaml" "config" | runInstallWrite -a "$logs_dir/$docker_log_file" 2>&1) diff --git a/containers/headscale/scripts/tailscale_install.sh b/containers/headscale/scripts/tailscale_install.sh index c82f085..70d9283 100755 --- a/containers/headscale/scripts/tailscale_install.sh +++ b/containers/headscale/scripts/tailscale_install.sh @@ -5,10 +5,10 @@ tailscaleInstallToContainer() local app_name="$1" local type="$2" - local result; result=$(createFolders "loud" $docker_install_user $containers_dir$app_name/tailscale) + local result; result=$(createFolders "loud" $docker_install_user $(appDir "$app_name")/tailscale) checkSuccess "Creating Tailscale folder" - copyFile "loud" "${install_containers_dir}headscale/resources/tailscale.sh" "$containers_dir$app_name/tailscale/tailscale.sh" $docker_install_user | runInstallWrite -a "$logs_dir/$docker_log_file" 2>&1 + copyFile "loud" "${install_containers_dir}headscale/resources/tailscale.sh" "$(appDir "$app_name")/tailscale/tailscale.sh" $docker_install_user | runInstallWrite -a "$logs_dir/$docker_log_file" 2>&1 if [[ "$type" != "install" ]]; then dockerComposeRestart $app_name; diff --git a/containers/jitsimeet/scripts/jitsimeet_install_hooks.sh b/containers/jitsimeet/scripts/jitsimeet_install_hooks.sh index 00f5c96..514b360 100644 --- a/containers/jitsimeet/scripts/jitsimeet_install_hooks.sh +++ b/containers/jitsimeet/scripts/jitsimeet_install_hooks.sh @@ -19,20 +19,20 @@ jitsimeet_install_post_setup() echo "The latest tag is: $latest_tag" local result - result=$(createFolders "loud" $docker_install_user $containers_dir$app_name) + result=$(createFolders "loud" $docker_install_user $(appDir "$app_name")) checkSuccess "Creating $app_name container installation folder" - result=$(cd $containers_dir$app_name && runFileOp rm -rf $containers_dir$app_name/$latest_tag.zip) + result=$(cd $(appDir "$app_name") && runFileOp rm -rf $(appDir "$app_name")/$latest_tag.zip) checkSuccess "Deleting zip file to prevent conflicts" - result=$(createTouch $containers_dir$app_name/$latest_tag.txt $docker_install_user && echo "Installed \"$latest_tag\" on \"$backupDate\"!" > $latest_tag.txt) + result=$(createTouch $(appDir "$app_name")/$latest_tag.txt $docker_install_user && echo "Installed \"$latest_tag\" on \"$backupDate\"!" > $latest_tag.txt) checkSuccess "Create logging txt file" - result=$(runFileOp wget -O $containers_dir$app_name/$latest_tag.zip $git_url/archive/refs/tags/$latest_tag.zip) + result=$(runFileOp wget -O $(appDir "$app_name")/$latest_tag.zip $git_url/archive/refs/tags/$latest_tag.zip) checkSuccess "Downloading tagged zip file from GitHub" - result=$(runFileOp unzip -o $containers_dir$app_name/$latest_tag.zip -d $containers_dir$app_name) + result=$(runFileOp unzip -o $(appDir "$app_name")/$latest_tag.zip -d $(appDir "$app_name")) checkSuccess "Unzip downloaded file" - result=$(runFileOp mv $containers_dir$app_name/docker-jitsi-meet-$latest_tag/* $containers_dir$app_name) + result=$(runFileOp mv $(appDir "$app_name")/docker-jitsi-meet-$latest_tag/* $(appDir "$app_name")) checkSuccess "Moving all files from zip file to install directory" - result=$(runFileOp rm -rf $containers_dir$app_name/$latest_tag.zip && runFileOp rm -rf $containers_dir$app_name/$latest_tag/) + result=$(runFileOp rm -rf $(appDir "$app_name")/$latest_tag.zip && runFileOp rm -rf $(appDir "$app_name")/$latest_tag/) checkSuccess "Removing downloaded zip file as no longer needed" } @@ -48,36 +48,36 @@ jitsimeet_install_post_compose() dockerSetupEnvFile local result - result=$(runFileOp sed -i "s|CONFIG=~/.jitsi-meet-cfg|CONFIG=$containers_dir$app_name/.jitsi-meet-cfg|g" $containers_dir$app_name/.env) + result=$(runFileOp sed -i "s|CONFIG=~/.jitsi-meet-cfg|CONFIG=$(appDir "$app_name")/.jitsi-meet-cfg|g" $(appDir "$app_name")/.env) checkSuccess "Updating .env file with new install path" - result=$(runFileOp sed -i "s|#PUBLIC_URL=https://meet.example.com|PUBLIC_URL=https://$host_setup|g" $containers_dir$app_name/.env) + result=$(runFileOp sed -i "s|#PUBLIC_URL=https://meet.example.com|PUBLIC_URL=https://$host_setup|g" $(appDir "$app_name")/.env) checkSuccess "Updating .env file with Public URL to $host_setup" - result=$(runFileOp sed -i "s|HTTP_PORT=8000|HTTP_PORT=$usedport1|g" $containers_dir$app_name/.env) + result=$(runFileOp sed -i "s|HTTP_PORT=8000|HTTP_PORT=$usedport1|g" $(appDir "$app_name")/.env) checkSuccess "Updating .env file with HTTP_PORT to $usedport1" - result=$(runFileOp sed -i "s|HTTPS_PORT=8443|HTTPS_PORT=$usedport2|g" $containers_dir$app_name/.env) + result=$(runFileOp sed -i "s|HTTPS_PORT=8443|HTTPS_PORT=$usedport2|g" $(appDir "$app_name")/.env) checkSuccess "Updating .env file with HTTP_PORT to $usedport2" # Defaults missing from the shipped .env (see jitsi/docker-jitsi-meet # commit 12051700562d…). Append them here so the install boots. - result=$(echo "XMPP_DOMAIN=meet.jitsi" | runFileWrite -a "$containers_dir$app_name/.env") + result=$(echo "XMPP_DOMAIN=meet.jitsi" | runFileWrite -a "$(appDir "$app_name")/.env") checkSuccess "Updating .env file with missing option : XMPP_DOMAIN" - result=$(echo "XMPP_SERVER=xmpp.meet.jitsi" | runFileWrite -a "$containers_dir$app_name/.env") + result=$(echo "XMPP_SERVER=xmpp.meet.jitsi" | runFileWrite -a "$(appDir "$app_name")/.env") checkSuccess "Updating .env file with missing option : XMPP_SERVER" - result=$(echo "JVB_PORT=$usedport4" | runFileWrite -a "$containers_dir$app_name/.env") + result=$(echo "JVB_PORT=$usedport4" | runFileWrite -a "$(appDir "$app_name")/.env") checkSuccess "Updating .env file with missing option : JVB_PORT" - result=$(echo "JVB_TCP_MAPPED_PORT=$usedport5" | runFileWrite -a "$containers_dir$app_name/.env") + result=$(echo "JVB_TCP_MAPPED_PORT=$usedport5" | runFileWrite -a "$(appDir "$app_name")/.env") checkSuccess "Updating .env file with missing option : JVB_TCP_MAPPED_PORT" - result=$(echo "JVB_TCP_PORT=$usedport5" | runFileWrite -a "$containers_dir$app_name/.env") + result=$(echo "JVB_TCP_PORT=$usedport5" | runFileWrite -a "$(appDir "$app_name")/.env") checkSuccess "Updating .env file with missing option : JVB_TCP_PORT" - result=$(cd "$containers_dir$app_name" && runFileOp ./gen-passwords.sh) + result=$(cd "$(appDir "$app_name")" && runFileOp ./gen-passwords.sh) checkSuccess "Running Jitsi Meet gen-passwords.sh script" } @@ -91,10 +91,10 @@ jitsimeet_install_post_start() echo "" local result - result=$(runFileOp sed -i "s|80|$usedport1|g" $containers_dir$app_name/web/rootfs/defaults/default) + result=$(runFileOp sed -i "s|80|$usedport1|g" $(appDir "$app_name")/web/rootfs/defaults/default) checkSuccess "Updating NGINX default site port 80 to $usedport1" - result=$(runFileOp sed -i "s|443|$usedport2|g" $containers_dir$app_name/web/rootfs/defaults/default) + result=$(runFileOp sed -i "s|443|$usedport2|g" $(appDir "$app_name")/web/rootfs/defaults/default) checkSuccess "Updating NGINX default site port 443 to $usedport2" dockerComposeRestart $app_name diff --git a/containers/libreportal/libreportal.sh b/containers/libreportal/libreportal.sh index 4add1b9..9cbc5ad 100644 --- a/containers/libreportal/libreportal.sh +++ b/containers/libreportal/libreportal.sh @@ -129,7 +129,7 @@ installLibrePortal() if [[ "$libreportal_bootstrap_install" != "true" ]]; then ((menu_number++)) echo "" - echo "---- $menu_number. You can find $app_name files at $containers_dir$app_name" + echo "---- $menu_number. You can find $app_name files at $(appDir "$app_name")" echo "" echo " You can now navigate to your new service using one of the options below : " echo "" diff --git a/containers/libreportal_catalog/scripts/libreportal_catalog_install_hooks.sh b/containers/libreportal_catalog/scripts/libreportal_catalog_install_hooks.sh index 4319302..73f412e 100644 --- a/containers/libreportal_catalog/scripts/libreportal_catalog_install_hooks.sh +++ b/containers/libreportal_catalog/scripts/libreportal_catalog_install_hooks.sh @@ -9,7 +9,7 @@ libreportal_catalog_install_post_setup() { local app_name="$1" local def="${install_containers_dir%/}/$app_name" - local live="$containers_dir$app_name" + local live="$(appDir "$app_name")" local dest="$live/data" # The compose bind-mounts nginx.conf as a FILE; the generic installer diff --git a/containers/mastodon/scripts/mastodon_install_hooks.sh b/containers/mastodon/scripts/mastodon_install_hooks.sh index 951e61e..f027e80 100644 --- a/containers/mastodon/scripts/mastodon_install_hooks.sh +++ b/containers/mastodon/scripts/mastodon_install_hooks.sh @@ -29,7 +29,7 @@ mastodon_install_post_setup() { local app_name="${1:-mastodon}" - local config_file="${containers_dir}${app_name}/${app_name}.config" + local config_file="$(appDir "$app_name")/${app_name}.config" [[ -f "$config_file" ]] || return 0 # Keep a pair that already has the right shape. The length test is also what diff --git a/containers/matrix/scripts/matrix_auth.sh b/containers/matrix/scripts/matrix_auth.sh index d2c88d1..b0ff8c4 100644 --- a/containers/matrix/scripts/matrix_auth.sh +++ b/containers/matrix/scripts/matrix_auth.sh @@ -48,7 +48,7 @@ _matrixApi() { local script="$1"; shift local admin_user="${CFG_MATRIX_ADMIN_USERNAME:-admin}" local admin_pass="${CFG_MATRIX_ADMIN_PASSWORD_1}" - local cache="${containers_dir}matrix/${_matrix_token_cache}" + local cache="$(appDir matrix)/${_matrix_token_cache}" if [[ -z "$admin_pass" || "$admin_pass" == RANDOMIZEDPASSWORD* ]]; then isError "No Matrix admin password in matrix.config — cannot authenticate to the admin API." diff --git a/containers/matrix/scripts/matrix_install_hooks.sh b/containers/matrix/scripts/matrix_install_hooks.sh index 6af588b..d791fcb 100644 --- a/containers/matrix/scripts/matrix_install_hooks.sh +++ b/containers/matrix/scripts/matrix_install_hooks.sh @@ -66,11 +66,11 @@ _matrixServerName() _matrixPublicBaseUrl() { local app_name="$1" - local compose="$containers_dir$app_name/docker-compose.yml" + local compose="$(appDir "$app_name")/docker-compose.yml" # Real HTTPS only when Traefik is actually installed AND a domain exists; # otherwise the router in the compose is decorative. - if [[ -d "${containers_dir}traefik" && -n "$domain_full" ]]; then + if [[ -d "$(appDir traefik)" && -n "$domain_full" ]]; then local host host=$(tagsManagerGetTagContent "$compose" "DOMAINSUBNAME_TAG_1") if [[ -n "$host" && "$host" != DOMAINSUBNAME_DATA* ]]; then @@ -94,7 +94,7 @@ _matrixPublicBaseUrl() matrix_install_post_compose() { local app_name="$1" - local app_dir="$containers_dir$app_name" + local app_dir="$(appDir "$app_name")" local data_dir="$app_dir/data" ((menu_number++)) @@ -254,7 +254,7 @@ matrix_install_post_compose() matrix_install_post_start() { local app_name="$1" - local app_dir="$containers_dir$app_name" + local app_dir="$(appDir "$app_name")" ((menu_number++)) echo "" diff --git a/containers/moneyapp/scripts/moneyapp_install_hooks.sh b/containers/moneyapp/scripts/moneyapp_install_hooks.sh index ec7cd34..b564eac 100644 --- a/containers/moneyapp/scripts/moneyapp_install_hooks.sh +++ b/containers/moneyapp/scripts/moneyapp_install_hooks.sh @@ -28,7 +28,7 @@ moneyapp_install_post_compose() # * https://. when traefik is on AND a real domain # is configured (CFG_DOMAIN_) # * http://: otherwise (raw port exposure) - local moneyapp_compose_file="$containers_dir$app_name/docker-compose.yml" + local moneyapp_compose_file="$(appDir "$app_name")/docker-compose.yml" local moneyapp_auth_url="" # host_setup is built as `.` — when CFG_DOMAIN_ is # empty it ends up as `.`, a valid-but-broken hostname. Reject diff --git a/containers/nextcloud/scripts/nextcloud_install_hooks.sh b/containers/nextcloud/scripts/nextcloud_install_hooks.sh index fca3a8f..e5dc83b 100644 --- a/containers/nextcloud/scripts/nextcloud_install_hooks.sh +++ b/containers/nextcloud/scripts/nextcloud_install_hooks.sh @@ -25,9 +25,9 @@ nextcloud_install_post_compose() # Clear a stub directory left behind by any previous attempt, otherwise the # copy lands INSIDE it (resources/nginx.conf/nginx.conf) and the mount fails # exactly as before. Shared helper — same repair the WebUI's own config uses. - repairStubDirForFile "$containers_dir$app_name/resources/nginx.conf" "loud" + repairStubDirForFile "$(appDir "$app_name")/resources/nginx.conf" "loud" local result result=$(copyResource "$app_name" "nginx.conf" "resources" | runInstallWrite -a "$logs_dir/$docker_log_file" 2>&1) - checkSuccess "Copying nginx.conf to $containers_dir$app_name/resources" + checkSuccess "Copying nginx.conf to $(appDir "$app_name")/resources" } diff --git a/containers/onlyoffice/scripts/onlyoffice_install_hooks.sh b/containers/onlyoffice/scripts/onlyoffice_install_hooks.sh index 9b22460..85792f9 100644 --- a/containers/onlyoffice/scripts/onlyoffice_install_hooks.sh +++ b/containers/onlyoffice/scripts/onlyoffice_install_hooks.sh @@ -8,7 +8,7 @@ onlyoffice_install_message_data() { local app_name="$1" local username password - username=$(grep -oP 'username=\K[^ ]+' "$containers_dir$app_name/docker-compose.yml" 2>/dev/null) - password=$(grep -oP 'password=\K[^ ]+' "$containers_dir$app_name/docker-compose.yml" 2>/dev/null) + username=$(grep -oP 'username=\K[^ ]+' "$(appDir "$app_name")/docker-compose.yml" 2>/dev/null) + password=$(grep -oP 'password=\K[^ ]+' "$(appDir "$app_name")/docker-compose.yml" 2>/dev/null) echo "$username $password" } diff --git a/containers/owncloud/scripts/owncloud_install_hooks.sh b/containers/owncloud/scripts/owncloud_install_hooks.sh index 4b5cfb9..6fafd58 100644 --- a/containers/owncloud/scripts/owncloud_install_hooks.sh +++ b/containers/owncloud/scripts/owncloud_install_hooks.sh @@ -31,7 +31,7 @@ owncloud_install_post_compose() isNotice "Failed to retrieve the web page." fi - local file_path="$containers_dir$app_name/docker-compose.yml" + local file_path="$(appDir "$app_name")/docker-compose.yml" local file_name="docker-compose.yml" local result result=$(runFileOp sed -i \ diff --git a/containers/prometheus/scripts/prometheus_install_hooks.sh b/containers/prometheus/scripts/prometheus_install_hooks.sh index 0129839..a098c0a 100644 --- a/containers/prometheus/scripts/prometheus_install_hooks.sh +++ b/containers/prometheus/scripts/prometheus_install_hooks.sh @@ -9,10 +9,10 @@ prometheus_install_post_compose() local app_name="$1" local result - result=$(createFolders "loud" $docker_install_user "$containers_dir$app_name/$app_name") + result=$(createFolders "loud" $docker_install_user "$(appDir "$app_name")/$app_name") checkSuccess "Created $app_name folder in $app_name" - result=$(createTouch "$containers_dir$app_name/$app_name/$app_name.yml" $docker_install_user) + result=$(createTouch "$(appDir "$app_name")/$app_name/$app_name.yml" $docker_install_user) checkSuccess "Created $app_name.yml file for $app_name" result=$(copyResource "$app_name" "$app_name.yml" "$app_name" | runInstallWrite -a "$logs_dir/$docker_log_file" 2>&1) @@ -23,8 +23,8 @@ prometheus_install_post_start() { local app_name="$1" - if [ -f "${containers_dir}prometheus/prometheus/prometheus.yml" ]; then - updateFileOwnership "${containers_dir}prometheus/prometheus/prometheus.yml" $docker_install_user $docker_install_user + if [ -f "$(appDir prometheus)/prometheus/prometheus.yml" ]; then + updateFileOwnership "$(appDir prometheus)/prometheus/prometheus.yml" $docker_install_user $docker_install_user fi # Prometheus runs as nobody (65534) inside the container, which rootless maps # to a subuid outside this user's authority (65534 -> 296605 here). Two @@ -48,14 +48,14 @@ prometheus_install_post_start() # every REINSTALL (a fresh install passed only because the dir # was still empty). Those files must keep prometheus's ownership # anyway — that is what lets it write them. - if [ -d "${containers_dir}prometheus/prometheus" ]; then + if [ -d "$(appDir prometheus)/prometheus" ]; then local result - result=$(runFileOp chmod -R a+rX,go-w "${containers_dir}prometheus/prometheus") + result=$(runFileOp chmod -R a+rX,go-w "$(appDir prometheus)/prometheus") checkSuccess "Set permissions to prometheus folder." fi - if [ -d "${containers_dir}prometheus/prom_data" ]; then + if [ -d "$(appDir prometheus)/prom_data" ]; then local result - result=$(runFileOp chmod 0777 "${containers_dir}prometheus/prom_data") + result=$(runFileOp chmod 0777 "$(appDir prometheus)/prom_data") checkSuccess "Set permissions to prom_data folder." fi } diff --git a/containers/rocketchat/scripts/rocketchat_auth.sh b/containers/rocketchat/scripts/rocketchat_auth.sh index 5c60a17..399ec9b 100644 --- a/containers/rocketchat/scripts/rocketchat_auth.sh +++ b/containers/rocketchat/scripts/rocketchat_auth.sh @@ -14,7 +14,7 @@ # way round needing a real account here. _rocketchatBaseUrl() { - local compose="${containers_dir}rocketchat/docker-compose.yml" + local compose="$(appDir rocketchat)/docker-compose.yml" local url url=$(runFileOp grep -oP '^\s*-\s*ROOT_URL=\K\S+' "$compose" 2>/dev/null | head -1) url="${url%%#*}" diff --git a/containers/searxng/scripts/searxng_install_hooks.sh b/containers/searxng/scripts/searxng_install_hooks.sh index d507643..54c0d0e 100644 --- a/containers/searxng/scripts/searxng_install_hooks.sh +++ b/containers/searxng/scripts/searxng_install_hooks.sh @@ -6,7 +6,7 @@ searxng_install_post_start() { local app_name="$1" - local searxng_settings="$containers_dir$app_name/searxng-data/settings.yml" + local searxng_settings="$(appDir "$app_name")/searxng-data/settings.yml" local searxng_timeout=10 local searxng_counter=0 while [ ! -f "$searxng_settings" ]; do diff --git a/containers/stalwart/scripts/stalwart_install_hooks.sh b/containers/stalwart/scripts/stalwart_install_hooks.sh index 63ab50c..439204e 100644 --- a/containers/stalwart/scripts/stalwart_install_hooks.sh +++ b/containers/stalwart/scripts/stalwart_install_hooks.sh @@ -47,7 +47,7 @@ stalwart_mode() { local mode="${CFG_STALWART_MODE:-auto}" if [[ "$mode" == "auto" ]]; then - if [[ -d "${containers_dir}traefik" ]]; then mode="public"; else mode="private"; fi + if [[ -d "$(appDir traefik)" ]]; then mode="public"; else mode="private"; fi fi [[ "$mode" == "public" || "$mode" == "private" ]] || mode="private" printf '%s' "$mode" @@ -105,7 +105,7 @@ stalwart_apply_port_access() stalwart_install_post_setup() { local app_name="$1" - local cfg="$containers_dir$app_name/$app_name.config" + local cfg="$(appDir "$app_name")/$app_name.config" [[ -f "$cfg" ]] || return 0 local mode; mode=$(stalwart_mode) @@ -223,7 +223,7 @@ stalwart_install_provision() # the only route left is DNS-01 — which needs the DNS provider integration. # Without it mail clients get a self-signed certificate on 993 and no amount # of waiting fixes it. - if [[ "$mode" == "public" && -d "${containers_dir}traefik" \ + if [[ "$mode" == "public" && -d "$(appDir traefik)" \ && ( "${CFG_STALWART_DNS_PROVIDER:-manual}" == "manual" || -z "${CFG_STALWART_DNS_API_TOKEN:-}" ) ]]; then isNotice "Traefik owns ports 80 and 443, so Stalwart cannot validate a certificate" isNotice " for its mail ports on its own. Set CFG_STALWART_DNS_PROVIDER and a token" @@ -351,7 +351,7 @@ stalwart_install_post_start() # Resolved admin port comes from the compose tag (format `external:internal`), # the same source adguard's hook reads — the legacy $usedport1 isn't populated # by the current install pipeline. - local compose_file="$containers_dir$app_name/docker-compose.yml" + local compose_file="$(appDir "$app_name")/docker-compose.yml" local admin_pair admin_port admin_pair=$(tagsManagerGetTagContent "$compose_file" "PORTS_TAG_1" 2>/dev/null) admin_port="${admin_pair%%:*}" @@ -440,7 +440,7 @@ stalwart_install_post_start() echo "" isNotice "Point mail and calendar clients at: ${mail_host}" isNotice " Certificates are self-signed, so clients will ask you to trust it once." - [[ -d "${containers_dir}headscale" ]] && \ + [[ -d "$(appDir headscale)" ]] && \ isNotice " Headscale is installed, so this also reaches you from anywhere on your tailnet." echo "" isNotice "To exchange mail with the internet later, switch this app to public" diff --git a/containers/stalwart/tools/stalwart_set_mode.sh b/containers/stalwart/tools/stalwart_set_mode.sh index 26ccf5d..209ab22 100644 --- a/containers/stalwart/tools/stalwart_set_mode.sh +++ b/containers/stalwart/tools/stalwart_set_mode.sh @@ -27,7 +27,7 @@ appStalwartSetMode() { return 1 fi - local cfg="${containers_dir}stalwart/stalwart.config" + local cfg="$(appDir stalwart)/stalwart.config" if [[ ! -f "$cfg" ]]; then isError "Stalwart does not look installed — no config at $cfg" return 1 @@ -96,7 +96,7 @@ appStalwartSetMode() { plan=$(printf '{"@type":"upsert","object":"AcmeProvider","matchOn":["directory"],"value":{"acme":{"directory":"https://acme-v02.api.letsencrypt.org/directory","contact":{"%s":true},"challengeType":"%s"}}}\n{"@type":"update","object":"Domain","id":"%s","value":{"certificateManagement":{"@type":"Automatic","acmeProviderId":"#acme"}}}' \ "$contact" "$challenge" "$domain_id") - if [[ "$challenge" == "TlsAlpn01" && -d "${containers_dir}traefik" ]]; then + if [[ "$challenge" == "TlsAlpn01" && -d "$(appDir traefik)" ]]; then isNotice "Traefik holds ports 80 and 443, so this certificate request will not" isNotice " validate. Set CFG_STALWART_DNS_PROVIDER and a token to validate over" isNotice " DNS instead — that is the only route that works alongside Traefik." diff --git a/containers/stoat/scripts/stoat_auth.sh b/containers/stoat/scripts/stoat_auth.sh index 0aa3caa..1d94e49 100644 --- a/containers/stoat/scripts/stoat_auth.sh +++ b/containers/stoat/scripts/stoat_auth.sh @@ -30,7 +30,7 @@ # advertised URL happens to name. _stoatApiLocal() { local app_name="${1:-stoat}" - local compose="$containers_dir$app_name/docker-compose.yml" + local compose="$(appDir "$app_name")/docker-compose.yml" local ports external ports=$(tagsManagerGetTagContent "$compose" "PORTS_TAG_1" 2>/dev/null) external="${ports%%:*}" diff --git a/containers/stoat/scripts/stoat_install_hooks.sh b/containers/stoat/scripts/stoat_install_hooks.sh index 2dec0e1..2a5b5ee 100644 --- a/containers/stoat/scripts/stoat_install_hooks.sh +++ b/containers/stoat/scripts/stoat_install_hooks.sh @@ -55,9 +55,9 @@ _stoatDomain() _stoatBaseUrl() { local app_name="$1" - local compose="$containers_dir$app_name/docker-compose.yml" + local compose="$(appDir "$app_name")/docker-compose.yml" - if [[ -d "${containers_dir}traefik" && -n "$domain_full" ]]; then + if [[ -d "$(appDir traefik)" && -n "$domain_full" ]]; then local host host=$(_stoatDomain) [[ -n "$host" ]] && { echo "https://${host}"; return 0; } @@ -226,7 +226,7 @@ _stoatOwnConfigFiles() { stoat_install_post_compose() { local app_name="$1" - local app_dir="$containers_dir$app_name" + local app_dir="$(appDir "$app_name")" ((menu_number++)) echo "" @@ -310,7 +310,7 @@ EOF stoat_install_post_start() { local app_name="$1" - local app_dir="$containers_dir$app_name" + local app_dir="$(appDir "$app_name")" # Ports are assigned during compose-up, so on a domain-less install the URL # baked in a moment ago was a guess. Correct it now and restart, but only if diff --git a/containers/traefik/scripts/traefik_install_hooks.sh b/containers/traefik/scripts/traefik_install_hooks.sh index 249f584..1872ab5 100644 --- a/containers/traefik/scripts/traefik_install_hooks.sh +++ b/containers/traefik/scripts/traefik_install_hooks.sh @@ -31,13 +31,13 @@ traefik_install_post_compose() local app_name="$1" local result - result=$(createFolders "loud" $docker_install_user "$containers_dir$app_name/etc" "$containers_dir$app_name/etc/certs" "$containers_dir$app_name/etc/dynamic" "$containers_dir$app_name/etc/dynamic/middlewears") + result=$(createFolders "loud" $docker_install_user "$(appDir "$app_name")/etc" "$(appDir "$app_name")/etc/certs" "$(appDir "$app_name")/etc/dynamic" "$(appDir "$app_name")/etc/dynamic/middlewears") checkSuccess "Created etc and certs & dynamic Directories" result=$(copyResource "$app_name" "traefik.yml" "etc") checkSuccess "Copy Traefik configuration file for $app_name" - result=$(runFileOp sed -i "s|DEBUGLEVEL|$CFG_TRAEFIK_LOGGING|g" "$containers_dir$app_name/etc/traefik.yml") + result=$(runFileOp sed -i "s|DEBUGLEVEL|$CFG_TRAEFIK_LOGGING|g" "$(appDir "$app_name")/etc/traefik.yml") checkSuccess "Configured Traefik debug level with: $CFG_TRAEFIK_LOGGING for $app_name" configSetupFileWithData $app_name "traefik.yml" "etc" @@ -45,8 +45,8 @@ traefik_install_post_compose() # Apply CFG_TRAEFIK_DASHBOARD_ACCESS: rewrites api.insecure + the # `traefik:` entrypoint in traefik.yml, and (for local-only) prefixes # the compose port mapping with 127.0.0.1: so :8080 binds to loopback. - local traefik_yml="$containers_dir$app_name/etc/traefik.yml" - local compose_yml="$containers_dir$app_name/docker-compose.yml" + local traefik_yml="$(appDir "$app_name")/etc/traefik.yml" + local compose_yml="$(appDir "$app_name")/docker-compose.yml" local access="${CFG_TRAEFIK_DASHBOARD_ACCESS:-local-only}" case "$access" in @@ -79,7 +79,7 @@ traefik_install_post_compose() result=$(copyResource "$app_name" "config.yml" "etc/dynamic") checkSuccess "Copy Traefik Dynamic config.yml configuration file for $app_name" - result=$(runFileOp sed -i "s|ERRORWEBSITE|$CFG_TRAEFIK_404_SITE|g" "$containers_dir$app_name/etc/dynamic/config.yml") + result=$(runFileOp sed -i "s|ERRORWEBSITE|$CFG_TRAEFIK_404_SITE|g" "$(appDir "$app_name")/etc/dynamic/config.yml") checkSuccess "Configured Traefik error website with URL: $CFG_TRAEFIK_404_SITE for $app_name" configSetupFileWithData $app_name "config.yml" "etc/dynamic" diff --git a/containers/trilium/scripts/trilium_install_hooks.sh b/containers/trilium/scripts/trilium_install_hooks.sh index 90d3ca0..7d47323 100644 --- a/containers/trilium/scripts/trilium_install_hooks.sh +++ b/containers/trilium/scripts/trilium_install_hooks.sh @@ -13,7 +13,7 @@ trilium_install_post_start() local trilium_timeout=10 local trilium_counter=0 - while [ ! -f "$containers_dir$app_name/trilium-data/config.ini" ]; do + while [ ! -f "$(appDir "$app_name")/trilium-data/config.ini" ]; do if [ "$trilium_counter" -ge "$trilium_timeout" ]; then isNotice "File not found after 10 seconds. Exiting..." return 0 @@ -24,7 +24,7 @@ trilium_install_post_start() done local result - result=$(runFileOp sed -i "s|port=8080|port=$usedport1|g" "$containers_dir$app_name/trilium-data/config.ini") + result=$(runFileOp sed -i "s|port=8080|port=$usedport1|g" "$(appDir "$app_name")/trilium-data/config.ini") checkSuccess "Configured $app_name from default 8080 to $usedport1" dockerComposeRestart $app_name diff --git a/containers/unbound/scripts/unbound_install_hooks.sh b/containers/unbound/scripts/unbound_install_hooks.sh index 007c496..39160b2 100644 --- a/containers/unbound/scripts/unbound_install_hooks.sh +++ b/containers/unbound/scripts/unbound_install_hooks.sh @@ -16,11 +16,11 @@ unbound_install_post_compose() # A previous attempt that started the container before this file existed # leaves conf.d as a Docker-created stub, and the copy below would then land # inside it. Same failure that kept Nextcloud's nginx from starting. - repairStubDirForFile "$containers_dir$app_name/conf.d/libreportal.conf" "loud" + repairStubDirForFile "$(appDir "$app_name")/conf.d/libreportal.conf" "loud" local result result=$(copyResource "$app_name" "libreportal.conf" "conf.d" | runInstallWrite -a "$logs_dir/$docker_log_file" 2>&1) - checkSuccess "Copying libreportal.conf to $containers_dir$app_name/conf.d" + checkSuccess "Copying libreportal.conf to $(appDir "$app_name")/conf.d" monitoringToggleAppConfig "$app_name" "conf.d/libreportal.conf" } diff --git a/containers/wireguard/scripts/wireguard_install_hooks.sh b/containers/wireguard/scripts/wireguard_install_hooks.sh index b5ab878..416d42a 100644 --- a/containers/wireguard/scripts/wireguard_install_hooks.sh +++ b/containers/wireguard/scripts/wireguard_install_hooks.sh @@ -37,7 +37,7 @@ wireguard_install_post_compose() # outside the docker network generator range (100-149), so the two # subnets cannot collide. Reusing the persisted value keeps existing # peer configs valid across reinstalls. - local _wg_compose_for_subnet="$containers_dir$app_name/docker-compose.yml" + local _wg_compose_for_subnet="$(appDir "$app_name")/docker-compose.yml" if [[ -z "$CFG_WIREGUARD_SUBNET" ]]; then local _wg_second=$(( RANDOM % 51 + 200 )) local _wg_third=$(( RANDOM % 256 )) @@ -57,7 +57,7 @@ wireguard_install_post_compose() echo "---- $menu_number. Resolving WG_HOST for peer configs" echo "" - local wg_compose_file="$containers_dir$app_name/docker-compose.yml" + local wg_compose_file="$(appDir "$app_name")/docker-compose.yml" local wg_host_value="" local _wg_traefik_installed=0 if declare -f checkServiceInstalled >/dev/null 2>&1 && checkServiceInstalled "traefik"; then diff --git a/scripts/app/app_get_key_data.sh b/scripts/app/app_get_key_data.sh index 50af51a..19e6bd7 100755 --- a/scripts/app/app_get_key_data.sh +++ b/scripts/app/app_get_key_data.sh @@ -7,9 +7,9 @@ appGetKeyData() local key="$3" # Check if the file exists - if [ -f "$containers_dir$app_name/$file_path" ]; then + if [ -f "$(appDir "$app_name")/$file_path" ]; then # Extract the line containing the key - local key_line=$(grep "^$key=" "$containers_dir$app_name/$file_path") + local key_line=$(grep "^$key=" "$(appDir "$app_name")/$file_path") # Extract the value using cut or awk local value=$(echo "$key_line" | cut -d '=' -f 2) diff --git a/scripts/app/app_status.sh b/scripts/app/app_status.sh index 8abf15c..ba30c07 100755 --- a/scripts/app/app_status.sh +++ b/scripts/app/app_status.sh @@ -13,7 +13,7 @@ appStatus() return 1 fi - local app_dir="$containers_dir/$app_name" + local app_dir="$(appDir "$app_name")" local compose_file="$app_dir/docker-compose.yml" if [[ ! -f "$compose_file" ]]; then diff --git a/scripts/app/auth_adapter.sh b/scripts/app/auth_adapter.sh index bcba8b7..5f33866 100644 --- a/scripts/app/auth_adapter.sh +++ b/scripts/app/auth_adapter.sh @@ -52,7 +52,7 @@ authAdapterCall() { # on showing the old one. Hence the explicit return 1 and warning below. authPersistCfg() { local app="$1" key="$2" value="$3" - local cfg="${containers_dir}${app}/${app}.config" + local cfg="$(appDir "$app")/${app}.config" [[ ! -f "$cfg" ]] && cfg="${install_containers_dir}/${app}/${app}.config" [[ ! -f "$cfg" ]] && return 1 diff --git a/scripts/app/install/app_install.sh b/scripts/app/install/app_install.sh index e4ffed7..dee38ef 100644 --- a/scripts/app/install/app_install.sh +++ b/scripts/app/install/app_install.sh @@ -212,7 +212,7 @@ installApp() ((menu_number++)) echo "" - echo "---- $menu_number. You can find $app_name files at $containers_dir$app_name" + echo "---- $menu_number. You can find $app_name files at $(appDir "$app_name")" echo "" # Final-message data — apps that want extra args (creds, URLs, etc.) diff --git a/scripts/backup/app/backup_app_hooks.sh b/scripts/backup/app/backup_app_hooks.sh index b01f5ad..b86e491 100644 --- a/scripts/backup/app/backup_app_hooks.sh +++ b/scripts/backup/app/backup_app_hooks.sh @@ -4,7 +4,7 @@ backupAppRunHook() { local app_name="$1" local phase="$2" - local hook="$containers_dir$app_name/backup.${phase}.hook.sh" + local hook="$(appDir "$app_name")/backup.${phase}.hook.sh" [[ ! -f "$hook" ]] && return 0 diff --git a/scripts/backup/app/backup_app_start.sh b/scripts/backup/app/backup_app_start.sh index 6374e44..b47dc1c 100755 --- a/scripts/backup/app/backup_app_start.sh +++ b/scripts/backup/app/backup_app_start.sh @@ -10,8 +10,8 @@ backupAppStart() return 1 fi - if [[ ! -d "$containers_dir$app_name" ]]; then - isError "Cannot back up '$app_name' — not installed at $containers_dir$app_name" + if [[ ! -d "$(appDir "$app_name")" ]]; then + isError "Cannot back up '$app_name' — not installed at $(appDir "$app_name")" return 1 fi @@ -43,7 +43,7 @@ backupAppStart() isNotice "Live strategy — containers stay running; databases dumped + private files captured via their containers" if ! backupDbDump "$stored_app_name" || ! backupFilesCapture "$stored_app_name"; then isError "Live capture failed — falling back to stop-snapshot-start for safety" - runFileOp rm -rf "${containers_dir:?}$stored_app_name/.lp-backup" + runFileOp rm -rf "$(appDir "$stored_app_name")/.lp-backup" strategy="stop-snapshot-start" dockerComposeDown "$stored_app_name" fi diff --git a/scripts/backup/db/backup_db.sh b/scripts/backup/db/backup_db.sh index df158f9..cf0696b 100644 --- a/scripts/backup/db/backup_db.sh +++ b/scripts/backup/db/backup_db.sh @@ -43,7 +43,7 @@ backup_db_dump_subdir=".lp-backup/db" backupDbDescriptors() { local app="$1" - local compose="$containers_dir$app/docker-compose.yml" + local compose="$(appDir "$app")/docker-compose.yml" [[ -f "$compose" ]] || return 0 grep -E '^[[:space:]]*libreportal\.backup\.db[[:space:]]*:' "$compose" 2>/dev/null \ @@ -65,7 +65,7 @@ backupDbHasDescriptors() backupAppIsLiveSafe() { local app="$1" - local compose="$containers_dir$app/docker-compose.yml" + local compose="$(appDir "$app")/docker-compose.yml" [[ -f "$compose" ]] || return 1 if grep -qE '^[[:space:]]*libreportal\.backup\.live[[:space:]]*:[[:space:]]*["'\'']?true' "$compose" 2>/dev/null; then return 0 @@ -204,7 +204,7 @@ _backupDbImport() backupDbDump() { local app="$1" - local app_dir="$containers_dir$app" + local app_dir="$(appDir "$app")" local dump_dir="$app_dir/$backup_db_dump_subdir" local desc kind container datadir path dump rc=0 @@ -286,7 +286,7 @@ backupDbDump() backupDbExcludePaths() { local app="$1" - local app_dir="$containers_dir$app" + local app_dir="$(appDir "$app")" local desc kind container datadir path while IFS= read -r desc; do @@ -315,7 +315,7 @@ backupDbExcludePaths() restoreDbRehydratePreStart() { local app="$1" - local app_dir="$containers_dir$app" + local app_dir="$(appDir "$app")" local dump_dir="$app_dir/$backup_db_dump_subdir" local desc kind container datadir path dump @@ -350,7 +350,7 @@ restoreDbRehydratePreStart() restoreDbReplayPostStart() { local app="$1" - local app_dir="$containers_dir$app" + local app_dir="$(appDir "$app")" local dump_dir="$app_dir/$backup_db_dump_subdir" local desc kind container datadir path dump diff --git a/scripts/backup/engine/borg_backup.sh b/scripts/backup/engine/borg_backup.sh index f2029a0..adde3b5 100644 --- a/scripts/backup/engine/borg_backup.sh +++ b/scripts/backup/engine/borg_backup.sh @@ -5,7 +5,7 @@ borgBackupAppToLocation() local idx="$1" local app_name="$2" local manifest_sha="$3" - local source_path="$containers_dir$app_name" + local source_path="$(appDir "$app_name")" if [[ ! -d "$source_path" ]]; then isError "Source path missing for $app_name: $source_path" diff --git a/scripts/backup/engine/kopia_backup.sh b/scripts/backup/engine/kopia_backup.sh index d62c3cf..9509b7f 100644 --- a/scripts/backup/engine/kopia_backup.sh +++ b/scripts/backup/engine/kopia_backup.sh @@ -5,7 +5,7 @@ kopiaBackupAppToLocation() local idx="$1" local app_name="$2" local manifest_sha="$3" - local source_path="$containers_dir$app_name" + local source_path="$(appDir "$app_name")" if [[ ! -d "$source_path" ]]; then isError "Source path missing for $app_name: $source_path" diff --git a/scripts/backup/engine/kopia_forget.sh b/scripts/backup/engine/kopia_forget.sh index 1eb9f27..5cf64d0 100644 --- a/scripts/backup/engine/kopia_forget.sh +++ b/scripts/backup/engine/kopia_forget.sh @@ -23,7 +23,7 @@ kopiaForgetApp() kopiaEnvExport "$idx" || return 1 - local src="$containers_dir$app_name" + local src="$(appDir "$app_name")" local host_tag="${CFG_INSTALL_NAME:-libreportal}" local policy_args=(policy set --global=false "$src") diff --git a/scripts/backup/engine/restic_backup.sh b/scripts/backup/engine/restic_backup.sh index 1a4d847..83d6867 100644 --- a/scripts/backup/engine/restic_backup.sh +++ b/scripts/backup/engine/restic_backup.sh @@ -5,7 +5,7 @@ resticBackupAppToLocation() local idx="$1" local app_name="$2" local manifest_sha="$3" - local source_path="$containers_dir$app_name" + local source_path="$(appDir "$app_name")" if [[ ! -d "$source_path" ]]; then isError "Source path missing for $app_name: $source_path" diff --git a/scripts/backup/engine/restic_restore.sh b/scripts/backup/engine/restic_restore.sh index ebed588..254b459 100644 --- a/scripts/backup/engine/restic_restore.sh +++ b/scripts/backup/engine/restic_restore.sh @@ -118,7 +118,7 @@ resticRestoreAppLatest() return 1 fi - local include_path="$containers_dir$app_name" + local include_path="$(appDir "$app_name")" resticRestoreSnapshot "$idx" "$snapshot_id" "$target_dir" "$include_path" } diff --git a/scripts/backup/files/backup_files.sh b/scripts/backup/files/backup_files.sh index 8674675..ee48298 100644 --- a/scripts/backup/files/backup_files.sh +++ b/scripts/backup/files/backup_files.sh @@ -49,7 +49,7 @@ backup_files_helper_image="busybox" backupFilesDescriptors() { local app="$1" - local compose="$containers_dir$app/docker-compose.yml" + local compose="$(appDir "$app")/docker-compose.yml" [[ -f "$compose" ]] || return 0 grep -E '^[[:space:]]*libreportal\.backup\.files[[:space:]]*:' "$compose" 2>/dev/null \ @@ -71,7 +71,7 @@ backupFilesHasDescriptors() backupFilesCapture() { local app="$1" - local app_dir="$containers_dir$app" + local app_dir="$(appDir "$app")" local desc container cpath subdir uid gid stage rc=0 backupFilesHasDescriptors "$app" || return 0 @@ -148,7 +148,7 @@ backupFilesCapture() backupFilesExcludePaths() { local app="$1" - local app_dir="$containers_dir$app" + local app_dir="$(appDir "$app")" local desc container cpath subdir uid gid while IFS= read -r desc; do @@ -165,7 +165,7 @@ backupFilesExcludePaths() restoreFilesRehydratePreStart() { local app="$1" - local app_dir="$containers_dir$app" + local app_dir="$(appDir "$app")" local desc container cpath subdir uid gid stage backupFilesHasDescriptors "$app" || return 0 diff --git a/scripts/backup/manifest/manifest_collect.sh b/scripts/backup/manifest/manifest_collect.sh index 27103b3..7f094f1 100644 --- a/scripts/backup/manifest/manifest_collect.sh +++ b/scripts/backup/manifest/manifest_collect.sh @@ -3,7 +3,7 @@ manifestCollect() { local app_name="$1" - local app_dir="$containers_dir$app_name" + local app_dir="$(appDir "$app_name")" local libreportal_commit libreportal_commit=$(git -C "${install_scripts_dir%/scripts/}" rev-parse --short HEAD 2>/dev/null || echo "unknown") diff --git a/scripts/backup/manifest/manifest_read.sh b/scripts/backup/manifest/manifest_read.sh index 61d9efe..a4c5170 100644 --- a/scripts/backup/manifest/manifest_read.sh +++ b/scripts/backup/manifest/manifest_read.sh @@ -6,7 +6,7 @@ manifestReadFromSnapshot() local snapshot_id="$2" local app_name="$3" - local manifest_path="$containers_dir$app_name/.libreportal-manifest.json" + local manifest_path="$(appDir "$app_name")/.libreportal-manifest.json" engineDumpFile "$repo_name" "$snapshot_id" "$manifest_path" 2>/dev/null } diff --git a/scripts/backup/manifest/manifest_write.sh b/scripts/backup/manifest/manifest_write.sh index 10ba675..8e5e526 100644 --- a/scripts/backup/manifest/manifest_write.sh +++ b/scripts/backup/manifest/manifest_write.sh @@ -3,7 +3,7 @@ manifestWrite() { local app_name="$1" - local app_dir="$containers_dir$app_name" + local app_dir="$(appDir "$app_name")" local manifest_path="$app_dir/.libreportal-manifest.json" if [[ ! -d "$app_dir" ]]; then @@ -25,6 +25,6 @@ manifestWrite() manifestRemove() { local app_name="$1" - local manifest_path="$containers_dir$app_name/.libreportal-manifest.json" + local manifest_path="$(appDir "$app_name")/.libreportal-manifest.json" [[ -f "$manifest_path" ]] && runFileOp rm -f "$manifest_path" } diff --git a/scripts/backup/verify/backup_verify.sh b/scripts/backup/verify/backup_verify.sh index 6bd2b39..0ff3bf8 100644 --- a/scripts/backup/verify/backup_verify.sh +++ b/scripts/backup/verify/backup_verify.sh @@ -18,7 +18,7 @@ backupVerifySnapshot() isNotice "Verifying ${snapshot_id:0:8} via scratch restore at $scratch" - if ! engineRestoreSnapshot "$idx" "$snapshot_id" "$scratch" "$containers_dir$app_name"; then + if ! engineRestoreSnapshot "$idx" "$snapshot_id" "$scratch" "$(appDir "$app_name")"; then isError "Verify restore FAILED for $app_name on $(resticLocationName "$idx")" runFileOp rm -rf "$scratch" return 1 @@ -31,7 +31,7 @@ backupVerifySnapshot() # and replaced by dumps/captures under .lp-backup) — so just sanity-check the # restore produced a non-empty tree. local restored_count - restored_count=$(runFileOp find "$scratch$containers_dir$app_name" -type f 2>/dev/null | wc -l) + restored_count=$(runFileOp find "$scratch$(appDir "$app_name")" -type f 2>/dev/null | wc -l) runFileOp rm -rf "$scratch" diff --git a/scripts/cli/commands/artifact/cli_artifact_apply.sh b/scripts/cli/commands/artifact/cli_artifact_apply.sh index f51d2a0..0a55c90 100644 --- a/scripts/cli/commands/artifact/cli_artifact_apply.sh +++ b/scripts/cli/commands/artifact/cli_artifact_apply.sh @@ -27,7 +27,7 @@ # never raw sudo. The install tree (our own code) is off-limits to hotfixes. # --- paths ------------------------------------------------------------------- -_artifactGenDir() { echo "${containers_dir%/}/libreportal/frontend/data/updater/generated"; } +_artifactGenDir() { echo "$(webuiDir)/frontend/data/updater/generated"; } _artifactAppliedDir() { echo "$(_artifactGenDir)/applied"; } _artifactRecordFile() { echo "$(_artifactAppliedDir)/$1.json"; } # $1=id @@ -116,7 +116,7 @@ _artifactResolve() { if [[ ! "$_ART_APP" =~ ^[a-z0-9][a-z0-9_]{0,31}$ ]]; then isError "artifact: app artifact '$id' has a missing/unsafe applies_when.app slug -- refusing."; return 1 fi - elif [[ "$_ART_SCOPE" == "app" && ! -d "${containers_dir%/}/$_ART_APP" ]]; then + elif [[ "$_ART_SCOPE" == "app" && ! -d "$(appDir "$_ART_APP")" ]]; then isNotice "artifact: '$id' targets '$_ART_APP' which is not installed -- not applicable."; return 2 fi @@ -210,7 +210,7 @@ _artifactPathAllowed() { [[ "$path" =~ ^[A-Za-z0-9._/@:+-]+$ ]] || return 1 real="$(realpath -m -- "$path" 2>/dev/null)"; [[ -n "$real" ]] || return 1 if [[ "$scope" == "app" ]]; then - root="$(realpath -m -- "${containers_dir%/}/$app" 2>/dev/null)" + root="$(realpath -m -- "$(appDir "$app")" 2>/dev/null)" [[ "$real" == "$root/"* ]] && return 0 return 1 fi @@ -221,7 +221,7 @@ _artifactPathAllowed() { # current image of an app's compose (first image: line), quotes/comment stripped. _artifactComposeImage() { - local app="$1" f="${containers_dir%/}/$1/docker-compose.yml" + local app="$1" f="$(appDir "$1")/docker-compose.yml" [[ -f "$f" ]] || return 1 grep -m1 -E '^\s*image:' "$f" 2>/dev/null | sed -E 's/^\s*image:\s*//; s/["'"'"']//g; s/\s+#.*$//; s/\s+$//' } @@ -320,7 +320,7 @@ _artifactBundleOwner() { _artifactApplyBundleFlow() { local id="$1" art="$2" serial="$3" local slug="$_ART_APP" - local live_dir="${containers_dir%/}/$slug" + local live_dir="$(appDir "$slug")" local def_root="${install_containers_dir%/}" local def_dir="$def_root/$slug" @@ -388,7 +388,7 @@ _artifactApplyBundleFlow() { if [[ "$placed" == "1" ]]; then isNotice "Definition placed -- refreshing the App Center catalog..." { declare -F lpRegenWebui >/dev/null 2>&1 && lpRegenWebui force >/dev/null 2>&1; } || true - local apps_json="${containers_dir%/}/libreportal/frontend/data/apps/generated/apps.json" + local apps_json="$(webuiDir)/frontend/data/apps/generated/apps.json" jq -e --arg app "$slug" '.apps[]? | select((.command // "") | endswith(" " + $app))' "$apps_json" >/dev/null 2>&1 || placed=0 fi @@ -457,7 +457,7 @@ _artifactOpPrecheck() { image="$(jq -r '.image // empty' <<<"$op_json")" { _artifactSafeScalar "$from" && _artifactSafeScalar "$image"; } || { isError "op set-compose-image: image/from has an unsafe character"; return 1; } [[ "$image" =~ ^[A-Za-z0-9._:/@-]+$ ]] || { isError "op set-compose-image: '$image' is not a valid image reference"; return 1; } - [[ -d "${containers_dir%/}/$app" ]] || { isError "op set-compose-image: app '$app' not installed"; return 1; } + [[ -d "$(appDir "$app")" ]] || { isError "op set-compose-image: app '$app' not installed"; return 1; } cur="$(_artifactComposeImage "$app")" [[ -n "$from" && "$cur" == "$from" ]] || { isError "op set-compose-image: image is '$cur', expected '$from' (drift)"; return 1; } ;; @@ -511,7 +511,7 @@ _artifactOpApply() { set-compose-image) local image cur f esc_img image="$(jq -r '.image' <<<"$op_json")" - f="${containers_dir%/}/$app/docker-compose.yml" + f="$(appDir "$app")/docker-compose.yml" cur="$(_artifactComposeImage "$app")" esc_img="$(printf '%s' "$image" | sed -e 's/[\/&]/\\&/g')" runFileOp sed -i "0,/^\([[:space:]]*\)image:.*/s//\1image: $esc_img/" "$f" || return 1 @@ -547,7 +547,7 @@ _artifactOpUndo() { case "$op" in set-compose-image) local image f esc_img; image="$(jq -r '.image' <<<"$op_json")" - f="${containers_dir%/}/$app/docker-compose.yml" + f="$(appDir "$app")/docker-compose.yml" esc_img="$(printf '%s' "$image" | sed -e 's/[\/&]/\\&/g')" runFileOp sed -i "0,/^\([[:space:]]*\)image:.*/s//\1image: $esc_img/" "$f" || return 1 ;; @@ -567,7 +567,7 @@ _artifactOpUndo() { local slug live def slug="$(jq -r '.app // empty' <<<"$op_json")" [[ "$slug" =~ ^[a-z0-9][a-z0-9_]{0,31}$ ]] || { isError "undo: unsafe app slug '$slug'"; return 1; } - live="${containers_dir%/}/$slug"; def="${install_containers_dir%/}/$slug" + live="$(appDir "$slug")"; def="${install_containers_dir%/}/$slug" if [[ -d "$live" ]]; then isError "undo: app '$slug' is INSTALLED -- uninstall it before removing its definition."; return 1 fi @@ -845,7 +845,7 @@ artifactApplyAuto() { # skip if app-scoped but the app isn't installed (applicable gate; full # gates re-checked at apply time) app="$(jq -r '.applies_when.app // empty' <<<"$art")" - [[ -n "$app" && ! -d "${containers_dir%/}/$app" ]] && continue + [[ -n "$app" && ! -d "$(appDir "$app")" ]] && continue # --detach: this runs inside the task processor's own poll, so following # the new task in the foreground would wait for a task the (single- # threaded) processor can't start until we return. diff --git a/scripts/cli/commands/artifact/cli_artifact_commands.sh b/scripts/cli/commands/artifact/cli_artifact_commands.sh index 40dd035..159eb48 100644 --- a/scripts/cli/commands/artifact/cli_artifact_commands.sh +++ b/scripts/cli/commands/artifact/cli_artifact_commands.sh @@ -73,7 +73,7 @@ cliHandleArtifactCommands() artifactListApplied() { isHeader "Applied hotfixes" - local dir; dir="${containers_dir%/}/libreportal/frontend/data/updater/generated/applied" + local dir; dir="$(webuiDir)/frontend/data/updater/generated/applied" if ! compgen -G "$dir/*.json" >/dev/null 2>&1; then isSuccessful "0 hotfixes applied." return 0 diff --git a/scripts/cli/commands/notify/cli_notify_commands.sh b/scripts/cli/commands/notify/cli_notify_commands.sh index 802419f..acf2c17 100644 --- a/scripts/cli/commands/notify/cli_notify_commands.sh +++ b/scripts/cli/commands/notify/cli_notify_commands.sh @@ -186,7 +186,7 @@ lpNotifyTaskResult() [[ "$events" == "off" ]] && return 0 lpNotifyAnyChannelEnabled || return 0 - local f="${containers_dir%/}/libreportal/frontend/data/tasks/${task_id}.json" + local f="$(webuiDir)/frontend/data/tasks/${task_id}.json" [[ -f "$f" ]] || return 0 local status type app exit_code if command -v jq >/dev/null 2>&1; then diff --git a/scripts/cli/commands/updater/cli_updater_auto.sh b/scripts/cli/commands/updater/cli_updater_auto.sh index de56613..e8dad49 100644 --- a/scripts/cli/commands/updater/cli_updater_auto.sh +++ b/scripts/cli/commands/updater/cli_updater_auto.sh @@ -30,7 +30,7 @@ # build changes the digest and is attempted normally; the Update button # stays available for a manual retry of the skipped one. -_updaterAutoGenDir() { echo "${containers_dir%/}/libreportal/frontend/data/updater/generated"; } +_updaterAutoGenDir() { echo "$(webuiDir)/frontend/data/updater/generated"; } _updaterAutoDir() { echo "$(_updaterAutoGenDir)/auto"; } _updaterAutoStamp() { echo "$(_updaterAutoDir)/$1.digest"; } # $1=app @@ -84,7 +84,7 @@ updaterAppPolicy() # this far (update available + policy auto). updaterAutoTaskPending() { - local app="$1" dir="${containers_dir%/}/libreportal/frontend/data/tasks" + local app="$1" dir="$(webuiDir)/frontend/data/tasks" command -v jq >/dev/null 2>&1 || return 1 local files=( "$dir"/task_*.json ) [[ -e "${files[0]}" ]] || return 1 diff --git a/scripts/cli/commands/updater/cli_updater_commands.sh b/scripts/cli/commands/updater/cli_updater_commands.sh index f6d4272..f09080f 100644 --- a/scripts/cli/commands/updater/cli_updater_commands.sh +++ b/scripts/cli/commands/updater/cli_updater_commands.sh @@ -35,7 +35,7 @@ cliHandleUpdaterCommands() local scan_interval="${CFG_UPDATER_SCAN_INTERVAL:-30}" [[ "$scan_interval" =~ ^[0-9]+$ ]] || scan_interval=30 (( scan_interval == 0 )) && return 0 - local scan_file="${containers_dir%/}/libreportal/frontend/data/updater/generated/updates.json" + local scan_file="$(webuiDir)/frontend/data/updater/generated/updates.json" if [[ -f "$scan_file" ]]; then local _now _last; _now=$(date +%s); _last=$(stat -c '%Y' "$scan_file" 2>/dev/null || echo 0) (( _now - _last < scan_interval * 60 )) && return 0 @@ -174,7 +174,7 @@ updaterRefDigest() updaterSetAnchorRef() { local app="$1" newref="$2" - local compose="${containers_dir%/}/$app/docker-compose.yml" + local compose="$(appDir "$app")/docker-compose.yml" [ -f "$compose" ] || return 1 local tmp; tmp="$(mktemp)" awk -v s="${app//_/-}-service" -v ref="$newref" ' @@ -215,7 +215,7 @@ updaterApplyApp() # "manual" when a person pressed Update. Recorded in History; changes nothing # about how the update is applied — both take the snapshot, both can roll back. local trigger="${2:-manual}" - local app_dir="$containers_dir/$_upd_app" + local app_dir="$(appDir "$_upd_app")" if [[ ! -d "$app_dir" ]]; then isError "App '$_upd_app' is not installed."; return 1; fi if [[ "$trigger" == "auto" ]]; then @@ -317,7 +317,7 @@ updaterRollbackApp() # explicitly first to actually pick up a new image. updaterComposePull() { - local app="$1" dir="${containers_dir%/}/$1" + local app="$1" dir="$(appDir "$1")" [ -d "$dir" ] || return 1 if [[ "$CFG_DOCKER_INSTALL_TYPE" == "rootless" ]]; then dockerCommandRunInstallUser "cd $dir && docker compose pull" >/dev/null 2>&1 diff --git a/scripts/cli/commands/updater/cli_updater_upgrade.sh b/scripts/cli/commands/updater/cli_updater_upgrade.sh index b617c55..ccf4947 100644 --- a/scripts/cli/commands/updater/cli_updater_upgrade.sh +++ b/scripts/cli/commands/updater/cli_updater_upgrade.sh @@ -25,7 +25,7 @@ # after reading release notes. The updater surfaces "34 available"; this runs # only when asked. -_updaterUpgradeGenDir() { echo "${containers_dir%/}/libreportal/frontend/data/updater/generated"; } +_updaterUpgradeGenDir() { echo "$(webuiDir)/frontend/data/updater/generated"; } # Rewrite the anchor image AND every image locked in step with it, plus their # version sentinels, so the live compose stays self-consistent. @@ -56,7 +56,7 @@ _updaterUpgradeGenDir() { echo "${containers_dir%/}/libreportal/frontend/data/up # return before this point. updaterSetAnchorVersion() { local app="$1" newtag="$2" - local compose="${containers_dir%/}/$app/docker-compose.yml" + local compose="$(appDir "$app")/docker-compose.yml" [ -f "$compose" ] || return 1 local up; up="$(printf '%s' "$app" | tr '[:lower:]' '[:upper:]')" @@ -118,7 +118,7 @@ updaterSetAnchorVersion() { # Current anchor tag for an app, straight from its live compose. updaterCurrentTag() { local app="$1" - local compose="${containers_dir%/}/$app/docker-compose.yml" + local compose="$(appDir "$app")/docker-compose.yml" [ -f "$compose" ] || return 1 updaterTagOf "$(updaterPrimaryImage "$app" "$compose")" } @@ -129,7 +129,7 @@ updaterUpgradeApp() { local app="$1" target="${2:-}" mode="${3:-}" [ "$target" = "--dry-run" ] && { mode="--dry-run"; target=""; } - local app_dir="${containers_dir%/}/$app" + local app_dir="$(appDir "$app")" [ -d "$app_dir" ] || { isError "App '$app' is not installed."; return 1; } local cur; cur="$(updaterCurrentTag "$app")" @@ -233,7 +233,7 @@ _updaterUpgradePruneImages() { (( ${#climbed[@]} >= 2 )) || return 0 # one step: previous IS the rollback target local anchor repo - anchor="$(updaterPrimaryImage "$app" "${containers_dir%/}/$app/docker-compose.yml")" + anchor="$(updaterPrimaryImage "$app" "$(appDir "$app")/docker-compose.yml")" repo="$(updaterRepoTag "$anchor")"; repo="${repo%:*}" # Everything we moved off, minus the last one (kept for rollback). diff --git a/scripts/cli/commands/updater/cli_updater_verify.sh b/scripts/cli/commands/updater/cli_updater_verify.sh index dd7f6de..c09d3d6 100644 --- a/scripts/cli/commands/updater/cli_updater_verify.sh +++ b/scripts/cli/commands/updater/cli_updater_verify.sh @@ -34,7 +34,7 @@ # most need verifying. Falls back to the convention when there is no compose. _updaterPrimaryContainer() { local app="$1" - local compose="${containers_dir%/}/$app/docker-compose.yml" + local compose="$(appDir "$app")/docker-compose.yml" local fallback; fallback="$(printf '%s-service' "${app//_/-}")" [ -f "$compose" ] || { printf '%s' "$fallback"; return 0; } diff --git a/scripts/config/application/application_edit_config.sh b/scripts/config/application/application_edit_config.sh index 061f234..272bddb 100755 --- a/scripts/config/application/application_edit_config.sh +++ b/scripts/config/application/application_edit_config.sh @@ -12,7 +12,7 @@ editAppConfig() fi # Use find to search for the app_name folder within $containers_dir - local app_dir=$containers_dir$app_name + local app_dir=$(appDir "$app_name") if [ -n "$app_dir" ]; then local config_file="$app_dir/$app_name.config" diff --git a/scripts/config/application/application_menu_apps.sh b/scripts/config/application/application_menu_apps.sh index 0e02ae0..40cfb98 100755 --- a/scripts/config/application/application_menu_apps.sh +++ b/scripts/config/application/application_menu_apps.sh @@ -7,12 +7,12 @@ viewAppConfigs() # Get all installed apps from containers directory local installed_apps=() - for app_dir in "$containers_dir"/*/; do + while IFS= read -r app_dir; do if [ -d "$app_dir" ]; then local app_name=$(basename "$app_dir") installed_apps+=("$app_name") fi - done + done < <(storageAppDirs) if [ ${#installed_apps[@]} -eq 0 ]; then isNotice "No installed applications found." @@ -47,7 +47,7 @@ viewAppConfigs() local selected_app="${installed_apps[index]}" # Get the config file for this app - local config_file="$containers_dir/${selected_app}/${selected_app}.config" + local config_file="$(appDir "$selected_app")/${selected_app}.config" if [ -f "$config_file" ]; then $CFG_TEXT_EDITOR "$config_file" diff --git a/scripts/config/application/application_menu_category.sh b/scripts/config/application/application_menu_category.sh index ca2446f..6d89c6c 100755 --- a/scripts/config/application/application_menu_category.sh +++ b/scripts/config/application/application_menu_category.sh @@ -12,7 +12,7 @@ viewAppCategoryConfigs() local other_apps=() # Collect all app_name folders and categorize them into installed and others - for app_dir in "$containers_dir"/*/; do + while IFS= read -r app_dir; do if [ -d "$app_dir" ]; then local app_name=$(basename "$app_dir") local app_config_file="$install_containers_dir$app_name/$app_name.sh" @@ -30,7 +30,7 @@ viewAppCategoryConfigs() fi fi fi - done + done < <(storageAppDirs) if [[ ${#installed_apps[@]} -eq 0 && ${#other_apps[@]} -eq 0 ]]; then echo "" diff --git a/scripts/config/application/application_missing_variables.sh b/scripts/config/application/application_missing_variables.sh index b69779a..0c8a71e 100755 --- a/scripts/config/application/application_missing_variables.sh +++ b/scripts/config/application/application_missing_variables.sh @@ -9,12 +9,12 @@ checkApplicationsConfigFilesMissingVariables() app=$(basename "$live" .config) remote="$install_containers_dir$app/$app.config" reconcileConfigFile "$live" "$remote" - # 2>/dev/null on the find: app data dirs (e.g. invidious/postgresdata, - # nextcloud/html) are owned by container sub-UIDs that the manager user - # can't read into, and they're harmless permission-denied chatter — find - # at this maxdepth doesn't need to enter them to satisfy -name '*.config' - # at depth 2. Keep the noise out of CLI/log output. - done < <(runFileOp find "$containers_dir" -maxdepth 2 -type f -name '*.config' ! -name '*.bak' 2>/dev/null) + # storageAppConfigs walks every storage root as the container user and + # yields only /.config, so app data dirs owned by container + # sub-UIDs (invidious/postgresdata, nextcloud/html) are never descended + # into — no permission-denied chatter, and no payload *.config mistaken + # for an app. + done < <(storageAppConfigs) isSuccessful "Application config reconciliation completed." } diff --git a/scripts/config/core/config_file_setup_data.sh b/scripts/config/core/config_file_setup_data.sh index a4bddf1..8f68195 100755 --- a/scripts/config/core/config_file_setup_data.sh +++ b/scripts/config/core/config_file_setup_data.sh @@ -11,9 +11,9 @@ configSetupFileWithData() fi if [[ $custom_path == "" ]]; then - local file_path="$containers_dir$app_name" + local file_path="$(appDir "$app_name")" elif [[ $custom_path != "" ]]; then - local file_path="$containers_dir$app_name/$custom_path/" + local file_path="$(appDir "$app_name")/$custom_path/" fi local full_file_path="$file_path/$file_name" diff --git a/scripts/config/core/config_find_file.sh b/scripts/config/core/config_find_file.sh index 2b18b2c..ebef97e 100755 --- a/scripts/config/core/config_find_file.sh +++ b/scripts/config/core/config_find_file.sh @@ -34,7 +34,7 @@ findConfigFileForOption() echo "$app_config" return 0 fi - done < <(runFileOp find "${containers_dir%/}" -mindepth 2 -maxdepth 2 -type f -name '*.config' 2>/dev/null) + done < <(storageAppConfigs) fi return 1 } diff --git a/scripts/config/docker/config_backfill_keys.sh b/scripts/config/docker/config_backfill_keys.sh index b0c5c72..02fcf63 100644 --- a/scripts/config/docker/config_backfill_keys.sh +++ b/scripts/config/docker/config_backfill_keys.sh @@ -129,7 +129,7 @@ configBackfillAllApps() [[ "$app" == "template" ]] && continue template="${dir}${app}.config" - deployed="${containers_dir}${app}/${app}.config" + deployed="$(appDir "$app")/${app}.config" [[ -f "$template" ]] || continue runFileOp test -f "$deployed" || continue diff --git a/scripts/config/docker/docker_compose_menu.sh b/scripts/config/docker/docker_compose_menu.sh index 8001567..259ce3b 100755 --- a/scripts/config/docker/docker_compose_menu.sh +++ b/scripts/config/docker/docker_compose_menu.sh @@ -11,13 +11,13 @@ viewComposeFiles() echo "" # Find all subdirectories under $containers_dir - for app_dir in "$containers_dir"/*/; do + while IFS= read -r app_dir; do if [[ -d "$app_dir" ]]; then # Extract the app name (folder name) local app_name=$(basename "$app_dir") local app_names+=("$app_name") fi - done + done < <(storageAppDirs) # Check if any apps were found if [ ${#app_names[@]} -eq 0 ]; then @@ -41,7 +41,7 @@ viewComposeFiles() # Check if the selected option is a valid number if ((selected_option >= 1 && selected_option <= ${#app_names[@]})); then local selected_app="${app_names[selected_option - 1]}" - local selected_app_dir="$containers_dir/$selected_app" + local selected_app_dir="$(appDir "$selected_app")" # List Docker Compose files in the selected app's folder echo "" diff --git a/scripts/config/docker/docker_config_setup_data.sh b/scripts/config/docker/docker_config_setup_data.sh index 08a3c41..3903861 100755 --- a/scripts/config/docker/docker_config_setup_data.sh +++ b/scripts/config/docker/docker_config_setup_data.sh @@ -10,7 +10,7 @@ dockerConfigSetupFileWithData() local file_name="docker-compose.$app_name.yml"; fi - local file_path="$containers_dir$app_name" + local file_path="$(appDir "$app_name")" local full_file_path="$file_path/$file_name" if command -v sqlite3 &> /dev/null && [[ -f "$docker_dir/$db_file" ]]; then diff --git a/scripts/config/docker/docker_config_to_container.sh b/scripts/config/docker/docker_config_to_container.sh index 8321be7..d659e91 100755 --- a/scripts/config/docker/docker_config_to_container.sh +++ b/scripts/config/docker/docker_config_to_container.sh @@ -7,7 +7,7 @@ dockerConfigSetupToContainer() local flags="$3" local config_overrides="$4" - local target_path="$containers_dir$app_name" + local target_path="$(appDir "$app_name")" local source_file="$install_containers_dir$app_name/$app_name.config" local config_file="$app_name.config" diff --git a/scripts/config/password/bcrypt/password_export_bcrypt.sh b/scripts/config/password/bcrypt/password_export_bcrypt.sh index 111c216..3db847a 100755 --- a/scripts/config/password/bcrypt/password_export_bcrypt.sh +++ b/scripts/config/password/bcrypt/password_export_bcrypt.sh @@ -7,7 +7,7 @@ exportBcryptPassword() local placeholder="$2" local raw_password="$3" local file="$4" # File where the placeholder was found - local log_file="$containers_dir/bcrypt.txt" + local log_file="$(primaryRoot)/bcrypt.txt" # bcrypt.txt lives under containers_dir (docker-install-owned) -> runFileOp. if [ ! -f "$log_file" ]; then diff --git a/scripts/config/password/bcrypt/password_retreive_bcrypt.sh b/scripts/config/password/bcrypt/password_retreive_bcrypt.sh index 079368d..a3b3de4 100755 --- a/scripts/config/password/bcrypt/password_retreive_bcrypt.sh +++ b/scripts/config/password/bcrypt/password_retreive_bcrypt.sh @@ -4,7 +4,7 @@ getStoredPassword() { local app_name="$1" local variable_name="$2" - local log_file="$containers_dir/bcrypt.txt" + local log_file="$(primaryRoot)/bcrypt.txt" if [ -f "$log_file" ]; then runFileOp grep "^$app_name $variable_name " "$log_file" | awk '{print $3}' | tail -n 1 diff --git a/scripts/database/app/db_app_scan.sh b/scripts/database/app/db_app_scan.sh index 4c2de4d..9f749aa 100755 --- a/scripts/database/app/db_app_scan.sh +++ b/scripts/database/app/db_app_scan.sh @@ -19,8 +19,13 @@ databaseAppScan() checkSuccess "Install path not found or not a directory: $containers_dir" fi - # Scan the folder and retrieve folder names - local folder_names=$(runFileOp find "$containers_dir" -mindepth 1 -maxdepth 1 -type d -exec basename {} \;) + # Scan every storage root. storageApps SKIPS a root whose drive is absent, + # so an app on an unplugged disk is simply missing from this list — which is + # why every "the folder is gone" branch below must first ask + # appStorageAvailable before it deletes anything. Without that guard, + # unplugging a drive would delete the database rows and port allocations of + # every app living on it. + local folder_names=$(storageApps) # Check if no folders are found if [ -z "$folder_names" ]; then @@ -38,8 +43,8 @@ databaseAppScan() while IFS='|' read -r folder_name status uninstall_date; do if [[ -n "$folder_name" ]]; then existing_folder_names+=("$folder_name") - # Check if the folder exists in the containers_dir - if [ -d "$containers_dir/$folder_name" ]; then + # Check if the folder exists on whichever storage root holds it + if [ -d "$(appDir "$folder_name")" ]; then if (( status == 0 )); then isNotice "The folder for $folder_name has been found." # Update the database to set the status to 1 (installed) and unset the uninstall_date @@ -51,8 +56,8 @@ databaseAppScan() fi done <<< "$existing_folders" - # Loop through immediate subdirectories of $containers_dir - for app_dir in "$containers_dir"/*/; do + # Loop through every app directory on every available storage root + while IFS= read -r app_dir; do # Get the app name from the folder name local app_name=$(basename "$app_dir") @@ -77,21 +82,27 @@ databaseAppScan() ((updated_count++)) # Increment updated_count fi fi - done + done < <(storageAppDirs) # Create an array to store folder names that should be removed from the database local folders_to_remove=() # Get a list of folder names that exist in the database but not in the current folder structure for folder_name in "${existing_folder_names[@]}"; do - if [ ! -d "$containers_dir/$folder_name" ]; then + # Not gone — just on a drive that is not attached. Leave it alone. + appStorageAvailable "$folder_name" || continue + if [ ! -d "$(appDir "$folder_name")" ]; then local folders_to_remove+=("$folder_name") fi done # Get a list of folder names that exist in the database but not in the current folder structure for folder_name in "${existing_folder_names[@]}"; do - if [ ! -d "$containers_dir/$folder_name" ]; then + if ! appStorageAvailable "$folder_name"; then + isNotice "Skipping $folder_name — its storage location is not mounted." + continue + fi + if [ ! -d "$(appDir "$folder_name")" ]; then # Check if this folder is actually associated with an entry in the database if [[ " ${folder_names[@]} " =~ " $folder_name " ]]; then isNotice "Folder $folder_name no longer exists. Removing from the Database." @@ -119,7 +130,7 @@ databaseAppScan() # when there was no data to wipe. for folder_name in $folder_names; do [[ "$folder_name" == "libreportal" ]] && continue - local folder_path="$containers_dir/$folder_name" + local folder_path="$(appDir "$folder_name")" if [ ! -d "$folder_path" ]; then isNotice "Folder $folder_name no longer exists — removing it from the database." diff --git a/scripts/database/app/db_list_installed_app.sh b/scripts/database/app/db_list_installed_app.sh index 5ef20c8..4831fca 100755 --- a/scripts/database/app/db_list_installed_app.sh +++ b/scripts/database/app/db_list_installed_app.sh @@ -104,7 +104,7 @@ databaseListInstalledApp() # row by matching service+name, then extract its url_path # (empty when the port has no web UI, e.g. DNS). local _port_url_path="" - local _url_cfg="${containers_dir}/${app_name}/${app_name}.config" + local _url_cfg="$(appDir "$app_name")/${app_name}.config" [[ ! -f "$_url_cfg" ]] && _url_cfg="${install_containers_dir}/${app_name}/${app_name}.config" if [[ -f "$_url_cfg" ]]; then while IFS='=' read -r _vn _vv || [[ -n "$_vn" ]]; do diff --git a/scripts/docker/app/compose/down_all.sh b/scripts/docker/app/compose/down_all.sh index c52e22f..5278ccd 100755 --- a/scripts/docker/app/compose/down_all.sh +++ b/scripts/docker/app/compose/down_all.sh @@ -6,7 +6,7 @@ dockerComposeDownAllApps() # runFileOp: containers/ is owned by the container user (dockerinstall, 751) # under rootless and isn't list-readable by the manager — enumerate as the # owner or this silently finds nothing and no apps get brought down. - local subdirectories=($(runFileOp find "$containers_dir" -mindepth 1 -maxdepth 1 -type d)) + local subdirectories=($(storageAppDirs)) for dir in "${subdirectories[@]}"; do local app_name=$(basename "$dir") diff --git a/scripts/docker/app/compose/down_app.sh b/scripts/docker/app/compose/down_app.sh index a43531b..09047df 100755 --- a/scripts/docker/app/compose/down_app.sh +++ b/scripts/docker/app/compose/down_app.sh @@ -31,7 +31,7 @@ dockerComposeDown() fi if [[ "$OS_TYPE" == "Ubuntu" || "$OS_TYPE" == "Debian" ]]; then - if [ ! -f "$containers_dir$app_name/$compose_file" ]; then + if [ ! -f "$(appDir "$app_name")/$compose_file" ]; then isNotice "Unable to find the compose file to docker compose down this application." return 1 fi @@ -41,10 +41,10 @@ dockerComposeDown() # header is never left without output. local mode="${type:-$CFG_DOCKER_INSTALL_TYPE}" if [[ $mode == "rootless" ]]; then - local result; result=$(dockerCommandRunInstallUser "cd $containers_dir$app_name && docker compose $setup_compose down" >/dev/null 2>&1) + local result; result=$(dockerCommandRunInstallUser "cd $(appDir "$app_name") && docker compose $setup_compose down" >/dev/null 2>&1) checkSuccess "Shutting down container for $app_name" elif [[ $mode == "rooted" ]]; then - local result; result=$(cd "$containers_dir$app_name" && docker compose $setup_compose down >/dev/null 2>&1) + local result; result=$(cd "$(appDir "$app_name")" && docker compose $setup_compose down >/dev/null 2>&1) checkSuccess "Shutting down container for $app_name" else isNotice "Unknown Docker install type '$mode' — cannot shut down $app_name." diff --git a/scripts/docker/app/compose/up_all.sh b/scripts/docker/app/compose/up_all.sh index e5b749b..5bf8052 100755 --- a/scripts/docker/app/compose/up_all.sh +++ b/scripts/docker/app/compose/up_all.sh @@ -6,7 +6,7 @@ dockerComposeUpAllApps() # runFileOp: containers/ is owned by the container user (dockerinstall, 751) # under rootless and isn't list-readable by the manager — enumerate as the # owner or this silently finds nothing and no apps come up. - local subdirectories=($(runFileOp find "$containers_dir" -mindepth 1 -maxdepth 1 -type d)) + local subdirectories=($(storageAppDirs)) for dir in "${subdirectories[@]}"; do local app_name=$(basename "$dir") diff --git a/scripts/docker/app/compose/up_app.sh b/scripts/docker/app/compose/up_app.sh index 7e8384c..21fd5a3 100755 --- a/scripts/docker/app/compose/up_app.sh +++ b/scripts/docker/app/compose/up_app.sh @@ -57,7 +57,7 @@ dockerComposeUp() fi if [[ "$OS_TYPE" == "Ubuntu" || "$OS_TYPE" == "Debian" ]]; then - if [ -f "$containers_dir$app_name/$compose_file" ]; then + if [ -f "$(appDir "$app_name")/$compose_file" ]; then # Quiet pull + plain progress so progress redraws don't flood the log. local _compose_quiet="--quiet-pull" export COMPOSE_PROGRESS=plain @@ -66,7 +66,7 @@ dockerComposeUp() # ignores any edits to Dockerfile / source between installs. local _compose_build_flag="" local _is_local_build=0 - if [[ -f "$containers_dir$app_name/Dockerfile" ]]; then + if [[ -f "$(appDir "$app_name")/Dockerfile" ]]; then _compose_build_flag="--build" _is_local_build=1 fi @@ -103,7 +103,7 @@ dockerComposeUp() } ' "$1" } - local _compose_path="$containers_dir$app_name/$compose_file" + local _compose_path="$(appDir "$app_name")/$compose_file" local _stale_tags _stale_tags=$(_scanStaleTags "$_compose_path") if [[ -n "$_stale_tags" ]]; then @@ -141,7 +141,7 @@ dockerComposeUp() fi if [[ $CFG_DOCKER_INSTALL_TYPE == "rootless" ]]; then isNotice "Starting container for $app_name, this may take a while..." - local result; result=$(dockerCommandRunInstallUser "cd $containers_dir$app_name && COMPOSE_PROGRESS=plain docker compose $setup_compose up $_compose_quiet $_compose_build_flag -d" 2>&1) + local result; result=$(dockerCommandRunInstallUser "cd $(appDir "$app_name") && COMPOSE_PROGRESS=plain docker compose $setup_compose up $_compose_quiet $_compose_build_flag -d" 2>&1) _rc=$? _upReportComposeFailure "$app_name" "$_rc" "$result" # Restore $? to the compose exit code — a bare `checkSuccess` @@ -150,7 +150,7 @@ dockerComposeUp() ( exit "$_rc" ); checkSuccess "Started container for $app_name" elif [[ $CFG_DOCKER_INSTALL_TYPE == "rooted" ]]; then isNotice "Starting container for $app_name, this may take a while..." - local result; result=$(cd "$containers_dir$app_name" && COMPOSE_PROGRESS=plain docker compose $setup_compose up $_compose_quiet $_compose_build_flag -d 2>&1) + local result; result=$(cd "$(appDir "$app_name")" && COMPOSE_PROGRESS=plain docker compose $setup_compose up $_compose_quiet $_compose_build_flag -d 2>&1) _rc=$? _upReportComposeFailure "$app_name" "$_rc" "$result" ( exit "$_rc" ); checkSuccess "Started container for $app_name" @@ -158,10 +158,10 @@ dockerComposeUp() # Used for the CLI dockertype switcher. else if [[ $type == "rootless" ]]; then - local result; result=$(dockerCommandRunInstallUser "cd $containers_dir$app_name && docker compose $setup_compose down"); _rc=$? + local result; result=$(dockerCommandRunInstallUser "cd $(appDir "$app_name") && docker compose $setup_compose down"); _rc=$? checkSuccess "Shutting down container for $app_name" elif [[ $type == "rooted" ]]; then - local result; result=$(cd "$containers_dir$app_name" && docker compose $setup_compose down); _rc=$? + local result; result=$(cd "$(appDir "$app_name")" && docker compose $setup_compose down); _rc=$? checkSuccess "Shutting down container for $app_name" fi fi diff --git a/scripts/docker/app/docker/restart_app.sh b/scripts/docker/app/docker/restart_app.sh index 02a031e..e91d6b0 100755 --- a/scripts/docker/app/docker/restart_app.sh +++ b/scripts/docker/app/docker/restart_app.sh @@ -20,7 +20,7 @@ dockerRestartApp() isError "Invalid service name: $service_name" return 1 fi - local app_dir="${containers_dir%/}/$app_name" + local app_dir="$(appDir "$app_name")" if [[ ! -f "$app_dir/docker-compose.yml" ]]; then isError "No compose file for '$app_name' at $app_dir/docker-compose.yml" return 1 diff --git a/scripts/docker/app/uninstall/down_remove_app.sh b/scripts/docker/app/uninstall/down_remove_app.sh index 27a68e6..b155089 100755 --- a/scripts/docker/app/uninstall/down_remove_app.sh +++ b/scripts/docker/app/uninstall/down_remove_app.sh @@ -21,12 +21,12 @@ dockerComposeDownRemove() return 1 fi - if [[ -d "$containers_dir$app_name" ]]; then + if [[ -d "$(appDir "$app_name")" ]]; then isNotice "Shutting down & removing all $app_name container data" if [[ "$CFG_DOCKER_INSTALL_TYPE" == "rootless" ]]; then - dockerCommandRunInstallUser "cd $containers_dir$app_name && docker compose down -v --remove-orphans" >/dev/null 2>&1 + dockerCommandRunInstallUser "cd $(appDir "$app_name") && docker compose down -v --remove-orphans" >/dev/null 2>&1 else - (cd "$containers_dir$app_name" && docker compose down -v --remove-orphans) >/dev/null 2>&1 + (cd "$(appDir "$app_name")" && docker compose down -v --remove-orphans) >/dev/null 2>&1 fi else isNotice "App directory '$app_name' not found — falling back to name-based container cleanup." diff --git a/scripts/docker/app/uninstall/remove_images.sh b/scripts/docker/app/uninstall/remove_images.sh index 1d5e2df..933dc02 100644 --- a/scripts/docker/app/uninstall/remove_images.sh +++ b/scripts/docker/app/uninstall/remove_images.sh @@ -11,7 +11,7 @@ dockerRemoveAppImages() isNotice "Removing Docker images for '$app_name'. Please wait..." - local compose_dir="$containers_dir$app_name" + local compose_dir="$(appDir "$app_name")" local compose_file="$compose_dir/docker-compose.yml" local compose_images="" diff --git a/scripts/docker/compose/copy_build_context.sh b/scripts/docker/compose/copy_build_context.sh index 7cb6083..3a47db7 100644 --- a/scripts/docker/compose/copy_build_context.sh +++ b/scripts/docker/compose/copy_build_context.sh @@ -16,7 +16,7 @@ dockerCopyBuildContext() { local app_name="$1" local source_dir="$install_containers_dir$app_name" - local target_dir="$containers_dir$app_name" + local target_dir="$(appDir "$app_name")" if [[ -z "$app_name" ]]; then isError "dockerCopyBuildContext: app_name is empty." diff --git a/scripts/docker/compose/setup_compose_yml.sh b/scripts/docker/compose/setup_compose_yml.sh index 8b462f2..3912301 100755 --- a/scripts/docker/compose/setup_compose_yml.sh +++ b/scripts/docker/compose/setup_compose_yml.sh @@ -28,7 +28,7 @@ dockerComposeSetupFile() local target_compose_file="docker-compose.$app_name.yml"; fi - local target_path="$containers_dir$app_name" + local target_path="$(appDir "$app_name")" local target_file="$target_path/$target_compose_file" diff --git a/scripts/docker/compose/update_compose_yml.sh b/scripts/docker/compose/update_compose_yml.sh index 055bbe9..2767015 100755 --- a/scripts/docker/compose/update_compose_yml.sh +++ b/scripts/docker/compose/update_compose_yml.sh @@ -30,7 +30,7 @@ dockerComposeUpdate() # Fail2ban specifics if [[ "$app_name" == "fail2ban" ]]; then - local jail_local_file="$containers_dir/$app_name/config/$app_name/jail.local" + local jail_local_file="$(appDir "$app_name")/config/$app_name/jail.local" if [ -f "$jail_local_file" ]; then if runFileOp grep -q "ignoreip = ips_whitelist" "$jail_local_file"; then diff --git a/scripts/docker/network/network_conflicts.sh b/scripts/docker/network/network_conflicts.sh index ed3b7a3..df87c09 100644 --- a/scripts/docker/network/network_conflicts.sh +++ b/scripts/docker/network/network_conflicts.sh @@ -26,7 +26,7 @@ # assignment carrying this exact IP means it IS live on the shared net. _netServiceIsRouted() { local app="$1" ip="$2" - local compose="${containers_dir}${app}/docker-compose.yml" + local compose="$(appDir "$app")/docker-compose.yml" [[ -f "$compose" ]] || return 1 # no compose to consult -> don't skip local esc_ip="${ip//./\\.}" grep -Eq "^[[:space:]]*ipv4_address:[[:space:]]*${esc_ip}([[:space:]]|#|$)" "$compose" && return 1 diff --git a/scripts/docker/setup_env.sh b/scripts/docker/setup_env.sh index e05d5aa..2f2c3f6 100755 --- a/scripts/docker/setup_env.sh +++ b/scripts/docker/setup_env.sh @@ -2,6 +2,6 @@ dockerSetupEnvFile() { - local result; result=$(copyFile "loud" $containers_dir$app_name/env.example $containers_dir$app_name/.env $docker_install_user) + local result; result=$(copyFile "loud" $(appDir "$app_name")/env.example $(appDir "$app_name")/.env $docker_install_user) checkSuccess "Setting up .env file to path" } diff --git a/scripts/docker/type_switcher/swap_docker_type.sh b/scripts/docker/type_switcher/swap_docker_type.sh index 4173497..7a4da28 100755 --- a/scripts/docker/type_switcher/swap_docker_type.sh +++ b/scripts/docker/type_switcher/swap_docker_type.sh @@ -152,7 +152,7 @@ switchMigrateBackupApps() # Enumerate under the OLD mode — containers/ is still owned by its container # user here, so runFileOp (now resolved to that user) can list it (the # manager can't list the 751 dockerinstall-owned dir under rootless). - local subdirectories=($(runFileOp find "$containers_dir" -mindepth 1 -maxdepth 1 -type d)) + local subdirectories=($(storageAppDirs)) local failed=() local dir app_name diff --git a/scripts/docker/type_switcher/switch_containers_type.sh b/scripts/docker/type_switcher/switch_containers_type.sh index e1eb91e..e2fba13 100755 --- a/scripts/docker/type_switcher/switch_containers_type.sh +++ b/scripts/docker/type_switcher/switch_containers_type.sh @@ -15,7 +15,7 @@ dockerSwitcherUpdateContainersToDockerType() CFG_DOCKER_INSTALL_TYPE="$old_mode" resolveDockerInstallUser fi - local subdirectories=($(runFileOp find "$containers_dir" -maxdepth 1 -type d)) + local subdirectories=($(storageAppDirs)) CFG_DOCKER_INSTALL_TYPE="$saved_type" resolveDockerInstallUser diff --git a/scripts/function/file/container/backup_files.sh b/scripts/function/file/container/backup_files.sh index cc7f526..a5a2b1f 100755 --- a/scripts/function/file/container/backup_files.sh +++ b/scripts/function/file/container/backup_files.sh @@ -3,7 +3,7 @@ backupContainerFilesToTemp() { local app_name="$1" - local source_folder="$containers_dir$app_name" + local source_folder="$(appDir "$app_name")" temp_backup_folder="temp_$(date +%Y%m%d%H%M%S)_$(tr -dc 'a-zA-Z0-9' < /dev/urandom | head -c 6)" diff --git a/scripts/function/file/container/restore_files.sh b/scripts/function/file/container/restore_files.sh index 32d8205..8fecb97 100755 --- a/scripts/function/file/container/restore_files.sh +++ b/scripts/function/file/container/restore_files.sh @@ -3,7 +3,7 @@ backupContainerFilesRestore() { local app_name="$1" - local source_folder="$containers_dir$app_name" + local source_folder="$(appDir "$app_name")" if [ -d "$temp_backup_folder" ]; then local result; result=$(copyFiles "loud" "$temp_backup_folder" "$source_folder" $docker_install_user) diff --git a/scripts/function/file/copy_resource.sh b/scripts/function/file/copy_resource.sh index 9650b47..15f0cda 100755 --- a/scripts/function/file/copy_resource.sh +++ b/scripts/function/file/copy_resource.sh @@ -13,7 +13,7 @@ copyResource() echo "App folder '$app_name' not found in '$install_containers_dir'." fi - local destination_dir="$containers_dir$app_name" + local destination_dir="$(appDir "$app_name")" if [ -n "$save_path" ]; then local destination_dir="$destination_dir/$save_path" diff --git a/scripts/function/permission/before_start.sh b/scripts/function/permission/before_start.sh index bf7bac9..43bc569 100755 --- a/scripts/function/permission/before_start.sh +++ b/scripts/function/permission/before_start.sh @@ -21,14 +21,14 @@ fixPermissionsBeforeStart() fi # Traefik - if [ -f "${containers_dir}traefik/etc/certs/acme.json" ]; then + if [ -f "$(appDir traefik)/etc/certs/acme.json" ]; then runOwnership app-file traefik etc/certs/acme.json - local result; result=$(runFileOp chmod 600 "${containers_dir}traefik/etc/certs/acme.json") + local result; result=$(runFileOp chmod 600 "$(appDir traefik)/etc/certs/acme.json") checkSuccess "Set permissions to acme.json file for traefik" fi - if [ -f "${containers_dir}traefik/etc/traefik.yml" ]; then + if [ -f "$(appDir traefik)/etc/traefik.yml" ]; then runOwnership app-file traefik etc/traefik.yml - local result; result=$(runFileOp chmod 600 "${containers_dir}traefik/etc/traefik.yml") + local result; result=$(runFileOp chmod 600 "$(appDir traefik)/etc/traefik.yml") checkSuccess "Set permissions to traefik.yml file for traefik" fi } diff --git a/scripts/function/permission/config.sh b/scripts/function/permission/config.sh index b0ea1dd..1e45f20 100755 --- a/scripts/function/permission/config.sh +++ b/scripts/function/permission/config.sh @@ -4,7 +4,7 @@ fixConfigPermissions() { local silent_flag="$1" local app_name="$2" - local config_file="$containers_dir$app_name/$app_name.config" + local config_file="$(appDir "$app_name")/$app_name.config" local result; result=$(runFileOp chmod g+rw $config_file) if [ "$silent_flag" == "loud" ]; then diff --git a/scripts/instance/instance_create.sh b/scripts/instance/instance_create.sh index d1ef5e6..9f554e4 100644 --- a/scripts/instance/instance_create.sh +++ b/scripts/instance/instance_create.sh @@ -430,16 +430,20 @@ _instanceRewriteTools() { sed -i -E "s/\b${type_u}_([A-Z0-9_]*_TAG)\b/${slug_u}_\1/g" "$f" # The app's own deployed directory. Hooks that build it from - # "${containers_dir}/..." instead of "$containers_dir$app_name/..." + # "${containers_dir}/..." instead of "$(appDir "$app_name")/..." # read and WRITE the base app's files — adguard's auth adapter edits - # ${containers_dir}adguard/conf/AdGuardHome.yaml, so an instance would + # $(appDir adguard)/conf/AdGuardHome.yaml, so an instance would # have rewritten the original's config. Anchored on containers_dir so a # bare mention of the app name in prose is left alone. - # Trailing / is NOT required: dashy tests [[ -d "${containers_dir}dashy" ]] - # and gluetun does (cd "${containers_dir}gluetun" && …), both ending at + # Trailing / is NOT required: dashy tests [[ -d "$(appDir dashy)" ]] + # and gluetun does (cd "$(appDir gluetun)" && …), both ending at # the quote. Only the first path component is touched, so a data subdir - # that repeats the app name (${containers_dir}prometheus/prometheus/…) + # that repeats the app name ($(appDir prometheus)/prometheus/…) # keeps its inner segment. + # Post-sweep form: hooks resolve their dir with $(appDir ). + sed -i -E "s@(\\\$\(appDir )${type}(\))@\1${slug}\2@g" "$f" + sed -i -E "s@(\\\$\(appDir \")${type}(\"\))@\1${slug}\2@g" "$f" + # Legacy form, for any hook that still builds the path by hand. sed -i -E "s@(\\\$\{containers_dir\})${type}([^A-Za-z0-9_-]|$)@\1${slug}\2@g" "$f" sed -i -E "s@(\\\$\{install_containers_dir\}/?)${type}([^A-Za-z0-9_-]|$)@\1${slug}\2@g" "$f" @@ -495,7 +499,7 @@ instanceCreate() { local slug="${type}_${id}" local slug_u="${slug^^}" - if [[ -d "${install_containers_dir%/}/$slug" || -d "${containers_dir%/}/$slug" ]]; then + if [[ -d "${install_containers_dir%/}/$slug" || -d "$(appDir "$slug")" ]]; then isError "An app or instance named '$slug' already exists. Pick a different name." return 1 fi diff --git a/scripts/menu/menu_app_uninstall.sh b/scripts/menu/menu_app_uninstall.sh index c3f8521..109a475 100755 --- a/scripts/menu/menu_app_uninstall.sh +++ b/scripts/menu/menu_app_uninstall.sh @@ -10,7 +10,7 @@ appUninstallMenu() { local installed_apps=() # Get all installed apps by checking for docker-compose.yml files - for app_dir in "$containers_dir"/*/; do + while IFS= read -r app_dir; do if [ -d "$app_dir" ]; then local compose_file="$app_dir/docker-compose.yml" if [ -f "$compose_file" ]; then @@ -18,7 +18,7 @@ appUninstallMenu() { installed_apps+=("$app_name") fi fi - done + done < <(storageAppDirs) if [ ${#installed_apps[@]} -eq 0 ]; then isNotice "No installed applications found." diff --git a/scripts/menu/menu_reset_to_menu.sh b/scripts/menu/menu_reset_to_menu.sh index de16247..13b5e84 100755 --- a/scripts/menu/menu_reset_to_menu.sh +++ b/scripts/menu/menu_reset_to_menu.sh @@ -4,7 +4,7 @@ resetToMenu() { # Find all directories in the containers folder and create variables for them if [ -d "$containers_dir" ]; then - for dir in "$containers_dir"/*/; do + while IFS= read -r dir; do if [ -d "$dir" ]; then # Get the directory name and remove any trailing slashes local app_name=$(basename "$dir") @@ -13,7 +13,7 @@ resetToMenu() # Set the variable eval "${app_name}=n" fi - done + done < <(storageAppDirs) fi # Set any additional non-container variables here diff --git a/scripts/migrate/migrate_pre_backup.sh b/scripts/migrate/migrate_pre_backup.sh index 86ba3d3..c7877e4 100644 --- a/scripts/migrate/migrate_pre_backup.sh +++ b/scripts/migrate/migrate_pre_backup.sh @@ -23,7 +23,7 @@ migratePreBackupDestination() fi # Only meaningful if the destination actually has this app installed. - if [[ ! -d "$containers_dir$app" ]]; then + if [[ ! -d "$(appDir "$app")" ]]; then isNotice "No existing $app on destination — pre-migrate backup skipped" migrateEmit phase=pre-backup status=skipped reason=no-existing-app app="$app" return 0 diff --git a/scripts/migrate/migrate_preflight.sh b/scripts/migrate/migrate_preflight.sh index 5dd6bae..b0c6ec7 100644 --- a/scripts/migrate/migrate_preflight.sh +++ b/scripts/migrate/migrate_preflight.sh @@ -95,7 +95,7 @@ migratePreflight() # ---- Destination probe ---------------------------------------------------- local app_installed=false local app_running=false - if [[ -d "$containers_dir$app" ]]; then + if [[ -d "$(appDir "$app")" ]]; then app_installed=true if dockerCommandRun "docker ps --filter name=^${app}\$ --format '{{.Names}}' 2>/dev/null" | grep -q "^$app\$"; then app_running=true @@ -130,7 +130,7 @@ migratePreflight() # Disk free below 2× the destination's existing app folder is concerning. if [[ "$app_installed" == "true" ]]; then local existing_kb - existing_kb=$(du -sk "$containers_dir$app" 2>/dev/null | awk '{print $1}') + existing_kb=$(du -sk "$(appDir "$app")" 2>/dev/null | awk '{print $1}') if [[ -n "$existing_kb" && "$disk_free_kb" -lt $((existing_kb * 2)) ]]; then _migratePreflightAppend warnings "Disk free ($((disk_free_kb / 1024)) MB) is less than 2× existing $app size ($((existing_kb / 1024)) MB)" fi diff --git a/scripts/migrate/migrate_url_rewrite.sh b/scripts/migrate/migrate_url_rewrite.sh index cfe155d..704adb4 100644 --- a/scripts/migrate/migrate_url_rewrite.sh +++ b/scripts/migrate/migrate_url_rewrite.sh @@ -57,7 +57,7 @@ migrateApplyUrlRewrite() fi local template="$install_containers_dir$app/$app.config" - local deployed="$containers_dir$app/$app.config" + local deployed="$(appDir "$app")/$app.config" if [[ ! -f "$template" || ! -f "$deployed" ]]; then isNotice "URL rewrite: missing template or deployed config for $app — skipping" diff --git a/scripts/network/dns/setup_dns.sh b/scripts/network/dns/setup_dns.sh index aa2c36b..5773544 100755 --- a/scripts/network/dns/setup_dns.sh +++ b/scripts/network/dns/setup_dns.sh @@ -98,7 +98,7 @@ updateDNS() elif [[ $compose_setup == "app" ]]; then local compose_file="docker-compose.$app_name.yml" fi - result=$(runFileOp sed -i "s/\(WG_DEFAULT_DNS=\).*/\1$adguard_ip/" $containers_dir$app_name/$compose_file) + result=$(runFileOp sed -i "s/\(WG_DEFAULT_DNS=\).*/\1$adguard_ip/" $(appDir "$app_name")/$compose_file) checkSuccess "Updated Wireguard default DNS to $adguard_ip" fi dnsRemoveNameservers; @@ -114,7 +114,7 @@ updateDNS() elif [[ $compose_setup == "app" ]]; then local compose_file="docker-compose.$app_name.yml" fi - result=$(runFileOp sed -i "s/\(WG_DEFAULT_DNS=\).*/\1$pihole_ip/" $containers_dir$app_name/$compose_file) + result=$(runFileOp sed -i "s/\(WG_DEFAULT_DNS=\).*/\1$pihole_ip/" $(appDir "$app_name")/$compose_file) checkSuccess "Updated Wireguard default DNS to $pihole_ip" fi dnsRemoveNameservers; @@ -130,7 +130,7 @@ updateDNS() elif [[ $compose_setup == "app" ]]; then local compose_file="docker-compose.$app_name.yml" fi - result=$(runFileOp sed -i "s/\(WG_DEFAULT_DNS=\).*/\1$adguard_ip/" $containers_dir$app_name/$compose_file) + result=$(runFileOp sed -i "s/\(WG_DEFAULT_DNS=\).*/\1$adguard_ip/" $(appDir "$app_name")/$compose_file) checkSuccess "Updated Wireguard default DNS to $adguard_ip" fi dnsRemoveNameservers; diff --git a/scripts/network/dns/setup_local_dns.sh b/scripts/network/dns/setup_local_dns.sh index c24476c..a82c3ce 100644 --- a/scripts/network/dns/setup_local_dns.sh +++ b/scripts/network/dns/setup_local_dns.sh @@ -42,7 +42,7 @@ localDnsDomains() { localDnsAppHosts() { local cfg app up d_idx dom portv sub host local -a parts - for cfg in "${containers_dir}"*/*.config; do + while IFS= read -r cfg; do [[ -f "$cfg" ]] || continue app=$(basename "$cfg" .config); up=${app^^} d_idx=$(grep -oE "CFG_${up}_DOMAIN=[0-9]+" "$cfg" | head -1 | cut -d= -f2) @@ -91,7 +91,7 @@ localDnsApplyAdguard() { isNotice "AdGuard rewrite for ${entry} not applied (check API URL/creds) — safe to retry." fi done - done + done < <(storageAppConfigs) } # Pi-hole: per-host A records in the mounted, supported custom.list, inside a @@ -99,7 +99,7 @@ localDnsApplyAdguard() { # via `pihole restartdns`, falling back to a container restart. localDnsApplyPihole() { local ip="$1" - local list="${containers_dir}pihole/pihole-dnsmasq-unbound/custom.list" # mounts to /etc/pihole + local list="$(appDir pihole)/pihole-dnsmasq-unbound/custom.list" # mounts to /etc/pihole local b="# >>> libreportal-local >>>" e="# <<< libreportal-local <<<" local hosts tmp h n hosts=$(localDnsAppHosts) diff --git a/scripts/network/firewall/rules/firewall_clear_rules.sh b/scripts/network/firewall/rules/firewall_clear_rules.sh index ed8d6ff..89ecfa5 100755 --- a/scripts/network/firewall/rules/firewall_clear_rules.sh +++ b/scripts/network/firewall/rules/firewall_clear_rules.sh @@ -15,7 +15,7 @@ firewallClearLibrePortalRules() local port_spec="${BASH_REMATCH[3]}" # Only clear LibrePortal app rules (not system rules) - if [[ -d "$containers_dir/$container" ]]; then + if [[ -d "$(appDir "$container")" ]]; then local result; result=$(runSystem ufw-docker delete "$action" "$container" "$port_spec" 2>&1) if [[ $? -eq 0 ]]; then ((total_cleared++)) diff --git a/scripts/network/firewall/rules/firewall_rebuild_from_db.sh b/scripts/network/firewall/rules/firewall_rebuild_from_db.sh index 4eaf409..aa17073 100755 --- a/scripts/network/firewall/rules/firewall_rebuild_from_db.sh +++ b/scripts/network/firewall/rules/firewall_rebuild_from_db.sh @@ -21,7 +21,7 @@ firewallRebuildFromDatabase() # Check if Traefik is installed and running local traefik_available=false - if [[ -d "$containers_dir/traefik" ]] && runFileOp docker ps --format "table {{.Names}}" | grep -q "traefik"; then + if [[ -d "$(appDir traefik)" ]] && runFileOp docker ps --format "table {{.Names}}" | grep -q "traefik"; then traefik_available=true isSuccessful "Traefik detected - respecting traefik_managed flags" else @@ -53,7 +53,7 @@ firewallRebuildFromDatabase() local stale_apps=() _stale_app while IFS= read -r _stale_app; do [[ -n "$_stale_app" ]] || continue - [[ -d "$containers_dir/$_stale_app" ]] && continue + [[ -d "$(appDir "$_stale_app")" ]] && continue [[ "$(sqlite3 "$docker_dir/$db_file" "SELECT status FROM apps WHERE name = '$_stale_app';" 2>/dev/null)" == "1" ]] && continue stale_apps+=("$_stale_app") done <<< "$(sqlite3 "$docker_dir/$db_file" "SELECT DISTINCT app_name FROM network_resources;" 2>/dev/null)" @@ -72,7 +72,7 @@ firewallRebuildFromDatabase() while IFS='|' read -r app_name port_value service_name traefik_managed port_mapping; do if [[ -n "$app_name" && -n "$port_value" ]]; then # Verify this app actually exists - if [[ -d "$containers_dir/$app_name" ]]; then + if [[ -d "$(appDir "$app_name")" ]]; then local port_spec="${port_value}/tcp" # Per-port fields from the mapping ext:int:access:proto. diff --git a/scripts/network/monitoring/monitoring.sh b/scripts/network/monitoring/monitoring.sh index c5ae46a..d4e7f45 100644 --- a/scripts/network/monitoring/monitoring.sh +++ b/scripts/network/monitoring/monitoring.sh @@ -35,7 +35,7 @@ monitoringIsInstalled() monitoringAppEnabled() { local app_name="$1" - local cfg="${containers_dir}${app_name}/${app_name}.config" + local cfg="$(appDir "$app_name")/${app_name}.config" [[ -f "$cfg" ]] || return 1 local upper="${app_name^^}"; upper="${upper//-/_}" local val @@ -54,7 +54,7 @@ monitoringToggleAppConfig() { local app_name="$1" local rel_path="$2" - local file="${containers_dir}${app_name}/${rel_path}" + local file="$(appDir "$app_name")/${rel_path}" if [[ ! -f "$file" ]]; then isNotice "monitoringToggleAppConfig: $file not found — skipping." return 0 @@ -122,15 +122,15 @@ monitoringRefreshPrometheus() return 0 fi - local scrape_dir="${containers_dir}prometheus/prometheus/scrape.d" + local scrape_dir="$(appDir prometheus)/prometheus/scrape.d" runFileOp mkdir -p "$scrape_dir" local count=0 app_name fragment for app_name in $(monitoringInstalledApps); do - fragment="${containers_dir}${app_name}/resources/monitoring/prometheus-scrape.yml" + fragment="$(appDir "$app_name")/resources/monitoring/prometheus-scrape.yml" if monitoringAppEnabled "$app_name" && [[ -f "$fragment" ]]; then runFileOp cp "$fragment" "$scrape_dir/${app_name}.yml" - monitoringResolveScrapeTags "$scrape_dir/${app_name}.yml" "${containers_dir}${app_name}/docker-compose.yml" "$app_name" + monitoringResolveScrapeTags "$scrape_dir/${app_name}.yml" "$(appDir "$app_name")/docker-compose.yml" "$app_name" count=$((count + 1)) else runFileOp rm -f "$scrape_dir/${app_name}.yml" @@ -156,7 +156,7 @@ monitoringRefreshGrafana() return 0 fi - local prov="${containers_dir}grafana/provisioning" + local prov="$(appDir grafana)/provisioning" local ds_dir="$prov/datasources" local dash_provider_dir="$prov/dashboards" local dash_dir="$prov/dashboards/libreportal" @@ -195,7 +195,7 @@ EOF runFileOp find "$dash_dir" -type f -name '*.json' -delete 2>/dev/null local count=0 app_name app_dash f for app_name in $(monitoringInstalledApps); do - app_dash="${containers_dir}${app_name}/resources/monitoring/grafana-dashboards" + app_dash="$(appDir "$app_name")/resources/monitoring/grafana-dashboards" if monitoringAppEnabled "$app_name" && [[ -d "$app_dash" ]]; then for f in "$app_dash"/*.json; do [[ -f "$f" ]] || continue diff --git a/scripts/network/traefik/traefik_login_credentials.sh b/scripts/network/traefik/traefik_login_credentials.sh index d1800b7..b0041fa 100755 --- a/scripts/network/traefik/traefik_login_credentials.sh +++ b/scripts/network/traefik/traefik_login_credentials.sh @@ -2,7 +2,7 @@ traefikSetupLoginCredentials() { - local protectionauth_file="$containers_dir/traefik/etc/dynamic/middlewears/protectionauth.yml" + local protectionauth_file="$(appDir traefik)/etc/dynamic/middlewears/protectionauth.yml" if [ -f "$protectionauth_file" ]; then # Refuse to write empty/placeholder credentials — htpasswd -Bbn "" "" # silently produces a hash that effectively allows blank-credential diff --git a/scripts/network/traefik/traefik_whitelist.sh b/scripts/network/traefik/traefik_whitelist.sh index 35fe544..b0dbf5d 100755 --- a/scripts/network/traefik/traefik_whitelist.sh +++ b/scripts/network/traefik/traefik_whitelist.sh @@ -2,7 +2,7 @@ traefikUpdateWhitelist() { - local whitelist_file="${containers_dir}traefik/etc/dynamic/whitelist.yml" + local whitelist_file="$(appDir traefik)/etc/dynamic/whitelist.yml" if [ -f "$whitelist_file" ]; then # Split the CFG_IPS_WHITELIST into an array IFS=',' read -ra IP_ARRAY <<< "$CFG_IPS_WHITELIST" diff --git a/scripts/network/variables/basic_scan.sh b/scripts/network/variables/basic_scan.sh index 8db8244..0c38b01 100755 --- a/scripts/network/variables/basic_scan.sh +++ b/scripts/network/variables/basic_scan.sh @@ -17,11 +17,11 @@ setupBasicScanVariables() # Determine the correct docker-compose.yml file path (same logic as dockerComposeSetupFile) if [[ $compose_setup == "default" ]]; then - docker_compose_file="$containers_dir$app_name/docker-compose.yml"; + docker_compose_file="$(appDir "$app_name")/docker-compose.yml"; elif [[ $compose_setup == "app" ]]; then - docker_compose_file="$containers_dir$app_name/docker-compose.$app_name.yml"; + docker_compose_file="$(appDir "$app_name")/docker-compose.$app_name.yml"; else # Default to standard docker-compose.yml if compose_setup is not set - docker_compose_file="$containers_dir$app_name/docker-compose.yml"; + docker_compose_file="$(appDir "$app_name")/docker-compose.yml"; fi } diff --git a/scripts/peer/peer_pull.sh b/scripts/peer/peer_pull.sh index 81f1e2a..52834dc 100644 --- a/scripts/peer/peer_pull.sh +++ b/scripts/peer/peer_pull.sh @@ -69,11 +69,11 @@ peerPullApp() # ---- 3. Stop + wipe ---------------------------------------------------- migrateEmit phase=stop status=running app="$app" - if declare -f dockerComposeDown >/dev/null 2>&1 && [[ -d "$containers_dir$app" ]]; then + if declare -f dockerComposeDown >/dev/null 2>&1 && [[ -d "$(appDir "$app")" ]]; then dockerComposeDown "$app" >/dev/null 2>&1 || true fi - if [[ -d "$containers_dir$app" ]]; then - runFileOp rm -rf "${containers_dir:?}$app" + if [[ -d "$(appDir "$app")" ]]; then + runFileOp rm -rf "$(appDir "$app")" fi migrateEmit phase=stop status=complete app="$app" @@ -82,9 +82,21 @@ peerPullApp() # The pipe gives us streaming throughput without staging the whole tarball # to disk. set -o pipefail catches ssh failures so we don't run the rest # of the flow on a partial extract. + # Untar into the root that will HOLD this app — with storage locations that + # is not necessarily the primary one. appDir resolves the intended location + # (CFG__STORAGE) for an app that isn't on disk yet, and refuses if that + # location's drive is absent, so we never extract onto a bare mountpoint. + local _dest_dir _dest_root + if ! _dest_dir=$(appDir "$app"); then + isError "Cannot pull $app — its storage location is not available" + migrateEmit phase=transfer status=failed detail="storage-unavailable" + return 1 + fi + _dest_root="${_dest_dir%/*}" + runFileOp mkdir -p "$_dest_root" ( set -o pipefail - peerExec "$peer_name" "stream-app $app" | runFileOp tar -C "$containers_dir" -xf - + peerExec "$peer_name" "stream-app $app" | runFileOp tar -C "$_dest_root" -xf - ) local transfer_rc=$? if (( transfer_rc != 0 )); then @@ -92,12 +104,12 @@ peerPullApp() migrateEmit phase=transfer status=failed rc="$transfer_rc" return 1 fi - if [[ ! -d "$containers_dir$app" ]]; then - isError "Transfer reported success but $containers_dir$app missing" + if [[ ! -d "$(appDir "$app")" ]]; then + isError "Transfer reported success but $(appDir "$app") missing" migrateEmit phase=transfer status=failed reason=missing-output return 1 fi - runFileOp chown -R "${docker_install_user:-$(whoami)}":"${docker_install_user:-$(whoami)}" "$containers_dir$app" 2>/dev/null || true + runFileOp chown -R "${docker_install_user:-$(whoami)}":"${docker_install_user:-$(whoami)}" "$(appDir "$app")" 2>/dev/null || true migrateEmit phase=transfer status=complete app="$app" # ---- 5. URL rewrite (default ON) + re-deploy compose ------------------- diff --git a/scripts/peer/peer_shell.sh b/scripts/peer/peer_shell.sh index 2f3c380..2eb85c8 100644 --- a/scripts/peer/peer_shell.sh +++ b/scripts/peer/peer_shell.sh @@ -41,6 +41,19 @@ if [[ -r "${HOME}/.libreportal-env" ]]; then fi : "${containers_dir:=/libreportal-containers/}" +# Storage locations: peers can serve apps from any registered root, so resolve +# through paths.sh when it is reachable. This is a restricted SSH shell that may +# run with no LibrePortal env at all, so fall back to the single primary root — +# which is exactly what this script did before locations existed. +if [[ -n "${install_scripts_dir:-}" && -r "${install_scripts_dir}source/paths.sh" ]]; then + # shellcheck disable=SC1090 + source "${install_scripts_dir}source/paths.sh" >/dev/null 2>&1 || true +fi +if ! declare -F storageAppDirs >/dev/null 2>&1; then + storageAppDirs() { find "${containers_dir%/}" -mindepth 1 -maxdepth 1 -type d 2>/dev/null; } + appDir() { printf '%s' "${containers_dir%/}/$1"; } +fi + CMD="${SSH_ORIGINAL_COMMAND:-}" if [[ -z "$CMD" ]]; then _die "no-command" @@ -66,8 +79,9 @@ verb_list_apps() { local first=1 printf '{"peer":"%s","apps":[' "$LP_PEER_NAME" local d slug size_kb - for d in "$containers_dir"*/; do + while IFS= read -r d; do [[ -d "$d" ]] || continue + d="${d%/}/" slug=$(basename "$d") [[ -f "${d}docker-compose.yml" || -f "${d}compose.yml" ]] || continue _valid_slug "$slug" || continue @@ -76,7 +90,7 @@ verb_list_apps() { (( first )) || printf ',' first=0 printf '{"slug":"%s","size_kb":%s}' "$slug" "$size_kb" - done + done < <(storageAppDirs) printf ']}\n' } @@ -86,7 +100,7 @@ verb_stream_app() { if [[ -z "$slug" ]] || ! _valid_slug "$slug"; then _die "invalid-slug" fi - if [[ ! -d "${containers_dir}${slug}" ]]; then + if [[ ! -d "$(appDir "$slug")" ]]; then _die "no-such-app" fi _log stream-app "$slug" @@ -94,8 +108,14 @@ verb_stream_app() { # live data dirs change during read; we accept eventual consistency. The # caller is the receiver's peer_pull.sh, which untars into a staging dir # and then runs the migrate-flow. + # -C the root that actually holds this app: with storage locations it is not + # necessarily the primary one, and tarring from the wrong root silently + # streams nothing. + local _dir _root + _dir=$(appDir "$slug") || _die "storage-unavailable" + _root="${_dir%/*}" tar --warning=no-file-changed --warning=no-file-removed \ - -C "$containers_dir" -cf - "$slug" + -C "$_root" -cf - "$slug" } case "$VERB" in diff --git a/scripts/restore/restore_app_hooks.sh b/scripts/restore/restore_app_hooks.sh index 514ecce..c0b988e 100644 --- a/scripts/restore/restore_app_hooks.sh +++ b/scripts/restore/restore_app_hooks.sh @@ -4,7 +4,7 @@ restoreAppRunHook() { local app_name="$1" local phase="$2" - local hook="$containers_dir$app_name/restore.${phase}.hook.sh" + local hook="$(appDir "$app_name")/restore.${phase}.hook.sh" [[ ! -f "$hook" ]] && return 0 diff --git a/scripts/restore/restore_app_start.sh b/scripts/restore/restore_app_start.sh index 28c01fe..ee7d408 100644 --- a/scripts/restore/restore_app_start.sh +++ b/scripts/restore/restore_app_start.sh @@ -55,7 +55,7 @@ restoreAppStart() echo "" echo "---- $menu_number. Wiping existing app folder" echo "" - if [[ -d "$containers_dir$stored_app_name" ]]; then + if [[ -d "$(appDir "$stored_app_name")" ]]; then # Root-owned helper, not runFileOp — restoring over an app that left # sub-UID data behind (postgres, www-data, …) needs to actually wipe # those dirs before laying the snapshot down. @@ -72,7 +72,7 @@ restoreAppStart() echo "" echo "---- $menu_number. Restoring snapshot ${chosen_id:0:8}" echo "" - local include_path="$containers_dir$stored_app_name" + local include_path="$(appDir "$stored_app_name")" engineRestoreSnapshot "$chosen_idx" "$chosen_id" "/" "$include_path" if [[ $? -ne 0 ]]; then isError "Restore failed — leaving app in stopped state" diff --git a/scripts/source/artifacts.sh b/scripts/source/artifacts.sh index 5034232..ae4dd54 100644 --- a/scripts/source/artifacts.sh +++ b/scripts/source/artifacts.sh @@ -36,7 +36,7 @@ lpArtifactIndexUrl() { echo "$(lpReleaseBaseUrl)/$(lpReleaseChannel)/index.json" # lives alongside the other generated updater data so it ships/clears with that # state; the dir is in the container tree, so writes go through the container # funnel. Reads are fine as any user (world-readable). -lpArtifactSerialFile() { echo "${containers_dir%/}/libreportal/frontend/data/updater/generated/.index_serial"; } +lpArtifactSerialFile() { echo "$(webuiDir)/frontend/data/updater/generated/.index_serial"; } lpArtifactLastSerial() { local v; v=$(cat "$(lpArtifactSerialFile)" 2>/dev/null | tr -dc '0-9'); echo "${v:-0}"; } lpArtifactRecordSerial() { local serial="$1" f; f="$(lpArtifactSerialFile)" diff --git a/scripts/source/paths.sh b/scripts/source/paths.sh index 951a8ca..ae620b4 100644 --- a/scripts/source/paths.sh +++ b/scripts/source/paths.sh @@ -363,6 +363,60 @@ _appDirScan() return 0 } +# Every app DIRECTORY across every AVAILABLE root, one absolute path per line. +# THE enumerator — replaces `find "$containers_dir" -mindepth 1 -maxdepth 1 +# -type d`, which only ever saw the primary root. +# +# Roots whose drive is absent are SKIPPED, not reported as empty. That +# distinction is load-bearing: callers that reap "folders that no longer exist" +# would otherwise delete database rows and port allocations for apps whose only +# crime is living on an unplugged disk. Such callers must gate on +# appStorageAvailable rather than on the absence of a directory here. +# +# Deduplicated by slug, primary root first, matching appDir's precedence — a +# stray copy on a second disk never doubles an app that is live on the primary. +storageAppDirs() +{ + _lpStorageRootsLoad + local scan_op="" root d slug + declare -F runFileOp >/dev/null 2>&1 && scan_op="runFileOp" + local -A seen=() + for root in "${LP_STORAGE_ROOTS[@]}"; do + [[ -z "$root" ]] && continue + storageRootAvailable "$root" || continue + while IFS= read -r d; do + [[ -z "$d" ]] && continue + slug="${d##*/}" + [[ -n "${seen[$slug]:-}" ]] && continue + seen["$slug"]=1 + printf '%s\n' "$d" + done < <($scan_op find "$root" -mindepth 1 -maxdepth 1 -type d 2>/dev/null) + done +} + +# Every app's OWN config file (//.config), across available +# roots, deduplicated by slug. Deliberately stricter than a bare +# `find -name '*.config'`: an app dir may ship other *.config payload files +# (they get sourced as bash — see scan_files.sh), and those are not apps. +storageAppConfigs() +{ + local d slug + while IFS= read -r d; do + [[ -z "$d" ]] && continue + slug="${d##*/}" + [[ -f "$d/$slug.config" ]] && printf '%s\n' "$d/$slug.config" + done < <(storageAppDirs) +} + +# Slug form of storageAppDirs. +storageApps() +{ + local d + while IFS= read -r d; do + [[ -n "$d" ]] && printf '%s\n' "${d##*/}" + done < <(storageAppDirs) +} + # THE resolver. Prints the app's directory (no trailing slash). # # Returns non-zero — and prints an unusable sentinel path — when the app's diff --git a/scripts/validation/validate_config.sh b/scripts/validation/validate_config.sh index 1b739a0..e55c421 100644 --- a/scripts/validation/validate_config.sh +++ b/scripts/validation/validate_config.sh @@ -31,9 +31,9 @@ _lpvWarn() { isNotice " $*"; } _lpvAppFiles() { local app="$1" - _lpv_cfg_live="${containers_dir}${app}/${app}.config" + _lpv_cfg_live="$(appDir "$app")/${app}.config" _lpv_cfg_tmpl="${install_containers_dir}${app}/${app}.config" - _lpv_comp_live="${containers_dir}${app}/docker-compose.yml" + _lpv_comp_live="$(appDir "$app")/docker-compose.yml" _lpv_comp_tmpl="${install_containers_dir}${app}/docker-compose.yml" [[ -f "$_lpv_cfg_live" ]] || _lpv_cfg_live="" [[ -f "$_lpv_cfg_tmpl" ]] || _lpv_cfg_tmpl="" diff --git a/scripts/webui/data/generators/apps/webui_app_config.sh b/scripts/webui/data/generators/apps/webui_app_config.sh index 0aa083c..33deed6 100755 --- a/scripts/webui/data/generators/apps/webui_app_config.sh +++ b/scripts/webui/data/generators/apps/webui_app_config.sh @@ -7,7 +7,7 @@ updateAppConfig() { local app_name="$1" local config_content="$2" - local config_file="$containers_dir/$app_name/$app_name.config" + local config_file="$(appDir "$app_name")/$app_name.config" if [ -z "$app_name" ] || [ -z "$config_content" ]; then echo "❌ Error: app_name and config_content are required" @@ -19,9 +19,11 @@ updateAppConfig() { if [ $? -eq 0 ]; then echo "✅ Config updated successfully for $app_name" - # Trigger web UI refresh if needed - if [ -f "$containers_dir/frontend/data/last_update" ]; then - date -Iseconds | runFileWrite "$containers_dir/frontend/data/last_update" + # Trigger web UI refresh if needed. The path was "$containers_dir/frontend" + # — one level short of the real tree, which lives under the libreportal + # app dir — so the -f test never matched and this never fired. + if [ -f "$(webuiDir)/frontend/data/last_update" ]; then + date -Iseconds | runFileWrite "$(webuiDir)/frontend/data/last_update" fi else echo "❌ Failed to update config for $app_name" diff --git a/scripts/webui/data/generators/apps/webui_app_status.sh b/scripts/webui/data/generators/apps/webui_app_status.sh index fec14cc..87315ef 100755 --- a/scripts/webui/data/generators/apps/webui_app_status.sh +++ b/scripts/webui/data/generators/apps/webui_app_status.sh @@ -14,7 +14,7 @@ webuiUpdateAppStatus() { else # Check if app is installed local is_installed=false - if [ -d "${containers_dir}${app_name}" ]; then + if [ -d "$(appDir "$app_name")" ]; then is_installed=true fi diff --git a/scripts/webui/data/generators/apps/webui_config.sh b/scripts/webui/data/generators/apps/webui_config.sh index 54c229d..52b4cc9 100644 --- a/scripts/webui/data/generators/apps/webui_config.sh +++ b/scripts/webui/data/generators/apps/webui_config.sh @@ -72,7 +72,7 @@ EOF for app_name in "${apps_to_process[@]}"; do dir="$install_containers_dir/$app_name" - deployed_config_file="$containers_dir/$app_name/$app_name.config" + deployed_config_file="$(appDir "$app_name")/$app_name.config" config_file="$dir/$app_name.config" [[ -f "$deployed_config_file" ]] && config_file="$deployed_config_file" [[ ! -f "$config_file" ]] && { isNotice "No config file for $app_name"; continue; } @@ -125,7 +125,7 @@ EOF is_installed="true" fi else - [[ -d "$containers_dir/$app_name" ]] && is_installed="true" + [[ -d "$(appDir "$app_name")" ]] && is_installed="true" fi # Categories: comma-list, lowercase + trimmed via bash param expansion diff --git a/scripts/webui/data/generators/apps/webui_config_patch.sh b/scripts/webui/data/generators/apps/webui_config_patch.sh index dcbac80..d5e332d 100644 --- a/scripts/webui/data/generators/apps/webui_config_patch.sh +++ b/scripts/webui/data/generators/apps/webui_config_patch.sh @@ -10,7 +10,7 @@ webuiPatchAppConfigJson() { 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" + local deployed_cfg="$(appDir "$app_name")/${app_name}.config" local install_cfg="${install_containers_dir}/${app_name}/${app_name}.config" local cfg_file if [[ -f "$deployed_cfg" ]]; then cfg_file="$deployed_cfg" diff --git a/scripts/webui/data/generators/apps/webui_registry_scan.sh b/scripts/webui/data/generators/apps/webui_registry_scan.sh index c9e896b..acbc2f0 100644 --- a/scripts/webui/data/generators/apps/webui_registry_scan.sh +++ b/scripts/webui/data/generators/apps/webui_registry_scan.sh @@ -43,9 +43,9 @@ _catalogRowsFrom() { } webuiRegistryCatalogScan() { - local out_dir="${containers_dir%/}/libreportal/frontend/data/apps/generated" + local out_dir="$(webuiDir)/frontend/data/apps/generated" local out="$out_dir/registry_catalog.json" - local icon_dir="${containers_dir%/}/libreportal/frontend/core/icons/apps/registry" + local icon_dir="$(webuiDir)/frontend/core/icons/apps/registry" local icon_web="/core/icons/apps/registry" local max_icon=262144 runFileOp mkdir -p "$out_dir" 2>/dev/null || true @@ -82,7 +82,7 @@ webuiRegistryCatalogScan() { defined="$(for d in "$install_containers_dir"/*/; do [[ -d "$d" ]] && basename "$d"; done | jq -R . | jq -cs .)" fi if [[ -d "$containers_dir" ]]; then - installed="$(for d in "$containers_dir"/*/; do [[ -d "$d" ]] && basename "$d"; done | jq -R . | jq -cs .)" + installed="$(storageApps | jq -R . | jq -cs .)" fi # Mirror the official catalog's pinned icons locally (slug -> web path). An diff --git a/scripts/webui/data/generators/backup/webui_backup_migrate.sh b/scripts/webui/data/generators/backup/webui_backup_migrate.sh index cbbf800..ff675ae 100644 --- a/scripts/webui/data/generators/backup/webui_backup_migrate.sh +++ b/scripts/webui/data/generators/backup/webui_backup_migrate.sh @@ -35,8 +35,9 @@ webuiGenerateBackupMigrate() local running_first=true running_apps_json="[" local app_dir app_slug - for app_dir in "$containers_dir"*/; do + while IFS= read -r app_dir; do [[ -d "$app_dir" ]] || continue + app_dir="${app_dir%/}/" app_slug=$(basename "$app_dir") [[ -f "${app_dir}docker-compose.yml" || -f "${app_dir}compose.yml" ]] || continue $installed_first || installed_apps_json+="," @@ -47,7 +48,7 @@ webuiGenerateBackupMigrate() running_first=false running_apps_json+="\"$app_slug\"" fi - done + done < <(storageAppDirs) installed_apps_json+="]" running_apps_json+="]" diff --git a/scripts/webui/data/generators/updater/webui_artifact_scan.sh b/scripts/webui/data/generators/updater/webui_artifact_scan.sh index 4e28e34..27a2eb6 100644 --- a/scripts/webui/data/generators/updater/webui_artifact_scan.sh +++ b/scripts/webui/data/generators/updater/webui_artifact_scan.sh @@ -13,7 +13,7 @@ # gracefully when the file is absent. Run by `libreportal updater check`. webuiArtifactScan() { - local out_dir="${containers_dir%/}/libreportal/frontend/data/updater/generated" + local out_dir="$(webuiDir)/frontend/data/updater/generated" local out="$out_dir/artifacts_available.json" local applied_dir="$out_dir/applied" runFileOp mkdir -p "$out_dir" 2>/dev/null || true @@ -46,7 +46,7 @@ webuiArtifactScan() { # Installed apps = dir names under containers_dir, for the applicable check. local installed="[]" d if [[ -d "$containers_dir" ]]; then - installed="$(for d in "$containers_dir"/*/; do [[ -d "$d" ]] && basename "$d"; done | jq -R . | jq -cs .)" + installed="$(storageApps | jq -R . | jq -cs .)" fi # The Improvements stream is HOTFIX-only. type:"app" rows belong to the diff --git a/scripts/webui/data/generators/updater/webui_updater_scan.sh b/scripts/webui/data/generators/updater/webui_updater_scan.sh index a189b1a..0ebcdbb 100644 --- a/scripts/webui/data/generators/updater/webui_updater_scan.sh +++ b/scripts/webui/data/generators/updater/webui_updater_scan.sh @@ -374,7 +374,7 @@ webuiUpdaterScan() { local b; b="$(basename "$d")" [[ "$b" == "libreportal" ]] && continue [ -f "$d/$b.config" ] && apps+=("$b") - done < <(runFileOp find "$containers_dir" -mindepth 1 -maxdepth 1 -type d 2>/dev/null) + done < <(storageAppDirs) fi # Registry lookups are the expensive step, so throttle them SEPARATELY from @@ -404,7 +404,7 @@ webuiUpdaterScan() { # Build the per-app updates array (one compact JSON object per line → jq -s). local objs; objs="$(mktemp)" for app in "${apps[@]}"; do - local compose="$containers_dir/$app/docker-compose.yml" + local compose="$(appDir "$app")/docker-compose.yml" [ -f "$compose" ] || continue local anchor; anchor="$(updaterPrimaryImage "$app" "$compose")" @@ -613,7 +613,7 @@ webuiUpdaterScan() { if [ "$scanner_state" = "ready" ] && command -v jq >/dev/null 2>&1 \ && declare -f trivyScanImageCves >/dev/null 2>&1; then for app in "${apps[@]}"; do - local cimg="" ccompose="$containers_dir/$app/docker-compose.yml" + local cimg="" ccompose="$(appDir "$app")/docker-compose.yml" # Same primary-image anchor as the version scan (scanning every # service's image is a documented future step, see the roadmap). [ -f "$ccompose" ] && cimg="$(updaterPrimaryImage "$app" "$ccompose")" diff --git a/scripts/webui/data/logs/webui_app_logs.sh b/scripts/webui/data/logs/webui_app_logs.sh index 5dd0b60..75068ba 100755 --- a/scripts/webui/data/logs/webui_app_logs.sh +++ b/scripts/webui/data/logs/webui_app_logs.sh @@ -14,7 +14,7 @@ webuiGenerateAppLogs() # Fast approach: Scan containers directory directly if [ -d "$containers_dir" ]; then - runFileOp find "$containers_dir" -maxdepth 1 -type d -not -path "$containers_dir" | while read -r dir; do + storageAppDirs | while read -r dir; do local result; result=$(basename "$dir") local app_name="$result" total_apps=$((total_apps + 1)) diff --git a/scripts/webui/webui_updater.sh b/scripts/webui/webui_updater.sh index b8bb830..edbfff7 100755 --- a/scripts/webui/webui_updater.sh +++ b/scripts/webui/webui_updater.sh @@ -83,7 +83,7 @@ webuiLibrePortalUpdate() { local _app _dir _hook for _dir in "${install_containers_dir}"*/; do _app="$(basename "$_dir")" - [[ -f "${containers_dir}${_app}/docker-compose.yml" ]] || continue + [[ -f "$(appDir "$_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.