#!/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: # # # role is one of: system | primary | storage: | backup: | 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) }