From 1ca19ea2e264ed6592d76c69783446e813c4ba1e Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Fri, 4 Sep 2026 23:14:57 -0700 Subject: [PATCH] mount: add -volumeName to name the disk explicitly (#11165) * mount: let volumeName take an explicit override volumeName only ever derived the disk's label from -filer.path, -dir, or the filer address, so a name that happened to collide with something else - e.g. a UNC share's own name - could not be changed without moving what was mounted. Give it an override parameter that wins over all three; nothing passes one yet. * mount: add -volumeName to name the disk explicitly Windows has no equivalent of the "weed fuse" -o passthrough that lets a Linux or macOS mount override its derived volname, so a name picked up from -dir - e.g. a UNC share's own name - could not be changed short of moving what was mounted. -volumeName overrides it on every platform. * mount: document -volumeName * mount: scope -volumeName's help text to macOS and Windows Linux has no volume-label mount option for -volumeName to feed, so the flag's own description says where it applies instead of leaving that unstated. * mount: forward -volumeName through the weed fuse option parser weed fuse (the /etc/fstab helper) turns -o key=value into the same MountOptions weed mount takes, but volumeName had no case, so it fell through to being forwarded as a literal, unrecognized FUSE option instead of ever reaching mountOptions.volumeName. * mount: apply -volumeName to FsName on Linux and FreeBSD FsName only ever took the filer address and -filer.path, so -volumeName had nothing to override there and silently did nothing; the skipAutofs case still forces "fuse", since that name is what util-linux/mount requires to recognize the pseudo filesystem. --- weed/command/fuse_std.go | 2 ++ weed/command/fuse_std_test.go | 29 +++++++++++++++++++++++++++++ weed/command/mount.go | 4 ++++ weed/command/mount_common.go | 12 ++++++++---- weed/command/mount_common_test.go | 20 ++++++++++++++++++-- weed/command/mount_std.go | 5 ++++- weed/command/mount_windows.go | 2 +- 7 files changed, 66 insertions(+), 8 deletions(-) diff --git a/weed/command/fuse_std.go b/weed/command/fuse_std.go index 1eb4ee072..31658ee67 100644 --- a/weed/command/fuse_std.go +++ b/weed/command/fuse_std.go @@ -124,6 +124,8 @@ func runFuse(cmd *Command, args []string) bool { mountOptions.filer = ¶meter.value case "filer.path": mountOptions.filerMountRootPath = ¶meter.value + case "volumeName": + mountOptions.volumeName = ¶meter.value case "dirAutoCreate": if parsed, err := strconv.ParseBool(parameter.value); err == nil { mountOptions.dirAutoCreate = &parsed diff --git a/weed/command/fuse_std_test.go b/weed/command/fuse_std_test.go index 64122bb3c..4026a943a 100644 --- a/weed/command/fuse_std_test.go +++ b/weed/command/fuse_std_test.go @@ -36,3 +36,32 @@ func TestRunFuseDoesNotSkipOptionAfterConcurrentWriters(t *testing.T) { "child=1,concurrentWriters=2,concurrentReaders=bad,umask=bad", }) } + +func TestRunFuseSetsVolumeName(t *testing.T) { + oldVolumeName := mountOptions.volumeName + oldConcurrentReaders := mountOptions.concurrentReaders + oldDir := mountOptions.dir + defer func() { + gotVolumeName := mountOptions.volumeName + mountOptions.volumeName = oldVolumeName + mountOptions.concurrentReaders = oldConcurrentReaders + mountOptions.dir = oldDir + + recovered := recover() + if recovered == nil { + t.Fatal("expected invalid concurrentReaders option to be parsed") + } + if gotVolumeName == nil { + t.Fatal("expected -o volumeName=MyDisk to set mountOptions.volumeName, got nil") + } + if *gotVolumeName != "MyDisk" { + t.Fatalf("expected -o volumeName=MyDisk to set mountOptions.volumeName, got %q", *gotVolumeName) + } + }() + + runFuse(cmdMount, []string{ + "/mnt", + "-o", + "volumeName=MyDisk,concurrentReaders=bad", + }) +} diff --git a/weed/command/mount.go b/weed/command/mount.go index 22fcd0c8a..db445264d 100644 --- a/weed/command/mount.go +++ b/weed/command/mount.go @@ -9,6 +9,7 @@ type MountOptions struct { filer *string filerMountRootPath *string dir *string + volumeName *string dirAutoCreate *bool collection *string collectionQuota *int @@ -92,6 +93,7 @@ func init() { mountOptions.filer = cmdMount.Flag.String("filer", "localhost:8888", "comma-separated weed filer location") mountOptions.filerMountRootPath = cmdMount.Flag.String("filer.path", "/", "mount this remote path from filer server") mountOptions.dir = cmdMount.Flag.String("dir", ".", "mount weed filer to this directory") + mountOptions.volumeName = cmdMount.Flag.String("volumeName", "", "name the mount shown by the platform (Finder/Explorer, or \"mount\"/\"df\" on Linux and FreeBSD), overriding the name taken from -filer.path or -dir") mountOptions.dirAutoCreate = cmdMount.Flag.Bool("dirAutoCreate", false, "auto create the directory to mount to") mountOptions.collection = cmdMount.Flag.String("collection", "", "collection to create the files") mountOptions.collectionQuota = cmdMount.Flag.Int("collectionQuotaMB", 0, "quota for the collection") @@ -188,6 +190,8 @@ var cmdMount = &Command{ the whole tree takes the name from the mount point instead, so -dir=\\seaweedfs\Images labels the disk "Images" while still mounting everything, and only a bare drive letter falls back to the filer address. + -volumeName overrides whatever name either of those would otherwise give it, + including what Linux and FreeBSD show for the mount in "mount" and "df". `, } diff --git a/weed/command/mount_common.go b/weed/command/mount_common.go index bdef9e83c..80b211081 100644 --- a/weed/command/mount_common.go +++ b/weed/command/mount_common.go @@ -278,10 +278,14 @@ func resolveCacheDirs(option *MountOptions) (string, string) { } // volumeName labels the mount where the platform shows one, in Finder and in -// Explorer. The mounted path names the disk, then the mount point; the filer -// address, which every mount from one filer shares, is the last resort. -func volumeName(filer, filerMountRootPath, dir string) string { - name := lastSegment(filerMountRootPath) +// Explorer. An explicit override always wins; failing that, the mounted path +// names the disk, then the mount point; the filer address, which every mount +// from one filer shares, is the last resort. +func volumeName(filer, filerMountRootPath, dir, override string) string { + name := override + if name == "" { + name = lastSegment(filerMountRootPath) + } if name == "" { name = lastSegment(dir) } diff --git a/weed/command/mount_common_test.go b/weed/command/mount_common_test.go index a2982d4c9..17f40a954 100644 --- a/weed/command/mount_common_test.go +++ b/weed/command/mount_common_test.go @@ -10,8 +10,24 @@ func Test_volumeName(t *testing.T) { filer string filerMountRootPath string dir string + override string expected string }{ + { + name: "an override outranks the mounted path and the mount point", + filer: "127.0.0.1:8888", + filerMountRootPath: "/buckets/videos", + dir: `\\seaweedfs\Images`, + override: "MyDisk", + expected: "MyDisk", + }, + { + name: "an override still gets commas replaced", + filer: "127.0.0.1:8888", + filerMountRootPath: "/", + override: "My, Disk", + expected: "My+ Disk", + }, { name: "a drive letter leaves only the filer to fall back on", filer: "127.0.0.1:8888", @@ -80,8 +96,8 @@ func Test_volumeName(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := volumeName(tt.filer, tt.filerMountRootPath, tt.dir); got != tt.expected { - t.Errorf("volumeName(%q, %q, %q) = %q, want %q", tt.filer, tt.filerMountRootPath, tt.dir, got, tt.expected) + if got := volumeName(tt.filer, tt.filerMountRootPath, tt.dir, tt.override); got != tt.expected { + t.Errorf("volumeName(%q, %q, %q, %q) = %q, want %q", tt.filer, tt.filerMountRootPath, tt.dir, tt.override, got, tt.expected) } }) } diff --git a/weed/command/mount_std.go b/weed/command/mount_std.go index 80e61f979..4bd85c174 100644 --- a/weed/command/mount_std.go +++ b/weed/command/mount_std.go @@ -118,6 +118,9 @@ func RunMount(option *MountOptions, umask os.FileMode) bool { // When autofs/systemd-mount is used, FsName must be "fuse" so util-linux/mount can recognize // it as a pseudo filesystem. Otherwise, preserve the descriptive name for mount/df output. fsName := serverFriendlyName + ":" + filerMountRootPath + if *option.volumeName != "" { + fsName = *option.volumeName + } if skipAutofs { fsName = "fuse" } @@ -179,7 +182,7 @@ func RunMount(option *MountOptions, umask os.FileMode) bool { fuseMountOptions.Options = append(fuseMountOptions.Options, "novncache") } fuseMountOptions.Options = append(fuseMountOptions.Options, "slow_statfs") - fuseMountOptions.Options = append(fuseMountOptions.Options, "volname="+volumeName(*option.filer, filerMountRootPath, dir)) + fuseMountOptions.Options = append(fuseMountOptions.Options, "volname="+volumeName(*option.filer, filerMountRootPath, dir, *option.volumeName)) fuseMountOptions.Options = append(fuseMountOptions.Options, fmt.Sprintf("iosize=%d", ioSizeMB*1024*1024)) } // Last, so an option given on the command line wins over the default diff --git a/weed/command/mount_windows.go b/weed/command/mount_windows.go index 316ad6736..75e372bca 100644 --- a/weed/command/mount_windows.go +++ b/weed/command/mount_windows.go @@ -104,7 +104,7 @@ func RunMount(option *MountOptions, umask os.FileMode) bool { } host := winfsp.New(seaweedFileSystem, winfsp.Options{ - VolumeName: volumeName(*option.filer, *option.filerMountRootPath, dir), + VolumeName: volumeName(*option.filer, *option.filerMountRootPath, dir, *option.volumeName), Uid: ownedByMounter, Gid: ownedByMounter, CacheTimeout: windowsCacheTimeout,