mirror of
https://github.com/rclone/rclone.git
synced 2025-12-28 06:03:43 +00:00
Fixes an issue on Windows where mounting the local filesystem in network mode failed
when not using option --volname. Reason was that the volume name in network mode
is a network share path in the basic UNC format, and characters that are invalid
in regular file and directory names are also invalid in such a path. And the default
volume name would typically include a '?', which is invalid, from the unc path of
the local, e.g. "\\server\\? C Temp".
The fix is to use an encoder to encode invalid characters such as '?' with the unicode
equivalent, similar to how rclone encodes filesystem paths in normal operations,
when mounting in network mode. Also performs some automatic cleanup of path separators,
but in general, tries to be conservative on restrictions, and instead rely on --volname
being set to something realistic.
Existing strategy to replace the two characters ':' and '/' with space, regardless of
mounting mode variant, was removed. For network mode the new approach handles these in
a better way. Also the existing method did not apply at all when using the implicit
network mode where volume names are taken from mountpath instead of volname option
("rclone mount remote:path/to/files \\cloud\remote"). For non-network mode they were not
needed.
Default volume names, when not specified by user, will be different with this change.
See: #6234
229 lines
10 KiB
Go
229 lines
10 KiB
Go
//go:build cmount && windows
|
|
// +build cmount,windows
|
|
|
|
package cmount
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/rclone/rclone/cmd/mountlib"
|
|
"github.com/rclone/rclone/fs"
|
|
"github.com/rclone/rclone/lib/encoder"
|
|
"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(`^\\\\[^\\\?]+\\[^\\]`)
|
|
var isAnyPathSeparatorRegex = regexp.MustCompile(`[/\\]+`) // Matches any path separators, slash or backslash, or sequences of them
|
|
|
|
// isNetworkSharePath returns true if the given string is a network share path,
|
|
// in the basic UNC format "\\Server\Share\Path". The first two path components
|
|
// are required ("\\Server\Share"), and represents the volume. The rest of the
|
|
// string can be anything, i.e. can be a nested path ("\\Server\Share\Path\Path\Path").
|
|
// Actual validity of the path, e.g. if it contains invalid characters, is not considered.
|
|
// Extended-length UNC format "\\?\UNC\Server\Share\Path" is not considered, as it is
|
|
// not supported by cgofuse/winfsp, so returns false for any paths with prefix "\\?\".
|
|
// 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 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(f fs.Fs, 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 "", fmt.Errorf("failed to retrieve mountpoint path information: %w", err)
|
|
}
|
|
if isDriveRootPath(mountpath) { // Assume intention with "X:\" was "X:"
|
|
mountpath = 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.Debugf(nil, "Ignoring --network-mode as it is not supported with directory mountpoint")
|
|
opt.NetworkMode = false
|
|
}
|
|
var err error
|
|
if mountpath, err = filepath.Abs(mountpath); err != nil { // Ensures parent is found but also more informative log messages
|
|
return "", fmt.Errorf("mountpoint path is not valid: %s: %w", mountpath, err)
|
|
}
|
|
parent := filepath.Join(mountpath, "..")
|
|
if _, err = os.Stat(parent); err != nil {
|
|
if os.IsNotExist(err) {
|
|
return "", errors.New("parent of mountpoint directory does not exist: " + parent)
|
|
}
|
|
return "", fmt.Errorf("failed to retrieve mountpoint directory parent information: %w", err)
|
|
}
|
|
if err = mountlib.CheckOverlap(f, mountpath); err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
return mountpath, nil
|
|
}
|
|
|
|
// networkSharePathEncoder is an encoder used to make strings valid as (part of) Windows network share UNC paths
|
|
const networkSharePathEncoder = (encoder.EncodeZero | // NUL(0x00)
|
|
encoder.EncodeCtl | // CTRL(0x01-0x1F)
|
|
encoder.EncodeDel | // DEL(0x7F)
|
|
encoder.EncodeWin | // :?"*<>|
|
|
encoder.EncodeInvalidUtf8) // Also encode invalid UTF-8 bytes as Go can't convert them to UTF-16.
|
|
|
|
// encodeNetworkSharePath makes a string valid to use as (part of) a Windows network share UNC path.
|
|
// Using backslash as path separator here, but forward slashes would also be treated as
|
|
// path separators by the library, and therefore does not encode either of them. For convenience,
|
|
// normalizes to backslashes-only. UNC paths always start with two path separators, but WinFsp
|
|
// requires volume prefix as UNC-like path but with only a single backslash prefix, and multiple
|
|
// separators are not valid in any other parts of network share paths, so therefore (unlike what
|
|
// filepath.FromSlash would do) replaces multiple separators with a single one (like filpath.Clean
|
|
// would do, but it does also more). A trailing path separator would just be ignored, but we
|
|
// remove it here as well for convenience.
|
|
func encodeNetworkSharePath(volumeName string) string {
|
|
return networkSharePathEncoder.Encode(strings.TrimRight(isAnyPathSeparatorRegex.ReplaceAllString(volumeName, `\`), `\`))
|
|
}
|
|
|
|
// handleVolumeName handles the volume name option.
|
|
func handleVolumeName(opt *mountlib.Options) {
|
|
// 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 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 = encodeNetworkSharePath(opt.VolumeName[1:]) // We know from isNetworkSharePath it has a duplicate path separator prefix, so removes that right away (but encodeNetworkSharePath would remove it also)
|
|
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 {
|
|
// Specified volume name is not a valid network share UNC path, but network mode is enabled, so append to a hard coded server prefix and use it as volume prefix
|
|
opt.VolumeName = `\server\` + strings.TrimLeft(encodeNetworkSharePath(opt.VolumeName), `\`)
|
|
}
|
|
} else if opt.NetworkMode {
|
|
// Use 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(f fs.Fs, mountpath string, opt *mountlib.Options) (mountpoint string, err error) {
|
|
// Inform about some options not relevant in this mode
|
|
if opt.AllowNonEmpty {
|
|
fs.Logf(nil, "--allow-non-empty flag does nothing on Windows")
|
|
}
|
|
if opt.AllowRoot {
|
|
fs.Logf(nil, "--allow-root flag does nothing on Windows")
|
|
}
|
|
if opt.AllowOther {
|
|
fs.Logf(nil, "--allow-other flag does nothing on Windows")
|
|
}
|
|
|
|
// Handle mountpath
|
|
if isDefaultPath(mountpath) {
|
|
// Mount path indicates defaults, which will automatically pick an unused drive letter.
|
|
if mountpoint, err = handleDefaultMountpath(); err != nil {
|
|
return
|
|
}
|
|
} else if isNetworkSharePath(mountpath) {
|
|
// Mount path is a valid network share path (UNC format, "\\Server\Share" prefix).
|
|
if mountpoint, err = handleNetworkShareMountpath(mountpath, opt); err != nil {
|
|
return
|
|
}
|
|
// In this case the volume name is taken from the mount path, it replaces any existing volume name option.
|
|
opt.VolumeName = mountpath
|
|
} else {
|
|
// Mount path is drive letter or directory path.
|
|
if mountpoint, err = handleLocalMountpath(f, mountpath, opt); err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
// Handle volume name
|
|
handleVolumeName(opt)
|
|
|
|
// 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
|
|
}
|