Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mounting functionality for latest versions of macOS using NFS #7254

Merged
merged 6 commits into from
Oct 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/all/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 2 additions & 1 deletion cmd/cmount/mount_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"testing"

"github.com/rclone/rclone/fstest/testy"
"github.com/rclone/rclone/vfs/vfscommon"
"github.com/rclone/rclone/vfs/vfstest"
)

Expand All @@ -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)
}
3 changes: 2 additions & 1 deletion cmd/mount/mount_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
3 changes: 2 additions & 1 deletion cmd/mount2/mount_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
10 changes: 9 additions & 1 deletion cmd/mountlib/help.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,11 +256,17 @@ does not suffer from the same limitations.
### Mounting on macOS
Mounting on macOS can be done either via [macFUSE](https://osxfuse.github.io/)
Mounting on macOS can be done either via [built-in NFS server](/commands/rclone_serve_nfs/), [macFUSE](https://osxfuse.github.io/)
(also known as osxfuse) or [FUSE-T](https://www.fuse-t.org/). macFUSE is a traditional
FUSE driver utilizing a macOS kernel extension (kext). FUSE-T is an alternative FUSE system
which "mounts" via an NFSv4 local server.
## NFS mount
This method spins up an NFS server using [serve nfs](/commands/rclone_serve_nfs/) command and mounts
it to the specified mountpoint. If you run this in background mode using |--daemon|, you will need to
send SIGTERM signal to the rclone process using |kill| command to stop the mount.
#### macFUSE Notes
If installing macFUSE using [dmg packages](https://github.com/osxfuse/osxfuse/releases) from
Expand Down Expand Up @@ -310,6 +316,8 @@ sequentially, it can only seek when reading. This means that many
applications won't work with their files on an rclone mount without
|--vfs-cache-mode writes| or |--vfs-cache-mode full|.
See the [VFS File Caching](#vfs-file-caching) section for more info.
When using NFS mount on macOS, if you don't specify |--vfs-cache-mode|
the mount point will be read-only.
The bucket-based remotes (e.g. Swift, S3, Google Compute Storage, B2)
do not support the concept of empty directories, so empty
Expand Down
69 changes: 69 additions & 0 deletions cmd/nfsmount/nfsmount.go
Original file line number Diff line number Diff line change
@@ -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{})
ncw marked this conversation as resolved.
Show resolved Hide resolved
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 changes: 15 additions & 0 deletions cmd/nfsmount/nfsmount_test.go
Original file line number Diff line number Diff line change
@@ -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 changes: 8 additions & 0 deletions cmd/nfsmount/nfsmount_unsupported.go
Original file line number Diff line number Diff line change
@@ -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
159 changes: 159 additions & 0 deletions cmd/serve/nfs/filesystem.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
//go:build unix
ncw marked this conversation as resolved.
Show resolved Hide resolved
// +build unix

package nfs

import (
"os"
"path"
"strings"
"time"

billy "github.com/go-git/go-billy/v5"
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/vfs"
"github.com/rclone/rclone/vfs/vfscommon"
)

// FS is our wrapper around the VFS to properly support billy.Filesystem interface
type FS struct {
vfs *vfs.VFS
ncw marked this conversation as resolved.
Show resolved Hide resolved
}

// ReadDir implements read dir
func (f *FS) ReadDir(path string) (dir []os.FileInfo, err error) {
return f.vfs.ReadDir(path)
}

// Create implements creating new files
func (f *FS) Create(filename string) (billy.File, error) {
return f.vfs.Create(filename)
}

// Open opens a file
func (f *FS) Open(filename string) (billy.File, error) {
return f.vfs.Open(filename)
}

// OpenFile opens a file
func (f *FS) OpenFile(filename string, flag int, perm os.FileMode) (billy.File, error) {
return f.vfs.OpenFile(filename, flag, perm)
}

// Stat gets the file stat
func (f *FS) Stat(filename string) (os.FileInfo, error) {
return f.vfs.Stat(filename)
}

// Rename renames a file
func (f *FS) Rename(oldpath, newpath string) error {
return f.vfs.Rename(oldpath, newpath)
}

// Remove deletes a file
func (f *FS) Remove(filename string) error {
return f.vfs.Remove(filename)
}

// Join joins path elements
func (f *FS) Join(elem ...string) string {
return path.Join(elem...)
}

// TempFile is not implemented
func (f *FS) TempFile(dir, prefix string) (billy.File, error) {
return nil, os.ErrInvalid
}

// MkdirAll creates a directory and all the ones above it
// it does not redirect to VFS.MkDirAll because that one doesn't
// honor the permissions
func (f *FS) MkdirAll(filename string, perm os.FileMode) error {
parts := strings.Split(filename, "/")
for i := range parts {
current := strings.Join(parts[:i+1], "/")
_, err := f.Stat(current)
if err == vfs.ENOENT {
err = f.vfs.Mkdir(current, perm)
if err != nil {
return err
}
}
}
return nil
}

// Lstat gets the stats for symlink
func (f *FS) Lstat(filename string) (os.FileInfo, error) {
return f.vfs.Stat(filename)
}

// Symlink is not supported over NFS
func (f *FS) Symlink(target, link string) error {
return os.ErrInvalid
}

// Readlink is not supported
func (f *FS) Readlink(link string) (string, error) {
return "", os.ErrInvalid
}

// Chmod changes the file modes
func (f *FS) Chmod(name string, mode os.FileMode) error {
file, err := f.vfs.Open(name)
if err != nil {
return err
}
defer func() {
if err := file.Close(); err != nil {
fs.Logf(f, "Error while closing file: %e", err)
}
}()
return file.Chmod(mode)
}

// Lchown changes the owner of symlink
func (f *FS) Lchown(name string, uid, gid int) error {
return f.Chown(name, uid, gid)
}

// Chown changes owner of the file
func (f *FS) Chown(name string, uid, gid int) error {
file, err := f.vfs.Open(name)
if err != nil {
return err
}
defer func() {
if err := file.Close(); err != nil {
fs.Logf(f, "Error while closing file: %e", err)
}
}()
return file.Chown(uid, gid)
}

// Chtimes changes the acces time and modified time
func (f *FS) Chtimes(name string, atime time.Time, mtime time.Time) error {
return f.vfs.Chtimes(name, atime, mtime)
}

// Chroot is not supported in VFS
func (f *FS) Chroot(path string) (billy.Filesystem, error) {
return nil, os.ErrInvalid
}

// Root returns the root of a VFS
func (f *FS) Root() string {
return f.vfs.Fs().Root()
}

// Capabilities exports the filesystem capabilities
func (f *FS) Capabilities() billy.Capability {
if f.vfs.Opt.CacheMode == vfscommon.CacheModeOff {
return billy.ReadCapability | billy.SeekCapability
}
return billy.WriteCapability | billy.ReadCapability |
billy.ReadAndWriteCapability | billy.SeekCapability | billy.TruncateCapability
}
ncw marked this conversation as resolved.
Show resolved Hide resolved

// Interface check
var _ billy.Filesystem = (*FS)(nil)