5ef38dc46f
- deps guards before -h|--help in docker-health/ps, network-scan, usb-server, media-mp3/mp4 (--dry-run pre-scan kept); system-firewall gains usage()/--help; autostart/usb-automount get flags.sh + template - install.sh: normalize N-M range syntax in --steps - bin/pos: INTERACTIVE_CMDS += docker-compose docker-vbox network-hotspot - common.sh: canonical XDG-aware CONFIG_DIR + DIM color var; notify.sh stderr fallback; ent_plugin_* registry renames (runtime plugin API kept) - docker-compose SCALE_DIR/CONFIG_ENV env seams; ffmpeg in PACKAGES; scrcpy.sh exec bit - docs: health is console-only (--send/--markdown removed), POS.md file refs for config/tree/entertainment, DEV.md no-guard exception, docmap/ filetable regenerated (make gen), hand-maintained line rows bumped - add scripts/lint-conventions.sh gate + Makefile lint target; record all VERIFIED outcomes in MAINTENANCE.md; AGENT_TODO Done entry (2026-08-14) - gates: make gen/check/lint all green (0 FAIL, 0 WARN); bash -n sweep clean; restricted-PATH dep tests + step-matrix dry-runs verified
139 lines
5.0 KiB
Bash
Executable File
139 lines
5.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# Robust flags.sh load — works from the repo checkout AND from
|
|
# /usr/local/bin after install.sh (which copies lib/flags.sh there).
|
|
source "$(dirname "$0")/../lib/flags.sh" 2>/dev/null || source "$(dirname "$0")/flags.sh"
|
|
|
|
# ────────────────────────────────────────────────────────────────
|
|
# USB automount feature — auto-mounts removable USB storage.
|
|
#
|
|
# Installed on demand with: ./install.sh --feature
|
|
# → copied to /usr/local/bin/usb-automount.sh (chmod 755)
|
|
# → flag "usb-automount" is set
|
|
#
|
|
# Triggers:
|
|
# - boot + hotplug via systemd/usb-automount.service (the udev
|
|
# rule below is self-installed on first root run)
|
|
# - manual: sudo usb-automount.sh
|
|
#
|
|
# Behavior: mounts every unmounted removable block device at
|
|
# /media/<label>, world-writable (-o umask=000) so non-root users
|
|
# can write — this is what lets `pos system backup` copy the
|
|
# verified archive onto a stick. Idempotent: already-mounted
|
|
# devices are skipped, repeated runs are no-ops.
|
|
# ────────────────────────────────────────────────────────────────
|
|
|
|
# Log — root/systemd runs have no $HOME (same convention as autostart.sh).
|
|
LOG="${HOME:-/root}/.usb-automount.log"
|
|
|
|
# Test-only seams (DEV.md env-seam registry):
|
|
# AUTOMOUNT_SKIP_ROOT_GUARD=1 skip the sudo re-exec (stub suites)
|
|
# UDEV_RULES_DIR=<dir> where the udev rule goes
|
|
# UDEVADM=<path> udevadm binary (or stub)
|
|
# MOUNT_BASE=<dir> mount point base (stub suites use a tmpdir)
|
|
RULES_DIR="${UDEV_RULES_DIR:-/etc/udev/rules.d}"
|
|
RULE_FILE="$RULES_DIR/99-usb-automount.rules"
|
|
UDEV_RULE='ACTION=="add", KERNEL=="sd[a-z]*", SUBSYSTEM=="block", ENV{ID_BUS}=="usb", TAG+="systemd", SYSTEMD_WANTS="usb-automount.service"'
|
|
MOUNT_BASE="${MOUNT_BASE:-/media}"
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
Usage: usb-automount.sh [options]
|
|
|
|
Auto-mount USB storage: every unmounted removable block device is
|
|
mounted at /media/<label> (world-writable). Idempotent.
|
|
|
|
Triggers:
|
|
- boot and hotplug via usb-automount.service (udev rule installed
|
|
on first root run, service flag-gated in postinstall.sh)
|
|
- manual: sudo usb-automount.sh
|
|
|
|
Installed via: ./install.sh --feature
|
|
Flag: usb-automount
|
|
Log: ${HOME:-/root}/.usb-automount.log
|
|
EOF
|
|
exit 0
|
|
}
|
|
|
|
case "${1:-}" in
|
|
-h|--help) usage ;;
|
|
esac
|
|
|
|
log() {
|
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG" 2>/dev/null || true
|
|
}
|
|
|
|
# ── dependency guard ────────────────────────────────────────────
|
|
for dep in lsblk jq mount mountpoint; do
|
|
command -v "$dep" &>/dev/null || {
|
|
log "missing dependency: $dep"
|
|
exit 1
|
|
}
|
|
done
|
|
|
|
# ── root guard: mounting + udev need root ───────────────────────
|
|
if [ "$(id -u)" -ne 0 ] && [ "${AUTOMOUNT_SKIP_ROOT_GUARD:-0}" != "1" ]; then
|
|
exec sudo "$0" "$@"
|
|
fi
|
|
|
|
# ── udev rule: hotplug trigger, self-installed once ─────────────
|
|
# Installed on first root run only — an existing (possibly
|
|
# user-edited) rule is never overwritten.
|
|
install_udev_rule() {
|
|
mkdir -p "$RULES_DIR"
|
|
if [ ! -f "$RULE_FILE" ]; then
|
|
printf '%s\n' "$UDEV_RULE" > "$RULE_FILE"
|
|
"${UDEVADM:-udevadm}" control --reload
|
|
"${UDEVADM:-udevadm}" trigger --subsystem-match=block
|
|
log "installed udev rule: $RULE_FILE"
|
|
fi
|
|
}
|
|
|
|
# ── mount one device at /media/<label> (bump -2, -3 on collision)
|
|
mount_one() {
|
|
local dev="$1" label="$2" mp n
|
|
|
|
label="${label//\//_}"
|
|
mp="$MOUNT_BASE/${label:-usb-$(basename "$dev")}"
|
|
|
|
# Label already in use as a mountpoint (another stick) → bump.
|
|
if [ -d "$mp" ] && mountpoint -q "$mp" 2>/dev/null; then
|
|
n=2
|
|
while [ -d "${mp}-${n}" ] && mountpoint -q "${mp}-${n}" 2>/dev/null; do
|
|
n=$((n + 1))
|
|
done
|
|
mp="${mp}-${n}"
|
|
fi
|
|
|
|
mkdir -p "$mp"
|
|
if mount -o umask=000 "$dev" "$mp" 2>/dev/null; then
|
|
log "mounted $dev -> $mp (umask=000, world-writable)"
|
|
elif mount "$dev" "$mp" 2>/dev/null; then
|
|
log "mounted $dev -> $mp (plain)"
|
|
else
|
|
log "FAILED to mount $dev -> $mp"
|
|
fi
|
|
}
|
|
|
|
# ── scan: unmounted removable partitions / raw whole-disk FS ────
|
|
mount_removable() {
|
|
local out
|
|
out="$(lsblk -J -o NAME,PATH,LABEL,MOUNTPOINT,RM,TYPE 2>/dev/null)" || {
|
|
log "lsblk failed"
|
|
return 1
|
|
}
|
|
while IFS=$'\t' read -r dev label; do
|
|
[ -n "$dev" ] || continue
|
|
mount_one "$dev" "$label"
|
|
done < <(printf '%s' "$out" | jq -r '
|
|
.. | objects
|
|
| select(.rm == true and .mountpoint == null and
|
|
(.type == "part" or (.type == "disk" and ((.children // []) | length) == 0)))
|
|
| [.path, (.label // "")] | @tsv')
|
|
}
|
|
|
|
install_udev_rule
|
|
mount_removable
|
|
exit 0
|