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>
155 lines
6.8 KiB
Bash
155 lines
6.8 KiB
Bash
#!/bin/bash
|
|
|
|
# Bring config options added to an app's template into an already-deployed copy.
|
|
#
|
|
# The deployed config at /docker/containers/<app>/<app>.config is written once,
|
|
# on first install, and never touched again — dockerConfigSetupToContainer only
|
|
# copies when the file is absent, precisely so a LibrePortal update can never
|
|
# overwrite the values someone has edited. That is the right default, but it has
|
|
# a consequence nobody chose: an app that gains a CFG_ option in a new release
|
|
# has that option on every FRESH install and on no EXISTING one.
|
|
#
|
|
# The failure is silent, which is the worst part. Nothing errors. The new key
|
|
# simply reads as empty, and whatever depends on it does something quietly
|
|
# different — a default it did not mean to take, or a value it needed and did
|
|
# not get. It only surfaces as "why does this work on my other box".
|
|
#
|
|
# So: copy across keys the template has and the deployed file does not, and
|
|
# nothing else.
|
|
#
|
|
# Deliberately NOT a merge or a regenerate. Rebuilding the deployed file from
|
|
# the template would place new keys in their proper section and refresh the
|
|
# documentation with them, which is genuinely nicer to read — but it would put a
|
|
# whole-file rewrite of every app's config in the path of every app action, and
|
|
# the worst case of a bug there is silently corrupting settings across the whole
|
|
# install. Appending cannot lose an existing line. That trade is not close.
|
|
#
|
|
# Existing keys are never touched, and keys the deployed file has but the
|
|
# template no longer does are left exactly where they are: a removed option is
|
|
# usually a rename, and deleting the user's value is not recoverable.
|
|
|
|
configBackfillMissingKeys()
|
|
{
|
|
local app_name="$1"
|
|
local template="$2"
|
|
local deployed="$3"
|
|
local silent_flag="${4:-silent}"
|
|
|
|
[[ -f "$template" ]] || return 0
|
|
[[ -f "$deployed" ]] || return 0
|
|
|
|
# Same file, or a fresh copy of it — nothing can be missing.
|
|
runFileOp cmp -s "$template" "$deployed" && return 0
|
|
|
|
local template_keys deployed_keys missing
|
|
template_keys=$(grep -oE '^CFG_[A-Z0-9_]+=' "$template" 2>/dev/null | sed 's/=$//' | sort -u)
|
|
deployed_keys=$(runFileOp grep -oE '^CFG_[A-Z0-9_]+=' "$deployed" 2>/dev/null | sed 's/=$//' | sort -u)
|
|
|
|
[[ -z "$template_keys" ]] && return 0
|
|
|
|
missing=$(comm -23 <(printf '%s\n' "$template_keys") <(printf '%s\n' "$deployed_keys"))
|
|
[[ -z "$missing" ]] && return 0
|
|
|
|
# Carry each key's comment block over with it. A bare `CFG_X=value` appended
|
|
# to the end of a heavily-documented file is an option nobody can act on;
|
|
# the comment above it in the template is the only explanation that exists.
|
|
local missing_csv
|
|
missing_csv=$(printf '%s\n' "$missing" | paste -sd, -)
|
|
|
|
local block
|
|
block=$(awk -v keys="$missing_csv" '
|
|
BEGIN { n = split(keys, K, ","); for (i = 1; i <= n; i++) want[K[i]] = 1 }
|
|
# Accumulate the contiguous comment block sitting directly above a key.
|
|
/^[[:space:]]*#/ { buf = buf $0 "\n"; next }
|
|
# A blank line ends a block — the comments above it belong to something else.
|
|
/^[[:space:]]*$/ { buf = ""; next }
|
|
/^CFG_[A-Z0-9_]+=/ {
|
|
k = $0; sub(/=.*/, "", k)
|
|
if (k in want) printf "%s%s\n\n", buf, $0
|
|
buf = ""
|
|
next
|
|
}
|
|
{ buf = "" }
|
|
' "$template")
|
|
|
|
[[ -z "$block" ]] && return 0
|
|
|
|
{
|
|
printf '\n#\n'
|
|
printf '# =============================================================================\n'
|
|
printf '# ADDED BY A LIBREPORTAL UPDATE\n'
|
|
printf '# =============================================================================\n'
|
|
printf '# These options did not exist when this app was installed. They are set to the\n'
|
|
printf '# defaults shipped with the new version — review them, they are yours to change.\n'
|
|
printf '#\n'
|
|
printf '%s\n' "$block"
|
|
} | runFileWrite -a "$deployed"
|
|
|
|
local count
|
|
count=$(printf '%s\n' "$missing" | grep -c .)
|
|
# Counter rather than a return code: the callers use this as a bare
|
|
# statement, and a non-zero "nothing to do" would read as a failure to any
|
|
# of them running under errexit.
|
|
LP_BACKFILL_ADDED=$(( ${LP_BACKFILL_ADDED:-0} + count ))
|
|
|
|
if [[ "$silent_flag" == "loud" ]]; then
|
|
isSuccessful "Added $count new config option(s) to $app_name:"
|
|
printf '%s\n' "$missing" | sed 's/^/ /'
|
|
else
|
|
isNotice "Added $count new config option(s) to $app_name's config."
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
# Sweep every installed app after a LibrePortal update.
|
|
#
|
|
# The per-app backfill above only fires when an app's config is set up, which
|
|
# happens on install — and an update installs exactly one app, LibrePortal
|
|
# itself. So without this sweep, an option added to some app in a new release
|
|
# reaches that app only when someone next reinstalls it, which for a working app
|
|
# may be never. Since the whole point is that new options arrive on upgrade,
|
|
# upgrade is where this has to run.
|
|
configBackfillAllApps()
|
|
{
|
|
local dir app template deployed
|
|
LP_BACKFILL_ADDED=0
|
|
|
|
# Driven from the template directory, not from the deployed one. Under
|
|
# rootless the container tree is mode drwxr-x--x and owned by the docker
|
|
# install user, so the manager running this can traverse it but cannot LIST
|
|
# it — a glob over it silently expands to nothing and the sweep would report
|
|
# success having examined no apps at all. The template dir is manager-owned
|
|
# and readable, and it defines the same set of apps; whether each one is
|
|
# actually installed is then just "does its deployed config exist", which is
|
|
# a traverse, not a list.
|
|
for dir in "$install_containers_dir"*/; do
|
|
[[ -d "$dir" ]] || continue
|
|
app="${dir%/}"; app="${app##*/}"
|
|
[[ "$app" == "template" ]] && continue
|
|
|
|
template="${dir}${app}.config"
|
|
deployed="$(appDir "$app")/${app}.config"
|
|
[[ -f "$template" ]] || continue
|
|
runFileOp test -f "$deployed" || continue
|
|
|
|
configBackfillMissingKeys "$app" "$template" "$deployed" "silent"
|
|
|
|
# A backfilled key whose default is a RANDOMIZED* placeholder has to be
|
|
# given a real value here — nothing else will run over this file until
|
|
# the app is next installed, and a placeholder left in place is a
|
|
# credential that is identical on every install that took this upgrade.
|
|
if runFileOp grep -qE 'RANDOMIZED(PASSWORD|USERNAME|BCRYPTPASSWORD|HEX|VAPID|APPKEY)[0-9]*' "$deployed" 2>/dev/null; then
|
|
scanFileForRandomPasswordKeysUsers "$deployed"
|
|
fi
|
|
done
|
|
|
|
if (( ${LP_BACKFILL_ADDED:-0} > 0 )); then
|
|
isSuccessful "Carried $LP_BACKFILL_ADDED new config option(s) into your existing apps."
|
|
isNotice " They are set to the defaults shipped with this version — review them"
|
|
isNotice " in each app's Config tab."
|
|
fi
|
|
|
|
return 0
|
|
}
|