From ef2ef8ef84e3f616861c144c5fe2432088262ec0 Mon Sep 17 00:00:00 2001 From: Saleh Dindar Date: Wed, 4 Oct 2023 10:33:12 -0700 Subject: [PATCH] nfsmount: New mount command to provide mount mechanism on macOS without FUSE Summary: In cases where cmount is not available in macOS, we alias nfsmount to mount command and transparently start the NFS server and mount it to the target dir. The NFS server is started on localhost on a random port so it is reasonably secure. Test Plan: ``` go run rclone.go mount --http-url https://beta.rclone.org :http: nfs-test ``` Added mount tests: ``` go test ./cmd/nfsmount ``` --- cmd/all/all.go | 1 + cmd/cmount/mount_test.go | 3 +- cmd/mount/mount_test.go | 3 +- cmd/mount2/mount_test.go | 3 +- cmd/nfsmount/nfsmount.go | 69 ++++++++++++++++++++++++++++ cmd/nfsmount/nfsmount_test.go | 15 ++++++ cmd/nfsmount/nfsmount_unsupported.go | 8 ++++ vfs/vfstest/fs.go | 13 ++++-- vfs/vfstest_test.go | 3 +- 9 files changed, 110 insertions(+), 8 deletions(-) create mode 100644 cmd/nfsmount/nfsmount.go create mode 100644 cmd/nfsmount/nfsmount_test.go create mode 100644 cmd/nfsmount/nfsmount_unsupported.go diff --git a/cmd/all/all.go b/cmd/all/all.go index 569d1821e..5fb87ed16 100644 --- a/cmd/all/all.go +++ b/cmd/all/all.go @@ -40,6 +40,7 @@ import ( _ "github.com/rclone/rclone/cmd/move" _ "github.com/rclone/rclone/cmd/moveto" _ "github.com/rclone/rclone/cmd/ncdu" + _ "github.com/rclone/rclone/cmd/nfsmount" _ "github.com/rclone/rclone/cmd/obscure" _ "github.com/rclone/rclone/cmd/purge" _ "github.com/rclone/rclone/cmd/rc" diff --git a/cmd/cmount/mount_test.go b/cmd/cmount/mount_test.go index 7c9c852f6..8da6116e2 100644 --- a/cmd/cmount/mount_test.go +++ b/cmd/cmount/mount_test.go @@ -15,6 +15,7 @@ import ( "testing" "github.com/rclone/rclone/fstest/testy" + "github.com/rclone/rclone/vfs/vfscommon" "github.com/rclone/rclone/vfs/vfstest" ) @@ -23,5 +24,5 @@ func TestMount(t *testing.T) { if runtime.GOOS == "darwin" { testy.SkipUnreliable(t) } - vfstest.RunTests(t, false, mount) + vfstest.RunTests(t, false, vfscommon.CacheModeOff, true, mount) } diff --git a/cmd/mount/mount_test.go b/cmd/mount/mount_test.go index ec5422cc9..4e1d832a9 100644 --- a/cmd/mount/mount_test.go +++ b/cmd/mount/mount_test.go @@ -6,9 +6,10 @@ package mount import ( "testing" + "github.com/rclone/rclone/vfs/vfscommon" "github.com/rclone/rclone/vfs/vfstest" ) func TestMount(t *testing.T) { - vfstest.RunTests(t, false, mount) + vfstest.RunTests(t, false, vfscommon.CacheModeOff, true, mount) } diff --git a/cmd/mount2/mount_test.go b/cmd/mount2/mount_test.go index e90632f02..5ff74ff3c 100644 --- a/cmd/mount2/mount_test.go +++ b/cmd/mount2/mount_test.go @@ -6,9 +6,10 @@ package mount2 import ( "testing" + "github.com/rclone/rclone/vfs/vfscommon" "github.com/rclone/rclone/vfs/vfstest" ) func TestMount(t *testing.T) { - vfstest.RunTests(t, false, mount) + vfstest.RunTests(t, false, vfscommon.CacheModeOff, true, mount) } diff --git a/cmd/nfsmount/nfsmount.go b/cmd/nfsmount/nfsmount.go new file mode 100644 index 000000000..755197fa2 --- /dev/null +++ b/cmd/nfsmount/nfsmount.go @@ -0,0 +1,69 @@ +//go:build darwin && !cmount +// +build darwin,!cmount + +// Package nfsmount implements mounting functionality using serve nfs command +// +// NFS mount is only needed for macOS since it has no +// support for FUSE-based file systems +package nfsmount + +import ( + "context" + "fmt" + "net" + "os/exec" + "runtime" + "strings" + + "github.com/rclone/rclone/cmd/mountlib" + "github.com/rclone/rclone/cmd/serve/nfs" + "github.com/rclone/rclone/vfs" +) + +func init() { + cmd := mountlib.NewMountCommand("mount", false, mount) + cmd.Aliases = append(cmd.Aliases, "nfsmount") + mountlib.AddRc("nfsmount", mount) +} + +func mount(VFS *vfs.VFS, mountpoint string, opt *mountlib.Options) (asyncerrors <-chan error, unmount func() error, err error) { + s, err := nfs.NewServer(context.Background(), VFS, &nfs.Options{}) + if err != nil { + return + } + errChan := make(chan error, 1) + go func() { + errChan <- s.Serve() + }() + // The port is always picked at random after the NFS server has started + // we need to query the server for the port number so we can mount it + _, port, err := net.SplitHostPort(s.Addr().String()) + if err != nil { + err = fmt.Errorf("cannot find port number in %s", s.Addr().String()) + return + } + optionsString := strings.Join(opt.ExtraOptions, ",") + err = exec.Command("mount", fmt.Sprintf("-oport=%s,mountport=%s,%s", port, port, optionsString), "localhost:", mountpoint).Run() + if err != nil { + err = fmt.Errorf("failed to mount NFS volume %e", err) + return + } + asyncerrors = errChan + unmount = func() error { + var umountErr error + if runtime.GOOS == "darwin" { + umountErr = exec.Command("diskutil", "umount", "force", mountpoint).Run() + } else { + umountErr = exec.Command("umount", "-f", mountpoint).Run() + } + shutdownErr := s.Shutdown() + VFS.Shutdown() + if umountErr != nil { + return fmt.Errorf("failed to umount the NFS volume %e", umountErr) + } else if shutdownErr != nil { + return fmt.Errorf("failed to shutdown NFS server: %e", shutdownErr) + } + return nil + } + return +} diff --git a/cmd/nfsmount/nfsmount_test.go b/cmd/nfsmount/nfsmount_test.go new file mode 100644 index 000000000..d4c9c3441 --- /dev/null +++ b/cmd/nfsmount/nfsmount_test.go @@ -0,0 +1,15 @@ +//go:build darwin && !cmount +// +build darwin,!cmount + +package nfsmount + +import ( + "testing" + + "github.com/rclone/rclone/vfs/vfscommon" + "github.com/rclone/rclone/vfs/vfstest" +) + +func TestMount(t *testing.T) { + vfstest.RunTests(t, false, vfscommon.CacheModeMinimal, false, mount) +} diff --git a/cmd/nfsmount/nfsmount_unsupported.go b/cmd/nfsmount/nfsmount_unsupported.go new file mode 100644 index 000000000..d24115837 --- /dev/null +++ b/cmd/nfsmount/nfsmount_unsupported.go @@ -0,0 +1,8 @@ +// Build for nfsmount for unsupported platforms to stop go complaining +// about "no buildable Go source files " + +//go:build !darwin || cmount +// +build !darwin cmount + +// Package nfsmount implements mount command using NFS, not needed on most platforms +package nfsmount diff --git a/vfs/vfstest/fs.go b/vfs/vfstest/fs.go index 6781fbe31..d895a7eaa 100644 --- a/vfs/vfstest/fs.go +++ b/vfs/vfstest/fs.go @@ -42,7 +42,7 @@ const ( // // If useVFS is not set then it runs the mount in a subprocess in // order to avoid kernel deadlocks. -func RunTests(t *testing.T, useVFS bool, mountFn mountlib.MountFn) { +func RunTests(t *testing.T, useVFS bool, minimumRequiredCacheMode vfscommon.CacheMode, enableCacheTests bool, mountFn mountlib.MountFn) { flag.Parse() if isSubProcess() { startMount(mountFn, useVFS, *runMount) @@ -59,6 +59,9 @@ func RunTests(t *testing.T, useVFS bool, mountFn mountlib.MountFn) { {cacheMode: vfscommon.CacheModeFull, writeBack: 100 * time.Millisecond}, } for _, test := range tests { + if test.cacheMode < minimumRequiredCacheMode { + continue + } vfsOpt := vfsflags.Opt vfsOpt.CacheMode = test.cacheMode vfsOpt.WriteBack = test.writeBack @@ -78,7 +81,9 @@ func RunTests(t *testing.T, useVFS bool, mountFn mountlib.MountFn) { t.Run("TestDirRenameEmptyDir", TestDirRenameEmptyDir) t.Run("TestDirRenameFullDir", TestDirRenameFullDir) t.Run("TestDirModTime", TestDirModTime) - t.Run("TestDirCacheFlush", TestDirCacheFlush) + if enableCacheTests { + t.Run("TestDirCacheFlush", TestDirCacheFlush) + } t.Run("TestDirCacheFlushOnDirRename", TestDirCacheFlushOnDirRename) t.Run("TestFileModTime", TestFileModTime) t.Run("TestFileModTimeWithOpenWriters", TestFileModTimeWithOpenWriters) @@ -310,7 +315,7 @@ func writeFile(filename string, data []byte, perm os.FileMode) error { func (r *Run) createFile(t *testing.T, filepath string, contents string) { filepath = r.path(filepath) - err := writeFile(filepath, []byte(contents), 0600) + err := writeFile(filepath, []byte(contents), 0644) require.NoError(t, err) r.waitForWriters() } @@ -324,7 +329,7 @@ func (r *Run) readFile(t *testing.T, filepath string) string { func (r *Run) mkdir(t *testing.T, filepath string) { filepath = r.path(filepath) - err := r.os.Mkdir(filepath, 0700) + err := r.os.Mkdir(filepath, 0755) require.NoError(t, err) } diff --git a/vfs/vfstest_test.go b/vfs/vfstest_test.go index babcdb4ff..df8fec685 100644 --- a/vfs/vfstest_test.go +++ b/vfs/vfstest_test.go @@ -9,6 +9,7 @@ import ( "github.com/rclone/rclone/cmd/mountlib" "github.com/rclone/rclone/fstest" "github.com/rclone/rclone/vfs" + "github.com/rclone/rclone/vfs/vfscommon" "github.com/rclone/rclone/vfs/vfstest" ) @@ -18,7 +19,7 @@ func TestFunctional(t *testing.T) { if *fstest.RemoteName != "" { t.Skip("Skip on non local") } - vfstest.RunTests(t, true, func(VFS *vfs.VFS, mountpoint string, opt *mountlib.Options) (unmountResult <-chan error, unmount func() error, err error) { + vfstest.RunTests(t, true, vfscommon.CacheModeOff, true, func(VFS *vfs.VFS, mountpoint string, opt *mountlib.Options) (unmountResult <-chan error, unmount func() error, err error) { unmountResultChan := make(chan (error), 1) unmount = func() error { unmountResultChan <- nil