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
```
This commit is contained in:
Saleh Dindar 2023-10-04 10:33:12 -07:00 committed by Nick Craig-Wood
parent c69cf46f06
commit ef2ef8ef84
9 changed files with 110 additions and 8 deletions

View File

@ -40,6 +40,7 @@ import (
_ "github.com/rclone/rclone/cmd/move" _ "github.com/rclone/rclone/cmd/move"
_ "github.com/rclone/rclone/cmd/moveto" _ "github.com/rclone/rclone/cmd/moveto"
_ "github.com/rclone/rclone/cmd/ncdu" _ "github.com/rclone/rclone/cmd/ncdu"
_ "github.com/rclone/rclone/cmd/nfsmount"
_ "github.com/rclone/rclone/cmd/obscure" _ "github.com/rclone/rclone/cmd/obscure"
_ "github.com/rclone/rclone/cmd/purge" _ "github.com/rclone/rclone/cmd/purge"
_ "github.com/rclone/rclone/cmd/rc" _ "github.com/rclone/rclone/cmd/rc"

View File

@ -15,6 +15,7 @@ import (
"testing" "testing"
"github.com/rclone/rclone/fstest/testy" "github.com/rclone/rclone/fstest/testy"
"github.com/rclone/rclone/vfs/vfscommon"
"github.com/rclone/rclone/vfs/vfstest" "github.com/rclone/rclone/vfs/vfstest"
) )
@ -23,5 +24,5 @@ func TestMount(t *testing.T) {
if runtime.GOOS == "darwin" { if runtime.GOOS == "darwin" {
testy.SkipUnreliable(t) testy.SkipUnreliable(t)
} }
vfstest.RunTests(t, false, mount) vfstest.RunTests(t, false, vfscommon.CacheModeOff, true, mount)
} }

View File

@ -6,9 +6,10 @@ package mount
import ( import (
"testing" "testing"
"github.com/rclone/rclone/vfs/vfscommon"
"github.com/rclone/rclone/vfs/vfstest" "github.com/rclone/rclone/vfs/vfstest"
) )
func TestMount(t *testing.T) { func TestMount(t *testing.T) {
vfstest.RunTests(t, false, mount) vfstest.RunTests(t, false, vfscommon.CacheModeOff, true, mount)
} }

View File

@ -6,9 +6,10 @@ package mount2
import ( import (
"testing" "testing"
"github.com/rclone/rclone/vfs/vfscommon"
"github.com/rclone/rclone/vfs/vfstest" "github.com/rclone/rclone/vfs/vfstest"
) )
func TestMount(t *testing.T) { func TestMount(t *testing.T) {
vfstest.RunTests(t, false, mount) vfstest.RunTests(t, false, vfscommon.CacheModeOff, true, mount)
} }

69
cmd/nfsmount/nfsmount.go Normal file
View File

@ -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
}

View File

@ -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)
}

View File

@ -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

View File

@ -42,7 +42,7 @@ const (
// //
// If useVFS is not set then it runs the mount in a subprocess in // If useVFS is not set then it runs the mount in a subprocess in
// order to avoid kernel deadlocks. // 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() flag.Parse()
if isSubProcess() { if isSubProcess() {
startMount(mountFn, useVFS, *runMount) 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}, {cacheMode: vfscommon.CacheModeFull, writeBack: 100 * time.Millisecond},
} }
for _, test := range tests { for _, test := range tests {
if test.cacheMode < minimumRequiredCacheMode {
continue
}
vfsOpt := vfsflags.Opt vfsOpt := vfsflags.Opt
vfsOpt.CacheMode = test.cacheMode vfsOpt.CacheMode = test.cacheMode
vfsOpt.WriteBack = test.writeBack vfsOpt.WriteBack = test.writeBack
@ -78,7 +81,9 @@ func RunTests(t *testing.T, useVFS bool, mountFn mountlib.MountFn) {
t.Run("TestDirRenameEmptyDir", TestDirRenameEmptyDir) t.Run("TestDirRenameEmptyDir", TestDirRenameEmptyDir)
t.Run("TestDirRenameFullDir", TestDirRenameFullDir) t.Run("TestDirRenameFullDir", TestDirRenameFullDir)
t.Run("TestDirModTime", TestDirModTime) t.Run("TestDirModTime", TestDirModTime)
if enableCacheTests {
t.Run("TestDirCacheFlush", TestDirCacheFlush) t.Run("TestDirCacheFlush", TestDirCacheFlush)
}
t.Run("TestDirCacheFlushOnDirRename", TestDirCacheFlushOnDirRename) t.Run("TestDirCacheFlushOnDirRename", TestDirCacheFlushOnDirRename)
t.Run("TestFileModTime", TestFileModTime) t.Run("TestFileModTime", TestFileModTime)
t.Run("TestFileModTimeWithOpenWriters", TestFileModTimeWithOpenWriters) 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) { func (r *Run) createFile(t *testing.T, filepath string, contents string) {
filepath = r.path(filepath) filepath = r.path(filepath)
err := writeFile(filepath, []byte(contents), 0600) err := writeFile(filepath, []byte(contents), 0644)
require.NoError(t, err) require.NoError(t, err)
r.waitForWriters() 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) { func (r *Run) mkdir(t *testing.T, filepath string) {
filepath = r.path(filepath) filepath = r.path(filepath)
err := r.os.Mkdir(filepath, 0700) err := r.os.Mkdir(filepath, 0755)
require.NoError(t, err) require.NoError(t, err)
} }

View File

@ -9,6 +9,7 @@ import (
"github.com/rclone/rclone/cmd/mountlib" "github.com/rclone/rclone/cmd/mountlib"
"github.com/rclone/rclone/fstest" "github.com/rclone/rclone/fstest"
"github.com/rclone/rclone/vfs" "github.com/rclone/rclone/vfs"
"github.com/rclone/rclone/vfs/vfscommon"
"github.com/rclone/rclone/vfs/vfstest" "github.com/rclone/rclone/vfs/vfstest"
) )
@ -18,7 +19,7 @@ func TestFunctional(t *testing.T) {
if *fstest.RemoteName != "" { if *fstest.RemoteName != "" {
t.Skip("Skip on non local") 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) unmountResultChan := make(chan (error), 1)
unmount = func() error { unmount = func() error {
unmountResultChan <- nil unmountResultChan <- nil