diff --git a/weed/command/mount.go b/weed/command/mount.go index 5784e80da..8bbdca2da 100644 --- a/weed/command/mount.go +++ b/weed/command/mount.go @@ -192,6 +192,13 @@ var cmdMount = &Command{ On OS X, it requires OSXFUSE (https://osxfuse.github.io/). + On Windows, it requires WinFsp (https://winfsp.dev/). The mount point can be + a drive letter (-dir=S:), a directory that does not exist yet, or a network + path (-dir=\\seaweedfs\share). A drive letter belongs to the logon session + that created it, so one mounted by a service is usually invisible to users + at the desktop; the network path form is reachable from every session, and + each user can map their own drive letter to it. + RDMA Acceleration: For ultra-fast reads, enable RDMA acceleration with an RDMA sidecar: weed mount -filer=localhost:8888 -dir=/mnt/seaweedfs \ diff --git a/weed/command/mount_windows.go b/weed/command/mount_windows.go index 6717a0b3c..fe52e565f 100644 --- a/weed/command/mount_windows.go +++ b/weed/command/mount_windows.go @@ -136,6 +136,9 @@ func RunMount(option *MountOptions, umask os.FileMode) bool { glog.V(0).Infof("mounting %s%s to %v", *option.filer, mountRoot, dir) glog.V(0).Infof("This is SeaweedFS version %s %s %s", version.Version(), runtime.GOOS, runtime.GOARCH) glog.V(0).Infof("Windows mount is beta: hard links are unavailable and byte-range locks are not shared across mounts") + if prefix, _ := winfsp.VolumePrefix(dir); prefix != "" { + glog.V(0).Infof(`network mount: every session on this machine can open \%s or map a drive letter to it`, prefix) + } if err := host.Serve(windowsMountPoint(dir)); err != nil { glog.Errorf("%v", err) @@ -157,6 +160,9 @@ func checkWindowsMountPoint(dir string) error { } return nil } + if prefix, err := winfsp.VolumePrefix(dir); err != nil || prefix != "" { + return err + } if strings.HasPrefix(dir, `\\`) { return nil } diff --git a/weed/mount/winfsp/host_windows.go b/weed/mount/winfsp/host_windows.go index 218c15679..9f1a9a421 100644 --- a/weed/mount/winfsp/host_windows.go +++ b/weed/mount/winfsp/host_windows.go @@ -65,9 +65,13 @@ func (h *Host) Notify(wfs *mount.WFS) { } // Serve attaches the filesystem at mountPoint, which is a drive letter ("S:"), -// a directory that does not yet exist, or a UNC path. It blocks until the -// filesystem is unmounted. +// a directory that does not yet exist, or a \\server\share UNC path. It blocks +// until the filesystem is unmounted. func (h *Host) Serve(mountPoint string) error { + prefix, err := VolumePrefix(mountPoint) + if err != nil { + return err + } opts := []string{ "-o", "volname=" + h.volumeName(), "-o", "uid=-1", @@ -99,6 +103,13 @@ func (h *Host) Serve(mountPoint string) error { "-o", "EaTimeout="+ms, ) } + if prefix != "" { + // The mount point itself becomes the network prefix; what WinFsp + // still wants a mount point for is the drive letter it gives the + // mounting session, and "*" lets it pick a free one. + opts = append(opts, "-o", "VolumePrefix="+prefix) + mountPoint = "*" + } if h.options.Debug { opts = append(opts, "-d") } diff --git a/weed/mount/winfsp/mountpoint.go b/weed/mount/winfsp/mountpoint.go new file mode 100644 index 000000000..29adf8da7 --- /dev/null +++ b/weed/mount/winfsp/mountpoint.go @@ -0,0 +1,32 @@ +package winfsp + +import ( + "fmt" + "strings" +) + +// VolumePrefix converts a \\server\share mount point into WinFsp's +// VolumePrefix option, which registers the mount as a network file system. +// That registration is what makes the mount reachable from every logon +// session by its UNC path — a drive letter lives inside the session that +// created it, which is why a service mount is invisible to the users logged +// on at the desktop. Handed through as a plain mount point instead, the UNC +// path would be taken for a directory on an actual remote server and the +// mount would fail. +// +// Mount points that are not UNC paths return "", including the \\?\ and \\.\ +// device forms, which WinFsp interprets itself. +func VolumePrefix(mountPoint string) (string, error) { + normalized := strings.ReplaceAll(mountPoint, "/", `\`) + if !strings.HasPrefix(normalized, `\\`) { + return "", nil + } + if strings.HasPrefix(normalized, `\\?\`) || strings.HasPrefix(normalized, `\\.\`) { + return "", nil + } + parts := strings.Split(strings.TrimRight(normalized[2:], `\`), `\`) + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + return "", fmt.Errorf(`a network mount point must be \\server\share, got %s`, mountPoint) + } + return `\` + parts[0] + `\` + parts[1], nil +} diff --git a/weed/mount/winfsp/mountpoint_test.go b/weed/mount/winfsp/mountpoint_test.go new file mode 100644 index 000000000..955171359 --- /dev/null +++ b/weed/mount/winfsp/mountpoint_test.go @@ -0,0 +1,37 @@ +package winfsp + +import "testing" + +func TestVolumePrefix(t *testing.T) { + tests := []struct { + name string + mountPoint string + prefix string + wantErr bool + }{ + {name: "drive letter", mountPoint: `S:`, prefix: ""}, + {name: "directory", mountPoint: `C:\mnt\weed`, prefix: ""}, + {name: "relative directory", mountPoint: `mnt\weed`, prefix: ""}, + {name: "unc", mountPoint: `\\seaweedfs\share`, prefix: `\seaweedfs\share`}, + {name: "unc trailing separator", mountPoint: `\\seaweedfs\share\`, prefix: `\seaweedfs\share`}, + {name: "unc forward slashes", mountPoint: `//seaweedfs/share`, prefix: `\seaweedfs\share`}, + {name: "unc dotted server", mountPoint: `\\fs.example.com\share`, prefix: `\fs.example.com\share`}, + {name: "mountmgr drive", mountPoint: `\\.\S:`, prefix: ""}, + {name: "extended drive", mountPoint: `\\?\S:`, prefix: ""}, + {name: "server only", mountPoint: `\\seaweedfs`, wantErr: true}, + {name: "too deep", mountPoint: `\\seaweedfs\share\dir`, wantErr: true}, + {name: "empty server", mountPoint: `\\\share`, wantErr: true}, + {name: "empty share", mountPoint: `\\seaweedfs\\`, wantErr: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + prefix, err := VolumePrefix(tt.mountPoint) + if (err != nil) != tt.wantErr { + t.Fatalf("VolumePrefix(%q) error = %v, wantErr %v", tt.mountPoint, err, tt.wantErr) + } + if prefix != tt.prefix { + t.Fatalf("VolumePrefix(%q) = %q, want %q", tt.mountPoint, prefix, tt.prefix) + } + }) + } +}