From 3f3c6310577ee79565dd8b2e3febbf6c44998238 Mon Sep 17 00:00:00 2001 From: nevaforget Date: Tue, 28 Apr 2026 15:48:54 +0200 Subject: [PATCH] feat: mount under \$XDG_RUNTIME_DIR/sshfs/ instead of ~/Servers/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aligns with XDG Base Directory spec: $XDG_RUNTIME_DIR is the defined location for non-essential runtime files (ephemeral, user-owned, session-scoped). sshfs mounts fit that definition exactly, and the tmpfs backing means orphaned mountpoint dirs vanish on logout instead of accumulating. - verify_mount_dir reads $XDG_RUNTIME_DIR, falls back to /run/user// via os.Getuid(). - Existing path-traversal guard and symlink rejection carry over unchanged. - Tests switched from t.Setenv("HOME") to t.Setenv("XDG_RUNTIME_DIR"). File-manager sidebar visibility is unaffected — gvfs surfaces FUSE mounts via /proc/mounts regardless of mountpoint location. --- DECISIONS.md | 30 ++++++++++++++++++++++++++++++ README.md | 2 +- main.go | 8 ++++---- main_test.go | 16 ++++++++-------- 4 files changed, 43 insertions(+), 13 deletions(-) diff --git a/DECISIONS.md b/DECISIONS.md index 9e91687..397a524 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -1,5 +1,35 @@ # Decisions +## 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 + `$XDG_RUNTIME_DIR` (= `/run/user//`) for "non-essential runtime files" + — ephemeral, user-owned, session-bezogen. sshfs mounts fit that definition + exactly. systemd-logind creates the directory at login with mode 0700, so + permissions are correct by construction. +- **Tradeoffs**: + - Filemanager sidebar visibility is unchanged — gvfs surfaces FUSE mounts + via `/proc/mounts` regardless of mountpoint location. + - Tab-completion in `~/` no longer reaches mounts; users targeting the + mount via shell need the explicit `$XDG_RUNTIME_DIR/sshfs/` path. + Acceptable given Dom's primary access is the filemanager sidebar. + - tmpfs-backed → mounts and their containing dir vanish on logout/reboot. + No more orphaned empty dirs. Tradeoff: bookmarks pinned to the absolute + path stay valid because UID is stable across sessions, but the directory + is gone between logins until the next mount recreates it. + - Fallback to `/run/user//` when `$XDG_RUNTIME_DIR` is unset (e.g. + cron, non-login shells without systemd-logind). Falls auf einem System + ohne systemd `/run/user//` nicht existiert, schlägt `MkdirAll` mit + klarem Fehler fehl — kein Silent-Fallback auf `~/Servers/`. +- **How**: + - `verify_mount_dir` reads `$XDG_RUNTIME_DIR`, falls back to + `/run/user//` via `os.Getuid()` + `strconv.Itoa`. + - Base = `/sshfs`, created with `MkdirAll(0700)`. + - Existing path-traversal guard (`EvalSymlinks` + `HasPrefix`) and symlink + rejection (`Lstat`) carry over unchanged. + - Tests switched from `t.Setenv("HOME", ...)` to + `t.Setenv("XDG_RUNTIME_DIR", ...)`. + ## 2026-04-28 – Use ssh_config alias as label for mountpoint and sshfs source - **Who**: Dom, ClaudeCode - **Why**: File managers showed the resolved `HostName` (often a raw IP) for diff --git a/README.md b/README.md index 236d496..a4e0269 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ Quickly mount remote systems via SSHFS based on your ssh_config -Static mount dir is currently `~/Servers/` (uses the ssh_config alias as label, not the resolved IP). +Mounts land under `$XDG_RUNTIME_DIR/sshfs/` (typically `/run/user/$UID/sshfs/`), using the ssh_config alias as label, not the resolved IP. The directory is auto-cleaned on logout (tmpfs). # Install diff --git a/main.go b/main.go index c1c9149..9a77466 100644 --- a/main.go +++ b/main.go @@ -183,11 +183,11 @@ func run_editor(mount string) error { } func verify_mount_dir(name string) (string, error) { - homedir, err := os.UserHomeDir() - if err != nil { - return "", fmt.Errorf("resolve home dir: %w", err) + runtime := os.Getenv("XDG_RUNTIME_DIR") + if runtime == "" { + runtime = filepath.Join("/run/user", strconv.Itoa(os.Getuid())) } - base := filepath.Join(homedir, "Servers") + base := filepath.Join(runtime, "sshfs") if err := os.MkdirAll(base, 0700); err != nil { return "", fmt.Errorf("create base %q: %w", base, err) } diff --git a/main_test.go b/main_test.go index a3a5d53..b84e986 100644 --- a/main_test.go +++ b/main_test.go @@ -1,5 +1,5 @@ -// ABOUTME: Unit tests for sshfsc helpers, primarily the path-traversal guard -// ABOUTME: in verify_mount_dir and the ssh_config field allowlist regexes. +// ABOUTME: Unit tests for sshfsc helpers — path-traversal guard in +// ABOUTME: verify_mount_dir (XDG_RUNTIME_DIR-rooted) and field allowlist regexes. package main @@ -11,9 +11,9 @@ import ( ) func TestVerifyMountDir(t *testing.T) { - home := t.TempDir() - t.Setenv("HOME", home) - base := filepath.Join(home, "Servers") + runtime := t.TempDir() + t.Setenv("XDG_RUNTIME_DIR", runtime) + base := filepath.Join(runtime, "sshfs") if err := os.MkdirAll(base, 0700); err != nil { t.Fatalf("setup base: %v", err) } @@ -123,9 +123,9 @@ func TestValidateSSHFieldRegexes(t *testing.T) { } func TestVerifyMountDirRejectsSymlink(t *testing.T) { - home := t.TempDir() - t.Setenv("HOME", home) - base := filepath.Join(home, "Servers") + runtime := t.TempDir() + t.Setenv("XDG_RUNTIME_DIR", runtime) + base := filepath.Join(runtime, "sshfs") if err := os.MkdirAll(base, 0700); err != nil { t.Fatalf("setup base: %v", err) }