//go:build cmount && ((linux && cgo) || (darwin && cgo) || (freebsd && cgo) || windows) // +build cmount // +build linux,cgo darwin,cgo freebsd,cgo windows // Package cmount implements a FUSE mounting system for rclone remotes. // // This uses the cgo based cgofuse library package cmount import ( "errors" "fmt" "os" "runtime" "strings" "time" "github.com/rclone/rclone/cmd/mountlib" "github.com/rclone/rclone/fs" "github.com/rclone/rclone/lib/atexit" "github.com/rclone/rclone/lib/buildinfo" "github.com/rclone/rclone/vfs" "github.com/winfsp/cgofuse/fuse" ) func init() { name := "cmount" cmountOnly := runtime.GOOS != "linux" // rclone mount only works for linux if cmountOnly { name = "mount" } cmd := mountlib.NewMountCommand(name, false, mount) if cmountOnly { cmd.Aliases = append(cmd.Aliases, "cmount") } mountlib.AddRc("cmount", mount) buildinfo.Tags = append(buildinfo.Tags, "cmount") } // Find the option string in the current options func findOption(name string, options []string) (found bool) { for _, option := range options { if option == "-o" { continue } if strings.Contains(option, name) { return true } } return false } // mountOptions configures the options from the command line flags func mountOptions(VFS *vfs.VFS, device string, mountpoint string, opt *mountlib.Options) (options []string) { // Options options = []string{ "-o", fmt.Sprintf("attr_timeout=%g", opt.AttrTimeout.Seconds()), } if opt.DebugFUSE { options = append(options, "-o", "debug") } if runtime.GOOS == "windows" { options = append(options, "-o", "uid=-1") options = append(options, "-o", "gid=-1") options = append(options, "--FileSystemName=rclone") if opt.VolumeName != "" { if opt.NetworkMode { options = append(options, "--VolumePrefix="+opt.VolumeName) } else { options = append(options, "-o", "volname="+opt.VolumeName) } } } else { options = append(options, "-o", "fsname="+device) options = append(options, "-o", "subtype=rclone") options = append(options, "-o", fmt.Sprintf("max_readahead=%d", opt.MaxReadAhead)) // 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. options = append(options, "-o", "atomic_o_trunc") if opt.DaemonTimeout != 0 { options = append(options, "-o", fmt.Sprintf("daemon_timeout=%d", int(opt.DaemonTimeout.Seconds()))) } if opt.AllowOther { options = append(options, "-o", "allow_other") } if opt.AllowRoot { options = append(options, "-o", "allow_root") } if opt.DefaultPermissions { options = append(options, "-o", "default_permissions") } if VFS.Opt.ReadOnly { options = append(options, "-o", "ro") } if opt.WritebackCache { // FIXME? options = append(options, "-o", WritebackCache()) } if runtime.GOOS == "darwin" { if opt.VolumeName != "" { options = append(options, "-o", "volname="+opt.VolumeName) } if opt.NoAppleDouble { options = append(options, "-o", "noappledouble") } if opt.NoAppleXattr { options = append(options, "-o", "noapplexattr") } } } for _, option := range opt.ExtraOptions { options = append(options, "-o", option) } for _, option := range opt.ExtraFlags { options = append(options, option) } if runtime.GOOS == "darwin" { if !findOption("modules=iconv", options) { iconv := "modules=iconv,from_code=UTF-8,to_code=UTF-8-MAC" options = append(options, "-o", iconv) fs.Debugf(nil, "Adding \"-o %s\" for macOS", iconv) } } 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(VFS *vfs.VFS, mountPath string, opt *mountlib.Options) (<-chan error, func() error, error) { // Get mountpoint using OS specific logic f := VFS.Fs() mountpoint, err := getMountpoint(f, mountPath, opt) if err != nil { return nil, nil, err } fs.Debugf(nil, "Mounting on %q (%q)", mountpoint, opt.VolumeName) // Create underlying FS fsys := NewFS(VFS) host := fuse.NewFileSystemHost(fsys) host.SetCapReaddirPlus(true) // only works on Windows if opt.CaseInsensitive.Valid { host.SetCapCaseInsensitive(opt.CaseInsensitive.Value) } else { host.SetCapCaseInsensitive(f.Features().CaseInsensitive) } // Create options options := mountOptions(VFS, opt.DeviceName, mountpoint, opt) 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() { defer func() { if r := recover(); r != nil { errChan <- fmt.Errorf("mount failed: %v", r) } }() 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() var umountOK bool if fsys.destroyed.Load() != 0 { fs.Debugf(nil, "Not calling host.Unmount as mount already Destroyed") umountOK = true } else if atexit.Signalled() { // If we have received a signal then FUSE will be shutting down already fs.Debugf(nil, "Not calling host.Unmount as signal received") umountOK = true } else { fs.Debugf(nil, "Calling host.Unmount") umountOK = host.Unmount() } if umountOK { fs.Debugf(nil, "Unmounted successfully") 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 = fmt.Errorf("mount stopped before calling Init: %w", err) return 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 errChan, unmount, nil }