diff --git a/cmd/cmount/mount.go b/cmd/cmount/mount.go index 07a45daab..4849592ee 100644 --- a/cmd/cmount/mount.go +++ b/cmd/cmount/mount.go @@ -85,9 +85,14 @@ func mountOptions(VFS *vfs.VFS, device string, mountpoint string, opt *mountlib. options = append(options, "-o", "gid=-1") } options = append(options, "--FileSystemName=rclone") - } - - if runtime.GOOS == "darwin" || runtime.GOOS == "windows" { + if opt.VolumeName != "" { + if opt.NetworkMode { + options = append(options, "--VolumePrefix="+opt.VolumeName) + } else { + options = append(options, "-o", "volname="+opt.VolumeName) + } + } + } else if runtime.GOOS == "darwin" { if opt.VolumeName != "" { options = append(options, "-o", "volname="+opt.VolumeName) } @@ -142,22 +147,16 @@ func waitFor(fn func() bool) (ok bool) { // // returns an error, and an error channel for the serve process to // report an error when fusermount is called. -func mount(VFS *vfs.VFS, mountpoint string, opt *mountlib.Options) (<-chan error, func() error, error) { - f := VFS.Fs() - 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, errors.Wrap(err, "mountpoint") - } - if !fi.IsDir() { - return nil, nil, errors.New("mountpoint is not a directory") - } +func mount(VFS *vfs.VFS, mountPath string, opt *mountlib.Options) (<-chan error, func() error, error) { + // Get mountpoint using OS specific logic + mountpoint, err := getMountpoint(mountPath, opt) + if err != nil { + return nil, nil, err } + fs.Debugf(nil, "Mounting on %q (%q)", mountpoint, opt.VolumeName) // Create underlying FS + f := VFS.Fs() fsys := NewFS(VFS) host := fuse.NewFileSystemHost(fsys) host.SetCapReaddirPlus(true) // only works on Windows diff --git a/cmd/cmount/mountpoint_other.go b/cmd/cmount/mountpoint_other.go new file mode 100644 index 000000000..8a0ae2140 --- /dev/null +++ b/cmd/cmount/mountpoint_other.go @@ -0,0 +1,23 @@ +// +build cmount +// +build cgo +// +build !windows + +package cmount + +import ( + "os" + + "github.com/pkg/errors" + "github.com/rclone/rclone/cmd/mountlib" +) + +func getMountpoint(mountPath string, opt *mountlib.Options) (string, error) { + fi, err := os.Stat(mountPath) + if err != nil { + return "", errors.Wrap(err, "failed to retrieve mount path information") + } + if !fi.IsDir() { + return "", errors.New("mount path is not a directory") + } + return mountPath, nil +} diff --git a/cmd/cmount/mountpoint_windows.go b/cmd/cmount/mountpoint_windows.go new file mode 100644 index 000000000..5018ae6a8 --- /dev/null +++ b/cmd/cmount/mountpoint_windows.go @@ -0,0 +1,189 @@ +// +build cmount +// +build cgo +// +build windows + +package cmount + +import ( + "os" + "path/filepath" + "regexp" + + "github.com/pkg/errors" + "github.com/rclone/rclone/cmd/mountlib" + "github.com/rclone/rclone/fs" + "github.com/rclone/rclone/lib/file" +) + +var isDriveRegex = regexp.MustCompile(`^[a-zA-Z]\:$`) +var isDriveRootPathRegex = regexp.MustCompile(`^[a-zA-Z]\:\\$`) +var isDriveOrRootPathRegex = regexp.MustCompile(`^[a-zA-Z]\:\\?$`) +var isNetworkSharePathRegex = regexp.MustCompile(`^\\\\[^\\]+\\[^\\]`) + +// isNetworkSharePath returns true if the given string is a valid network share path, +// in the basic UNC format "\\Server\Share\Path", where the first two path components +// are required ("\\Server\Share", which represents the volume). +// Extended-length UNC format "\\?\UNC\Server\Share\Path" is not considered, as it is +// not supported by cgofuse/winfsp. +// Note: There is a UNCPath function in lib/file, but it refers to any extended-length +// paths using prefix "\\?\", and not necessarily network resource UNC paths. +func isNetworkSharePath(l string) bool { + return isNetworkSharePathRegex.MatchString(l) +} + +// isDrive returns true if given string is a drive letter followed by the volume separator, e.g. "X:". +// This is the format supported by cgofuse/winfsp for mounting as drive. +// Extended-length format "\\?\X:" is not considered, as it is not supported by cgofuse/winfsp. +func isDrive(l string) bool { + return isDriveRegex.MatchString(l) +} + +// isDriveRootPath returns true if given string is a drive letter followed by the volume separator, +// as well as a path separator, e.g. "X:\". This is a format often used instead of the format without the +// trailing path separator to denote a drive or volume, in addition to representing the drive's root directory. +// This format is not accepted by cgofuse/winfsp for mounting as drive, but can easily be by trimming off +// the path separator. Extended-length format "\\?\X:\" is not considered. +func isDriveRootPath(l string) bool { + return isDriveRootPathRegex.MatchString(l) +} + +// isDriveOrRootPath returns true if given string is a drive letter followed by the volume separator, +// and optionally a path separator. See isDrive and isDriveRootPath functions. +func isDriveOrRootPath(l string) bool { + return isDriveOrRootPathRegex.MatchString(l) +} + +// isDefaultPath returns true if given string is a special keyword used to trigger default mount. +func isDefaultPath(l string) bool { + return l == "" || l == "*" +} + +// getUnusedDrive find unused drive letter and returns string with drive letter followed by volume separator. +func getUnusedDrive() (string, error) { + driveLetter := file.FindUnusedDriveLetter() + if driveLetter == 0 { + return "", errors.New("could not find unused drive letter") + } + mountpoint := string(driveLetter) + ":" // Drive letter with volume separator only, no trailing backslash, which is what cgofuse/winfsp expects + fs.Logf(nil, "Assigning drive letter %q", mountpoint) + return mountpoint, nil +} + +// handleDefaultMountpath handles the case where mount path is not set, or set to a special keyword. +// This will automatically pick an unused drive letter to use as mountpoint. +func handleDefaultMountpath() (string, error) { + return getUnusedDrive() +} + +// handleNetworkShareMountpath handles the case where mount path is a network share path. +// Sets volume name option and returns a mountpoint string. +func handleNetworkShareMountpath(mountpath string, opt *mountlib.Options) (string, error) { + // Assuming mount path is a valid network share path (UNC format, "\\Server\Share"). + // Always mount as network drive, regardless of the NetworkMode option. + // Find an unused drive letter to use as mountpoint, the the supplied path can + // be used as volume prefix (network share path) instead of mountpoint. + if !opt.NetworkMode { + fs.Debugf(nil, "Forcing --network-mode because mountpoint path is network share UNC format") + opt.NetworkMode = true + } + mountpoint, err := getUnusedDrive() + if err != nil { + return "", err + } + return mountpoint, nil +} + +// handleLocalMountpath handles the case where mount path is a local file system path. +func handleLocalMountpath(mountpath string, opt *mountlib.Options) (string, error) { + // Assuming path is drive letter or directory path, not network share (UNC) path. + // If drive letter: Must be given as a single character followed by ":" and nothing else. + // Else, assume directory path: Directory must not exist, but its parent must. + if _, err := os.Stat(mountpath); err == nil { + return "", errors.New("mountpoint path already exists: " + mountpath) + } else if !os.IsNotExist(err) { + return "", errors.Wrap(err, "failed to retrieve mountpoint path information") + } + //if isDriveRootPath(mountpath) { // Assume intention with "X:\" was "X:" + // mountpoint = mountpath[:len(mountpath)-1] // WinFsp needs drive mountpoints without trailing path separator + if !isDrive(mountpath) { + // Assuming directory path, since it is not a pure drive letter string such as "X:". + // Drive letter string can be used as is, since we have already checked it does not exist, + // but directory path needs more checks. + if opt.NetworkMode { + fs.Errorf(nil, "Ignoring --network-mode as it is not supported with directory mountpoint") + opt.NetworkMode = false + } + parent := filepath.Join(mountpath, "..") + if parent == "" || parent == "." { + return "", errors.New("mountpoint directory is not valid: " + parent) + } + if os.IsPathSeparator(parent[len(parent)-1]) { // Ends in a separator only if it is the root directory + return "", errors.New("mountpoint directory is at root: " + parent) + } + if _, err := os.Stat(parent); err != nil { + if os.IsNotExist(err) { + return "", errors.New("parent of mountpoint directory does not exist: " + parent) + } + return "", errors.Wrap(err, "failed to retrieve mountpoint directory parent information") + } + } + return mountpath, nil +} + +// handleVolumeName handles the volume name option. +func handleVolumeName(opt *mountlib.Options, volumeName string) { + // If volumeName parameter is set, then just set that into options replacing any existing value. + // Else, ensure the volume name option is a valid network share UNC path if network mode, + // and ensure network mode if configured volume name is already UNC path. + if volumeName != "" { + opt.VolumeName = volumeName + } else if opt.VolumeName != "" { // Should always be true due to code in mountlib caller + // Use value of given volume name option, but check if it is disk volume name or network volume prefix + if isNetworkSharePath(opt.VolumeName) { + // Specified volume name is network share UNC path, assume network mode and use it as volume prefix + opt.VolumeName = opt.VolumeName[1:] // WinFsp requires volume prefix as UNC-like path but with only a single backslash + if !opt.NetworkMode { + // Specified volume name is network share UNC path, force network mode and use it as volume prefix + fs.Debugf(nil, "Forcing network mode due to network share (UNC) volume name") + opt.NetworkMode = true + } + } else if opt.NetworkMode { + // Plain volume name treated as share name in network mode, append to hard coded "\\server" prefix to get full volume prefix. + opt.VolumeName = "\\server\\" + opt.VolumeName + } + } else if opt.NetworkMode { + // Hard coded default + opt.VolumeName = "\\server\\share" + } +} + +// getMountpoint handles mounting details on Windows, +// where disk and network based file systems are treated different. +func getMountpoint(mountpath string, opt *mountlib.Options) (mountpoint string, err error) { + + // First handle mountpath + var volumeName string + if isDefaultPath(mountpath) { + // Mount path indicates defaults, which will automatically pick an unused drive letter. + mountpoint, err = handleDefaultMountpath() + } else if isNetworkSharePath(mountpath) { + // Mount path is a valid network share path (UNC format, "\\Server\Share" prefix). + mountpoint, err = handleNetworkShareMountpath(mountpath, opt) + // In this case the volume name is taken from the mount path, will replace any existing volume name option. + volumeName = mountpath[1:] // WinFsp requires volume prefix as UNC-like path but with only a single backslash + } else { + // Mount path is drive letter or directory path. + mountpoint, err = handleLocalMountpath(mountpath, opt) + } + + // Second handle volume name + handleVolumeName(opt, volumeName) + + // Done, return mountpoint to be used, together with updated mount options. + if opt.NetworkMode { + fs.Debugf(nil, "Network mode mounting is enabled") + } else { + fs.Debugf(nil, "Network mode mounting is disabled") + } + return +} diff --git a/cmd/mountlib/mount.go b/cmd/mountlib/mount.go index 74e48051f..d026610d1 100644 --- a/cmd/mountlib/mount.go +++ b/cmd/mountlib/mount.go @@ -44,6 +44,7 @@ type Options struct { NoAppleXattr bool DaemonTimeout time.Duration // OSXFUSE only AsyncRead bool + NetworkMode bool // Windows only } // DefaultOpt is the default values for creating the mount @@ -99,6 +100,8 @@ func AddFlags(flagSet *pflag.FlagSet) { if runtime.GOOS == "darwin" { flags.BoolVarP(flagSet, &Opt.NoAppleDouble, "noappledouble", "", Opt.NoAppleDouble, "Sets the OSXFUSE option noappledouble.") flags.BoolVarP(flagSet, &Opt.NoAppleXattr, "noapplexattr", "", Opt.NoAppleXattr, "Sets the OSXFUSE option noapplexattr.") + } else if runtime.GOOS == "windows" { + flags.BoolVarP(flagSet, &Opt.NetworkMode, "network-mode", "", Opt.NetworkMode, "Mount as remote network drive, instead of fixed disk drive.") } } diff --git a/lib/file/driveletter_other.go b/lib/file/driveletter_other.go new file mode 100644 index 000000000..16e6e641f --- /dev/null +++ b/lib/file/driveletter_other.go @@ -0,0 +1,8 @@ +//+build !windows + +package file + +// FindUnusedDriveLetter does nothing except on Windows. +func FindUnusedDriveLetter() (driveLetter uint8) { + return 0 +} diff --git a/lib/file/driveletter_windows.go b/lib/file/driveletter_windows.go new file mode 100644 index 000000000..ca080fc9c --- /dev/null +++ b/lib/file/driveletter_windows.go @@ -0,0 +1,22 @@ +//+build windows + +package file + +import ( + "os" +) + +// FindUnusedDriveLetter searches mounted drive list on the system +// (starting from Z: and ending at D:) for unused drive letter. +// Returns the letter found (like 'Z') or zero value. +func FindUnusedDriveLetter() (driveLetter uint8) { + // Do not use A: and B:, because they are reserved for floppy drive. + // Do not use C:, because it is normally used for main drive. + for l := uint8('Z'); l >= uint8('D'); l-- { + _, err := os.Stat(string(l) + ":" + string(os.PathSeparator)) + if os.IsNotExist(err) { + return l + } + } + return 0 +} diff --git a/vfs/vfstest/fs.go b/vfs/vfstest/fs.go index fefd52d41..51b1e59d7 100644 --- a/vfs/vfstest/fs.go +++ b/vfs/vfstest/fs.go @@ -24,6 +24,7 @@ import ( "github.com/rclone/rclone/fs" "github.com/rclone/rclone/fs/walk" "github.com/rclone/rclone/fstest" + "github.com/rclone/rclone/lib/file" "github.com/rclone/rclone/vfs" "github.com/rclone/rclone/vfs/vfscommon" "github.com/rclone/rclone/vfs/vfsflags" @@ -155,16 +156,13 @@ func findMountPath() string { } // Find a free drive letter + letter := file.FindUnusedDriveLetter() drive := "" - for letter := 'E'; letter <= 'Z'; letter++ { + if letter == 0 { + log.Fatalf("Couldn't find free drive letter for test") + } else { drive = string(letter) + ":" - _, err := os.Stat(drive + "\\") - if os.IsNotExist(err) { - goto found - } } - log.Fatalf("Couldn't find free drive letter for test") -found: return drive }