feat: add -l (list) and -u (unmount) flags
Update PKGBUILD version / update-pkgver (push) Successful in 5s

A second sshfsc <alias> call only printed "Already mounted"; tearing
down a mount required ls + fusermount by hand. -l lists active mounts
verified via mountinfo.Mounted, -u <Host> unmounts and removes the
empty mountpoint dir. Flags are mutually exclusive.
This commit is contained in:
2026-05-04 09:25:55 +02:00
parent 3f3c631057
commit 8edddc5a28
4 changed files with 234 additions and 8 deletions
+43
View File
@@ -1,5 +1,48 @@
# Decisions
## 2026-05-04 Add `-l` (list) and `-u` (unmount) flags
- **Who**: Dom, ClaudeCode
- **Why**: Mounts under `$XDG_RUNTIME_DIR/sshfs/<alias>` had to be listed and
torn down with `ls` and `fusermount -u` by hand. A second `sshfsc <alias>`
call only prints `!!! Already mounted` and exits, so the tool gave no first-
class way to undo what it set up. `-l` and `-u` close that gap without
changing the default mount flow.
- **Tradeoffs**:
- Flag-based UX over subcommands keeps the existing `-e`/`-v`/`-r` style.
`-l` and `-u` are mutually exclusive (exit 2 when both passed). `-u` reuses
the positional `<Host>` argument; no separate value flag needed.
- `-l` only lists *real* mounts (verified via `mountinfo.Mounted`). Stale
empty dirs left behind by failed mounts are silently filtered, not
surfaced — keeps the output a true list of teardownable targets. Cleanup
of stale dirs is not implemented; tmpfs reclaims them on logout.
- `-u` shells out to `fusermount -u` and best-effort `os.Remove`s the empty
mount dir afterwards. A failed `Remove` warns but does not fail the
command — the unmount itself already succeeded.
- `mount_base(create bool)` returns `fs.ErrNotExist` when `create=false` and
the base is missing. `list_mounts` swallows that into "no mounts";
`unmount_sshfs` translates it into `not mounted: <alias>` so the caller
sees a single coherent error regardless of whether the alias-dir, the
base, or `XDG_RUNTIME_DIR` was missing.
- **How**:
- `verify_mount_dir` split into `mount_base(create bool)` (XDG resolve +
optional mkdir + EvalSymlinks) and `mount_path(base, name)` (join +
traversal + symlink guard, no mkdir). The original `verify_mount_dir`
becomes a thin wrapper that composes both and creates the mountpoint —
only the mount path needs that mkdir.
- `list_mounts(io.Writer)` reads the base dir, runs `mountinfo.Mounted` per
entry, prints alias names that pass. Takes a writer so tests can capture
output without stdout redirection.
- `unmount_sshfs(alias)` validates against `rxHostUser`, resolves the
mountpoint, checks `mountinfo.Mounted`, then `exec.Command("fusermount",
"-u", mount).Run()` and `os.Remove(mount)`.
- `main()` flow: after `flag.Parse()`, branch on `*lFlag` / `*uFlag` before
the existing mount path. Mutex check up front. Exit codes 8 (list) and 9
(unmount) extend the existing 2..7 range.
- Tests: `TestListMountsMissingBase`, `TestListMountsFiltersUnmounted`,
`TestUnmountRejectsBadAlias`, `TestUnmountNotMountedMissingBase`,
`TestUnmountNotMountedExistingDir`. The real `fusermount` path is not
exercised in unit tests — the not-mounted check returns before the exec.
## 2026-04-28 Move mount base from `~/Servers/` to `$XDG_RUNTIME_DIR/sshfs/`
- **Who**: Dom, ClaudeCode
- **Why**: `~/Servers/` is non-standard. XDG Base Directory spec defines