Follow-up to 928e244, which stopped configs/ subdirectories being sourced without a .category marker. That closed the hole; this removes the thing that fell into it. storageIndexFile pointed at configs/storage/app_locations. The file's requirements are only "manager-owned" and "not on a removable disk" — configs/ satisfies both, which is why I put it there, and it was still wrong: that tree carries a third property the file violates. sourceScanFiles SOURCES what it finds under configs/, and sourcing means executing. The index is a TSV of "<slug><TAB><root>", which bash reads as a command and its argument. Harmless while no slug matched a real executable. The row for the app named `libreportal` armed it, because that IS the CLI on PATH: sourcing ran `libreportal /libreportal-containers`, which re-entered the scan, which sourced the file again — one process pair per level until the host OOMed and took the desktop session with it. It now lives at $system_dir/storage/app_locations, with a one-shot migration so an install that already has an index keeps knowing where its apps live rather than silently forgetting. libreportal-ownership reconciles the new directory, and scan_files.sh gained a note that configs/storage/ carries no .category on purpose. scripts/dev/lp-configs-guard-test covers both ends: the index never lands in configs/, a legacy one migrates, and a file of the exact detonating shape placed in an unmarked configs/ subdirectory is not executed while a marked category still loads. Also wires sourceStorageLocations into the config scan beside sourceBackupLocations — per-location configs sit at depth 3, below the generic scan, and need their own walker like the backup ones do. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
149 lines
6.0 KiB
Bash
149 lines
6.0 KiB
Bash
#!/bin/bash
|
|
|
|
# Candidate discovery — "what else could hold app data on this box?"
|
|
#
|
|
# Shared by the setup wizard's Storage step, `libreportal storage scan`, and the
|
|
# Disks view, so all three show the same answer.
|
|
#
|
|
# Filtering has to be aggressive rather than optional: a desktop-class install
|
|
# carries a dozen-plus snap loop mounts, and without pruning them the real answer
|
|
# is invisible. `lsblk -e7` drops loop devices at the source; the rest are pruned
|
|
# by mount point and filesystem type below.
|
|
|
|
# Filesystem types that are never a candidate.
|
|
_STORAGE_SKIP_FSTYPES="squashfs|overlay|overlay2|aufs|tmpfs|devtmpfs|ramfs|proc|sysfs|cgroup|cgroup2|configfs|debugfs|tracefs|securityfs|pstore|efivarfs|autofs|binfmt_misc|fusectl|mqueue|hugetlbfs|bpf|nsfs|iso9660|udf"
|
|
|
|
# Mount points that are never a candidate.
|
|
_storageSkipTarget()
|
|
{
|
|
local t="$1"
|
|
case "$t" in
|
|
""|/boot|/boot/*|/efi|/proc|/proc/*|/sys|/sys/*|/dev|/dev/*|/run|/run/*|/snap|/snap/*|/var/snap/*|/tmp|/var/lib/docker/*)
|
|
return 0 ;;
|
|
esac
|
|
return 1
|
|
}
|
|
|
|
# Emit one TSV record per candidate filesystem:
|
|
# <target> <source> <fstype> <size> <avail> <uuid> <removable> <role>
|
|
#
|
|
# role is one of: system | primary | storage:<id> | backup:<idx> | free
|
|
# — the thing that lets the Disks view show what LibrePortal does with a device
|
|
# without every caller re-deriving it.
|
|
storageScanCandidates()
|
|
{
|
|
command -v findmnt >/dev/null 2>&1 || return 0
|
|
|
|
local target source fstype uuid size avail rm_flag role dev
|
|
local primary_dev backup_devs=""
|
|
primary_dev=$(stat -c '%d' -- "$(primaryRoot)" 2>/dev/null)
|
|
|
|
# Registered storage roots, by device
|
|
local -A storage_dev=()
|
|
local _id _path _rest
|
|
if [[ -r "$lp_storage_registry" ]]; then
|
|
while IFS=$'\t' read -r _id _path _rest || [[ -n "$_id" ]]; do
|
|
[[ -z "$_path" || "$_id" == \#* ]] && continue
|
|
dev=$(stat -c '%d' -- "${_path%/}" 2>/dev/null) || continue
|
|
[[ -n "$dev" ]] && storage_dev["$dev"]="$_id"
|
|
done < "$lp_storage_registry"
|
|
fi
|
|
|
|
# Enabled backup locations, by device
|
|
local -A backup_dev=()
|
|
if declare -f resticEnabledLocations >/dev/null 2>&1 \
|
|
&& declare -f backupLocationResolvedPath >/dev/null 2>&1; then
|
|
local idx bpath probe
|
|
while IFS= read -r idx; do
|
|
[[ -z "$idx" ]] && continue
|
|
bpath=$(backupLocationResolvedPath "$idx" 2>/dev/null); bpath="${bpath%/}"
|
|
[[ -z "$bpath" ]] && continue
|
|
probe="$bpath"
|
|
while [[ -n "$probe" && "$probe" != "/" && ! -d "$probe" ]]; do probe="${probe%/*}"; done
|
|
dev=$(stat -c '%d' -- "${probe:-/}" 2>/dev/null) || continue
|
|
[[ -n "$dev" ]] && backup_dev["$dev"]="$idx"
|
|
done < <(resticEnabledLocations 2>/dev/null)
|
|
fi
|
|
|
|
local system_dev; system_dev=$(stat -c '%d' -- / 2>/dev/null)
|
|
|
|
# findmnt -P emits KEY="value" pairs: the only output form that survives a
|
|
# mount point containing spaces. -r/-n is space-separated and silently
|
|
# collapses every field into the first variable.
|
|
local line
|
|
while IFS= read -r line; do
|
|
[[ -z "$line" ]] && continue
|
|
target=""; source=""; fstype=""; size=""; avail=""; uuid=""
|
|
local kv key val
|
|
for kv in TARGET SOURCE FSTYPE SIZE AVAIL UUID; do
|
|
val="${line#*${kv}=\"}"
|
|
[[ "$val" == "$line" ]] && continue
|
|
val="${val%%\"*}"
|
|
case "$kv" in
|
|
TARGET) target="$val" ;;
|
|
SOURCE) source="$val" ;;
|
|
FSTYPE) fstype="$val" ;;
|
|
SIZE) size="$val" ;;
|
|
AVAIL) avail="$val" ;;
|
|
UUID) uuid="$val" ;;
|
|
esac
|
|
done
|
|
[[ -z "$target" ]] && continue
|
|
_storageSkipTarget "$target" && continue
|
|
[[ "$fstype" =~ ^($_STORAGE_SKIP_FSTYPES)$ ]] && continue
|
|
|
|
dev=$(stat -c '%d' -- "$target" 2>/dev/null)
|
|
|
|
rm_flag=0
|
|
if [[ "$source" == /dev/* ]] && command -v lsblk >/dev/null 2>&1; then
|
|
local r h
|
|
r=$(lsblk -no RM "$source" 2>/dev/null | head -1 | tr -d ' ')
|
|
h=$(lsblk -no HOTPLUG "$source" 2>/dev/null | head -1 | tr -d ' ')
|
|
[[ "$r" == "1" || "$h" == "1" ]] && rm_flag=1
|
|
fi
|
|
|
|
# Most specific role wins — a device can hold more than one.
|
|
if [[ -n "$dev" && -n "${storage_dev[$dev]:-}" ]]; then
|
|
role="storage:${storage_dev[$dev]}"
|
|
elif [[ -n "$dev" && "$dev" == "$primary_dev" ]]; then
|
|
role="primary"
|
|
elif [[ -n "$dev" && -n "${backup_dev[$dev]:-}" ]]; then
|
|
role="backup:${backup_dev[$dev]}"
|
|
elif [[ -n "$dev" && "$dev" == "$system_dev" ]]; then
|
|
role="system"
|
|
else
|
|
role="free"
|
|
fi
|
|
|
|
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
|
|
"$target" "$source" "$fstype" "$size" "$avail" "${uuid:-}" "$rm_flag" "$role"
|
|
done < <(findmnt -Pno TARGET,SOURCE,FSTYPE,SIZE,AVAIL,UUID 2>/dev/null)
|
|
}
|
|
|
|
# Human-readable form of the above, with a fitness verdict per candidate.
|
|
storageScan()
|
|
{
|
|
local target source fstype size avail uuid rm_flag role
|
|
isHeader "Storage candidates"
|
|
printf '%-26s %-10s %-8s %-8s %-12s %s\n' "MOUNT" "FS" "SIZE" "FREE" "ROLE" "VERDICT"
|
|
|
|
while IFS=$'\t' read -r target source fstype size avail uuid rm_flag role; do
|
|
[[ -z "$target" ]] && continue
|
|
local verdict="usable" sev check msg
|
|
local refusals="" warnings=""
|
|
while IFS=$'\t' read -r sev check msg; do
|
|
case "$sev" in
|
|
refuse) refusals+="${refusals:+; }$check" ;;
|
|
warn) warnings+="${warnings:+; }$check" ;;
|
|
esac
|
|
done < <(storageCheckPath "$target" 2>/dev/null)
|
|
if [[ -n "$refusals" ]]; then
|
|
verdict="unusable ($refusals)"
|
|
elif [[ -n "$warnings" ]]; then
|
|
verdict="usable, note: $warnings"
|
|
fi
|
|
printf '%-26s %-10s %-8s %-8s %-12s %s\n' \
|
|
"$target" "$fstype" "$size" "$avail" "$role" "$verdict"
|
|
done < <(storageScanCandidates)
|
|
}
|