feat: mount under \$XDG_RUNTIME_DIR/sshfs/ instead of ~/Servers/
Update PKGBUILD version / update-pkgver (push) Successful in 3s

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/<uid>/ 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.
This commit is contained in:
2026-04-28 15:48:54 +02:00
parent afb51f1d61
commit 3f3c631057
4 changed files with 43 additions and 13 deletions
+30
View File
@@ -1,5 +1,35 @@
# Decisions # 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/<uid>/`) 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/<alias>` 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/<uid>/` when `$XDG_RUNTIME_DIR` is unset (e.g.
cron, non-login shells without systemd-logind). Falls auf einem System
ohne systemd `/run/user/<uid>/` 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/<uid>/` via `os.Getuid()` + `strconv.Itoa`.
- Base = `<runtime>/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 ## 2026-04-28 Use ssh_config alias as label for mountpoint and sshfs source
- **Who**: Dom, ClaudeCode - **Who**: Dom, ClaudeCode
- **Why**: File managers showed the resolved `HostName` (often a raw IP) for - **Why**: File managers showed the resolved `HostName` (often a raw IP) for
+1 -1
View File
@@ -1,6 +1,6 @@
Quickly mount remote systems via SSHFS based on your ssh_config Quickly mount remote systems via SSHFS based on your ssh_config
Static mount dir is currently `~/Servers/<Host>` (uses the ssh_config alias as label, not the resolved IP). Mounts land under `$XDG_RUNTIME_DIR/sshfs/<Host>` (typically `/run/user/$UID/sshfs/<Host>`), using the ssh_config alias as label, not the resolved IP. The directory is auto-cleaned on logout (tmpfs).
# Install # Install
+4 -4
View File
@@ -183,11 +183,11 @@ func run_editor(mount string) error {
} }
func verify_mount_dir(name string) (string, error) { func verify_mount_dir(name string) (string, error) {
homedir, err := os.UserHomeDir() runtime := os.Getenv("XDG_RUNTIME_DIR")
if err != nil { if runtime == "" {
return "", fmt.Errorf("resolve home dir: %w", err) 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 { if err := os.MkdirAll(base, 0700); err != nil {
return "", fmt.Errorf("create base %q: %w", base, err) return "", fmt.Errorf("create base %q: %w", base, err)
} }
+8 -8
View File
@@ -1,5 +1,5 @@
// ABOUTME: Unit tests for sshfsc helpers, primarily the path-traversal guard // ABOUTME: Unit tests for sshfsc helpers path-traversal guard in
// ABOUTME: in verify_mount_dir and the ssh_config field allowlist regexes. // ABOUTME: verify_mount_dir (XDG_RUNTIME_DIR-rooted) and field allowlist regexes.
package main package main
@@ -11,9 +11,9 @@ import (
) )
func TestVerifyMountDir(t *testing.T) { func TestVerifyMountDir(t *testing.T) {
home := t.TempDir() runtime := t.TempDir()
t.Setenv("HOME", home) t.Setenv("XDG_RUNTIME_DIR", runtime)
base := filepath.Join(home, "Servers") base := filepath.Join(runtime, "sshfs")
if err := os.MkdirAll(base, 0700); err != nil { if err := os.MkdirAll(base, 0700); err != nil {
t.Fatalf("setup base: %v", err) t.Fatalf("setup base: %v", err)
} }
@@ -123,9 +123,9 @@ func TestValidateSSHFieldRegexes(t *testing.T) {
} }
func TestVerifyMountDirRejectsSymlink(t *testing.T) { func TestVerifyMountDirRejectsSymlink(t *testing.T) {
home := t.TempDir() runtime := t.TempDir()
t.Setenv("HOME", home) t.Setenv("XDG_RUNTIME_DIR", runtime)
base := filepath.Join(home, "Servers") base := filepath.Join(runtime, "sshfs")
if err := os.MkdirAll(base, 0700); err != nil { if err := os.MkdirAll(base, 0700); err != nil {
t.Fatalf("setup base: %v", err) t.Fatalf("setup base: %v", err)
} }