mount: changed handling of volume name (Windows and OSX)

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
This commit is contained in:
albertony 2022-06-10 23:06:28 +02:00
parent 11443e4491
commit 0093e23e42
6 changed files with 63 additions and 48 deletions

View File

@ -26,5 +26,6 @@ func getMountpoint(f fs.Fs, mountPath string, opt *mountlib.Options) (string, er
if err = mountlib.CheckAllowNonEmpty(mountPath, opt); err != nil { if err = mountlib.CheckAllowNonEmpty(mountPath, opt); err != nil {
return "", err return "", err
} }
opt.VolumeName = mountlib.MakeVolumeNameValidOnUnix(opt.VolumeName)
return mountPath, nil return mountPath, nil
} }

View File

@ -9,9 +9,11 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
"strings"
"github.com/rclone/rclone/cmd/mountlib" "github.com/rclone/rclone/cmd/mountlib"
"github.com/rclone/rclone/fs" "github.com/rclone/rclone/fs"
"github.com/rclone/rclone/lib/encoder"
"github.com/rclone/rclone/lib/file" "github.com/rclone/rclone/lib/file"
) )
@ -19,10 +21,13 @@ var isDriveRegex = regexp.MustCompile(`^[a-zA-Z]\:$`)
var isDriveRootPathRegex = regexp.MustCompile(`^[a-zA-Z]\:\\$`) var isDriveRootPathRegex = regexp.MustCompile(`^[a-zA-Z]\:\\$`)
var isDriveOrRootPathRegex = regexp.MustCompile(`^[a-zA-Z]\:\\?$`) var isDriveOrRootPathRegex = regexp.MustCompile(`^[a-zA-Z]\:\\?$`)
var isNetworkSharePathRegex = regexp.MustCompile(`^\\\\[^\\\?]+\\[^\\]`) 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 valid network share path, // isNetworkSharePath returns true if the given string is a network share path,
// in the basic UNC format "\\Server\Share\Path", where the first two path components // in the basic UNC format "\\Server\Share\Path". The first two path components
// are required ("\\Server\Share", which represents the volume). // 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 // 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 "\\?\". // 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 // Note: There is a UNCPath function in lib/file, but it refers to any extended-length
@ -132,30 +137,47 @@ func handleLocalMountpath(f fs.Fs, mountpath string, opt *mountlib.Options) (str
return mountpath, nil 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. // handleVolumeName handles the volume name option.
func handleVolumeName(opt *mountlib.Options, volumeName string) { func handleVolumeName(opt *mountlib.Options) {
// If volumeName parameter is set, then just set that into options replacing any existing value. // Ensure the volume name option is a valid network share UNC path if network mode,
// 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. // and ensure network mode if configured volume name is already UNC path.
if volumeName != "" { if opt.VolumeName != "" { // Should always be true due to code in mountlib caller
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 // Use value of given volume name option, but check if it is disk volume name or network volume prefix
if isNetworkSharePath(opt.VolumeName) { if isNetworkSharePath(opt.VolumeName) {
// Specified volume name is network share UNC path, assume network mode and use it as volume prefix // 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 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 { if !opt.NetworkMode {
// Specified volume name is network share UNC path, force network mode and use it as volume prefix // 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") fs.Debugf(nil, "Forcing network mode due to network share (UNC) volume name")
opt.NetworkMode = true opt.NetworkMode = true
} }
} else if opt.NetworkMode { } 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. // 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\\" + opt.VolumeName opt.VolumeName = `\server\` + strings.TrimLeft(encodeNetworkSharePath(opt.VolumeName), `\`)
} }
} else if opt.NetworkMode { } else if opt.NetworkMode {
// Hard coded default // Use hard coded default
opt.VolumeName = "\\server\\share" opt.VolumeName = `\server\share`
} }
} }
@ -174,22 +196,27 @@ func getMountpoint(f fs.Fs, mountpath string, opt *mountlib.Options) (mountpoint
} }
// Handle mountpath // Handle mountpath
var volumeName string
if isDefaultPath(mountpath) { if isDefaultPath(mountpath) {
// Mount path indicates defaults, which will automatically pick an unused drive letter. // Mount path indicates defaults, which will automatically pick an unused drive letter.
mountpoint, err = handleDefaultMountpath() if mountpoint, err = handleDefaultMountpath(); err != nil {
return
}
} else if isNetworkSharePath(mountpath) { } else if isNetworkSharePath(mountpath) {
// Mount path is a valid network share path (UNC format, "\\Server\Share" prefix). // Mount path is a valid network share path (UNC format, "\\Server\Share" prefix).
mountpoint, err = handleNetworkShareMountpath(mountpath, opt) if mountpoint, err = handleNetworkShareMountpath(mountpath, opt); err != nil {
// In this case the volume name is taken from the mount path, will replace any existing volume name option. return
volumeName = mountpath[1:] // WinFsp requires volume prefix as UNC-like path but with only a single backslash }
// In this case the volume name is taken from the mount path, it replaces any existing volume name option.
opt.VolumeName = mountpath
} else { } else {
// Mount path is drive letter or directory path. // Mount path is drive letter or directory path.
mountpoint, err = handleLocalMountpath(f, mountpath, opt) if mountpoint, err = handleLocalMountpath(f, mountpath, opt); err != nil {
return
}
} }
// Handle volume name // Handle volume name
handleVolumeName(opt, volumeName) handleVolumeName(opt)
// Done, return mountpoint to be used, together with updated mount options. // Done, return mountpoint to be used, together with updated mount options.
if opt.NetworkMode { if opt.NetworkMode {

View File

@ -79,6 +79,7 @@ func mount(VFS *vfs.VFS, mountpoint string, opt *mountlib.Options) (<-chan error
if err := mountlib.CheckAllowNonEmpty(mountpoint, opt); err != nil { if err := mountlib.CheckAllowNonEmpty(mountpoint, opt); err != nil {
return nil, nil, err return nil, nil, err
} }
opt.VolumeName = mountlib.MakeVolumeNameValidOnUnix(opt.VolumeName)
fs.Debugf(f, "Mounting on %q", mountpoint) fs.Debugf(f, "Mounting on %q", mountpoint)
if opt.DebugFUSE { if opt.DebugFUSE {

View File

@ -151,6 +151,7 @@ func mount(VFS *vfs.VFS, mountpoint string, opt *mountlib.Options) (<-chan error
if err := mountlib.CheckAllowNonEmpty(mountpoint, opt); err != nil { if err := mountlib.CheckAllowNonEmpty(mountpoint, opt); err != nil {
return nil, nil, err return nil, nil, err
} }
opt.VolumeName = mountlib.MakeVolumeNameValidOnUnix(opt.VolumeName)
fs.Debugf(f, "Mounting on %q", mountpoint) fs.Debugf(f, "Mounting on %q", mountpoint)
fsys := NewFS(VFS, opt) fsys := NewFS(VFS, opt)

View File

@ -240,8 +240,12 @@ func NewMountCommand(commandName string, hidden bool, mount MountFn) *cobra.Comm
func (m *MountPoint) Mount() (daemon *os.Process, err error) { func (m *MountPoint) Mount() (daemon *os.Process, err error) {
// Ensure sensible defaults // Ensure sensible defaults
m.SetVolumeName(m.MountOpt.VolumeName) if m.MountOpt.VolumeName == "" {
m.SetDeviceName(m.MountOpt.DeviceName) m.MountOpt.VolumeName = fs.ConfigString(m.Fs)
}
if m.MountOpt.DeviceName == "" {
m.MountOpt.DeviceName = fs.ConfigString(m.Fs)
}
// Start background task if --daemon is specified // Start background task if --daemon is specified
if m.MountOpt.Daemon { if m.MountOpt.Daemon {

View File

@ -97,29 +97,10 @@ func checkMountEmpty(mountpoint string) error {
return fmt.Errorf(msg+": %w", mountpoint, err) return fmt.Errorf(msg+": %w", mountpoint, err)
} }
// SetVolumeName with sensible default // MakeVolumeNameValidOnUnix takes a volume name and returns a variant that is valid on unix systems.
func (m *MountPoint) SetVolumeName(vol string) { func MakeVolumeNameValidOnUnix(volumeName string) string {
if vol == "" { volumeName = strings.ReplaceAll(volumeName, ":", " ")
vol = fs.ConfigString(m.Fs) volumeName = strings.ReplaceAll(volumeName, "/", " ")
} volumeName = strings.TrimSpace(volumeName)
m.MountOpt.SetVolumeName(vol) return volumeName
}
// SetVolumeName removes special characters from volume name if necessary
func (o *Options) SetVolumeName(vol string) {
vol = strings.ReplaceAll(vol, ":", " ")
vol = strings.ReplaceAll(vol, "/", " ")
vol = strings.TrimSpace(vol)
if runtime.GOOS == "windows" && len(vol) > 32 {
vol = vol[:32]
}
o.VolumeName = vol
}
// SetDeviceName with sensible default
func (m *MountPoint) SetDeviceName(dev string) {
if dev == "" {
dev = fs.ConfigString(m.Fs)
}
m.MountOpt.DeviceName = dev
} }