#!/bin/bash # The Disks view โ€” one row per filesystem, showing what LibrePortal does with it. # # This is the answer to "storage location" and "backup location" being two # competing top-level nouns: the device becomes the organising concept and the # registries become ROLES on it. See ยง7.1 of docs/roadmap/storage-locations.md. # # The union is the part that matters more than the enrichment. A registered # location whose drive is unplugged does not appear in findmnt or lsblk at all โ€” # and that is precisely when someone looks at this page. So rows come from the # REGISTRY first and attached hardware second: a missing device still renders, # marked not-attached, naming the apps stranded on it. A row vanishing when the # disk is pulled is the one failure this view cannot have. # Emit one TSV record per row: # # # key fs UUID when known, else the mount point (device names reorder across # reboots and would scramble the table) # state ok | not-attached # roles comma-separated: system, primary, storage:, backups: # apps comma-separated app slugs on this filesystem ("-" when none/unknown) storageDisksData() { local -A row_mount=() row_source=() row_fstype=() row_size=() row_free=() local -A row_rm=() row_state=() row_roles=() row_apps=() row_order=() local order=0 _addRole() { local k="$1" r="$2" if [[ -z "${row_roles[$k]:-}" ]]; then row_roles["$k"]="$r" elif [[ ",${row_roles[$k]}," != *",$r,"* ]]; then row_roles["$k"]="${row_roles[$k]},$r"; fi } # ---- 1. attached filesystems ------------------------------------------- local target source fstype size avail uuid key line kv val if command -v findmnt >/dev/null 2>&1; then while IFS= read -r line; do [[ -z "$line" ]] && continue target=""; source=""; fstype=""; size=""; avail=""; uuid="" 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 key="${uuid:-$target}" [[ -n "${row_mount[$key]:-}" ]] && continue row_order["$key"]=$((order++)) row_mount["$key"]="$target"; row_source["$key"]="$source" row_fstype["$key"]="$fstype"; row_size["$key"]="$size" row_free["$key"]="$avail"; row_state["$key"]="ok" row_rm["$key"]=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" ]] && row_rm["$key"]=1 fi done < <(findmnt -Pno TARGET,SOURCE,FSTYPE,SIZE,AVAIL,UUID 2>/dev/null) fi # Map a path to the row that holds it (by mount point, longest match wins). _rowFor() { local p="${1%/}" best="" best_len=0 k m for k in "${!row_mount[@]}"; do m="${row_mount[$k]%/}"; [[ -z "$m" ]] && m="/" if [[ "$p" == "$m" || "$p" == "$m/"* || "$m" == "/" ]]; then (( ${#m} >= best_len )) && { best="$k"; best_len=${#m}; } fi done printf '%s' "$best" } # ---- 2. roles ------------------------------------------------------------ local k k=$(_rowFor "/"); [[ -n "$k" ]] && _addRole "$k" "system" k=$(_rowFor "$(primaryRoot)"); [[ -n "$k" ]] && _addRole "$k" "primary" # Backup locations 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=$(_storageProbeDir "$bpath") k=$(_rowFor "$probe"); [[ -n "$k" ]] && _addRole "$k" "backups:$idx" done < <(resticEnabledLocations 2>/dev/null) fi # ---- 3. registered storage locations, INCLUDING absent ones ------------- 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 _path="${_path%/}" if storageRootAvailable "$_path"; then k=$(_rowFor "$_path") [[ -n "$k" ]] && { _addRole "$k" "storage:$_id"; row_apps["$k"]="$(storageAppsOnRoot "$_path" | paste -sd, -)"; } else # Not attached: synthesise the row from the registry so it still # appears, and name what is stranded on it. key="absent:$_id" row_order["$key"]=$((order++)) row_mount["$key"]="$_path"; row_source["$key"]="-" row_fstype["$key"]="-"; row_size["$key"]="-" row_free["$key"]="-"; row_rm["$key"]=0 row_state["$key"]="not-attached" _addRole "$key" "storage:$_id" local stranded="" while IFS=$'\t' read -r _s _r; do [[ "${_r%/}" == "$_path" ]] && stranded+="${stranded:+,}$_s" done < <(cat "$(storageIndexFile)" 2>/dev/null) row_apps["$key"]="${stranded:--}" fi done < "$lp_storage_registry" fi # Apps on the primary root k=$(_rowFor "$(primaryRoot)") [[ -n "$k" && -z "${row_apps[$k]:-}" ]] && row_apps["$k"]="$(storageAppsOnRoot "$(primaryRoot)" | paste -sd, -)" # ---- 4. emit, in discovery order ---------------------------------------- local sorted sorted=$(for k in "${!row_order[@]}"; do printf '%s\t%s\n' "${row_order[$k]}" "$k"; done | sort -n | cut -f2) while IFS= read -r k; do [[ -z "$k" ]] && continue printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ "$k" "${row_mount[$k]}" "${row_source[$k]}" "${row_fstype[$k]}" \ "${row_size[$k]}" "${row_free[$k]}" "${row_rm[$k]}" \ "${row_state[$k]}" "${row_roles[$k]:--}" "${row_apps[$k]:--}" done <<< "$sorted" unset -f _addRole _rowFor } # Human-readable Disks view. storageDisks() { local key mount source fstype size free rm_flag state roles apps isHeader "Disks" printf '%-24s %-9s %-8s %-8s %-22s %-14s %s\n' \ "MOUNT" "FS" "SIZE" "FREE" "ROLES" "STATE" "APPS" while IFS=$'\t' read -r key mount source fstype size free rm_flag state roles apps; do [[ -z "$key" ]] && continue local shown_state="$state" [[ "$rm_flag" == "1" ]] && shown_state="$state, removable" printf '%-24s %-9s %-8s %-8s %-22s %-14s %s\n' \ "$mount" "$fstype" "$size" "$free" "$roles" "$shown_state" "$apps" done < <(storageDisksData) echo "" isNotice "A location shown as 'not-attached' is registered but its drive is not mounted." isNotice "Apps listed against it will refuse to start until it is back โ€” deliberately," isNotice "so they are never rebuilt empty on the bare mount point." }