Phase 0 of docs/roadmap/storage-locations.md — the resolver layer. No
behaviour change yet: with no registry present, every function here
returns exactly what the old single-root code did, which is what makes
the ~200-site sweep that follows safe to land incrementally.
primaryRoot / webuiDir the install-time root, and the one tree that
never moves
storageRoots every registered root, primary first
storageRootAvailable marker present == drive mounted
pathIsContainerData replaces the `== "$containers_dir"*` idiom
that picks manager vs container-user elevation
appDir / appDirSlash THE resolver, memoised
storageLocationPath/Name name <-> path, via the root-owned registry
storageIndexGet/Set app -> location cache
appDir resolves discovery-first: whichever root actually holds
<slug>/<slug>.config wins, so a hand-move or half-finished migration
self-heals rather than corrupting.
The index exists because of a bug the unit test caught immediately.
Discovery cannot see an unmounted disk, so an installed app on an
unplugged drive looked identical to a brand-new app — and the fallback
handed back the PRIMARY root. Docker would then have created the bind
mounts there and booted the app empty on the wrong disk, which is the
precise failure the availability design exists to prevent. The index is
manager-owned (deliberately not on the removable disk: it must be
readable exactly when that disk is absent), consulted only when the scan
comes up empty, and rewritten by every successful scan so the disk stays
authoritative whenever it is actually present.
Availability is gated in appDir alone rather than at each caller: every
site reaches it by construction. It returns non-zero AND prints an
unusable sentinel path, so the many callers that will never check $?
still fail loudly on something harmless.
scripts/dev/lp-storage-test covers all of it against a throwaway tree.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
68 lines
3.5 KiB
Bash
Executable File
68 lines
3.5 KiB
Bash
Executable File
#!/bin/bash
|
|
# Self-contained unit test for the storage-location primitives in
|
|
# scripts/source/paths.sh. Runs against a throwaway tree in $TMPDIR — never
|
|
# touches a real install. No arguments; exits non-zero on the first failure.
|
|
#
|
|
# scripts/dev/lp-storage-test
|
|
#
|
|
# The case worth keeping honest is the last one: an app whose drive is not
|
|
# mounted must resolve to the sentinel, NOT to the primary root. Silently
|
|
# falling back is how an app gets rebuilt empty on the wrong disk.
|
|
REPO="$(cd "$(dirname "$0")/../.." && pwd)"
|
|
BASE="$(mktemp -d "${TMPDIR:-/tmp}/lp-storage-test-XXXXXX")"
|
|
trap 'rm -rf "$BASE"' EXIT
|
|
export LP_SYSTEM_DIR="$BASE/sys"
|
|
export LP_CONTAINERS_DIR="$BASE/primary"
|
|
export LP_BACKUPS_DIR="$BASE/bk"
|
|
export LP_STORAGE_REGISTRY="$BASE/storage.roots"
|
|
mkdir -p "$BASE/primary" "$BASE/disk2" "$BASE/sys/configs"
|
|
source "$REPO/scripts/source/paths.sh"
|
|
|
|
fail=0
|
|
chk(){ if [[ "$2" == "$3" ]]; then echo " ok $1"; else echo " FAIL $1: got '$2' want '$3'"; fail=1; fi; }
|
|
|
|
echo "--- no registry (behaves exactly like the old single-root code) ---"
|
|
chk "primaryRoot" "$(primaryRoot)" "$BASE/primary"
|
|
chk "webuiDir" "$(webuiDir)" "$BASE/primary/libreportal"
|
|
chk "roots" "$(storageRoots)" "$BASE/primary"
|
|
chk "appDir new" "$(appDir bookstack)" "$BASE/primary/bookstack"
|
|
pathIsContainerData "$BASE/primary/bookstack/x" && echo " ok pathIsContainerData inside" || { echo " FAIL inside"; fail=1; }
|
|
pathIsContainerData "$BASE/sys/configs" && { echo " FAIL outside matched"; fail=1; } || echo " ok pathIsContainerData outside"
|
|
|
|
echo "--- discovery: app deployed on the primary root ---"
|
|
mkdir -p "$BASE/primary/bookstack" && : > "$BASE/primary/bookstack/bookstack.config"
|
|
storageCacheReset
|
|
chk "appDir found" "$(appDir bookstack)" "$BASE/primary/bookstack"
|
|
|
|
echo "--- second root, mounted (marker present) ---"
|
|
printf '2\t%s\t0:0\tuuid-2\n' "$BASE/disk2" > "$BASE/storage.roots"
|
|
: > "$BASE/disk2/.libreportal-storage"
|
|
mkdir -p "$BASE/disk2/nextcloud" && : > "$BASE/disk2/nextcloud/nextcloud.config"
|
|
storageCacheReset
|
|
chk "roots both" "$(storageRoots | tr '\n' ' ')" "$BASE/primary $BASE/disk2 "
|
|
chk "appDir disk2" "$(appDir nextcloud)" "$BASE/disk2/nextcloud"
|
|
chk "appDir primary still" "$(appDir bookstack)" "$BASE/primary/bookstack"
|
|
pathIsContainerData "$BASE/disk2/nextcloud" && echo " ok pathIsContainerData disk2" || { echo " FAIL disk2"; fail=1; }
|
|
chk "locName disk2" "$(storageLocationName "$BASE/disk2")" "2"
|
|
chk "locPath by id" "$(storageLocationPath 2)" "$BASE/disk2"
|
|
CFG_STORAGE_LOC_2_NAME="bigdisk"
|
|
chk "locPath by name" "$(storageLocationPath bigdisk)" "$BASE/disk2"
|
|
chk "locName friendly" "$(storageLocationName "$BASE/disk2")" "bigdisk"
|
|
|
|
echo "--- intent for an app not yet deployed ---"
|
|
CFG_JELLYFIN_STORAGE="bigdisk"
|
|
storageCacheReset
|
|
chk "appDir intent" "$(appDir jellyfin)" "$BASE/disk2/jellyfin"
|
|
|
|
echo "--- drive unmounted: marker gone ---"
|
|
rm -f "$BASE/disk2/.libreportal-storage"
|
|
storageCacheReset
|
|
out=$(appDir nextcloud); rc=$?
|
|
chk "appDir sentinel" "$out" "/nonexistent-libreportal-unavailable/nextcloud"
|
|
chk "appDir rc" "$rc" "1"
|
|
appStorageAvailable nextcloud && { echo " FAIL available"; fail=1; } || echo " ok appStorageAvailable false"
|
|
appStorageAvailable bookstack && echo " ok primary still available" || { echo " FAIL primary"; fail=1; }
|
|
storageRootAvailable "$BASE/disk2" && { echo " FAIL root available"; fail=1; } || echo " ok storageRootAvailable false"
|
|
|
|
echo; [[ $fail -eq 0 ]] && echo "ALL PASS" || echo "FAILURES"; exit $fail
|