fd8faeb0e6
Before this change, the VFS layer did not properly handle unicode normalization, which caused problems particularly for users of macOS. While attempts were made to handle it with various `-o modules=iconv` combinations, this was an imperfect solution, as no one combination allowed both NFC and NFD content to simultaneously be both visible and editable via Finder. After this change, the VFS supports `--no-unicode-normalization` (default `false`) via the existing `--vfs-case-insensitive` logic, which is extended to apply to both case insensitivity and unicode normalization form. This change also adds an additional flag, `--vfs-block-norm-dupes`, to address a probably rare but potentially possible scenario where a directory contains multiple duplicate filenames after applying case and unicode normalization settings. In such a scenario, this flag (disabled by default) hides the duplicates. This comes with a performance tradeoff, as rclone will have to scan the entire directory for duplicates when listing a directory. For this reason, it is recommended to leave this disabled if not needed. However, macOS users may wish to consider using it, as otherwise, if a remote directory contains both NFC and NFD versions of the same filename, an odd situation will occur: both versions of the file will be visible in the mount, and both will appear to be editable, however, editing either version will actually result in only the NFD version getting edited under the hood. `--vfs-block-norm-dupes` prevents this confusion by detecting this scenario, hiding the duplicates, and logging an error, similar to how this is handled in `rclone sync`.
235 lines
6.5 KiB
Go
235 lines
6.5 KiB
Go
//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)
|
|
}
|
|
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
|
|
}
|