From 35eb90a58b4cf00e5c3e58486117c05be6a18d1b Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 7 Sep 2026 08:08:02 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20share=20clients=20=E2=80=94=20t=3Dtype?= =?UTF-8?q?=20manual=20mountpoint=20(existing=20path=20without=20create)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- DOC/AGENT_Context_Project.md | 4 ++-- DOC/howto/share.md | 21 ++++++++++++--------- bin/pos-share-nfs-client | 33 ++++++++++++++++++++------------- bin/pos-share-smb-client | 33 ++++++++++++++++++++------------- 4 files changed, 54 insertions(+), 37 deletions(-) diff --git a/DOC/AGENT_Context_Project.md b/DOC/AGENT_Context_Project.md index 2492d92..bcbf1f1 100644 --- a/DOC/AGENT_Context_Project.md +++ b/DOC/AGENT_Context_Project.md @@ -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 | diff --git a/DOC/howto/share.md b/DOC/howto/share.md index a31a5a0..d41e1cf 100644 --- a/DOC/howto/share.md +++ b/DOC/howto/share.md @@ -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 `` 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 `` 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 `` picks and asks for confirmation before unmounting (typed fallback when nothing is mounted). diff --git a/bin/pos-share-nfs-client b/bin/pos-share-nfs-client index 52d3bcf..f2cb060 100755 --- a/bin/pos-share-nfs-client +++ b/bin/pos-share-nfs-client @@ -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() { diff --git a/bin/pos-share-smb-client b/bin/pos-share-smb-client index 0180c6b..c972a78 100755 --- a/bin/pos-share-smb-client +++ b/bin/pos-share-smb-client @@ -495,8 +495,8 @@ smb_server_path() { # — 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