The Storage step was a technical dump: every check's full sentence concatenated onto the card, so the fstab line the user is meant to act on was buried in prose nobody reads. The card now shows plain facts and at most two short flags — "Low on space · Won't be mounted after a reboot" — with everything else behind a Details button. The modal carries the technical spec (device, UUID, mount options, removable), every check with its full explanation, and the fstab offer. That needed the shell to stop joining checks into one string: the generator emits a record per check, plus the fstab line as its own field, so neither the card nor the modal has to parse anything back out of the other. The screenshot caught a bug this restructure introduced: summaries keyed on check id alone, so a PASSING check printed the failure wording next to a green tick — "This drive's format can't store file ownership" above "Filesystem: ext4". Now severity-aware. On writing /etc/fstab — §1 ruled it out and §6.3 now records why that reverses. The warning is useless to the audience this is for: "add this line to fstab" assumes SSH, root, an editor, and knowing what fstab is, and the likely outcome is a reboot where nothing starts. What makes it defensible is nofail + x-systemd.device-timeout, which mean a missing device can never block boot — without that pair it would stay a non-goal, because the failure being risked (an unbootable machine) is worse than the one being fixed. Enforced in the root helper: UUID never /dev/sdX, append inside a marked block, refuse a target or UUID already described, refuse the root filesystem, require a live mount, timestamped backup, and `findmnt --verify` before the file is installed — a file that doesn't parse never reaches /etc. Opt-in only. Verified against a real filesystem: entry added and verifies, the persistence warning then disappears on the next scan, and duplicate / root-fs / non-mountpoint / relative are each refused with the reason. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
491 lines
19 KiB
Bash
Executable File
491 lines
19 KiB
Bash
Executable File
#!/bin/bash
|
|
# LibrePortal storage-location helper — the ONLY writer of the root-owned
|
|
# storage registry, and the only thing that may hand a new directory to the
|
|
# container user.
|
|
#
|
|
# Why this exists: with storage locations, the set of paths root will chown is no
|
|
# longer fixed at install. If that set came from a manager-writable config, the
|
|
# manager could aim a root `chown -R dockerinstall` at /etc and own the box. So
|
|
# the registry lives root:root here, this script is its only writer, and every
|
|
# candidate must clear the admission rules below before it is accepted.
|
|
#
|
|
# The rule that makes it safe:
|
|
#
|
|
# root only ever chowns a directory that is EMPTY.
|
|
#
|
|
# An empty directory contains nothing to give away, so acceptance cannot transfer
|
|
# anything that already existed. Everything created underneath afterwards is ours
|
|
# by construction. The one relaxation — a directory already carrying OUR marker,
|
|
# so a drive full of app data can be adopted — costs nothing: writing that marker
|
|
# requires write access you would have had to already possess.
|
|
#
|
|
# Self-contained ON PURPOSE: it must NOT source any manager-owned code (incl.
|
|
# paths.sh), or it would re-open the very escalation it exists to close. init.sh
|
|
# bakes the roots and the manager name into the installed copy.
|
|
#
|
|
# Actions:
|
|
# add <path> [name] validate, accept, mark, chown, append to the registry
|
|
# remove <id|path> drop a location (refuses while app dirs remain)
|
|
# list print the registry (id<TAB>path<TAB>dev<TAB>uuid)
|
|
# verify [id] re-check marker + device of one/all locations
|
|
# path <id> print one location's path
|
|
|
|
set -u
|
|
|
|
[[ $EUID -eq 0 ]] || { echo "libreportal-storage: must run as root" >&2; exit 1; }
|
|
|
|
# Baked by init.sh at install (placeholders replaced). An unbaked copy still
|
|
# contains the "__" sentinel, which no real absolute path does.
|
|
MANAGER="__MANAGER__"
|
|
SYSTEM_DIR="__SYSTEM_DIR__"
|
|
CONTAINERS_DIR="__CONTAINERS_DIR__"
|
|
BACKUPS_DIR="__BACKUPS_DIR__"
|
|
[[ "$MANAGER" == *"__"* || -z "$MANAGER" ]] && MANAGER="libreportal"
|
|
[[ "$SYSTEM_DIR" == *"__"* || -z "$SYSTEM_DIR" ]] && SYSTEM_DIR="/libreportal-system"
|
|
[[ "$CONTAINERS_DIR" == *"__"* || -z "$CONTAINERS_DIR" ]] && CONTAINERS_DIR="/libreportal-containers"
|
|
[[ "$BACKUPS_DIR" == *"__"* || -z "$BACKUPS_DIR" ]] && BACKUPS_DIR="/libreportal-backups"
|
|
|
|
LIB_DIR="/usr/local/lib/libreportal"
|
|
REGISTRY="$LIB_DIR/storage.roots"
|
|
MARKER=".libreportal-storage"
|
|
DB_CFG="$SYSTEM_DIR/configs/general/general_docker_install"
|
|
|
|
# Paths that must never become a storage location, whatever the caller says.
|
|
# /home is excluded here and only reachable with --allow-home (see _protected).
|
|
PROTECTED=(/ /etc /usr /bin /sbin /lib /lib32 /lib64 /libx32 /boot /proc /sys
|
|
/dev /run /var /tmp /root /home /srv/../ /media/../)
|
|
|
|
_err() { echo "libreportal-storage: $*" >&2; }
|
|
|
|
_mode() {
|
|
local m
|
|
m=$(grep -h '^CFG_DOCKER_INSTALL_TYPE=' "$DB_CFG" 2>/dev/null | head -1 | cut -d= -f2 | awk '{print $1}')
|
|
echo "${m:-rootless}"
|
|
}
|
|
|
|
_container_owner() {
|
|
local appusr=""
|
|
if [[ "$(_mode)" == "rootless" ]]; then
|
|
appusr=$(grep -h '^CFG_DOCKER_INSTALL_USER=' "$DB_CFG" 2>/dev/null | head -1 | cut -d= -f2 | awk '{print $1}')
|
|
if [[ -n "$appusr" ]] && id -u "$appusr" >/dev/null 2>&1; then echo "$appusr"; return; fi
|
|
echo "dockerinstall"; return
|
|
fi
|
|
echo "$MANAGER"
|
|
}
|
|
|
|
_install_id() {
|
|
# Stable per-install identity, so an adopted drive can say which install
|
|
# wrote it. Derived from the machine id; never a secret.
|
|
local mid=""
|
|
[[ -r /etc/machine-id ]] && mid=$(cat /etc/machine-id 2>/dev/null)
|
|
[[ -z "$mid" && -r /var/lib/dbus/machine-id ]] && mid=$(cat /var/lib/dbus/machine-id 2>/dev/null)
|
|
echo "${mid:-unknown}" | cut -c1-16
|
|
}
|
|
|
|
_protected() {
|
|
local d="$1" allow_home="$2" p
|
|
for p in "${PROTECTED[@]}"; do
|
|
[[ "$p" == */../ ]] && continue
|
|
if [[ "$d" == "$p" ]]; then
|
|
[[ "$allow_home" == "1" && "$p" == "/home" ]] && continue
|
|
return 0
|
|
fi
|
|
# Inside a protected tree. /home is special: allowed with --allow-home,
|
|
# matching init.sh's existing --allow-home for the install-time roots.
|
|
if [[ "$d" == "$p"/* ]]; then
|
|
[[ "$allow_home" == "1" && "$p" == "/home" ]] && continue
|
|
return 0
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# Refuse a candidate that nests with any root we already know about, in EITHER
|
|
# direction. A storage location containing a backup repo (or the reverse) is a
|
|
# recursive-inclusion trap: the backup engine would walk a tree holding its own
|
|
# repository.
|
|
_nests() {
|
|
local d="$1" other
|
|
local -a known=("$SYSTEM_DIR" "$CONTAINERS_DIR" "$BACKUPS_DIR")
|
|
local _id _path _rest
|
|
if [[ -r "$REGISTRY" ]]; then
|
|
while IFS=$'\t' read -r _id _path _rest || [[ -n "$_id" ]]; do
|
|
[[ -z "$_path" || "$_id" == \#* ]] && continue
|
|
known+=("${_path%/}")
|
|
done < "$REGISTRY"
|
|
fi
|
|
for other in "${known[@]}"; do
|
|
other="${other%/}"
|
|
[[ -z "$other" ]] && continue
|
|
if [[ "$d" == "$other" || "$d" == "$other"/* || "$other" == "$d"/* ]]; then
|
|
echo "$other"; return 0
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# Empty means: nothing but lost+found (a filesystem's own artefact) and our own
|
|
# marker. Anything else and we refuse — see the header.
|
|
_is_empty_enough() {
|
|
local d="$1" e
|
|
shopt -s nullglob dotglob
|
|
for e in "$d"/*; do
|
|
e="${e##*/}"
|
|
[[ "$e" == "lost+found" || "$e" == "$MARKER" ]] && continue
|
|
shopt -u nullglob dotglob
|
|
return 1
|
|
done
|
|
shopt -u nullglob dotglob
|
|
return 0
|
|
}
|
|
|
|
_has_marker() { [[ -f "$1/$MARKER" ]]; }
|
|
|
|
_next_id() {
|
|
local max=0 _id _rest
|
|
if [[ -r "$REGISTRY" ]]; then
|
|
while IFS=$'\t' read -r _id _rest || [[ -n "$_id" ]]; do
|
|
[[ "$_id" =~ ^[0-9]+$ ]] || continue
|
|
(( _id > max )) && max=$_id
|
|
done < "$REGISTRY"
|
|
fi
|
|
echo $(( max + 1 ))
|
|
}
|
|
|
|
_dev_of() { stat -c '%d' -- "$1" 2>/dev/null || echo 0; }
|
|
_uuid_of() { findmnt -no UUID --target "$1" 2>/dev/null | tail -1; }
|
|
|
|
# Probe whether a filesystem can actually hold app data: POSIX ownership, the
|
|
# high sub-UIDs rootless containers map into, and a write that reads back.
|
|
#
|
|
# Runs as root because it must: for a CANDIDATE the directory is not ours yet
|
|
# (a fresh /mnt/disk is root-owned 0755), so an unprivileged probe can only ever
|
|
# report "cannot create a directory here" — which says nothing about the
|
|
# filesystem. The probe creates one uniquely-named directory, tests it, and
|
|
# removes it; the path is validated by the same protected-path rules as `add`
|
|
# before anything is created.
|
|
#
|
|
# Prints one <severity>\t<check>\t<message> record per finding, matching the
|
|
# manager-side checks. Exit non-zero if anything refused.
|
|
probe() {
|
|
local raw="${1:-}" allow_home=0
|
|
[[ "${2:-}" == "--allow-home" ]] && allow_home=1
|
|
[[ -n "$raw" ]] || { _err "probe requires a path"; return 2; }
|
|
[[ "$raw" == /* ]] || { _err "path must be absolute"; return 2; }
|
|
|
|
local d
|
|
d=$(realpath -e -- "$raw" 2>/dev/null) || { echo -e "refuse\tpath\tNo such directory: $raw"; return 1; }
|
|
d="${d%/}"
|
|
if _protected "$d" "$allow_home"; then
|
|
echo -e "refuse\tprotected\tInside a protected system path."
|
|
return 1
|
|
fi
|
|
|
|
local t="$d/.lp-storage-probe.$$"
|
|
local rc=0
|
|
if ! mkdir -p "$t" 2>/dev/null; then
|
|
echo -e "refuse\twritable\tCannot create a directory here."
|
|
return 1
|
|
fi
|
|
|
|
local cowner; cowner=$(_container_owner)
|
|
if ! chown "$cowner:$cowner" "$t" 2>/dev/null; then
|
|
echo -e "refuse\townership\tCannot set file ownership here (an NFS export with root_squash, or a filesystem without POSIX ownership)."
|
|
rc=1
|
|
fi
|
|
|
|
if (( rc == 0 )) && ! chown 165536:165536 "$t" 2>/dev/null; then
|
|
echo -e "refuse\tsubuid\tCannot store the high UIDs rootless containers use (tried 165536)."
|
|
rc=1
|
|
fi
|
|
|
|
if (( rc == 0 )); then
|
|
if ! echo libreportal > "$t/probe" 2>/dev/null; then
|
|
echo -e "refuse\twrite\tWrite failed."
|
|
rc=1
|
|
else
|
|
sync -f "$t/probe" 2>/dev/null || true
|
|
if [[ "$(cat "$t/probe" 2>/dev/null)" != "libreportal" ]]; then
|
|
echo -e "refuse\treadback\tWrote a file but read back different content — the device may be failing."
|
|
rc=1
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
rm -rf -- "$t"
|
|
return $rc
|
|
}
|
|
|
|
# Make a mount permanent by adding one line to /etc/fstab.
|
|
#
|
|
# fstab is the most dangerous file LibrePortal touches: a bad entry does not
|
|
# break an app, it can leave the machine unbootable and needing rescue media.
|
|
# Everything below exists to make that impossible:
|
|
#
|
|
# * nofail + x-systemd.device-timeout=10s — a missing device can then never
|
|
# block boot. This single pair is what makes writing fstab defensible at
|
|
# all; without it an unplugged drive strands the box at a systemd timeout
|
|
# or drops it to emergency mode.
|
|
# * UUID=, never /dev/sdX — device names reorder between boots.
|
|
# * append only, inside a marked block. Existing lines are never rewritten,
|
|
# so anything the user or another tool manages is untouched.
|
|
# * refuse when a line for that target or device already exists — we do not
|
|
# get to be the second opinion on a mount someone else configured.
|
|
# * a timestamped backup, and `findmnt --verify` before the new file is put
|
|
# in place. A file that does not verify is discarded, not installed.
|
|
#
|
|
# Opt-in only: nothing calls this unless a person asked for it.
|
|
fstab_add() {
|
|
local target="${1:-}"
|
|
[[ -n "$target" && "$target" == /* ]] \
|
|
|| { _err "fstab-add requires an absolute mount point"; return 2; }
|
|
target="${target%/}"
|
|
[[ "$target" == *..* ]] && { _err "invalid mount point"; return 2; }
|
|
|
|
command -v findmnt >/dev/null 2>&1 || { _err "findmnt unavailable"; return 1; }
|
|
|
|
# Refuse the root filesystem before anything else, so the reason given is the
|
|
# real one rather than a confusing "not a mount point".
|
|
[[ "$target" == "/" || -z "$target" ]] \
|
|
&& { _err "refusing to touch the root filesystem's fstab entry"; return 1; }
|
|
|
|
# Must currently be a real mount — we describe reality, we do not invent it.
|
|
local now_target
|
|
now_target=$(findmnt -no TARGET --target "$target" 2>/dev/null | tail -1)
|
|
if [[ "$now_target" != "$target" ]]; then
|
|
_err "'$target' is not a mount point right now. Mount it first, then add it."
|
|
return 1
|
|
fi
|
|
|
|
local uuid fstype
|
|
uuid=$(findmnt -no UUID --target "$target" 2>/dev/null | tail -1)
|
|
fstype=$(findmnt -no FSTYPE --target "$target" 2>/dev/null | tail -1)
|
|
if [[ -z "$uuid" ]]; then
|
|
_err "'$target' has no filesystem UUID, so no stable fstab entry can be written for it."
|
|
return 1
|
|
fi
|
|
[[ -z "$fstype" ]] && fstype="auto"
|
|
|
|
# Already described? Leave it alone — do not add a second opinion.
|
|
local existing
|
|
existing=$(grep -vE '^[[:space:]]*#' /etc/fstab 2>/dev/null \
|
|
| awk -v t="$target" -v u="UUID=$uuid" '$2==t || $1==u {print; exit}')
|
|
if [[ -n "$existing" ]]; then
|
|
_err "/etc/fstab already has an entry for this mount: $existing"
|
|
return 1
|
|
fi
|
|
|
|
local line="UUID=$uuid $target $fstype defaults,nofail,x-systemd.device-timeout=10s 0 2"
|
|
|
|
local stamp backup tmp
|
|
stamp=$(date +%Y%m%d-%H%M%S)
|
|
backup="/etc/fstab.libreportal-$stamp.bak"
|
|
cp -a /etc/fstab "$backup" || { _err "could not back up /etc/fstab"; return 1; }
|
|
|
|
tmp=$(mktemp) || return 1
|
|
cat /etc/fstab > "$tmp"
|
|
# Guarantee the file ends with a newline before appending, or the new entry
|
|
# would be glued onto whatever the last line was.
|
|
[[ -s "$tmp" && -n "$(tail -c1 "$tmp")" ]] && printf '\n' >> "$tmp"
|
|
{
|
|
printf '\n# Added by LibrePortal (%s) — storage location %s\n' "$stamp" "$target"
|
|
printf '# nofail: a missing device must never block boot.\n'
|
|
printf '%s\n' "$line"
|
|
} >> "$tmp"
|
|
|
|
# Verify BEFORE installing. findmnt --verify parses fstab and reports
|
|
# structural problems; a file that does not pass is thrown away.
|
|
if ! findmnt --verify --tab-file "$tmp" >/dev/null 2>&1; then
|
|
local why; why=$(findmnt --verify --tab-file "$tmp" 2>&1 | head -5)
|
|
rm -f "$tmp"
|
|
_err "the resulting /etc/fstab did not verify, so it was NOT installed: $why"
|
|
return 1
|
|
fi
|
|
|
|
install -m 0644 -o root -g root "$tmp" /etc/fstab || { rm -f "$tmp"; return 1; }
|
|
rm -f "$tmp"
|
|
|
|
# Let systemd pick up the new unit now, so the entry is live rather than
|
|
# only true after the next boot.
|
|
systemctl daemon-reload >/dev/null 2>&1 || true
|
|
|
|
echo "$line"
|
|
return 0
|
|
}
|
|
|
|
add() {
|
|
local raw="" name="" allow_home=0 a
|
|
for a in "$@"; do
|
|
case "$a" in
|
|
--allow-home) allow_home=1 ;;
|
|
--name=*) name="${a#--name=}" ;;
|
|
-*) _err "unknown option $a"; return 2 ;;
|
|
*) [[ -z "$raw" ]] && raw="$a" || name="$a" ;;
|
|
esac
|
|
done
|
|
[[ -n "$raw" ]] || { _err "add requires a path"; return 2; }
|
|
|
|
# --- absolute, and free of symlinks/.. ---------------------------------
|
|
[[ "$raw" == /* ]] || { _err "path must be absolute (got '$raw')"; return 1; }
|
|
local d
|
|
d=$(realpath -e -- "$raw" 2>/dev/null) || { _err "no such directory: $raw"; return 1; }
|
|
d="${d%/}"
|
|
if [[ "$d" != "${raw%/}" ]]; then
|
|
_err "refusing '$raw' — it resolves to '$d' (symlinked or non-canonical). Register the real path."
|
|
return 1
|
|
fi
|
|
[[ -d "$d" ]] || { _err "not a directory: $d"; return 1; }
|
|
|
|
# --- protected system paths --------------------------------------------
|
|
if _protected "$d" "$allow_home"; then
|
|
_err "refusing '$d' — inside a protected system path."
|
|
return 1
|
|
fi
|
|
|
|
# --- no nesting with any root we know ----------------------------------
|
|
local clash
|
|
if clash=$(_nests "$d"); then
|
|
# Re-adding the same path is idempotent when it already carries our marker.
|
|
if [[ "$clash" == "$d" ]] && _has_marker "$d"; then
|
|
local existing
|
|
existing=$(awk -F'\t' -v p="$d" '$2==p{print $1}' "$REGISTRY" 2>/dev/null | head -1)
|
|
[[ -n "$existing" ]] && { echo "$existing"; return 0; }
|
|
fi
|
|
_err "refusing '$d' — it nests with '$clash'. Use a sibling directory (e.g. '$d/apps') instead."
|
|
return 1
|
|
fi
|
|
|
|
# --- the parent must not be manager-writable ---------------------------
|
|
# Closes the validate-then-chown race: if the manager can rename or replace
|
|
# the directory between the checks below and the chown, the checks prove
|
|
# nothing. /mnt, /srv, /media are root-owned, which is the intended home.
|
|
local parent="${d%/*}"; [[ -z "$parent" ]] && parent="/"
|
|
if [[ -w "$parent" ]] && sudo -u "$MANAGER" test -w "$parent" 2>/dev/null; then
|
|
_err "refusing '$d' — its parent '$parent' is writable by $MANAGER, which would make the safety checks racy. Use a location under a root-owned parent such as /mnt or /srv."
|
|
return 1
|
|
fi
|
|
|
|
# --- empty, or already ours --------------------------------------------
|
|
if ! _is_empty_enough "$d"; then
|
|
if _has_marker "$d"; then
|
|
: # adopt: it is already a LibrePortal storage location
|
|
else
|
|
_err "refusing '$d' — it is not empty. Root only ever takes ownership of an empty directory. Create an empty subdirectory (e.g. '$d/apps') and register that."
|
|
return 1
|
|
fi
|
|
fi
|
|
|
|
# --- accept -------------------------------------------------------------
|
|
local id
|
|
id=$(_next_id)
|
|
local cowner; cowner=$(_container_owner)
|
|
|
|
umask 022
|
|
mkdir -p "$LIB_DIR"
|
|
{
|
|
echo "# LibrePortal storage location. Managed by libreportal-storage; do not edit."
|
|
echo "location_id=$id"
|
|
echo "install_id=$(_install_id)"
|
|
echo "created=$(date -Iseconds)"
|
|
echo "name=${name:-location-$id}"
|
|
} > "$d/$MARKER"
|
|
chown root:root "$d/$MARKER"
|
|
chmod 0644 "$d/$MARKER"
|
|
|
|
chown "$cowner:$cowner" "$d"
|
|
chmod 0751 "$d"
|
|
|
|
printf '%s\t%s\t%s\t%s\n' "$id" "$d" "$(_dev_of "$d")" "$(_uuid_of "$d")" >> "$REGISTRY"
|
|
chown root:root "$REGISTRY"
|
|
chmod 0644 "$REGISTRY"
|
|
|
|
echo "$id"
|
|
}
|
|
|
|
remove() {
|
|
local want="${1:-}"
|
|
[[ -n "$want" ]] || { _err "remove requires an id or path"; return 2; }
|
|
[[ -r "$REGISTRY" ]] || { _err "no storage registry"; return 1; }
|
|
|
|
local _id _path _dev _uuid found_path="" found_id=""
|
|
while IFS=$'\t' read -r _id _path _dev _uuid || [[ -n "$_id" ]]; do
|
|
[[ -z "$_path" || "$_id" == \#* ]] && continue
|
|
if [[ "$_id" == "$want" || "${_path%/}" == "${want%/}" ]]; then
|
|
found_path="${_path%/}"; found_id="$_id"; break
|
|
fi
|
|
done < "$REGISTRY"
|
|
[[ -n "$found_id" ]] || { _err "no such location: $want"; return 1; }
|
|
|
|
# Refuse while app data remains. A location whose drive is absent cannot be
|
|
# proven empty, so refuse that too rather than dropping the only record of
|
|
# where those apps live.
|
|
if [[ ! -f "$found_path/$MARKER" ]]; then
|
|
_err "refusing to remove location $found_id — '$found_path' has no marker, so its drive is probably not mounted. Mount it first, or the apps on it would be orphaned."
|
|
return 1
|
|
fi
|
|
local e leftovers=0
|
|
shopt -s nullglob
|
|
for e in "$found_path"/*/; do
|
|
[[ -d "$e" ]] && leftovers=$((leftovers + 1))
|
|
done
|
|
shopt -u nullglob
|
|
if (( leftovers > 0 )); then
|
|
_err "refusing to remove location $found_id — '$found_path' still holds $leftovers app director$( ((leftovers==1)) && echo y || echo ies). Move or uninstall them first."
|
|
return 1
|
|
fi
|
|
|
|
local tmp; tmp=$(mktemp)
|
|
awk -F'\t' -v id="$found_id" '$1!=id' "$REGISTRY" > "$tmp"
|
|
cat "$tmp" > "$REGISTRY"
|
|
rm -f "$tmp"
|
|
chown root:root "$REGISTRY"; chmod 0644 "$REGISTRY"
|
|
rm -f "$found_path/$MARKER"
|
|
echo "$found_id"
|
|
}
|
|
|
|
list() {
|
|
[[ -r "$REGISTRY" ]] || return 0
|
|
cat "$REGISTRY"
|
|
}
|
|
|
|
path() {
|
|
local want="${1:-}"
|
|
[[ -r "$REGISTRY" ]] || return 1
|
|
awk -F'\t' -v id="$want" '$1==id{print $2; found=1} END{exit !found}' "$REGISTRY"
|
|
}
|
|
|
|
# Re-check a location: marker present (i.e. drive mounted) and still the same
|
|
# filesystem it was registered on. Prints "<id> <state> <path>" per location.
|
|
verify() {
|
|
local only="${1:-}"
|
|
[[ -r "$REGISTRY" ]] || return 0
|
|
local _id _path _dev _uuid state now_uuid
|
|
while IFS=$'\t' read -r _id _path _dev _uuid || [[ -n "$_id" ]]; do
|
|
[[ -z "$_path" || "$_id" == \#* ]] && continue
|
|
[[ -n "$only" && "$only" != "$_id" ]] && continue
|
|
if [[ ! -f "${_path%/}/$MARKER" ]]; then
|
|
state="unmounted"
|
|
else
|
|
now_uuid=$(_uuid_of "${_path%/}")
|
|
if [[ -n "$_uuid" && -n "$now_uuid" && "$_uuid" != "$now_uuid" ]]; then
|
|
state="different-device"
|
|
else
|
|
state="ok"
|
|
fi
|
|
fi
|
|
printf '%s\t%s\t%s\n' "$_id" "$state" "${_path%/}"
|
|
done < "$REGISTRY"
|
|
}
|
|
|
|
action="${1:-}"; shift 2>/dev/null || true
|
|
case "$action" in
|
|
add) add "$@" ;;
|
|
probe) probe "${1:-}" "${2:-}" ;;
|
|
fstab-add) fstab_add "${1:-}" ;;
|
|
remove) remove "${1:-}" ;;
|
|
list) list ;;
|
|
path) path "${1:-}" ;;
|
|
verify) verify "${1:-}" ;;
|
|
*) echo "usage: libreportal-storage {add <path> [--name=NAME] [--allow-home]|probe <path> [--allow-home]|fstab-add <mountpoint>|remove <id|path>|list|path <id>|verify [id]}" >&2; exit 2 ;;
|
|
esac
|