mirror of
https://github.com/rclone/rclone.git
synced 2024-12-23 15:38:57 +01:00
264 lines
6.8 KiB
Go
264 lines
6.8 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)
|
|
}
|
|
host.SetCapCaseInsensitive(f.Features().CaseInsensitive)
|
|
|
|
// 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
|
|
}
|