mirror of
https://github.com/rclone/rclone.git
synced 2024-11-07 09:04:52 +01:00
mount: more user friendly mounting as network drive on windows
Add --network-mode option to activate mounting as network drive without having to set volume prefix. Add support for automatic drive letter assignment (not specific to network drive mounting). Allow full network share unc path in --volname, which will also implicitely activate network drive mounting. Allow full network share unc path as mountpoint, which will also implicitely activate network drive mounting, and the specified path will be used as volume prefix and the remote will be mounted on an automatically assigned drive letter instead.
This commit is contained in:
parent
9ea990d5a2
commit
e92cb9e8f8
@ -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
|
||||
|
23
cmd/cmount/mountpoint_other.go
Normal file
23
cmd/cmount/mountpoint_other.go
Normal file
@ -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
|
||||
}
|
189
cmd/cmount/mountpoint_windows.go
Normal file
189
cmd/cmount/mountpoint_windows.go
Normal file
@ -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
|
||||
}
|
@ -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.")
|
||||
}
|
||||
}
|
||||
|
||||
|
8
lib/file/driveletter_other.go
Normal file
8
lib/file/driveletter_other.go
Normal file
@ -0,0 +1,8 @@
|
||||
//+build !windows
|
||||
|
||||
package file
|
||||
|
||||
// FindUnusedDriveLetter does nothing except on Windows.
|
||||
func FindUnusedDriveLetter() (driveLetter uint8) {
|
||||
return 0
|
||||
}
|
22
lib/file/driveletter_windows.go
Normal file
22
lib/file/driveletter_windows.go
Normal file
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user