LibrePortal/scripts/migrate/migrate_url_rewrite.sh
librelad 8b5e02c760 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>
2026-08-24 04:09:51 +01:00

103 lines
4.1 KiB
Bash

#!/bin/bash
# Cross-host migrate inherits the source's CFG_<APP>_* values via the restored
# config file. Most of them (DB passwords, API keys, user preferences) are
# data and must survive the move. A small set are host-bound — they describe
# where the app lives — and would 404 or break TLS if we kept them pointing
# at the source. This module identifies and rewrites those.
#
# Host-bound fields are detected by suffix (URL, HOST, DOMAIN, DOMAIN_PREFIX),
# and the rewrite source-of-truth is the destination's install-template
# config (containers/<app>/<app>.config under the install tree) — that's what
# a fresh install on this box would produce.
#
# Per-app opt-out: set CFG_<APP>_MIGRATE_URL_REWRITE="false" in the app's
# install-template config to skip the rewrite (e.g. for apps with hardcoded
# URLs that mustn't change).
# Field-name suffixes treated as host-bound. Compared after stripping the
# CFG_<APP>_ prefix.
_MIGRATE_HOST_BOUND_SUFFIXES=(URL HOST DOMAIN DOMAIN_PREFIX HOSTNAME PUBLIC_URL)
# Echo "true" or "false" — should we rewrite host-bound URLs for this app?
# Defaults true unless the app explicitly opts out.
migrateUrlRewriteEnabled()
{
local app="$1"
local template_config="$install_containers_dir$app/$app.config"
[[ ! -f "$template_config" ]] && { echo "true"; return; }
local opt_key="CFG_${app^^}_MIGRATE_URL_REWRITE"
local val
val=$(grep -E "^${opt_key}=" "$template_config" 2>/dev/null \
| head -1 | cut -d'=' -f2- | tr -d '"' | tr -d "'")
if [[ "$val" == "false" || "$val" == "no" || "$val" == "0" ]]; then
echo "false"
else
echo "true"
fi
}
# Read the destination's install template, find host-bound CFG_<APP>_* keys,
# and overwrite the same keys in the deployed (just-restored) config with the
# template's values. Anything not in the host-bound list is left alone — the
# source's data/preferences survive.
#
# Args: <app_name>
# Returns: count of fields rewritten (emitted as `migrate.url_rewrite.fields`).
migrateApplyUrlRewrite()
{
local app="$1"
[[ -z "$app" ]] && { isError "migrateApplyUrlRewrite: app required"; return 1; }
if [[ "$(migrateUrlRewriteEnabled "$app")" != "true" ]]; then
isNotice "URL rewrite disabled by app config — keeping source URLs for $app"
migrateEmit phase=url-rewrite status=skipped reason=app-opt-out app="$app"
return 0
fi
local template="$install_containers_dir$app/$app.config"
local deployed="$(appDir "$app")/$app.config"
if [[ ! -f "$template" || ! -f "$deployed" ]]; then
isNotice "URL rewrite: missing template or deployed config for $app — skipping"
migrateEmit phase=url-rewrite status=skipped reason=missing-config app="$app"
return 0
fi
local rewritten=0
local key value bare_suffix
while IFS='=' read -r key value; do
[[ -z "$key" ]] && continue
[[ "$key" =~ ^# ]] && continue
[[ ! "$key" =~ ^CFG_ ]] && continue
bare_suffix="${key##*_}"
# Also catch the two-word _DOMAIN_PREFIX / _PUBLIC_URL cases.
local two_word="${key%_*}"
two_word="${two_word##*_}_${bare_suffix}"
local is_host_bound=0
local suffix
for suffix in "${_MIGRATE_HOST_BOUND_SUFFIXES[@]}"; do
if [[ "$bare_suffix" == "$suffix" || "$two_word" == "$suffix" ]]; then
is_host_bound=1
break
fi
done
(( is_host_bound )) || continue
# Strip surrounding quotes; keep whatever the template specifies, even
# if empty (a deliberately-blank URL is a valid state).
value="${value%\"}"; value="${value#\"}"
value="${value%\'}"; value="${value#\'}"
updateConfigOption "$key" "$value" "$deployed" >/dev/null 2>&1
((rewritten++))
migrateEmit phase=url-rewrite status=field app="$app" key="$key" new_value="$value"
done < <(grep -E '^CFG_' "$template" 2>/dev/null)
isSuccessful "URL rewrite: $rewritten host-bound field(s) repointed to this host for $app"
migrateEmit phase=url-rewrite status=complete app="$app" fields="$rewritten"
}