refactor(storage): resolve every app directory through appDir

The main sweep — ~260 call sites across ~100 files move from string
concatenation on a single root to appDir/storageAppDirs/storageAppConfigs.
On a single-root install the resolved paths are identical, so this is a
no-op until a location is registered.

Enumerators were the interesting half. `for d in "$containers_dir"/*/`
appears in the menus, the registry/artifact scanners and the DNS setup —
and a shell glob cannot list a rootless 751 tree at all, which is the
same bug config_find_file.sh already documents in a comment. Routing them
through storageAppDirs (which enumerates as the owning user) fixes that
alongside the multi-root work.

Three places needed judgement rather than substitution:

db_app_scan.sh deletes database rows and port allocations for apps whose
folder is missing, and reaps "empty" app dirs. With a storage location
unmounted, every app on it looks exactly like that. Each of those
branches now gates on appStorageAvailable first — an app on an unplugged
drive is skipped with a notice, never deleted.

instance_create.sh rewrites cloned hooks so an instance touches its own
directory instead of the base app's. Its sed matched ${containers_dir}<type>,
which this sweep just replaced with $(appDir <type>) — so it would have
silently stopped redirecting, and an instance would have written to the
original's files (the adguard auth adapter case its own comment warns
about). Now matches both appDir forms, verified against bare, quoted,
unrelated-app, legacy and prose cases.

peer_shell/peer_pull streamed and extracted relative to the primary root.
Both now use the app's own root, and peer_shell keeps a single-root
fallback since it runs as a restricted SSH shell with no LibrePortal env.

Also fixes a pre-existing bug found on the way: webui_app_config.sh
tested "$containers_dir/frontend/data/last_update", one level short of the
real tree under the libreportal app dir, so the WebUI refresh trigger
after a config update has never once fired.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
librelad 2026-08-24 04:09:51 +01:00
parent 2d24a764a8
commit 8b5e02c760
127 changed files with 400 additions and 296 deletions

View File

@ -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."

View File

@ -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"

View File

@ -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

View File

@ -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%%:*}"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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."

View File

@ -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.

View File

@ -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"

View File

@ -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"

View File

@ -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
}

View File

@ -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

View File

@ -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}')

View File

@ -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}')

View File

@ -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)

View File

@ -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;

View File

@ -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

View File

@ -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 ""

View File

@ -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

View File

@ -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

View File

@ -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."

View File

@ -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 ""

View File

@ -28,7 +28,7 @@ moneyapp_install_post_compose()
# * https://<host>.<domain> when traefik is on AND a real domain
# is configured (CFG_DOMAIN_<n>)
# * http://<server-ip>:<port> 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 `<app>.<domain>` — when CFG_DOMAIN_<n> is
# empty it ends up as `<app>.`, a valid-but-broken hostname. Reject

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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 \

View File

@ -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
}

View File

@ -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%%#*}"

View File

@ -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

View File

@ -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"

View File

@ -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."

View File

@ -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%%:*}"

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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"
}

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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.)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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"

View File

@ -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")

View File

@ -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"

View File

@ -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"
}

View File

@ -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

View File

@ -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")

View File

@ -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
}

View File

@ -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"
}

View File

@ -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"

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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).

View File

@ -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; }

View File

@ -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"

View File

@ -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"

View File

@ -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 ""

View File

@ -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 <slug>/<slug>.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."
}

View File

@ -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"

View File

@ -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
}

View File

@ -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

View File

@ -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 ""

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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."

View File

@ -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

View File

@ -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")

View File

@ -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."

View File

@ -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")

View File

@ -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

View File

@ -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

View File

@ -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."

View File

@ -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=""

View File

@ -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."

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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"
}

View File

@ -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

View File

@ -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

View File

@ -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)"

View File

@ -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)

View File

@ -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"

View File

@ -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
}

View File

@ -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

View File

@ -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}<type>/..." instead of "$containers_dir$app_name/..."
# "${containers_dir}<type>/..." 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 <name>).
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

View File

@ -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."

View File

@ -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

View File

@ -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

Some files were not shown because too many files have changed in this diff Show More