mirror of
https://github.com/rclone/rclone.git
synced 2024-11-22 09:11:11 +08:00
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:
parent
c69cf46f06
commit
ef2ef8ef84
|
@ -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"
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
69
cmd/nfsmount/nfsmount.go
Normal 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
|
||||||
|
}
|
15
cmd/nfsmount/nfsmount_test.go
Normal file
15
cmd/nfsmount/nfsmount_test.go
Normal 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)
|
||||||
|
}
|
8
cmd/nfsmount/nfsmount_unsupported.go
Normal file
8
cmd/nfsmount/nfsmount_unsupported.go
Normal 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
|
|
@ -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)
|
||||||
t.Run("TestDirCacheFlush", TestDirCacheFlush)
|
if enableCacheTests {
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue
Block a user