feat: add -l (list) and -u (unmount) flags
Update PKGBUILD version / update-pkgver (push) Successful in 5s
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:
@@ -4,8 +4,11 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -26,6 +29,8 @@ var (
|
||||
var (
|
||||
eFlag = flag.Bool("e", false, "open mountpoint in your editor")
|
||||
vFlag = flag.Bool("v", false, "verbose: print resolved ssh_config fields")
|
||||
lFlag = flag.Bool("l", false, "list active mounts and exit")
|
||||
uFlag = flag.Bool("u", false, "unmount the given <Host> and exit")
|
||||
rDir string
|
||||
)
|
||||
|
||||
@@ -38,13 +43,39 @@ func main() {
|
||||
flag.Usage = func() {
|
||||
fmt.Fprintf(os.Stderr,
|
||||
"usage: sshfsc [flags] <Host>\n\n"+
|
||||
"Mount a remote home directory via sshfs based on ~/.ssh/config entries.\n\n"+
|
||||
"Mount a remote home directory via sshfs based on ~/.ssh/config entries.\n"+
|
||||
"Use -l to list active mounts; -u <Host> to unmount.\n\n"+
|
||||
"Flags:\n")
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
flag.Parse()
|
||||
args := flag.Args()
|
||||
|
||||
if *lFlag && *uFlag {
|
||||
fmt.Fprintln(os.Stderr, "-l and -u are mutually exclusive")
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
if *lFlag {
|
||||
if err := list_mounts(os.Stdout); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "list_mounts() failed:", err)
|
||||
os.Exit(8)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if *uFlag {
|
||||
if len(args) == 0 {
|
||||
fmt.Fprintln(os.Stderr, "-u requires <Host> argument")
|
||||
os.Exit(2)
|
||||
}
|
||||
if err := unmount_sshfs(args[0]); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "unmount_sshfs() failed:", err)
|
||||
os.Exit(9)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if len(args) == 0 {
|
||||
fmt.Fprintln(os.Stderr, "No hostname specified.")
|
||||
os.Exit(2)
|
||||
@@ -182,32 +213,118 @@ func run_editor(mount string) error {
|
||||
return cmd.Start()
|
||||
}
|
||||
|
||||
func verify_mount_dir(name string) (string, error) {
|
||||
// mount_base resolves the per-user sshfs mount base under $XDG_RUNTIME_DIR.
|
||||
// When create is true, the base directory is mkdir'd; otherwise a missing
|
||||
// base bubbles up as fs.ErrNotExist so callers can decide how to react.
|
||||
func mount_base(create bool) (string, error) {
|
||||
runtime := os.Getenv("XDG_RUNTIME_DIR")
|
||||
if runtime == "" {
|
||||
runtime = filepath.Join("/run/user", strconv.Itoa(os.Getuid()))
|
||||
}
|
||||
base := filepath.Join(runtime, "sshfs")
|
||||
if err := os.MkdirAll(base, 0700); err != nil {
|
||||
return "", fmt.Errorf("create base %q: %w", base, err)
|
||||
if create {
|
||||
if err := os.MkdirAll(base, 0700); err != nil {
|
||||
return "", fmt.Errorf("create base %q: %w", base, err)
|
||||
}
|
||||
} else if _, err := os.Stat(base); err != nil {
|
||||
return "", err
|
||||
}
|
||||
realBase, err := filepath.EvalSymlinks(base)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("resolve base %q: %w", base, err)
|
||||
}
|
||||
mount := filepath.Clean(filepath.Join(realBase, name))
|
||||
if !strings.HasPrefix(mount, realBase+string(os.PathSeparator)) {
|
||||
return "", fmt.Errorf("name %q escapes mount base %q", name, realBase)
|
||||
return realBase, nil
|
||||
}
|
||||
|
||||
// mount_path joins base + name with traversal and symlink guards.
|
||||
// It does not create the mountpoint directory.
|
||||
func mount_path(base, name string) (string, error) {
|
||||
mount := filepath.Clean(filepath.Join(base, name))
|
||||
if !strings.HasPrefix(mount, base+string(os.PathSeparator)) {
|
||||
return "", fmt.Errorf("name %q escapes mount base %q", name, base)
|
||||
}
|
||||
if info, err := os.Lstat(mount); err == nil && info.Mode()&os.ModeSymlink != 0 {
|
||||
return "", fmt.Errorf("mount path %q is a symlink", mount)
|
||||
}
|
||||
return mount, nil
|
||||
}
|
||||
|
||||
func verify_mount_dir(name string) (string, error) {
|
||||
base, err := mount_base(true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
mount, err := mount_path(base, name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := os.MkdirAll(mount, 0700); err != nil {
|
||||
return "", fmt.Errorf("create mount dir %q: %w", mount, err)
|
||||
}
|
||||
return mount, nil
|
||||
}
|
||||
|
||||
func list_mounts(out io.Writer) error {
|
||||
base, err := mount_base(false)
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
entries, err := os.ReadDir(base)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read base %q: %w", base, err)
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
path := filepath.Join(base, entry.Name())
|
||||
ok, merr := mountinfo.Mounted(path)
|
||||
if merr != nil || !ok {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintln(out, entry.Name())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func unmount_sshfs(alias string) error {
|
||||
if err := validate_ssh_field("alias", alias, rxHostUser); err != nil {
|
||||
return err
|
||||
}
|
||||
base, err := mount_base(false)
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return fmt.Errorf("not mounted: %s", alias)
|
||||
}
|
||||
return err
|
||||
}
|
||||
mount, err := mount_path(base, alias)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ok, err := mountinfo.Mounted(mount)
|
||||
if err != nil {
|
||||
return fmt.Errorf("mountinfo.Mounted(%q): %w", mount, err)
|
||||
}
|
||||
if !ok {
|
||||
return fmt.Errorf("not mounted: %s", alias)
|
||||
}
|
||||
cmd := exec.Command("fusermount", "-u", mount)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("fusermount -u %q: %w", mount, err)
|
||||
}
|
||||
if err := os.Remove(mount); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "warning: remove empty mount dir %q: %v\n", mount, err)
|
||||
}
|
||||
fmt.Println("Unmounted:", alias)
|
||||
return nil
|
||||
}
|
||||
|
||||
func mount_sshfs(alias string, user string, ifile string, port string, mount string, remoteDir string) error {
|
||||
cmd := exec.Command("sshfs", "-p", port,
|
||||
"-o", "IdentityFile="+ifile,
|
||||
|
||||
Reference in New Issue
Block a user