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