mirror of
https://github.com/rclone/rclone.git
synced 2024-11-07 17:14:44 +01:00
d260238f99
Before this change Windows would read a directory then immedately stat every item in the directory. After this change we return the stat information along with the directory which stops so many callbacks.
263 lines
6.7 KiB
Go
263 lines
6.7 KiB
Go
// Package cmount implents a FUSE mounting system for rclone remotes.
|
|
//
|
|
// This uses the cgo based cgofuse library
|
|
|
|
// +build cmount
|
|
// +build cgo
|
|
// +build linux darwin freebsd windows
|
|
|
|
package cmount
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/signal"
|
|
"runtime"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/billziss-gh/cgofuse/fuse"
|
|
"github.com/okzk/sdnotify"
|
|
"github.com/pkg/errors"
|
|
"github.com/rclone/rclone/cmd/mountlib"
|
|
"github.com/rclone/rclone/fs"
|
|
"github.com/rclone/rclone/lib/atexit"
|
|
"github.com/rclone/rclone/vfs"
|
|
"github.com/rclone/rclone/vfs/vfsflags"
|
|
)
|
|
|
|
const (
|
|
// SetCapReaddirPlus informs the host that the hosted file system has the readdir-plus
|
|
// capability [Windows only]. A file system that has the readdir-plus capability can send
|
|
// full stat information during Readdir, thus avoiding extraneous Getattr calls.
|
|
usingReaddirPlus = runtime.GOOS == "windows"
|
|
)
|
|
|
|
func init() {
|
|
name := "cmount"
|
|
if runtime.GOOS == "windows" {
|
|
name = "mount"
|
|
}
|
|
mountlib.NewMountCommand(name, false, Mount)
|
|
}
|
|
|
|
// mountOptions configures the options from the command line flags
|
|
func mountOptions(device string, mountpoint string) (options []string) {
|
|
// Options
|
|
options = []string{
|
|
"-o", "fsname=" + device,
|
|
"-o", "subtype=rclone",
|
|
"-o", fmt.Sprintf("max_readahead=%d", mountlib.MaxReadAhead),
|
|
"-o", fmt.Sprintf("attr_timeout=%g", mountlib.AttrTimeout.Seconds()),
|
|
// This causes FUSE to supply O_TRUNC with the Open
|
|
// call which is more efficient for cmount. However
|
|
// it does not work with cgofuse on Windows with
|
|
// WinFSP so cmount must work with or without it.
|
|
"-o", "atomic_o_trunc",
|
|
}
|
|
if mountlib.DebugFUSE {
|
|
options = append(options, "-o", "debug")
|
|
}
|
|
|
|
// OSX options
|
|
if runtime.GOOS == "darwin" {
|
|
if mountlib.NoAppleDouble {
|
|
options = append(options, "-o", "noappledouble")
|
|
}
|
|
if mountlib.NoAppleXattr {
|
|
options = append(options, "-o", "noapplexattr")
|
|
}
|
|
}
|
|
|
|
// Windows options
|
|
if runtime.GOOS == "windows" {
|
|
// These cause WinFsp to mean the current user
|
|
options = append(options, "-o", "uid=-1")
|
|
options = append(options, "-o", "gid=-1")
|
|
options = append(options, "--FileSystemName=rclone")
|
|
}
|
|
|
|
if runtime.GOOS == "darwin" || runtime.GOOS == "windows" {
|
|
if mountlib.VolumeName != "" {
|
|
options = append(options, "-o", "volname="+mountlib.VolumeName)
|
|
}
|
|
}
|
|
if mountlib.AllowNonEmpty {
|
|
options = append(options, "-o", "nonempty")
|
|
}
|
|
if mountlib.AllowOther {
|
|
options = append(options, "-o", "allow_other")
|
|
}
|
|
if mountlib.AllowRoot {
|
|
options = append(options, "-o", "allow_root")
|
|
}
|
|
if mountlib.DefaultPermissions {
|
|
options = append(options, "-o", "default_permissions")
|
|
}
|
|
if vfsflags.Opt.ReadOnly {
|
|
options = append(options, "-o", "ro")
|
|
}
|
|
if mountlib.WritebackCache {
|
|
// FIXME? options = append(options, "-o", WritebackCache())
|
|
}
|
|
if mountlib.DaemonTimeout != 0 {
|
|
options = append(options, "-o", fmt.Sprintf("daemon_timeout=%d", int(mountlib.DaemonTimeout.Seconds())))
|
|
}
|
|
for _, option := range mountlib.ExtraOptions {
|
|
options = append(options, "-o", option)
|
|
}
|
|
for _, option := range mountlib.ExtraFlags {
|
|
options = append(options, option)
|
|
}
|
|
return options
|
|
}
|
|
|
|
// waitFor runs fn() until it returns true or the timeout expires
|
|
func waitFor(fn func() bool) (ok bool) {
|
|
const totalWait = 10 * time.Second
|
|
const individualWait = 10 * time.Millisecond
|
|
for i := 0; i < int(totalWait/individualWait); i++ {
|
|
ok = fn()
|
|
if ok {
|
|
return ok
|
|
}
|
|
time.Sleep(individualWait)
|
|
}
|
|
return false
|
|
}
|
|
|
|
// mount the file system
|
|
//
|
|
// The mount point will be ready when this returns.
|
|
//
|
|
// returns an error, and an error channel for the serve process to
|
|
// report an error when fusermount is called.
|
|
func mount(f fs.Fs, mountpoint string) (*vfs.VFS, <-chan error, func() error, error) {
|
|
fs.Debugf(f, "Mounting on %q", mountpoint)
|
|
|
|
// Check the mountpoint - in Windows the mountpoint mustn't exist before the mount
|
|
if runtime.GOOS != "windows" {
|
|
fi, err := os.Stat(mountpoint)
|
|
if err != nil {
|
|
return nil, nil, nil, errors.Wrap(err, "mountpoint")
|
|
}
|
|
if !fi.IsDir() {
|
|
return nil, nil, nil, errors.New("mountpoint is not a directory")
|
|
}
|
|
}
|
|
|
|
// Create underlying FS
|
|
fsys := NewFS(f)
|
|
host := fuse.NewFileSystemHost(fsys)
|
|
if usingReaddirPlus {
|
|
host.SetCapReaddirPlus(true)
|
|
}
|
|
|
|
// Create options
|
|
options := mountOptions(f.Name()+":"+f.Root(), mountpoint)
|
|
fs.Debugf(f, "Mounting with options: %q", options)
|
|
|
|
// Serve the mount point in the background returning error to errChan
|
|
errChan := make(chan error, 1)
|
|
go func() {
|
|
var err error
|
|
ok := host.Mount(mountpoint, options)
|
|
if !ok {
|
|
err = errors.New("mount failed")
|
|
fs.Errorf(f, "Mount failed")
|
|
}
|
|
errChan <- err
|
|
}()
|
|
|
|
// unmount
|
|
unmount := func() error {
|
|
// Shutdown the VFS
|
|
fsys.VFS.Shutdown()
|
|
fs.Debugf(nil, "Calling host.Unmount")
|
|
if host.Unmount() {
|
|
fs.Debugf(nil, "host.Unmount succeeded")
|
|
if runtime.GOOS == "windows" {
|
|
if !waitFor(func() bool {
|
|
_, err := os.Stat(mountpoint)
|
|
return err != nil
|
|
}) {
|
|
fs.Errorf(nil, "mountpoint %q didn't disappear after unmount - continuing anyway", mountpoint)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
fs.Debugf(nil, "host.Unmount failed")
|
|
return errors.New("host unmount failed")
|
|
}
|
|
|
|
// Wait for the filesystem to become ready, checking the file
|
|
// system didn't blow up before starting
|
|
select {
|
|
case err := <-errChan:
|
|
err = errors.Wrap(err, "mount stopped before calling Init")
|
|
return nil, nil, nil, err
|
|
case <-fsys.ready:
|
|
}
|
|
|
|
// Wait for the mount point to be available on Windows
|
|
// On Windows the Init signal comes slightly before the mount is ready
|
|
if runtime.GOOS == "windows" {
|
|
if !waitFor(func() bool {
|
|
_, err := os.Stat(mountpoint)
|
|
return err == nil
|
|
}) {
|
|
fs.Errorf(nil, "mountpoint %q didn't became available on mount - continuing anyway", mountpoint)
|
|
}
|
|
}
|
|
|
|
return fsys.VFS, errChan, unmount, nil
|
|
}
|
|
|
|
// Mount mounts the remote at mountpoint.
|
|
//
|
|
// If noModTime is set then it
|
|
func Mount(f fs.Fs, mountpoint string) error {
|
|
// Mount it
|
|
FS, errChan, unmount, err := mount(f, mountpoint)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to mount FUSE fs")
|
|
}
|
|
|
|
// Note cgofuse unmounts the fs on SIGINT etc
|
|
|
|
sigHup := make(chan os.Signal, 1)
|
|
signal.Notify(sigHup, syscall.SIGHUP)
|
|
|
|
atexit.Register(func() {
|
|
_ = unmount()
|
|
})
|
|
|
|
if err := sdnotify.Ready(); err != nil && err != sdnotify.ErrSdNotifyNoSocket {
|
|
return errors.Wrap(err, "failed to notify systemd")
|
|
}
|
|
|
|
waitloop:
|
|
for {
|
|
select {
|
|
// umount triggered outside the app
|
|
case err = <-errChan:
|
|
break waitloop
|
|
// user sent SIGHUP to clear the cache
|
|
case <-sigHup:
|
|
root, err := FS.Root()
|
|
if err != nil {
|
|
fs.Errorf(f, "Error reading root: %v", err)
|
|
} else {
|
|
root.ForgetAll()
|
|
}
|
|
}
|
|
}
|
|
|
|
_ = sdnotify.Stopping()
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to umount FUSE fs")
|
|
}
|
|
|
|
return nil
|
|
}
|