feat: share clients — t=type manual mountpoint (existing path without create)
gates / consistency-and-conventions (push) Successful in 22s
gates / consistency-and-conventions (push) Successful in 22s
User report: smb-client/nfs-client mountpoint step could only auto-suggest
candidates, or create a fresh dir behind a hidden 'n=new' key — no way to
type an arbitrary existing path as the mountpoint, so the manual option
was effectively invisible (candidates from /media etc. always populated
the picker, hiding the typing path entirely). Designer framing: capability
gap + discoverability gap; backend already handled arbitrary paths (CLI
cmd_mount + ensure_mountpoint), only the interactive menu blocked it.
Change (identical in bin/pos-share-smb-client and bin/pos-share-nfs-client):
- pick_mountpoint hint 'n=new' -> 't=type'; key arm n -> t
- ask_new_mountpoint generalized to ask_mountpoint: an existing
directory is now used AS-IS (no create, no confirm); a non-existent
path keeps the 'Create mountpoint?' confirm + sudo mkdir flow; existing
non-directory rejected ('has a file there'); shape checks and system-path
refusal unchanged; stream contract (display->stderr, path->stdout) kept
- menu_ask_mountpoint empty-candidate fall-through now routes through the
same ask_mountpoint validator (single source of truth)
Docs: DOC/howto/share.md NFS+SMB mountpoint sections updated from n=new to
t=type and describe existing-path-without-create behavior.
Scoped to the two client files + howto doc; persistence/automount units,
unmount/remove flows, cmd_* CLIs, share_folder_candidates, and
lib/menu-lib.sh untouched.
Verified: 9-scenario smoke matrix x2 files (~19 assertions each: existing
dir as-is, new-dir confirm+create, decline, relative/trailing-slash/system/
empty rejections, non-dir reject, mkdir-fail), make gen idempotent, make
check OK, make lint 0 FAIL/0 WARN, make test 17 files / 299 checks green,
bash -n clean, git diff --check clean. Designer ACCEPT framing+spec;
Reviewer ACCEPT after doc fix.
This commit is contained in:
@@ -644,9 +644,9 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
|
||||
| `bin/pos-network-hotspot` | 93 | Wi-Fi hotspot via create_ap + wihotspot-gui |
|
||||
| `bin/pos-network-ip` | 69 | Show interfaces, routes, public IP + location |
|
||||
| `bin/pos-network-scan` | 272 | Parallel ping sweep of CIDR |
|
||||
| `bin/pos-share-nfs-client` | 504 | Mount NFS shares (ephemeral or persistent systemd mount units) |
|
||||
| `bin/pos-share-nfs-client` | 511 | Mount NFS shares (ephemeral or persistent systemd mount units) |
|
||||
| `bin/pos-share-nfs-server` | 245 | Manage the NFS kernel server (status, share/unshare exports, enable/disable) |
|
||||
| `bin/pos-share-smb-client` | 766 | Mount SMB/CIFS shares (ephemeral or persistent systemd mount units) |
|
||||
| `bin/pos-share-smb-client` | 773 | Mount SMB/CIFS shares (ephemeral or persistent systemd mount units) |
|
||||
| `bin/pos-share-smb-server` | 441 | Manage the Samba server (status, share/unshare exports, users, enable/disable) |
|
||||
| `bin/pos-share-usb-server` | 362 | USB Redirector server control (--ls, --share; prompts when args omitted) |
|
||||
| `bin/pos-ssh-load-keys` | 31 | Load all SSH keys into the agent |
|
||||
|
||||
+12
-9
@@ -155,12 +155,13 @@ up — a down/unreachable NFS server can't break boot (with fstab it could).
|
||||
(mount / persist / unmount / unpersist / list). Mountpoints are offered from
|
||||
existing mount-layout candidates with manual entry as fallback — the picker
|
||||
also accepts the server-side export path as a "(as on server)" pick when it
|
||||
differs from your local layout, and `n=new` creates a fresh directory in
|
||||
place (y/N confirmed; a failure just returns to the picker). Unmount lists
|
||||
the active NFS mounts as `<mountpoint> ← <source>` picks and asks for
|
||||
confirmation before unmounting (with a typed fallback when nothing is
|
||||
mounted); unmount and unpersist tolerate already-absent targets instead of
|
||||
erroring.
|
||||
differs from your local layout, and `t=type` lets you type an absolute
|
||||
mountpoint — an existing directory is used as-is, a non-existent path
|
||||
creates it in place (y/N confirmed; a failure just returns to the picker).
|
||||
Unmount lists the active NFS mounts as `<mountpoint> ← <source>` picks and
|
||||
asks for confirmation before unmounting (with a typed fallback when nothing
|
||||
is mounted); unmount and unpersist tolerate already-absent targets instead
|
||||
of erroring.
|
||||
|
||||
**Troubleshooting:**
|
||||
- "mount.nfs not found" → `nfs-common` isn't installed; `sudo apt install nfs-common`
|
||||
@@ -292,9 +293,11 @@ fstab it could). `enable --now` arms the automount immediately.
|
||||
an empty user tries guest enumeration first (with an auth retry on denial),
|
||||
then shares and mountpoints are offered as pickers with manual fallback —
|
||||
the account you authenticated with is reused for the mount. The mountpoint
|
||||
picker accepts `n=new` to create a fresh directory in place (y/N confirmed;
|
||||
a failure just returns to the picker); when the server is this machine, its
|
||||
underlying share directory is offered as a "(as on server)" pick too.
|
||||
picker accepts `t=type` to type an absolute mountpoint — an existing
|
||||
directory is used as-is, a non-existent path creates it in place (y/N
|
||||
confirmed; a failure just returns to the picker); when the server is this
|
||||
machine, its underlying share directory is offered as a "(as on server)"
|
||||
pick too.
|
||||
Unmount lists the active CIFS mounts as `<mountpoint> ← <source>` picks and
|
||||
asks for confirmation before unmounting (typed fallback when nothing is
|
||||
mounted).
|
||||
|
||||
+20
-13
@@ -223,8 +223,8 @@ cmd_unpersist() {
|
||||
# ── Interactive menu flows ─────────────────────────────────────
|
||||
|
||||
# Mountpoint picker — local port of the share_pick primitive with exactly
|
||||
# two deltas: the hint line offers `n=new`, and typing n runs the
|
||||
# create-new-dir flow below. share_pick cannot intercept `n` (it filters on
|
||||
# two deltas: the hint line offers `t=type`, and typing t runs the
|
||||
# manual-entry flow below. share_pick cannot intercept `t` (it filters on
|
||||
# it) and lib/menu-lib.sh is shared, so the fork lives here. Rendering of
|
||||
# numbered picks / text filter / 0=back is byte-identical to menu_pick.
|
||||
# stdout: chosen item text (or the freshly created dir) · rc 1 = back/cancel.
|
||||
@@ -263,16 +263,16 @@ pick_mountpoint() {
|
||||
done
|
||||
fi
|
||||
} >&2
|
||||
if ! read -rp "${prompt} [1-${n}], n=new, text=filter, 0=back " ans; then
|
||||
if ! read -rp "${prompt} [1-${n}], t=type, text=filter, 0=back " ans; then
|
||||
return 1 # EOF — cancel
|
||||
fi
|
||||
case "$ans" in
|
||||
"") [ -z "$filter" ] || filter="" ; continue ;;
|
||||
"/") filter="" ; continue ;;
|
||||
0 | q | Q | b | B) return 1 ;;
|
||||
n | N)
|
||||
made="$(ask_new_mountpoint)" && { echo "$made"; return 0; }
|
||||
continue # declined/invalid/mkdir-failed → redraw
|
||||
t | T)
|
||||
made="$(ask_mountpoint)" && { echo "$made"; return 0; }
|
||||
continue # cancelled/invalid/mkdir-failed → redraw
|
||||
;;
|
||||
*[!0-9]*)
|
||||
filter="$ans"
|
||||
@@ -289,15 +289,16 @@ pick_mountpoint() {
|
||||
done
|
||||
}
|
||||
|
||||
# Create-new-dir flow behind the picker's `n` key. Validates the shape
|
||||
# (absolute, no trailing slash), confirm-gates the creation, then mkdir -p.
|
||||
# Manual entry flow behind the picker's `t` key. Validates the shape
|
||||
# (absolute, no trailing slash, system-path refusal), then branches:
|
||||
# existing dir → use as-is; new dir → confirm-gate the creation + mkdir -p.
|
||||
# Any decline, invalid input, EOF or mkdir failure is a warning + rc 1 —
|
||||
# the picker redraws, the tool never aborts.
|
||||
ask_new_mountpoint() { # stdout: created dir · rc 1 = cancelled/failed
|
||||
ask_mountpoint() { # stdout: absolute path (existing or newly created) · rc 1 = cancelled/failed
|
||||
# NOTE: runs inside $( ) from the picker — every display line MUST go to
|
||||
# stderr (menu-lib contract: display → stderr, result → stdout).
|
||||
local dir
|
||||
dir="$(share_ask_value "New mountpoint (absolute path)")" || return 1
|
||||
dir="$(share_ask_value "Mountpoint (absolute path)")" || return 1
|
||||
case "$dir" in
|
||||
/*) ;;
|
||||
*) warn "'$dir' is not an absolute path — must start with /" >&2; return 1 ;;
|
||||
@@ -311,6 +312,14 @@ ask_new_mountpoint() { # stdout: created dir · rc 1 = cancelled/failed
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
if [ -d "$dir" ]; then
|
||||
echo "$dir"
|
||||
return 0
|
||||
fi
|
||||
if [ -e "$dir" ]; then
|
||||
warn "$dir exists and is not a directory — pick another mount point" >&2
|
||||
return 1
|
||||
fi
|
||||
confirm "Create mountpoint ${dir}?" n || return 1
|
||||
if ! run sudo mkdir -p "$dir"; then
|
||||
warn "Could not create ${dir}" >&2
|
||||
@@ -371,9 +380,7 @@ menu_ask_mountpoint() { # [server_path] — stdout: absolute path · rc 1 cancel
|
||||
esac
|
||||
fi
|
||||
fi
|
||||
dir="$(share_ask_value "Mountpoint (absolute path)")" || return 1
|
||||
[ -n "$dir" ] || { warn "No mountpoint given"; return 1; }
|
||||
echo "$dir"
|
||||
ask_mountpoint
|
||||
}
|
||||
|
||||
menu_mount() {
|
||||
|
||||
+20
-13
@@ -495,8 +495,8 @@ smb_server_path() { # <host> <share> — stdout: server-side dir · rc 1 = unres
|
||||
}
|
||||
|
||||
# Mountpoint picker — local port of the share_pick primitive with exactly
|
||||
# two deltas: the hint line offers `n=new`, and typing n runs the
|
||||
# create-new-dir flow below. share_pick cannot intercept `n` (it filters on
|
||||
# two deltas: the hint line offers `t=type`, and typing t runs the
|
||||
# manual-entry flow below. share_pick cannot intercept `t` (it filters on
|
||||
# it) and lib/menu-lib.sh is shared, so the fork lives here. Rendering of
|
||||
# numbered picks / text filter / 0=back is byte-identical to menu_pick.
|
||||
# stdout: chosen item text (or the freshly created dir) · rc 1 = back/cancel.
|
||||
@@ -535,16 +535,16 @@ pick_mountpoint() {
|
||||
done
|
||||
fi
|
||||
} >&2
|
||||
if ! read -rp "${prompt} [1-${n}], n=new, text=filter, 0=back " ans; then
|
||||
if ! read -rp "${prompt} [1-${n}], t=type, text=filter, 0=back " ans; then
|
||||
return 1 # EOF — cancel
|
||||
fi
|
||||
case "$ans" in
|
||||
"") [ -z "$filter" ] || filter="" ; continue ;;
|
||||
"/") filter="" ; continue ;;
|
||||
0 | q | Q | b | B) return 1 ;;
|
||||
n | N)
|
||||
made="$(ask_new_mountpoint)" && { echo "$made"; return 0; }
|
||||
continue # declined/invalid/mkdir-failed → redraw
|
||||
t | T)
|
||||
made="$(ask_mountpoint)" && { echo "$made"; return 0; }
|
||||
continue # cancelled/invalid/mkdir-failed → redraw
|
||||
;;
|
||||
*[!0-9]*)
|
||||
filter="$ans"
|
||||
@@ -561,15 +561,16 @@ pick_mountpoint() {
|
||||
done
|
||||
}
|
||||
|
||||
# Create-new-dir flow behind the picker's `n` key. Validates the shape
|
||||
# (absolute, no trailing slash), confirm-gates the creation, then mkdir -p.
|
||||
# Manual entry flow behind the picker's `t` key. Validates the shape
|
||||
# (absolute, no trailing slash, system-path refusal), then branches:
|
||||
# existing dir → use as-is; new dir → confirm-gate the creation + mkdir -p.
|
||||
# Any decline, invalid input, EOF or mkdir failure is a warning + rc 1 —
|
||||
# the picker redraws, the tool never aborts.
|
||||
ask_new_mountpoint() { # stdout: created dir · rc 1 = cancelled/failed
|
||||
ask_mountpoint() { # stdout: absolute path (existing or newly created) · rc 1 = cancelled/failed
|
||||
# NOTE: runs inside $( ) from the picker — every display line MUST go to
|
||||
# stderr (menu-lib contract: display → stderr, result → stdout).
|
||||
local dir
|
||||
dir="$(share_ask_value "New mountpoint (absolute path)")" || return 1
|
||||
dir="$(share_ask_value "Mountpoint (absolute path)")" || return 1
|
||||
case "$dir" in
|
||||
/*) ;;
|
||||
*) warn "'$dir' is not an absolute path — must start with /" >&2; return 1 ;;
|
||||
@@ -583,6 +584,14 @@ ask_new_mountpoint() { # stdout: created dir · rc 1 = cancelled/failed
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
if [ -d "$dir" ]; then
|
||||
echo "$dir"
|
||||
return 0
|
||||
fi
|
||||
if [ -e "$dir" ]; then
|
||||
warn "$dir exists and is not a directory — pick another mount point" >&2
|
||||
return 1
|
||||
fi
|
||||
confirm "Create mountpoint ${dir}?" n || return 1
|
||||
if ! run sudo mkdir -p "$dir"; then
|
||||
warn "Could not create ${dir}" >&2
|
||||
@@ -624,9 +633,7 @@ menu_ask_mountpoint() { # [server_path] — stdout: absolute path · rc 1 cancel
|
||||
esac
|
||||
fi
|
||||
fi
|
||||
dir="$(share_ask_value "Mountpoint (absolute path)")" || return 1
|
||||
[ -n "$dir" ] || { warn "No mountpoint given"; return 1; }
|
||||
echo "$dir"
|
||||
ask_mountpoint
|
||||
}
|
||||
|
||||
menu_mount() { # ephemeral|persist
|
||||
|
||||
Reference in New Issue
Block a user