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}}" user="${user:-${CFG_ADGUARD_ADMIN_USER:-admin}}"
[[ -z "$password" ]] && password=$(generateRandomPassword) [[ -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; } [[ ! -f "$yaml" ]] && { isError "AdGuardHome.yaml not found at $yaml."; return 1; }
if ! command -v htpasswd >/dev/null 2>&1; then if ! command -v htpasswd >/dev/null 2>&1; then
isError "htpasswd is required to bcrypt the new password." 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 # install pipeline; the resolved host port is stored in the PORTS_TAG_1
# docker-compose tag (format `external:internal`). Pull it from there # docker-compose tag (format `external:internal`). Pull it from there
# so the curl + URL printout actually point somewhere real. # 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 local adguard_port_pair
adguard_port_pair=$(tagsManagerGetTagContent "$adguard_compose_file" "PORTS_TAG_1") adguard_port_pair=$(tagsManagerGetTagContent "$adguard_compose_file" "PORTS_TAG_1")
local adguard_admin_port="${adguard_port_pair%%:*}" local adguard_admin_port="${adguard_port_pair%%:*}"
@ -89,11 +89,11 @@ JSON
local result local result
if [[ "$public" == "true" ]]; then 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" checkSuccess "Setting allow_unencrypted_doh to false for Traefik"
fi 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" 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. # 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 # the install API call AdGuardHome sometimes ends up bound to
# 0.0.0.0:80 (its build-time default) — exactly what causes "unable # 0.0.0.0:80 (its build-time default) — exactly what causes "unable
# to connect" on the host port. # 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 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*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" 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 local result
result=$(copyResource "$app_name" "configuration.yml" "config" | runInstallWrite -a "$logs_dir/$docker_log_file" 2>&1) 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) 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_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_DOMAIN_PLACEHOLDER|$domain_full|g" "$authelia_config_file"
runFileOp sed -i "s|AUTHELIA_HOST_PLACEHOLDER|$host_setup|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)" 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" runFileOp mkdir -p "$authelia_secrets_dir"
local secret_name secret_file local secret_name secret_file
for secret_name in JWT_SECRET SESSION_SECRET STORAGE_ENCRYPTION_KEY; do 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_user="${CFG_AUTHELIA_ADMIN_USERNAME:-admin}"
local authelia_admin_pass="${CFG_AUTHELIA_ADMIN_PASSWORD_1:-authelia}" 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 local authelia_attempts=0
while ((authelia_attempts < 30)); do while ((authelia_attempts < 30)); do
if runFileOp docker exec authelia-service authelia --version >/dev/null 2>&1; then 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_email="${CFG_BOOKSTACK_ADMIN_EMAIL:-admin@admin.com}"
local bookstack_target_pass="${CFG_BOOKSTACK_ADMIN_PASSWORD_1:-password}" 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 local bookstack_port_pair
bookstack_port_pair=$(tagsManagerGetTagContent "$bookstack_compose_file" "PORTS_TAG_1") bookstack_port_pair=$(tagsManagerGetTagContent "$bookstack_compose_file" "PORTS_TAG_1")
local bookstack_host_port="${bookstack_port_pair%%:*}" 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 # -f guard always failed, and the key was never mirrored. The key
# is declared in crowdsec.config, so that is where it belongs. # is declared in crowdsec.config, so that is where it belongs.
# The helper handles cscli + tee + chown + chmod atomically. # 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" local key_file="/etc/crowdsec/traefik_bouncer.key"
# What the config currently holds, if anything. Quotes and whitespace # 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 # 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 # file agree. updateConfigOption escapes the value, writes as the owner of
# the containers tree, and re-sources. # 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 if [[ -f "$cfg_file" ]]; then
updateConfigOption "CFG_CROWDSEC_TRAEFIK_LAPI_KEY" "$bouncer_key" "$cfg_file" updateConfigOption "CFG_CROWDSEC_TRAEFIK_LAPI_KEY" "$bouncer_key" "$cfg_file"
else else
@ -41,7 +41,7 @@ appCrowdsecRotateBouncerKey()
# Traefik reads the key from the bind-mounted file at startup, so it keeps # 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 # presenting the revoked key until it restarts. Without this the rotation
# looks successful while every bouncer check fails with 403. # looks successful while every bouncer check fails with 403.
if [[ -d "${containers_dir}traefik" ]]; then if [[ -d "$(appDir traefik)" ]]; then
dockerComposeRestart traefik dockerComposeRestart traefik
checkSuccess "Restarting Traefik to load the new bouncer key" checkSuccess "Restarting Traefik to load the new bouncer key"
else else

View File

@ -8,7 +8,7 @@
# (read from the app's `# Category :` header). # (read from the app's `# Category :` header).
appDashyUpdateConf() 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 services_json="$(webuiDir)/frontend/data/apps/generated/apps-services.json"
local icons_src_dir="$(webuiDir)/frontend/icons/apps" local icons_src_dir="$(webuiDir)/frontend/icons/apps"
@ -18,7 +18,7 @@ appDashyUpdateConf()
# Look at the actual docker container instead — if the container # Look at the actual docker container instead — if the container
# exists or the install dir is present, generate the conf. # 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)$' \ 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 return 0
fi fi

View File

@ -16,7 +16,7 @@ appDashyManageShortcuts()
selected=$(toolArgsGet "$tool_args" "selected") selected=$(toolArgsGet "$tool_args" "selected")
selected="${selected//%7C/|}" 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" updateConfigOption "CFG_DASHY_SHORTCUTS" "$selected" "$cfg_file"
export CFG_DASHY_SHORTCUTS="$selected" export CFG_DASHY_SHORTCUTS="$selected"
isSuccessful "Saved 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 if [[ -n "$CFG_GITEA_METRICS_TOKEN_1" ]]; then
local result local result
result=$(runFileOp sed -i "s|GITEA_METRICS_TOKEN_PLACEHOLDER|${CFG_GITEA_METRICS_TOKEN_1}|g" \ 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" checkSuccess "Synced Gitea /metrics token to the Prometheus scrape config"
else else
isNotice "CFG_GITEA_METRICS_TOKEN_1 is empty — Gitea /metrics scrape may 401." 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. # apps drop out automatically. Self-skips if gluetun isn't installed.
appNetworkRegisterPorts_gluetun() 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 [[ ! -f "$gluetun_compose" ]]; then return 0; fi
if ! command -v sqlite3 >/dev/null 2>&1; 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 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. # leaves an undeclared name EMPTY in the CALLER's scope at EOF.
while IFS= read -r app; do while IFS= read -r app; do
[[ -z "$app" || "$app" == "gluetun" ]] && continue [[ -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 [[ -f "$app_config_file" ]] || continue
local net local net
net=$(grep -E "^CFG_${app^^}_NETWORK=" "$app_config_file" 2>/dev/null | cut -d'=' -f2 | tr -d '"') 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" runFileOp mv "$tmp" "$gluetun_compose"
if runFileOp docker ps --format '{{.Names}}' 2>/dev/null | grep -q '^gluetun-service$'; then if runFileOp docker ps --format '{{.Names}}' 2>/dev/null | grep -q '^gluetun-service$'; then
isNotice "Gluetun forwarded ports changed; recreating gluetun-service to apply." 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 # Recreating gluetun gives it a new container ID, which orphans every
# `network_mode: container:gluetun-service` reference. Re-attach all # `network_mode: container:gluetun-service` reference. Re-attach all
# routed apps so they share the new netns instead of getting their own. # 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. # leaves an undeclared name EMPTY in the CALLER's scope at EOF.
while IFS= read -r app; do while IFS= read -r app; do
[[ -z "$app" || "$app" == "gluetun" ]] && continue [[ -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 [[ -f "$app_config_file" ]] || continue
local net local net
@ -45,7 +45,7 @@ appGluetunRecreateRouted()
| cut -d'=' -f2 | tr -d '"') | cut -d'=' -f2 | tr -d '"')
[[ "$net" != "gluetun" ]] && continue [[ "$net" != "gluetun" ]] && continue
local app_compose="${containers_dir}${app}/docker-compose.yml" local app_compose="$(appDir "$app")/docker-compose.yml"
[[ -f "$app_compose" ]] || continue [[ -f "$app_compose" ]] || continue
# Skip apps with no running/created container — recreate would # Skip apps with no running/created container — recreate would
@ -56,7 +56,7 @@ appGluetunRecreateRouted()
fi fi
isNotice "Re-attaching ${app} to gluetun's namespace (force-recreate)..." 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++)) ((recreated++))
done <<< "$installed_apps" done <<< "$installed_apps"

View File

@ -32,7 +32,7 @@ gluetunRouteExistingAppsPrompt()
# leaves an undeclared name EMPTY in the CALLER's scope at EOF. # leaves an undeclared name EMPTY in the CALLER's scope at EOF.
while IFS= read -r app; do while IFS= read -r app; do
[[ -z "$app" ]] && continue [[ -z "$app" ]] && continue
local cfg_file="${containers_dir}${app}/${app}.config" local cfg_file="$(appDir "$app")/${app}.config"
[[ -f "$cfg_file" ]] || continue [[ -f "$cfg_file" ]] || continue
local category local category
category=$(grep -E "^CFG_${app^^}_CATEGORY=" "$cfg_file" 2>/dev/null \ 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:" echo " The following installed apps can be routed through Gluetun:"
for app in "${eligible[@]}"; do for app in "${eligible[@]}"; do
local cfg_file="${containers_dir}${app}/${app}.config" local cfg_file="$(appDir "$app")/${app}.config"
local current local current
current=$(grep -E "^CFG_${app^^}_NETWORK=" "$cfg_file" 2>/dev/null | head -1 | cut -d'=' -f2) current=$(grep -E "^CFG_${app^^}_NETWORK=" "$cfg_file" 2>/dev/null | head -1 | cut -d'=' -f2)
[[ -z "$current" ]] && current="default" [[ -z "$current" ]] && current="default"
@ -73,7 +73,7 @@ gluetunRouteExistingAppsPrompt()
esac esac
for app in "${eligible[@]}"; do for app in "${eligible[@]}"; do
local cfg_file="${containers_dir}${app}/${app}.config" local cfg_file="$(appDir "$app")/${app}.config"
local current local current
current=$(grep -E "^CFG_${app^^}_NETWORK=" "$cfg_file" 2>/dev/null | head -1 | cut -d'=' -f2) current=$(grep -E "^CFG_${app^^}_NETWORK=" "$cfg_file" 2>/dev/null | head -1 | cut -d'=' -f2)
[[ -z "$current" ]] && current="default" [[ -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 # 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 # step on every reinstall — while a fresh install passed, because the dir was
# still empty. Grafana's files must keep Grafana's ownership regardless. # 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 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." checkSuccess "Set permissions to grafana_storage folder."
fi fi
} }

View File

@ -15,13 +15,13 @@ headscaleCommands()
isHeader "Install Headscale Apps List" isHeader "Install Headscale Apps List"
# Find all subdirectories under the directory where your apps are installed # 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 if [[ -d "$app_dir" ]]; then
# Extract the app name (folder name) # Extract the app name (folder name)
local app_name=$(basename "$app_dir") local app_name=$(basename "$app_dir")
local app_names+=("$app_name") local app_names+=("$app_name")
fi fi
done done < <(storageAppDirs)
# Check if any apps were found # Check if any apps were found
if [ ${#app_names[@]} -eq 0 ]; then if [ ${#app_names[@]} -eq 0 ]; then

View File

@ -2,7 +2,7 @@
headscaleEditConfig() 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}') local previous_md5=$(md5sum "$config_file" | awk '{print $1}')
$CFG_TEXT_EDITOR "$config_file" $CFG_TEXT_EDITOR "$config_file"
local current_md5=$(md5sum "$config_file" | awk '{print $1}') local current_md5=$(md5sum "$config_file" | awk '{print $1}')

View File

@ -2,7 +2,7 @@
setupHeadscaleGetHostname() setupHeadscaleGetHostname()
{ {
local config_file="${containers_dir}headscale/config/config.yaml" local config_file="$(appDir headscale)/config/config.yaml"
if [ -f "$config_file" ]; then if [ -f "$config_file" ]; then
# Read the line with "server_url" and extract the hostname # Read the line with "server_url" and extract the hostname
headscale_live_hostname=$(grep "server_url:" "$config_file" | awk -F'server_url: ' '{print $2}') 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 app_name="$1"
local result 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" checkSuccess "Create config folder"
result=$(copyResource "$app_name" "config.yaml" "config" | runInstallWrite -a "$logs_dir/$docker_log_file" 2>&1) 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 app_name="$1"
local type="$2" 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" 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 if [[ "$type" != "install" ]]; then
dockerComposeRestart $app_name; dockerComposeRestart $app_name;

View File

@ -19,20 +19,20 @@ jitsimeet_install_post_setup()
echo "The latest tag is: $latest_tag" echo "The latest tag is: $latest_tag"
local result 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" 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" 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" 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" 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" 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" 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" checkSuccess "Removing downloaded zip file as no longer needed"
} }
@ -48,36 +48,36 @@ jitsimeet_install_post_compose()
dockerSetupEnvFile dockerSetupEnvFile
local result 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" 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" 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" 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" checkSuccess "Updating .env file with HTTP_PORT to $usedport2"
# Defaults missing from the shipped .env (see jitsi/docker-jitsi-meet # Defaults missing from the shipped .env (see jitsi/docker-jitsi-meet
# commit 12051700562d…). Append them here so the install boots. # 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" 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" 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" 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" 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" 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" checkSuccess "Running Jitsi Meet gen-passwords.sh script"
} }
@ -91,10 +91,10 @@ jitsimeet_install_post_start()
echo "" echo ""
local result 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" 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" checkSuccess "Updating NGINX default site port 443 to $usedport2"
dockerComposeRestart $app_name dockerComposeRestart $app_name

View File

@ -129,7 +129,7 @@ installLibrePortal()
if [[ "$libreportal_bootstrap_install" != "true" ]]; then if [[ "$libreportal_bootstrap_install" != "true" ]]; then
((menu_number++)) ((menu_number++))
echo "" 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 ""
echo " You can now navigate to your new service using one of the options below : " echo " You can now navigate to your new service using one of the options below : "
echo "" echo ""

View File

@ -9,7 +9,7 @@ libreportal_catalog_install_post_setup()
{ {
local app_name="$1" local app_name="$1"
local def="${install_containers_dir%/}/$app_name" local def="${install_containers_dir%/}/$app_name"
local live="$containers_dir$app_name" local live="$(appDir "$app_name")"
local dest="$live/data" local dest="$live/data"
# The compose bind-mounts nginx.conf as a FILE; the generic installer # The compose bind-mounts nginx.conf as a FILE; the generic installer

View File

@ -29,7 +29,7 @@
mastodon_install_post_setup() mastodon_install_post_setup()
{ {
local app_name="${1:-mastodon}" 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 [[ -f "$config_file" ]] || return 0
# Keep a pair that already has the right shape. The length test is also what # 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 script="$1"; shift
local admin_user="${CFG_MATRIX_ADMIN_USERNAME:-admin}" local admin_user="${CFG_MATRIX_ADMIN_USERNAME:-admin}"
local admin_pass="${CFG_MATRIX_ADMIN_PASSWORD_1}" 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 if [[ -z "$admin_pass" || "$admin_pass" == RANDOMIZEDPASSWORD* ]]; then
isError "No Matrix admin password in matrix.config — cannot authenticate to the admin API." isError "No Matrix admin password in matrix.config — cannot authenticate to the admin API."

View File

@ -66,11 +66,11 @@ _matrixServerName()
_matrixPublicBaseUrl() _matrixPublicBaseUrl()
{ {
local app_name="$1" 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; # Real HTTPS only when Traefik is actually installed AND a domain exists;
# otherwise the router in the compose is decorative. # 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 local host
host=$(tagsManagerGetTagContent "$compose" "DOMAINSUBNAME_TAG_1") host=$(tagsManagerGetTagContent "$compose" "DOMAINSUBNAME_TAG_1")
if [[ -n "$host" && "$host" != DOMAINSUBNAME_DATA* ]]; then if [[ -n "$host" && "$host" != DOMAINSUBNAME_DATA* ]]; then
@ -94,7 +94,7 @@ _matrixPublicBaseUrl()
matrix_install_post_compose() matrix_install_post_compose()
{ {
local app_name="$1" local app_name="$1"
local app_dir="$containers_dir$app_name" local app_dir="$(appDir "$app_name")"
local data_dir="$app_dir/data" local data_dir="$app_dir/data"
((menu_number++)) ((menu_number++))
@ -254,7 +254,7 @@ matrix_install_post_compose()
matrix_install_post_start() matrix_install_post_start()
{ {
local app_name="$1" local app_name="$1"
local app_dir="$containers_dir$app_name" local app_dir="$(appDir "$app_name")"
((menu_number++)) ((menu_number++))
echo "" echo ""

View File

@ -28,7 +28,7 @@ moneyapp_install_post_compose()
# * https://<host>.<domain> when traefik is on AND a real domain # * https://<host>.<domain> when traefik is on AND a real domain
# is configured (CFG_DOMAIN_<n>) # is configured (CFG_DOMAIN_<n>)
# * http://<server-ip>:<port> otherwise (raw port exposure) # * 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="" local moneyapp_auth_url=""
# host_setup is built as `<app>.<domain>` — when CFG_DOMAIN_<n> is # host_setup is built as `<app>.<domain>` — when CFG_DOMAIN_<n> is
# empty it ends up as `<app>.`, a valid-but-broken hostname. Reject # 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 # 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 # 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. # 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 local result
result=$(copyResource "$app_name" "nginx.conf" "resources" | runInstallWrite -a "$logs_dir/$docker_log_file" 2>&1) 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 app_name="$1"
local username password local username password
username=$(grep -oP 'username=\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[^ ]+' "$containers_dir$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" echo "$username $password"
} }

View File

@ -31,7 +31,7 @@ owncloud_install_post_compose()
isNotice "Failed to retrieve the web page." isNotice "Failed to retrieve the web page."
fi 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 file_name="docker-compose.yml"
local result local result
result=$(runFileOp sed -i \ result=$(runFileOp sed -i \

View File

@ -9,10 +9,10 @@ prometheus_install_post_compose()
local app_name="$1" local app_name="$1"
local result 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" 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" 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) 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" local app_name="$1"
if [ -f "${containers_dir}prometheus/prometheus/prometheus.yml" ]; then if [ -f "$(appDir prometheus)/prometheus/prometheus.yml" ]; then
updateFileOwnership "${containers_dir}prometheus/prometheus/prometheus.yml" $docker_install_user $docker_install_user updateFileOwnership "$(appDir prometheus)/prometheus/prometheus.yml" $docker_install_user $docker_install_user
fi fi
# Prometheus runs as nobody (65534) inside the container, which rootless maps # Prometheus runs as nobody (65534) inside the container, which rootless maps
# to a subuid outside this user's authority (65534 -> 296605 here). Two # 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 # every REINSTALL (a fresh install passed only because the dir
# was still empty). Those files must keep prometheus's ownership # was still empty). Those files must keep prometheus's ownership
# anyway — that is what lets it write them. # anyway — that is what lets it write them.
if [ -d "${containers_dir}prometheus/prometheus" ]; then if [ -d "$(appDir prometheus)/prometheus" ]; then
local result 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." checkSuccess "Set permissions to prometheus folder."
fi fi
if [ -d "${containers_dir}prometheus/prom_data" ]; then if [ -d "$(appDir prometheus)/prom_data" ]; then
local result 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." checkSuccess "Set permissions to prom_data folder."
fi fi
} }

View File

@ -14,7 +14,7 @@
# way round needing a real account here. # way round needing a real account here.
_rocketchatBaseUrl() { _rocketchatBaseUrl() {
local compose="${containers_dir}rocketchat/docker-compose.yml" local compose="$(appDir rocketchat)/docker-compose.yml"
local url local url
url=$(runFileOp grep -oP '^\s*-\s*ROOT_URL=\K\S+' "$compose" 2>/dev/null | head -1) url=$(runFileOp grep -oP '^\s*-\s*ROOT_URL=\K\S+' "$compose" 2>/dev/null | head -1)
url="${url%%#*}" url="${url%%#*}"

View File

@ -6,7 +6,7 @@ searxng_install_post_start()
{ {
local app_name="$1" 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_timeout=10
local searxng_counter=0 local searxng_counter=0
while [ ! -f "$searxng_settings" ]; do while [ ! -f "$searxng_settings" ]; do

View File

@ -47,7 +47,7 @@ stalwart_mode()
{ {
local mode="${CFG_STALWART_MODE:-auto}" local mode="${CFG_STALWART_MODE:-auto}"
if [[ "$mode" == "auto" ]]; then 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 fi
[[ "$mode" == "public" || "$mode" == "private" ]] || mode="private" [[ "$mode" == "public" || "$mode" == "private" ]] || mode="private"
printf '%s' "$mode" printf '%s' "$mode"
@ -105,7 +105,7 @@ stalwart_apply_port_access()
stalwart_install_post_setup() stalwart_install_post_setup()
{ {
local app_name="$1" 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 [[ -f "$cfg" ]] || return 0
local mode; mode=$(stalwart_mode) 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. # 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 # Without it mail clients get a self-signed certificate on 993 and no amount
# of waiting fixes it. # 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 && ( "${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 "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" 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`), # Resolved admin port comes from the compose tag (format `external:internal`),
# the same source adguard's hook reads — the legacy $usedport1 isn't populated # the same source adguard's hook reads — the legacy $usedport1 isn't populated
# by the current install pipeline. # 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 local admin_pair admin_port
admin_pair=$(tagsManagerGetTagContent "$compose_file" "PORTS_TAG_1" 2>/dev/null) admin_pair=$(tagsManagerGetTagContent "$compose_file" "PORTS_TAG_1" 2>/dev/null)
admin_port="${admin_pair%%:*}" admin_port="${admin_pair%%:*}"
@ -440,7 +440,7 @@ stalwart_install_post_start()
echo "" echo ""
isNotice "Point mail and calendar clients at: ${mail_host}" isNotice "Point mail and calendar clients at: ${mail_host}"
isNotice " Certificates are self-signed, so clients will ask you to trust it once." 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." isNotice " Headscale is installed, so this also reaches you from anywhere on your tailnet."
echo "" echo ""
isNotice "To exchange mail with the internet later, switch this app to public" isNotice "To exchange mail with the internet later, switch this app to public"

View File

@ -27,7 +27,7 @@ appStalwartSetMode() {
return 1 return 1
fi fi
local cfg="${containers_dir}stalwart/stalwart.config" local cfg="$(appDir stalwart)/stalwart.config"
if [[ ! -f "$cfg" ]]; then if [[ ! -f "$cfg" ]]; then
isError "Stalwart does not look installed — no config at $cfg" isError "Stalwart does not look installed — no config at $cfg"
return 1 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"}}}' \ 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") "$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 "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 " validate. Set CFG_STALWART_DNS_PROVIDER and a token to validate over"
isNotice " DNS instead — that is the only route that works alongside Traefik." isNotice " DNS instead — that is the only route that works alongside Traefik."

View File

@ -30,7 +30,7 @@
# advertised URL happens to name. # advertised URL happens to name.
_stoatApiLocal() { _stoatApiLocal() {
local app_name="${1:-stoat}" 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 local ports external
ports=$(tagsManagerGetTagContent "$compose" "PORTS_TAG_1" 2>/dev/null) ports=$(tagsManagerGetTagContent "$compose" "PORTS_TAG_1" 2>/dev/null)
external="${ports%%:*}" external="${ports%%:*}"

View File

@ -55,9 +55,9 @@ _stoatDomain()
_stoatBaseUrl() _stoatBaseUrl()
{ {
local app_name="$1" 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 local host
host=$(_stoatDomain) host=$(_stoatDomain)
[[ -n "$host" ]] && { echo "https://${host}"; return 0; } [[ -n "$host" ]] && { echo "https://${host}"; return 0; }
@ -226,7 +226,7 @@ _stoatOwnConfigFiles() {
stoat_install_post_compose() stoat_install_post_compose()
{ {
local app_name="$1" local app_name="$1"
local app_dir="$containers_dir$app_name" local app_dir="$(appDir "$app_name")"
((menu_number++)) ((menu_number++))
echo "" echo ""
@ -310,7 +310,7 @@ EOF
stoat_install_post_start() stoat_install_post_start()
{ {
local app_name="$1" 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 # 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 # 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 app_name="$1"
local result 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" checkSuccess "Created etc and certs & dynamic Directories"
result=$(copyResource "$app_name" "traefik.yml" "etc") result=$(copyResource "$app_name" "traefik.yml" "etc")
checkSuccess "Copy Traefik configuration file for $app_name" 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" checkSuccess "Configured Traefik debug level with: $CFG_TRAEFIK_LOGGING for $app_name"
configSetupFileWithData $app_name "traefik.yml" "etc" configSetupFileWithData $app_name "traefik.yml" "etc"
@ -45,8 +45,8 @@ traefik_install_post_compose()
# Apply CFG_TRAEFIK_DASHBOARD_ACCESS: rewrites api.insecure + the # Apply CFG_TRAEFIK_DASHBOARD_ACCESS: rewrites api.insecure + the
# `traefik:` entrypoint in traefik.yml, and (for local-only) prefixes # `traefik:` entrypoint in traefik.yml, and (for local-only) prefixes
# the compose port mapping with 127.0.0.1: so :8080 binds to loopback. # 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 traefik_yml="$(appDir "$app_name")/etc/traefik.yml"
local compose_yml="$containers_dir$app_name/docker-compose.yml" local compose_yml="$(appDir "$app_name")/docker-compose.yml"
local access="${CFG_TRAEFIK_DASHBOARD_ACCESS:-local-only}" local access="${CFG_TRAEFIK_DASHBOARD_ACCESS:-local-only}"
case "$access" in case "$access" in
@ -79,7 +79,7 @@ traefik_install_post_compose()
result=$(copyResource "$app_name" "config.yml" "etc/dynamic") result=$(copyResource "$app_name" "config.yml" "etc/dynamic")
checkSuccess "Copy Traefik Dynamic config.yml configuration file for $app_name" 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" checkSuccess "Configured Traefik error website with URL: $CFG_TRAEFIK_404_SITE for $app_name"
configSetupFileWithData $app_name "config.yml" "etc/dynamic" configSetupFileWithData $app_name "config.yml" "etc/dynamic"

View File

@ -13,7 +13,7 @@ trilium_install_post_start()
local trilium_timeout=10 local trilium_timeout=10
local trilium_counter=0 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 if [ "$trilium_counter" -ge "$trilium_timeout" ]; then
isNotice "File not found after 10 seconds. Exiting..." isNotice "File not found after 10 seconds. Exiting..."
return 0 return 0
@ -24,7 +24,7 @@ trilium_install_post_start()
done done
local result 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" checkSuccess "Configured $app_name from default 8080 to $usedport1"
dockerComposeRestart $app_name dockerComposeRestart $app_name

View File

@ -16,11 +16,11 @@ unbound_install_post_compose()
# A previous attempt that started the container before this file existed # 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 # 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. # 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 local result
result=$(copyResource "$app_name" "libreportal.conf" "conf.d" | runInstallWrite -a "$logs_dir/$docker_log_file" 2>&1) 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" 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 # outside the docker network generator range (100-149), so the two
# subnets cannot collide. Reusing the persisted value keeps existing # subnets cannot collide. Reusing the persisted value keeps existing
# peer configs valid across reinstalls. # 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 if [[ -z "$CFG_WIREGUARD_SUBNET" ]]; then
local _wg_second=$(( RANDOM % 51 + 200 )) local _wg_second=$(( RANDOM % 51 + 200 ))
local _wg_third=$(( RANDOM % 256 )) local _wg_third=$(( RANDOM % 256 ))
@ -57,7 +57,7 @@ wireguard_install_post_compose()
echo "---- $menu_number. Resolving WG_HOST for peer configs" echo "---- $menu_number. Resolving WG_HOST for peer configs"
echo "" 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_host_value=""
local _wg_traefik_installed=0 local _wg_traefik_installed=0
if declare -f checkServiceInstalled >/dev/null 2>&1 && checkServiceInstalled "traefik"; then if declare -f checkServiceInstalled >/dev/null 2>&1 && checkServiceInstalled "traefik"; then

View File

@ -7,9 +7,9 @@ appGetKeyData()
local key="$3" local key="$3"
# Check if the file exists # 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 # 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 # Extract the value using cut or awk
local value=$(echo "$key_line" | cut -d '=' -f 2) local value=$(echo "$key_line" | cut -d '=' -f 2)

View File

@ -13,7 +13,7 @@ appStatus()
return 1 return 1
fi fi
local app_dir="$containers_dir/$app_name" local app_dir="$(appDir "$app_name")"
local compose_file="$app_dir/docker-compose.yml" local compose_file="$app_dir/docker-compose.yml"
if [[ ! -f "$compose_file" ]]; then 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. # on showing the old one. Hence the explicit return 1 and warning below.
authPersistCfg() { authPersistCfg() {
local app="$1" key="$2" value="$3" 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" ]] && cfg="${install_containers_dir}/${app}/${app}.config"
[[ ! -f "$cfg" ]] && return 1 [[ ! -f "$cfg" ]] && return 1

View File

@ -212,7 +212,7 @@ installApp()
((menu_number++)) ((menu_number++))
echo "" 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 ""
# Final-message data — apps that want extra args (creds, URLs, etc.) # Final-message data — apps that want extra args (creds, URLs, etc.)

View File

@ -4,7 +4,7 @@ backupAppRunHook()
{ {
local app_name="$1" local app_name="$1"
local phase="$2" 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 [[ ! -f "$hook" ]] && return 0

View File

@ -10,8 +10,8 @@ backupAppStart()
return 1 return 1
fi fi
if [[ ! -d "$containers_dir$app_name" ]]; then if [[ ! -d "$(appDir "$app_name")" ]]; then
isError "Cannot back up '$app_name' — not installed at $containers_dir$app_name" isError "Cannot back up '$app_name' — not installed at $(appDir "$app_name")"
return 1 return 1
fi fi
@ -43,7 +43,7 @@ backupAppStart()
isNotice "Live strategy — containers stay running; databases dumped + private files captured via their containers" isNotice "Live strategy — containers stay running; databases dumped + private files captured via their containers"
if ! backupDbDump "$stored_app_name" || ! backupFilesCapture "$stored_app_name"; then if ! backupDbDump "$stored_app_name" || ! backupFilesCapture "$stored_app_name"; then
isError "Live capture failed — falling back to stop-snapshot-start for safety" 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" strategy="stop-snapshot-start"
dockerComposeDown "$stored_app_name" dockerComposeDown "$stored_app_name"
fi fi

View File

@ -43,7 +43,7 @@ backup_db_dump_subdir=".lp-backup/db"
backupDbDescriptors() backupDbDescriptors()
{ {
local app="$1" local app="$1"
local compose="$containers_dir$app/docker-compose.yml" local compose="$(appDir "$app")/docker-compose.yml"
[[ -f "$compose" ]] || return 0 [[ -f "$compose" ]] || return 0
grep -E '^[[:space:]]*libreportal\.backup\.db[[:space:]]*:' "$compose" 2>/dev/null \ grep -E '^[[:space:]]*libreportal\.backup\.db[[:space:]]*:' "$compose" 2>/dev/null \
@ -65,7 +65,7 @@ backupDbHasDescriptors()
backupAppIsLiveSafe() backupAppIsLiveSafe()
{ {
local app="$1" local app="$1"
local compose="$containers_dir$app/docker-compose.yml" local compose="$(appDir "$app")/docker-compose.yml"
[[ -f "$compose" ]] || return 1 [[ -f "$compose" ]] || return 1
if grep -qE '^[[:space:]]*libreportal\.backup\.live[[:space:]]*:[[:space:]]*["'\'']?true' "$compose" 2>/dev/null; then if grep -qE '^[[:space:]]*libreportal\.backup\.live[[:space:]]*:[[:space:]]*["'\'']?true' "$compose" 2>/dev/null; then
return 0 return 0
@ -204,7 +204,7 @@ _backupDbImport()
backupDbDump() backupDbDump()
{ {
local app="$1" local app="$1"
local app_dir="$containers_dir$app" local app_dir="$(appDir "$app")"
local dump_dir="$app_dir/$backup_db_dump_subdir" local dump_dir="$app_dir/$backup_db_dump_subdir"
local desc kind container datadir path dump rc=0 local desc kind container datadir path dump rc=0
@ -286,7 +286,7 @@ backupDbDump()
backupDbExcludePaths() backupDbExcludePaths()
{ {
local app="$1" local app="$1"
local app_dir="$containers_dir$app" local app_dir="$(appDir "$app")"
local desc kind container datadir path local desc kind container datadir path
while IFS= read -r desc; do while IFS= read -r desc; do
@ -315,7 +315,7 @@ backupDbExcludePaths()
restoreDbRehydratePreStart() restoreDbRehydratePreStart()
{ {
local app="$1" local app="$1"
local app_dir="$containers_dir$app" local app_dir="$(appDir "$app")"
local dump_dir="$app_dir/$backup_db_dump_subdir" local dump_dir="$app_dir/$backup_db_dump_subdir"
local desc kind container datadir path dump local desc kind container datadir path dump
@ -350,7 +350,7 @@ restoreDbRehydratePreStart()
restoreDbReplayPostStart() restoreDbReplayPostStart()
{ {
local app="$1" local app="$1"
local app_dir="$containers_dir$app" local app_dir="$(appDir "$app")"
local dump_dir="$app_dir/$backup_db_dump_subdir" local dump_dir="$app_dir/$backup_db_dump_subdir"
local desc kind container datadir path dump local desc kind container datadir path dump

View File

@ -5,7 +5,7 @@ borgBackupAppToLocation()
local idx="$1" local idx="$1"
local app_name="$2" local app_name="$2"
local manifest_sha="$3" local manifest_sha="$3"
local source_path="$containers_dir$app_name" local source_path="$(appDir "$app_name")"
if [[ ! -d "$source_path" ]]; then if [[ ! -d "$source_path" ]]; then
isError "Source path missing for $app_name: $source_path" isError "Source path missing for $app_name: $source_path"

View File

@ -5,7 +5,7 @@ kopiaBackupAppToLocation()
local idx="$1" local idx="$1"
local app_name="$2" local app_name="$2"
local manifest_sha="$3" local manifest_sha="$3"
local source_path="$containers_dir$app_name" local source_path="$(appDir "$app_name")"
if [[ ! -d "$source_path" ]]; then if [[ ! -d "$source_path" ]]; then
isError "Source path missing for $app_name: $source_path" isError "Source path missing for $app_name: $source_path"

View File

@ -23,7 +23,7 @@ kopiaForgetApp()
kopiaEnvExport "$idx" || return 1 kopiaEnvExport "$idx" || return 1
local src="$containers_dir$app_name" local src="$(appDir "$app_name")"
local host_tag="${CFG_INSTALL_NAME:-libreportal}" local host_tag="${CFG_INSTALL_NAME:-libreportal}"
local policy_args=(policy set --global=false "$src") local policy_args=(policy set --global=false "$src")

View File

@ -5,7 +5,7 @@ resticBackupAppToLocation()
local idx="$1" local idx="$1"
local app_name="$2" local app_name="$2"
local manifest_sha="$3" local manifest_sha="$3"
local source_path="$containers_dir$app_name" local source_path="$(appDir "$app_name")"
if [[ ! -d "$source_path" ]]; then if [[ ! -d "$source_path" ]]; then
isError "Source path missing for $app_name: $source_path" isError "Source path missing for $app_name: $source_path"

View File

@ -118,7 +118,7 @@ resticRestoreAppLatest()
return 1 return 1
fi fi
local include_path="$containers_dir$app_name" local include_path="$(appDir "$app_name")"
resticRestoreSnapshot "$idx" "$snapshot_id" "$target_dir" "$include_path" resticRestoreSnapshot "$idx" "$snapshot_id" "$target_dir" "$include_path"
} }

View File

@ -49,7 +49,7 @@ backup_files_helper_image="busybox"
backupFilesDescriptors() backupFilesDescriptors()
{ {
local app="$1" local app="$1"
local compose="$containers_dir$app/docker-compose.yml" local compose="$(appDir "$app")/docker-compose.yml"
[[ -f "$compose" ]] || return 0 [[ -f "$compose" ]] || return 0
grep -E '^[[:space:]]*libreportal\.backup\.files[[:space:]]*:' "$compose" 2>/dev/null \ grep -E '^[[:space:]]*libreportal\.backup\.files[[:space:]]*:' "$compose" 2>/dev/null \
@ -71,7 +71,7 @@ backupFilesHasDescriptors()
backupFilesCapture() backupFilesCapture()
{ {
local app="$1" local app="$1"
local app_dir="$containers_dir$app" local app_dir="$(appDir "$app")"
local desc container cpath subdir uid gid stage rc=0 local desc container cpath subdir uid gid stage rc=0
backupFilesHasDescriptors "$app" || return 0 backupFilesHasDescriptors "$app" || return 0
@ -148,7 +148,7 @@ backupFilesCapture()
backupFilesExcludePaths() backupFilesExcludePaths()
{ {
local app="$1" local app="$1"
local app_dir="$containers_dir$app" local app_dir="$(appDir "$app")"
local desc container cpath subdir uid gid local desc container cpath subdir uid gid
while IFS= read -r desc; do while IFS= read -r desc; do
@ -165,7 +165,7 @@ backupFilesExcludePaths()
restoreFilesRehydratePreStart() restoreFilesRehydratePreStart()
{ {
local app="$1" local app="$1"
local app_dir="$containers_dir$app" local app_dir="$(appDir "$app")"
local desc container cpath subdir uid gid stage local desc container cpath subdir uid gid stage
backupFilesHasDescriptors "$app" || return 0 backupFilesHasDescriptors "$app" || return 0

View File

@ -3,7 +3,7 @@
manifestCollect() manifestCollect()
{ {
local app_name="$1" local app_name="$1"
local app_dir="$containers_dir$app_name" local app_dir="$(appDir "$app_name")"
local libreportal_commit local libreportal_commit
libreportal_commit=$(git -C "${install_scripts_dir%/scripts/}" rev-parse --short HEAD 2>/dev/null || echo "unknown") 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 snapshot_id="$2"
local app_name="$3" 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 engineDumpFile "$repo_name" "$snapshot_id" "$manifest_path" 2>/dev/null
} }

View File

@ -3,7 +3,7 @@
manifestWrite() manifestWrite()
{ {
local app_name="$1" 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" local manifest_path="$app_dir/.libreportal-manifest.json"
if [[ ! -d "$app_dir" ]]; then if [[ ! -d "$app_dir" ]]; then
@ -25,6 +25,6 @@ manifestWrite()
manifestRemove() manifestRemove()
{ {
local app_name="$1" 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" [[ -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" 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")" isError "Verify restore FAILED for $app_name on $(resticLocationName "$idx")"
runFileOp rm -rf "$scratch" runFileOp rm -rf "$scratch"
return 1 return 1
@ -31,7 +31,7 @@ backupVerifySnapshot()
# and replaced by dumps/captures under .lp-backup) — so just sanity-check the # and replaced by dumps/captures under .lp-backup) — so just sanity-check the
# restore produced a non-empty tree. # restore produced a non-empty tree.
local restored_count 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" runFileOp rm -rf "$scratch"

View File

@ -27,7 +27,7 @@
# never raw sudo. The install tree (our own code) is off-limits to hotfixes. # never raw sudo. The install tree (our own code) is off-limits to hotfixes.
# --- paths ------------------------------------------------------------------- # --- paths -------------------------------------------------------------------
_artifactGenDir() { echo "${containers_dir%/}/libreportal/frontend/data/updater/generated"; } _artifactGenDir() { echo "$(webuiDir)/frontend/data/updater/generated"; }
_artifactAppliedDir() { echo "$(_artifactGenDir)/applied"; } _artifactAppliedDir() { echo "$(_artifactGenDir)/applied"; }
_artifactRecordFile() { echo "$(_artifactAppliedDir)/$1.json"; } # $1=id _artifactRecordFile() { echo "$(_artifactAppliedDir)/$1.json"; } # $1=id
@ -116,7 +116,7 @@ _artifactResolve() {
if [[ ! "$_ART_APP" =~ ^[a-z0-9][a-z0-9_]{0,31}$ ]]; then 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 isError "artifact: app artifact '$id' has a missing/unsafe applies_when.app slug -- refusing."; return 1
fi 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 isNotice "artifact: '$id' targets '$_ART_APP' which is not installed -- not applicable."; return 2
fi fi
@ -210,7 +210,7 @@ _artifactPathAllowed() {
[[ "$path" =~ ^[A-Za-z0-9._/@:+-]+$ ]] || return 1 [[ "$path" =~ ^[A-Za-z0-9._/@:+-]+$ ]] || return 1
real="$(realpath -m -- "$path" 2>/dev/null)"; [[ -n "$real" ]] || return 1 real="$(realpath -m -- "$path" 2>/dev/null)"; [[ -n "$real" ]] || return 1
if [[ "$scope" == "app" ]]; then 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 [[ "$real" == "$root/"* ]] && return 0
return 1 return 1
fi fi
@ -221,7 +221,7 @@ _artifactPathAllowed() {
# current image of an app's compose (first image: line), quotes/comment stripped. # current image of an app's compose (first image: line), quotes/comment stripped.
_artifactComposeImage() { _artifactComposeImage() {
local app="$1" f="${containers_dir%/}/$1/docker-compose.yml" local app="$1" f="$(appDir "$1")/docker-compose.yml"
[[ -f "$f" ]] || return 1 [[ -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+$//' 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() { _artifactApplyBundleFlow() {
local id="$1" art="$2" serial="$3" local id="$1" art="$2" serial="$3"
local slug="$_ART_APP" local slug="$_ART_APP"
local live_dir="${containers_dir%/}/$slug" local live_dir="$(appDir "$slug")"
local def_root="${install_containers_dir%/}" local def_root="${install_containers_dir%/}"
local def_dir="$def_root/$slug" local def_dir="$def_root/$slug"
@ -388,7 +388,7 @@ _artifactApplyBundleFlow() {
if [[ "$placed" == "1" ]]; then if [[ "$placed" == "1" ]]; then
isNotice "Definition placed -- refreshing the App Center catalog..." isNotice "Definition placed -- refreshing the App Center catalog..."
{ declare -F lpRegenWebui >/dev/null 2>&1 && lpRegenWebui force >/dev/null 2>&1; } || true { 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 jq -e --arg app "$slug" '.apps[]? | select((.command // "") | endswith(" " + $app))' "$apps_json" >/dev/null 2>&1 || placed=0
fi fi
@ -457,7 +457,7 @@ _artifactOpPrecheck() {
image="$(jq -r '.image // empty' <<<"$op_json")" image="$(jq -r '.image // empty' <<<"$op_json")"
{ _artifactSafeScalar "$from" && _artifactSafeScalar "$image"; } || { isError "op set-compose-image: image/from has an unsafe character"; return 1; } { _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; } [[ "$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")" cur="$(_artifactComposeImage "$app")"
[[ -n "$from" && "$cur" == "$from" ]] || { isError "op set-compose-image: image is '$cur', expected '$from' (drift)"; return 1; } [[ -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) set-compose-image)
local image cur f esc_img local image cur f esc_img
image="$(jq -r '.image' <<<"$op_json")" image="$(jq -r '.image' <<<"$op_json")"
f="${containers_dir%/}/$app/docker-compose.yml" f="$(appDir "$app")/docker-compose.yml"
cur="$(_artifactComposeImage "$app")" cur="$(_artifactComposeImage "$app")"
esc_img="$(printf '%s' "$image" | sed -e 's/[\/&]/\\&/g')" esc_img="$(printf '%s' "$image" | sed -e 's/[\/&]/\\&/g')"
runFileOp sed -i "0,/^\([[:space:]]*\)image:.*/s//\1image: $esc_img/" "$f" || return 1 runFileOp sed -i "0,/^\([[:space:]]*\)image:.*/s//\1image: $esc_img/" "$f" || return 1
@ -547,7 +547,7 @@ _artifactOpUndo() {
case "$op" in case "$op" in
set-compose-image) set-compose-image)
local image f esc_img; image="$(jq -r '.image' <<<"$op_json")" 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')" esc_img="$(printf '%s' "$image" | sed -e 's/[\/&]/\\&/g')"
runFileOp sed -i "0,/^\([[:space:]]*\)image:.*/s//\1image: $esc_img/" "$f" || return 1 runFileOp sed -i "0,/^\([[:space:]]*\)image:.*/s//\1image: $esc_img/" "$f" || return 1
;; ;;
@ -567,7 +567,7 @@ _artifactOpUndo() {
local slug live def local slug live def
slug="$(jq -r '.app // empty' <<<"$op_json")" slug="$(jq -r '.app // empty' <<<"$op_json")"
[[ "$slug" =~ ^[a-z0-9][a-z0-9_]{0,31}$ ]] || { isError "undo: unsafe app slug '$slug'"; return 1; } [[ "$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 if [[ -d "$live" ]]; then
isError "undo: app '$slug' is INSTALLED -- uninstall it before removing its definition."; return 1 isError "undo: app '$slug' is INSTALLED -- uninstall it before removing its definition."; return 1
fi fi
@ -845,7 +845,7 @@ artifactApplyAuto() {
# skip if app-scoped but the app isn't installed (applicable gate; full # skip if app-scoped but the app isn't installed (applicable gate; full
# gates re-checked at apply time) # gates re-checked at apply time)
app="$(jq -r '.applies_when.app // empty' <<<"$art")" 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 # --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- # the new task in the foreground would wait for a task the (single-
# threaded) processor can't start until we return. # threaded) processor can't start until we return.

View File

@ -73,7 +73,7 @@ cliHandleArtifactCommands()
artifactListApplied() artifactListApplied()
{ {
isHeader "Applied hotfixes" 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 if ! compgen -G "$dir/*.json" >/dev/null 2>&1; then
isSuccessful "0 hotfixes applied." isSuccessful "0 hotfixes applied."
return 0 return 0

View File

@ -186,7 +186,7 @@ lpNotifyTaskResult()
[[ "$events" == "off" ]] && return 0 [[ "$events" == "off" ]] && return 0
lpNotifyAnyChannelEnabled || 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 [[ -f "$f" ]] || return 0
local status type app exit_code local status type app exit_code
if command -v jq >/dev/null 2>&1; then 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 # build changes the digest and is attempted normally; the Update button
# stays available for a manual retry of the skipped one. # 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"; } _updaterAutoDir() { echo "$(_updaterAutoGenDir)/auto"; }
_updaterAutoStamp() { echo "$(_updaterAutoDir)/$1.digest"; } # $1=app _updaterAutoStamp() { echo "$(_updaterAutoDir)/$1.digest"; } # $1=app
@ -84,7 +84,7 @@ updaterAppPolicy()
# this far (update available + policy auto). # this far (update available + policy auto).
updaterAutoTaskPending() 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 command -v jq >/dev/null 2>&1 || return 1
local files=( "$dir"/task_*.json ) local files=( "$dir"/task_*.json )
[[ -e "${files[0]}" ]] || return 1 [[ -e "${files[0]}" ]] || return 1

View File

@ -35,7 +35,7 @@ cliHandleUpdaterCommands()
local scan_interval="${CFG_UPDATER_SCAN_INTERVAL:-30}" local scan_interval="${CFG_UPDATER_SCAN_INTERVAL:-30}"
[[ "$scan_interval" =~ ^[0-9]+$ ]] || scan_interval=30 [[ "$scan_interval" =~ ^[0-9]+$ ]] || scan_interval=30
(( scan_interval == 0 )) && return 0 (( 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 if [[ -f "$scan_file" ]]; then
local _now _last; _now=$(date +%s); _last=$(stat -c '%Y' "$scan_file" 2>/dev/null || echo 0) local _now _last; _now=$(date +%s); _last=$(stat -c '%Y' "$scan_file" 2>/dev/null || echo 0)
(( _now - _last < scan_interval * 60 )) && return 0 (( _now - _last < scan_interval * 60 )) && return 0
@ -174,7 +174,7 @@ updaterRefDigest()
updaterSetAnchorRef() updaterSetAnchorRef()
{ {
local app="$1" newref="$2" local app="$1" newref="$2"
local compose="${containers_dir%/}/$app/docker-compose.yml" local compose="$(appDir "$app")/docker-compose.yml"
[ -f "$compose" ] || return 1 [ -f "$compose" ] || return 1
local tmp; tmp="$(mktemp)" local tmp; tmp="$(mktemp)"
awk -v s="${app//_/-}-service" -v ref="$newref" ' awk -v s="${app//_/-}-service" -v ref="$newref" '
@ -215,7 +215,7 @@ updaterApplyApp()
# "manual" when a person pressed Update. Recorded in History; changes nothing # "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. # about how the update is applied — both take the snapshot, both can roll back.
local trigger="${2:-manual}" 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 [[ ! -d "$app_dir" ]]; then isError "App '$_upd_app' is not installed."; return 1; fi
if [[ "$trigger" == "auto" ]]; then if [[ "$trigger" == "auto" ]]; then
@ -317,7 +317,7 @@ updaterRollbackApp()
# explicitly first to actually pick up a new image. # explicitly first to actually pick up a new image.
updaterComposePull() updaterComposePull()
{ {
local app="$1" dir="${containers_dir%/}/$1" local app="$1" dir="$(appDir "$1")"
[ -d "$dir" ] || return 1 [ -d "$dir" ] || return 1
if [[ "$CFG_DOCKER_INSTALL_TYPE" == "rootless" ]]; then if [[ "$CFG_DOCKER_INSTALL_TYPE" == "rootless" ]]; then
dockerCommandRunInstallUser "cd $dir && docker compose pull" >/dev/null 2>&1 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 # after reading release notes. The updater surfaces "34 available"; this runs
# only when asked. # 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 # Rewrite the anchor image AND every image locked in step with it, plus their
# version sentinels, so the live compose stays self-consistent. # 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. # return before this point.
updaterSetAnchorVersion() { updaterSetAnchorVersion() {
local app="$1" newtag="$2" local app="$1" newtag="$2"
local compose="${containers_dir%/}/$app/docker-compose.yml" local compose="$(appDir "$app")/docker-compose.yml"
[ -f "$compose" ] || return 1 [ -f "$compose" ] || return 1
local up; up="$(printf '%s' "$app" | tr '[:lower:]' '[:upper:]')" 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. # Current anchor tag for an app, straight from its live compose.
updaterCurrentTag() { updaterCurrentTag() {
local app="$1" local app="$1"
local compose="${containers_dir%/}/$app/docker-compose.yml" local compose="$(appDir "$app")/docker-compose.yml"
[ -f "$compose" ] || return 1 [ -f "$compose" ] || return 1
updaterTagOf "$(updaterPrimaryImage "$app" "$compose")" updaterTagOf "$(updaterPrimaryImage "$app" "$compose")"
} }
@ -129,7 +129,7 @@ updaterUpgradeApp() {
local app="$1" target="${2:-}" mode="${3:-}" local app="$1" target="${2:-}" mode="${3:-}"
[ "$target" = "--dry-run" ] && { mode="--dry-run"; target=""; } [ "$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; } [ -d "$app_dir" ] || { isError "App '$app' is not installed."; return 1; }
local cur; cur="$(updaterCurrentTag "$app")" local cur; cur="$(updaterCurrentTag "$app")"
@ -233,7 +233,7 @@ _updaterUpgradePruneImages() {
(( ${#climbed[@]} >= 2 )) || return 0 # one step: previous IS the rollback target (( ${#climbed[@]} >= 2 )) || return 0 # one step: previous IS the rollback target
local anchor repo 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%:*}" repo="$(updaterRepoTag "$anchor")"; repo="${repo%:*}"
# Everything we moved off, minus the last one (kept for rollback). # 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. # most need verifying. Falls back to the convention when there is no compose.
_updaterPrimaryContainer() { _updaterPrimaryContainer() {
local app="$1" 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//_/-}")" local fallback; fallback="$(printf '%s-service' "${app//_/-}")"
[ -f "$compose" ] || { printf '%s' "$fallback"; return 0; } [ -f "$compose" ] || { printf '%s' "$fallback"; return 0; }

View File

@ -12,7 +12,7 @@ editAppConfig()
fi fi
# Use find to search for the app_name folder within $containers_dir # 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 if [ -n "$app_dir" ]; then
local config_file="$app_dir/$app_name.config" local config_file="$app_dir/$app_name.config"

View File

@ -7,12 +7,12 @@ viewAppConfigs()
# Get all installed apps from containers directory # Get all installed apps from containers directory
local installed_apps=() local installed_apps=()
for app_dir in "$containers_dir"/*/; do while IFS= read -r app_dir; do
if [ -d "$app_dir" ]; then if [ -d "$app_dir" ]; then
local app_name=$(basename "$app_dir") local app_name=$(basename "$app_dir")
installed_apps+=("$app_name") installed_apps+=("$app_name")
fi fi
done done < <(storageAppDirs)
if [ ${#installed_apps[@]} -eq 0 ]; then if [ ${#installed_apps[@]} -eq 0 ]; then
isNotice "No installed applications found." isNotice "No installed applications found."
@ -47,7 +47,7 @@ viewAppConfigs()
local selected_app="${installed_apps[index]}" local selected_app="${installed_apps[index]}"
# Get the config file for this app # 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 if [ -f "$config_file" ]; then
$CFG_TEXT_EDITOR "$config_file" $CFG_TEXT_EDITOR "$config_file"

View File

@ -12,7 +12,7 @@ viewAppCategoryConfigs()
local other_apps=() local other_apps=()
# Collect all app_name folders and categorize them into installed and others # 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 if [ -d "$app_dir" ]; then
local app_name=$(basename "$app_dir") local app_name=$(basename "$app_dir")
local app_config_file="$install_containers_dir$app_name/$app_name.sh" local app_config_file="$install_containers_dir$app_name/$app_name.sh"
@ -30,7 +30,7 @@ viewAppCategoryConfigs()
fi fi
fi fi
fi fi
done done < <(storageAppDirs)
if [[ ${#installed_apps[@]} -eq 0 && ${#other_apps[@]} -eq 0 ]]; then if [[ ${#installed_apps[@]} -eq 0 && ${#other_apps[@]} -eq 0 ]]; then
echo "" echo ""

View File

@ -9,12 +9,12 @@ checkApplicationsConfigFilesMissingVariables()
app=$(basename "$live" .config) app=$(basename "$live" .config)
remote="$install_containers_dir$app/$app.config" remote="$install_containers_dir$app/$app.config"
reconcileConfigFile "$live" "$remote" reconcileConfigFile "$live" "$remote"
# 2>/dev/null on the find: app data dirs (e.g. invidious/postgresdata, # storageAppConfigs walks every storage root as the container user and
# nextcloud/html) are owned by container sub-UIDs that the manager user # yields only <slug>/<slug>.config, so app data dirs owned by container
# can't read into, and they're harmless permission-denied chatter — find # sub-UIDs (invidious/postgresdata, nextcloud/html) are never descended
# at this maxdepth doesn't need to enter them to satisfy -name '*.config' # into — no permission-denied chatter, and no payload *.config mistaken
# at depth 2. Keep the noise out of CLI/log output. # for an app.
done < <(runFileOp find "$containers_dir" -maxdepth 2 -type f -name '*.config' ! -name '*.bak' 2>/dev/null) done < <(storageAppConfigs)
isSuccessful "Application config reconciliation completed." isSuccessful "Application config reconciliation completed."
} }

View File

@ -11,9 +11,9 @@ configSetupFileWithData()
fi fi
if [[ $custom_path == "" ]]; then if [[ $custom_path == "" ]]; then
local file_path="$containers_dir$app_name" local file_path="$(appDir "$app_name")"
elif [[ $custom_path != "" ]]; then elif [[ $custom_path != "" ]]; then
local file_path="$containers_dir$app_name/$custom_path/" local file_path="$(appDir "$app_name")/$custom_path/"
fi fi
local full_file_path="$file_path/$file_name" local full_file_path="$file_path/$file_name"

View File

@ -34,7 +34,7 @@ findConfigFileForOption()
echo "$app_config" echo "$app_config"
return 0 return 0
fi fi
done < <(runFileOp find "${containers_dir%/}" -mindepth 2 -maxdepth 2 -type f -name '*.config' 2>/dev/null) done < <(storageAppConfigs)
fi fi
return 1 return 1
} }

View File

@ -129,7 +129,7 @@ configBackfillAllApps()
[[ "$app" == "template" ]] && continue [[ "$app" == "template" ]] && continue
template="${dir}${app}.config" template="${dir}${app}.config"
deployed="${containers_dir}${app}/${app}.config" deployed="$(appDir "$app")/${app}.config"
[[ -f "$template" ]] || continue [[ -f "$template" ]] || continue
runFileOp test -f "$deployed" || continue runFileOp test -f "$deployed" || continue

View File

@ -11,13 +11,13 @@ viewComposeFiles()
echo "" echo ""
# Find all subdirectories under $containers_dir # 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 if [[ -d "$app_dir" ]]; then
# Extract the app name (folder name) # Extract the app name (folder name)
local app_name=$(basename "$app_dir") local app_name=$(basename "$app_dir")
local app_names+=("$app_name") local app_names+=("$app_name")
fi fi
done done < <(storageAppDirs)
# Check if any apps were found # Check if any apps were found
if [ ${#app_names[@]} -eq 0 ]; then if [ ${#app_names[@]} -eq 0 ]; then
@ -41,7 +41,7 @@ viewComposeFiles()
# Check if the selected option is a valid number # Check if the selected option is a valid number
if ((selected_option >= 1 && selected_option <= ${#app_names[@]})); then if ((selected_option >= 1 && selected_option <= ${#app_names[@]})); then
local selected_app="${app_names[selected_option - 1]}" 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 # List Docker Compose files in the selected app's folder
echo "" echo ""

View File

@ -10,7 +10,7 @@ dockerConfigSetupFileWithData()
local file_name="docker-compose.$app_name.yml"; local file_name="docker-compose.$app_name.yml";
fi fi
local file_path="$containers_dir$app_name" local file_path="$(appDir "$app_name")"
local full_file_path="$file_path/$file_name" local full_file_path="$file_path/$file_name"
if command -v sqlite3 &> /dev/null && [[ -f "$docker_dir/$db_file" ]]; then if command -v sqlite3 &> /dev/null && [[ -f "$docker_dir/$db_file" ]]; then

View File

@ -7,7 +7,7 @@ dockerConfigSetupToContainer()
local flags="$3" local flags="$3"
local config_overrides="$4" 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 source_file="$install_containers_dir$app_name/$app_name.config"
local config_file="$app_name.config" local config_file="$app_name.config"

View File

@ -7,7 +7,7 @@ exportBcryptPassword()
local placeholder="$2" local placeholder="$2"
local raw_password="$3" local raw_password="$3"
local file="$4" # File where the placeholder was found 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. # bcrypt.txt lives under containers_dir (docker-install-owned) -> runFileOp.
if [ ! -f "$log_file" ]; then if [ ! -f "$log_file" ]; then

View File

@ -4,7 +4,7 @@ getStoredPassword()
{ {
local app_name="$1" local app_name="$1"
local variable_name="$2" local variable_name="$2"
local log_file="$containers_dir/bcrypt.txt" local log_file="$(primaryRoot)/bcrypt.txt"
if [ -f "$log_file" ]; then if [ -f "$log_file" ]; then
runFileOp grep "^$app_name $variable_name " "$log_file" | awk '{print $3}' | tail -n 1 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" checkSuccess "Install path not found or not a directory: $containers_dir"
fi fi
# Scan the folder and retrieve folder names # Scan every storage root. storageApps SKIPS a root whose drive is absent,
local folder_names=$(runFileOp find "$containers_dir" -mindepth 1 -maxdepth 1 -type d -exec basename {} \;) # 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 # Check if no folders are found
if [ -z "$folder_names" ]; then if [ -z "$folder_names" ]; then
@ -38,8 +43,8 @@ databaseAppScan()
while IFS='|' read -r folder_name status uninstall_date; do while IFS='|' read -r folder_name status uninstall_date; do
if [[ -n "$folder_name" ]]; then if [[ -n "$folder_name" ]]; then
existing_folder_names+=("$folder_name") existing_folder_names+=("$folder_name")
# Check if the folder exists in the containers_dir # Check if the folder exists on whichever storage root holds it
if [ -d "$containers_dir/$folder_name" ]; then if [ -d "$(appDir "$folder_name")" ]; then
if (( status == 0 )); then if (( status == 0 )); then
isNotice "The folder for $folder_name has been found." isNotice "The folder for $folder_name has been found."
# Update the database to set the status to 1 (installed) and unset the uninstall_date # Update the database to set the status to 1 (installed) and unset the uninstall_date
@ -51,8 +56,8 @@ databaseAppScan()
fi fi
done <<< "$existing_folders" done <<< "$existing_folders"
# Loop through immediate subdirectories of $containers_dir # Loop through every app directory on every available storage root
for app_dir in "$containers_dir"/*/; do while IFS= read -r app_dir; do
# Get the app name from the folder name # Get the app name from the folder name
local app_name=$(basename "$app_dir") local app_name=$(basename "$app_dir")
@ -77,21 +82,27 @@ databaseAppScan()
((updated_count++)) # Increment updated_count ((updated_count++)) # Increment updated_count
fi fi
fi fi
done done < <(storageAppDirs)
# Create an array to store folder names that should be removed from the database # Create an array to store folder names that should be removed from the database
local folders_to_remove=() local folders_to_remove=()
# Get a list of folder names that exist in the database but not in the current folder structure # 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 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") local folders_to_remove+=("$folder_name")
fi fi
done done
# Get a list of folder names that exist in the database but not in the current folder structure # 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 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 # Check if this folder is actually associated with an entry in the database
if [[ " ${folder_names[@]} " =~ " $folder_name " ]]; then if [[ " ${folder_names[@]} " =~ " $folder_name " ]]; then
isNotice "Folder $folder_name no longer exists. Removing from the Database." isNotice "Folder $folder_name no longer exists. Removing from the Database."
@ -119,7 +130,7 @@ databaseAppScan()
# when there was no data to wipe. # when there was no data to wipe.
for folder_name in $folder_names; do for folder_name in $folder_names; do
[[ "$folder_name" == "libreportal" ]] && continue [[ "$folder_name" == "libreportal" ]] && continue
local folder_path="$containers_dir/$folder_name" local folder_path="$(appDir "$folder_name")"
if [ ! -d "$folder_path" ]; then if [ ! -d "$folder_path" ]; then
isNotice "Folder $folder_name no longer exists — removing it from the database." 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 # row by matching service+name, then extract its url_path
# (empty when the port has no web UI, e.g. DNS). # (empty when the port has no web UI, e.g. DNS).
local _port_url_path="" 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" [[ ! -f "$_url_cfg" ]] && _url_cfg="${install_containers_dir}/${app_name}/${app_name}.config"
if [[ -f "$_url_cfg" ]]; then if [[ -f "$_url_cfg" ]]; then
while IFS='=' read -r _vn _vv || [[ -n "$_vn" ]]; do 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) # runFileOp: containers/ is owned by the container user (dockerinstall, 751)
# under rootless and isn't list-readable by the manager — enumerate as the # 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. # 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 for dir in "${subdirectories[@]}"; do
local app_name=$(basename "$dir") local app_name=$(basename "$dir")

View File

@ -31,7 +31,7 @@ dockerComposeDown()
fi fi
if [[ "$OS_TYPE" == "Ubuntu" || "$OS_TYPE" == "Debian" ]]; then 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." isNotice "Unable to find the compose file to docker compose down this application."
return 1 return 1
fi fi
@ -41,10 +41,10 @@ dockerComposeDown()
# header is never left without output. # header is never left without output.
local mode="${type:-$CFG_DOCKER_INSTALL_TYPE}" local mode="${type:-$CFG_DOCKER_INSTALL_TYPE}"
if [[ $mode == "rootless" ]]; then 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" checkSuccess "Shutting down container for $app_name"
elif [[ $mode == "rooted" ]]; then 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" checkSuccess "Shutting down container for $app_name"
else else
isNotice "Unknown Docker install type '$mode' — cannot shut down $app_name." 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) # runFileOp: containers/ is owned by the container user (dockerinstall, 751)
# under rootless and isn't list-readable by the manager — enumerate as the # under rootless and isn't list-readable by the manager — enumerate as the
# owner or this silently finds nothing and no apps come up. # 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 for dir in "${subdirectories[@]}"; do
local app_name=$(basename "$dir") local app_name=$(basename "$dir")

View File

@ -57,7 +57,7 @@ dockerComposeUp()
fi fi
if [[ "$OS_TYPE" == "Ubuntu" || "$OS_TYPE" == "Debian" ]]; then 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. # Quiet pull + plain progress so progress redraws don't flood the log.
local _compose_quiet="--quiet-pull" local _compose_quiet="--quiet-pull"
export COMPOSE_PROGRESS=plain export COMPOSE_PROGRESS=plain
@ -66,7 +66,7 @@ dockerComposeUp()
# ignores any edits to Dockerfile / source between installs. # ignores any edits to Dockerfile / source between installs.
local _compose_build_flag="" local _compose_build_flag=""
local _is_local_build=0 local _is_local_build=0
if [[ -f "$containers_dir$app_name/Dockerfile" ]]; then if [[ -f "$(appDir "$app_name")/Dockerfile" ]]; then
_compose_build_flag="--build" _compose_build_flag="--build"
_is_local_build=1 _is_local_build=1
fi fi
@ -103,7 +103,7 @@ dockerComposeUp()
} }
' "$1" ' "$1"
} }
local _compose_path="$containers_dir$app_name/$compose_file" local _compose_path="$(appDir "$app_name")/$compose_file"
local _stale_tags local _stale_tags
_stale_tags=$(_scanStaleTags "$_compose_path") _stale_tags=$(_scanStaleTags "$_compose_path")
if [[ -n "$_stale_tags" ]]; then if [[ -n "$_stale_tags" ]]; then
@ -141,7 +141,7 @@ dockerComposeUp()
fi fi
if [[ $CFG_DOCKER_INSTALL_TYPE == "rootless" ]]; then if [[ $CFG_DOCKER_INSTALL_TYPE == "rootless" ]]; then
isNotice "Starting container for $app_name, this may take a while..." 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=$? _rc=$?
_upReportComposeFailure "$app_name" "$_rc" "$result" _upReportComposeFailure "$app_name" "$_rc" "$result"
# Restore $? to the compose exit code — a bare `checkSuccess` # Restore $? to the compose exit code — a bare `checkSuccess`
@ -150,7 +150,7 @@ dockerComposeUp()
( exit "$_rc" ); checkSuccess "Started container for $app_name" ( exit "$_rc" ); checkSuccess "Started container for $app_name"
elif [[ $CFG_DOCKER_INSTALL_TYPE == "rooted" ]]; then elif [[ $CFG_DOCKER_INSTALL_TYPE == "rooted" ]]; then
isNotice "Starting container for $app_name, this may take a while..." 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=$? _rc=$?
_upReportComposeFailure "$app_name" "$_rc" "$result" _upReportComposeFailure "$app_name" "$_rc" "$result"
( exit "$_rc" ); checkSuccess "Started container for $app_name" ( exit "$_rc" ); checkSuccess "Started container for $app_name"
@ -158,10 +158,10 @@ dockerComposeUp()
# Used for the CLI dockertype switcher. # Used for the CLI dockertype switcher.
else else
if [[ $type == "rootless" ]]; then 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" checkSuccess "Shutting down container for $app_name"
elif [[ $type == "rooted" ]]; then 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" checkSuccess "Shutting down container for $app_name"
fi fi
fi fi

View File

@ -20,7 +20,7 @@ dockerRestartApp()
isError "Invalid service name: $service_name" isError "Invalid service name: $service_name"
return 1 return 1
fi fi
local app_dir="${containers_dir%/}/$app_name" local app_dir="$(appDir "$app_name")"
if [[ ! -f "$app_dir/docker-compose.yml" ]]; then if [[ ! -f "$app_dir/docker-compose.yml" ]]; then
isError "No compose file for '$app_name' at $app_dir/docker-compose.yml" isError "No compose file for '$app_name' at $app_dir/docker-compose.yml"
return 1 return 1

View File

@ -21,12 +21,12 @@ dockerComposeDownRemove()
return 1 return 1
fi fi
if [[ -d "$containers_dir$app_name" ]]; then if [[ -d "$(appDir "$app_name")" ]]; then
isNotice "Shutting down & removing all $app_name container data" isNotice "Shutting down & removing all $app_name container data"
if [[ "$CFG_DOCKER_INSTALL_TYPE" == "rootless" ]]; then 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 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 fi
else else
isNotice "App directory '$app_name' not found — falling back to name-based container cleanup." 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..." 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_file="$compose_dir/docker-compose.yml"
local compose_images="" local compose_images=""

View File

@ -16,7 +16,7 @@ dockerCopyBuildContext()
{ {
local app_name="$1" local app_name="$1"
local source_dir="$install_containers_dir$app_name" 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 if [[ -z "$app_name" ]]; then
isError "dockerCopyBuildContext: app_name is empty." isError "dockerCopyBuildContext: app_name is empty."

View File

@ -28,7 +28,7 @@ dockerComposeSetupFile()
local target_compose_file="docker-compose.$app_name.yml"; local target_compose_file="docker-compose.$app_name.yml";
fi fi
local target_path="$containers_dir$app_name" local target_path="$(appDir "$app_name")"
local target_file="$target_path/$target_compose_file" local target_file="$target_path/$target_compose_file"

View File

@ -30,7 +30,7 @@ dockerComposeUpdate()
# Fail2ban specifics # Fail2ban specifics
if [[ "$app_name" == "fail2ban" ]]; then 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 [ -f "$jail_local_file" ]; then
if runFileOp grep -q "ignoreip = ips_whitelist" "$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. # assignment carrying this exact IP means it IS live on the shared net.
_netServiceIsRouted() { _netServiceIsRouted() {
local app="$1" ip="$2" 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 [[ -f "$compose" ]] || return 1 # no compose to consult -> don't skip
local esc_ip="${ip//./\\.}" local esc_ip="${ip//./\\.}"
grep -Eq "^[[:space:]]*ipv4_address:[[:space:]]*${esc_ip}([[:space:]]|#|$)" "$compose" && return 1 grep -Eq "^[[:space:]]*ipv4_address:[[:space:]]*${esc_ip}([[:space:]]|#|$)" "$compose" && return 1

View File

@ -2,6 +2,6 @@
dockerSetupEnvFile() 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" 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 # 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 # user here, so runFileOp (now resolved to that user) can list it (the
# manager can't list the 751 dockerinstall-owned dir under rootless). # 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 failed=()
local dir app_name local dir app_name

View File

@ -15,7 +15,7 @@ dockerSwitcherUpdateContainersToDockerType()
CFG_DOCKER_INSTALL_TYPE="$old_mode" CFG_DOCKER_INSTALL_TYPE="$old_mode"
resolveDockerInstallUser resolveDockerInstallUser
fi fi
local subdirectories=($(runFileOp find "$containers_dir" -maxdepth 1 -type d)) local subdirectories=($(storageAppDirs))
CFG_DOCKER_INSTALL_TYPE="$saved_type" CFG_DOCKER_INSTALL_TYPE="$saved_type"
resolveDockerInstallUser resolveDockerInstallUser

View File

@ -3,7 +3,7 @@
backupContainerFilesToTemp() backupContainerFilesToTemp()
{ {
local app_name="$1" 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)" 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() backupContainerFilesRestore()
{ {
local app_name="$1" local app_name="$1"
local source_folder="$containers_dir$app_name" local source_folder="$(appDir "$app_name")"
if [ -d "$temp_backup_folder" ]; then if [ -d "$temp_backup_folder" ]; then
local result; result=$(copyFiles "loud" "$temp_backup_folder" "$source_folder" $docker_install_user) 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'." echo "App folder '$app_name' not found in '$install_containers_dir'."
fi fi
local destination_dir="$containers_dir$app_name" local destination_dir="$(appDir "$app_name")"
if [ -n "$save_path" ]; then if [ -n "$save_path" ]; then
local destination_dir="$destination_dir/$save_path" local destination_dir="$destination_dir/$save_path"

View File

@ -21,14 +21,14 @@ fixPermissionsBeforeStart()
fi fi
# Traefik # 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 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" checkSuccess "Set permissions to acme.json file for traefik"
fi 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 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" checkSuccess "Set permissions to traefik.yml file for traefik"
fi fi
} }

View File

@ -4,7 +4,7 @@ fixConfigPermissions()
{ {
local silent_flag="$1" local silent_flag="$1"
local app_name="$2" 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) local result; result=$(runFileOp chmod g+rw $config_file)
if [ "$silent_flag" == "loud" ]; then 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" 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 # 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 # 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 # have rewritten the original's config. Anchored on containers_dir so a
# bare mention of the app name in prose is left alone. # bare mention of the app name in prose is left alone.
# Trailing / is NOT required: dashy tests [[ -d "${containers_dir}dashy" ]] # Trailing / is NOT required: dashy tests [[ -d "$(appDir dashy)" ]]
# and gluetun does (cd "${containers_dir}gluetun" && …), both ending at # and gluetun does (cd "$(appDir gluetun)" && …), both ending at
# the quote. Only the first path component is touched, so a data subdir # 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. # 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@(\\\$\{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" 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="${type}_${id}"
local slug_u="${slug^^}" 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." isError "An app or instance named '$slug' already exists. Pick a different name."
return 1 return 1
fi fi

View File

@ -10,7 +10,7 @@ appUninstallMenu() {
local installed_apps=() local installed_apps=()
# Get all installed apps by checking for docker-compose.yml files # 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 if [ -d "$app_dir" ]; then
local compose_file="$app_dir/docker-compose.yml" local compose_file="$app_dir/docker-compose.yml"
if [ -f "$compose_file" ]; then if [ -f "$compose_file" ]; then
@ -18,7 +18,7 @@ appUninstallMenu() {
installed_apps+=("$app_name") installed_apps+=("$app_name")
fi fi
fi fi
done done < <(storageAppDirs)
if [ ${#installed_apps[@]} -eq 0 ]; then if [ ${#installed_apps[@]} -eq 0 ]; then
isNotice "No installed applications found." isNotice "No installed applications found."

View File

@ -4,7 +4,7 @@ resetToMenu()
{ {
# Find all directories in the containers folder and create variables for them # Find all directories in the containers folder and create variables for them
if [ -d "$containers_dir" ]; then if [ -d "$containers_dir" ]; then
for dir in "$containers_dir"/*/; do while IFS= read -r dir; do
if [ -d "$dir" ]; then if [ -d "$dir" ]; then
# Get the directory name and remove any trailing slashes # Get the directory name and remove any trailing slashes
local app_name=$(basename "$dir") local app_name=$(basename "$dir")
@ -13,7 +13,7 @@ resetToMenu()
# Set the variable # Set the variable
eval "${app_name}=n" eval "${app_name}=n"
fi fi
done done < <(storageAppDirs)
fi fi
# Set any additional non-container variables here # Set any additional non-container variables here

View File

@ -23,7 +23,7 @@ migratePreBackupDestination()
fi fi
# Only meaningful if the destination actually has this app installed. # 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" isNotice "No existing $app on destination — pre-migrate backup skipped"
migrateEmit phase=pre-backup status=skipped reason=no-existing-app app="$app" migrateEmit phase=pre-backup status=skipped reason=no-existing-app app="$app"
return 0 return 0

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