refactor(storage): resolve every app directory through appDir
The main sweep — ~260 call sites across ~100 files move from string
concatenation on a single root to appDir/storageAppDirs/storageAppConfigs.
On a single-root install the resolved paths are identical, so this is a
no-op until a location is registered.
Enumerators were the interesting half. `for d in "$containers_dir"/*/`
appears in the menus, the registry/artifact scanners and the DNS setup —
and a shell glob cannot list a rootless 751 tree at all, which is the
same bug config_find_file.sh already documents in a comment. Routing them
through storageAppDirs (which enumerates as the owning user) fixes that
alongside the multi-root work.
Three places needed judgement rather than substitution:
db_app_scan.sh deletes database rows and port allocations for apps whose
folder is missing, and reaps "empty" app dirs. With a storage location
unmounted, every app on it looks exactly like that. Each of those
branches now gates on appStorageAvailable first — an app on an unplugged
drive is skipped with a notice, never deleted.
instance_create.sh rewrites cloned hooks so an instance touches its own
directory instead of the base app's. Its sed matched ${containers_dir}<type>,
which this sweep just replaced with $(appDir <type>) — so it would have
silently stopped redirecting, and an instance would have written to the
original's files (the adguard auth adapter case its own comment warns
about). Now matches both appDir forms, verified against bare, quoted,
unrelated-app, legacy and prose cases.
peer_shell/peer_pull streamed and extracted relative to the primary root.
Both now use the app's own root, and peer_shell keeps a single-root
fallback since it runs as a restricted SSH shell with no LibrePortal env.
Also fixes a pre-existing bug found on the way: webui_app_config.sh
tested "$containers_dir/frontend/data/last_update", one level short of the
real tree under the libreportal app dir, so the WebUI refresh trigger
after a config update has never once fired.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
2d24a764a8
commit
8b5e02c760
@ -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."
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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%%:*}"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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."
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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"
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}')
|
||||
|
||||
@ -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}')
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 ""
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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."
|
||||
|
||||
@ -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 ""
|
||||
|
||||
@ -28,7 +28,7 @@ moneyapp_install_post_compose()
|
||||
# * https://<host>.<domain> when traefik is on AND a real domain
|
||||
# is configured (CFG_DOMAIN_<n>)
|
||||
# * http://<server-ip>:<port> otherwise (raw port exposure)
|
||||
local moneyapp_compose_file="$containers_dir$app_name/docker-compose.yml"
|
||||
local moneyapp_compose_file="$(appDir "$app_name")/docker-compose.yml"
|
||||
local moneyapp_auth_url=""
|
||||
# host_setup is built as `<app>.<domain>` — when CFG_DOMAIN_<n> is
|
||||
# empty it ends up as `<app>.`, a valid-but-broken hostname. Reject
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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 \
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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%%#*}"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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."
|
||||
|
||||
@ -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%%:*}"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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.)
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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).
|
||||
|
||||
@ -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; }
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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 ""
|
||||
|
||||
@ -9,12 +9,12 @@ checkApplicationsConfigFilesMissingVariables()
|
||||
app=$(basename "$live" .config)
|
||||
remote="$install_containers_dir$app/$app.config"
|
||||
reconcileConfigFile "$live" "$remote"
|
||||
# 2>/dev/null on the find: app data dirs (e.g. invidious/postgresdata,
|
||||
# nextcloud/html) are owned by container sub-UIDs that the manager user
|
||||
# can't read into, and they're harmless permission-denied chatter — find
|
||||
# at this maxdepth doesn't need to enter them to satisfy -name '*.config'
|
||||
# at depth 2. Keep the noise out of CLI/log output.
|
||||
done < <(runFileOp find "$containers_dir" -maxdepth 2 -type f -name '*.config' ! -name '*.bak' 2>/dev/null)
|
||||
# storageAppConfigs walks every storage root as the container user and
|
||||
# yields only <slug>/<slug>.config, so app data dirs owned by container
|
||||
# sub-UIDs (invidious/postgresdata, nextcloud/html) are never descended
|
||||
# into — no permission-denied chatter, and no payload *.config mistaken
|
||||
# for an app.
|
||||
done < <(storageAppConfigs)
|
||||
|
||||
isSuccessful "Application config reconciliation completed."
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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 ""
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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."
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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."
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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."
|
||||
|
||||
@ -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=""
|
||||
|
||||
|
||||
@ -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."
|
||||
|
||||
@ -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"
|
||||
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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)"
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -430,16 +430,20 @@ _instanceRewriteTools() {
|
||||
sed -i -E "s/\b${type_u}_([A-Z0-9_]*_TAG)\b/${slug_u}_\1/g" "$f"
|
||||
|
||||
# The app's own deployed directory. Hooks that build it from
|
||||
# "${containers_dir}<type>/..." instead of "$containers_dir$app_name/..."
|
||||
# "${containers_dir}<type>/..." instead of "$(appDir "$app_name")/..."
|
||||
# read and WRITE the base app's files — adguard's auth adapter edits
|
||||
# ${containers_dir}adguard/conf/AdGuardHome.yaml, so an instance would
|
||||
# $(appDir adguard)/conf/AdGuardHome.yaml, so an instance would
|
||||
# have rewritten the original's config. Anchored on containers_dir so a
|
||||
# bare mention of the app name in prose is left alone.
|
||||
# Trailing / is NOT required: dashy tests [[ -d "${containers_dir}dashy" ]]
|
||||
# and gluetun does (cd "${containers_dir}gluetun" && …), both ending at
|
||||
# Trailing / is NOT required: dashy tests [[ -d "$(appDir dashy)" ]]
|
||||
# and gluetun does (cd "$(appDir gluetun)" && …), both ending at
|
||||
# the quote. Only the first path component is touched, so a data subdir
|
||||
# that repeats the app name (${containers_dir}prometheus/prometheus/…)
|
||||
# that repeats the app name ($(appDir prometheus)/prometheus/…)
|
||||
# keeps its inner segment.
|
||||
# Post-sweep form: hooks resolve their dir with $(appDir <name>).
|
||||
sed -i -E "s@(\\\$\(appDir )${type}(\))@\1${slug}\2@g" "$f"
|
||||
sed -i -E "s@(\\\$\(appDir \")${type}(\"\))@\1${slug}\2@g" "$f"
|
||||
# Legacy form, for any hook that still builds the path by hand.
|
||||
sed -i -E "s@(\\\$\{containers_dir\})${type}([^A-Za-z0-9_-]|$)@\1${slug}\2@g" "$f"
|
||||
sed -i -E "s@(\\\$\{install_containers_dir\}/?)${type}([^A-Za-z0-9_-]|$)@\1${slug}\2@g" "$f"
|
||||
|
||||
@ -495,7 +499,7 @@ instanceCreate() {
|
||||
|
||||
local slug="${type}_${id}"
|
||||
local slug_u="${slug^^}"
|
||||
if [[ -d "${install_containers_dir%/}/$slug" || -d "${containers_dir%/}/$slug" ]]; then
|
||||
if [[ -d "${install_containers_dir%/}/$slug" || -d "$(appDir "$slug")" ]]; then
|
||||
isError "An app or instance named '$slug' already exists. Pick a different name."
|
||||
return 1
|
||||
fi
|
||||
|
||||
@ -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."
|
||||
|
||||
@ -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
|
||||
|
||||
@ -23,7 +23,7 @@ migratePreBackupDestination()
|
||||
fi
|
||||
|
||||
# Only meaningful if the destination actually has this app installed.
|
||||
if [[ ! -d "$containers_dir$app" ]]; then
|
||||
if [[ ! -d "$(appDir "$app")" ]]; then
|
||||
isNotice "No existing $app on destination — pre-migrate backup skipped"
|
||||
migrateEmit phase=pre-backup status=skipped reason=no-existing-app app="$app"
|
||||
return 0
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user