A registered drive that is unplugged rendered through the same path as any
other candidate — a "needs care" badge, "free of" with no numbers on either
side, an empty meter. To a first-time installer that reads as two broken disks
the scan turned up, with nothing tying the card back to a drive they registered
and later unplugged. Say "not connected", name the path, and draw no meter: a
meter with nothing in it is a claim about free space nobody measured. The same
locations are withheld from the dropdowns, since the wizard cannot stat a
directory on a drive that is absent.
Both dropdowns now end in "Custom path…", for a NAS mount or an LVM volume the
disk heuristics never rank as a candidate. Validation goes through
validateStep(3) rather than a disabled button: the apply side already refuses a
relative or system path, but its refusal is to fall back to the system disk,
and that is indistinguishable from having chosen the system disk on purpose.
A typed path is not a registered location, so setup_apply registers it via
storageAdd — which is what keeps the empty-directory admission rule and the
fitness checks in play — named after its basename, so it reads as "nas" rather
than "location-3" in the placement menus.
libreportal-storage: accept the name the listing prints. remove matched id and
path only, so `remove location-3` failed against a row displayed as
location-3. Root-owned helper changed, so footprint_version 10 -> 11.
Expose window.setupWizard: the instance was local to a promise in the
orchestrator and unreachable from the console or a test.
lp-storage-custom-test drives the step in a browser. Two holes it found in the
tests themselves, both the shape it exists to catch — a check whose failure
mode is to not run:
- It counted the cards that say "not connected" and asserted over those.
Turn the feature off and the count is zero, every() over an empty list is
true, and the block passed having checked nothing. The expectation now
comes from the feed.
- Both browser tests exited 0 whenever the page returned nothing. Under sudo,
where chromium will not start, they reported PASS having asserted nothing.
They now probe with `lp-shot --url` and curl: if the WebUI answers HTTP the
browser is the only thing that can have broken, and that is a failure.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
501 lines
20 KiB
Bash
Executable File
501 lines
20 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
|
|
# Also match the name the listing prints, since that is what a person
|
|
# reading the table will type. Includes the "location-N" fallback the
|
|
# marker records when a location was added without one.
|
|
if [[ -f "${_path%/}/$MARKER" ]]; then
|
|
local _name
|
|
_name=$(sed -n 's/^name=//p' "${_path%/}/$MARKER" 2>/dev/null | head -1)
|
|
if [[ -n "$_name" && "$_name" == "$want" ]]; then
|
|
found_path="${_path%/}"; found_id="$_id"; break
|
|
fi
|
|
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
|