mountlib: support flat VFS and Mount options in mount RC command

This commit is contained in:
Hakan İSMAİL
2026-07-29 19:42:45 +01:00
committed by Nick Craig-Wood
parent ab93058560
commit 71aef129cf
3 changed files with 195 additions and 2 deletions
+105
View File
@@ -0,0 +1,105 @@
package mountlib
import (
"reflect"
"sync"
"github.com/rclone/rclone/fs/config/configstruct"
"github.com/rclone/rclone/fs/rc"
"github.com/rclone/rclone/vfs/vfscommon"
)
var (
vfsOptionsOnce sync.Once
vfsOptionsMap map[string]bool
mountOptionsOnce sync.Once
mountOptionsMap map[string]bool
)
func initVfsOptions() {
vfsOptionsOnce.Do(func() {
vfsOptionsMap = make(map[string]bool, len(vfscommon.OptionsInfo))
for _, opt := range vfscommon.OptionsInfo {
vfsOptionsMap[opt.Name] = true
}
})
}
func initMountOptions() {
mountOptionsOnce.Do(func() {
mountOptionsMap = make(map[string]bool, len(OptionsInfo))
for _, opt := range OptionsInfo {
mountOptionsMap[opt.Name] = true
}
})
}
// isMap returns true if v's underlying type is a map
func isMap(v any) bool {
if v == nil {
return false
}
t := reflect.TypeOf(v)
for t.Kind() == reflect.Pointer {
t = t.Elem()
}
return t.Kind() == reflect.Map
}
// parseVfsOptions parses VFS options from in (both flat and nested) and updates vfsOpt
func parseVfsOptions(in rc.Params, vfsOpt *vfscommon.Options) error {
initVfsOptions()
flatVfs := make(map[string]any)
for k, v := range in {
if vfsOptionsMap[k] {
if isMap(v) {
continue
}
flatVfs[k] = v
}
}
if len(flatVfs) > 0 {
err := configstruct.SetAny(flatVfs, vfsOpt)
if err != nil {
return err
}
for k := range flatVfs {
delete(in, k)
}
}
err := in.GetStructMissingOK("vfsOpt", vfsOpt)
if err != nil {
return err
}
delete(in, "vfsOpt")
return nil
}
// parseMountOptions parses Mount options from in (both flat and nested) and updates mountOpt
func parseMountOptions(in rc.Params, mountOpt *Options) error {
initMountOptions()
flatMount := make(map[string]any)
for k, v := range in {
if mountOptionsMap[k] {
if isMap(v) {
continue
}
flatMount[k] = v
}
}
if len(flatMount) > 0 {
err := configstruct.SetAny(flatMount, mountOpt)
if err != nil {
return err
}
for k := range flatMount {
delete(in, k)
}
}
err := in.GetStructMissingOK("mountOpt", mountOpt)
if err != nil {
return err
}
delete(in, "mountOpt")
return nil
}
+7 -2
View File
@@ -62,6 +62,10 @@ This takes the following parameters:
- mountOpt: a JSON object with Mount options in.
- vfsOpt: a JSON object with VFS options in.
Alternatively, you can pass VFS and Mount options flat at the top level of the parameter map. The option names are the same as their CLI flags without '--' and with '-' replaced by '_' (e.g. 'vfs_cache_mode' instead of 'CacheMode' inside 'vfsOpt', and 'volname' instead of 'VolName' inside 'mountOpt').
If both flat parameters and nested 'vfsOpt'/'mountOpt' blocks are supplied, the parameters in the nested blocks will take precedence.
On Windows mountPoint may be set to "*" to assign the next available
drive letter automatically, or a network share UNC path (e.g.
"\\server\share") to mount as a network drive. In these cases the
@@ -79,6 +83,7 @@ Example:
rclone rc mount/mount fs=mydrive: mountPoint=/home/<user>/mountPoint
rclone rc mount/mount fs=mydrive: mountPoint=/home/<user>/mountPoint mountType=mount
rclone rc mount/mount fs=TestDrive: mountPoint=/mnt/tmp vfsOpt='{"CacheMode": 2}' mountOpt='{"AllowOther": true}'
rclone rc mount/mount fs=TestDrive: mountPoint=/mnt/tmp vfs_cache_mode=writes volname=MyTestVolume
rclone rc mount/mount fs=mydrive: mountPoint=* mountType=cmount
` + "```" + `
@@ -100,13 +105,13 @@ func mountRc(ctx context.Context, in rc.Params) (out rc.Params, err error) {
}
vfsOpt := vfscommon.Opt
err = in.GetStructMissingOK("vfsOpt", &vfsOpt)
err = parseVfsOptions(in, &vfsOpt)
if err != nil {
return nil, err
}
mountOpt := Opt
err = in.GetStructMissingOK("mountOpt", &mountOpt)
err = parseMountOptions(in, &mountOpt)
if err != nil {
return nil, err
}
+83
View File
@@ -127,3 +127,86 @@ func TestRc(t *testing.T) {
})
})
}
func TestRcFlatOptions(t *testing.T) {
// Disable tests under macOS and the CI since they are locking up
if runtime.GOOS == "darwin" {
testy.SkipUnreliable(t)
}
ctx := context.Background()
configfile.Install()
mount := rc.Calls.Get("mount/mount")
assert.NotNil(t, mount)
unmount := rc.Calls.Get("mount/unmount")
assert.NotNil(t, unmount)
getMountTypes := rc.Calls.Get("mount/types")
assert.NotNil(t, getMountTypes)
localDir := t.TempDir()
err := os.WriteFile(filepath.Join(localDir, "file.txt"), []byte("hello"), 0666)
require.NoError(t, err)
out, err := getMountTypes.Fn(ctx, nil)
require.NoError(t, err)
var mountTypes []string
err = out.GetStruct("mountTypes", &mountTypes)
require.NoError(t, err)
if len(mountTypes) == 0 {
t.Skip("Can't mount")
}
mountPointFlat := t.TempDir()
if runtime.GOOS == "windows" {
require.NoError(t, os.RemoveAll(mountPointFlat))
}
in := rc.Params{
"fs": localDir,
"mountPoint": mountPointFlat,
"file_perms": 0400, // flat VFS option
"volname": "MyTestVolume", // flat Mount option
}
// mount
out, err = mount.Fn(ctx, in)
if err != nil {
t.Skipf("Mount failed - skipping test: %v", err)
}
// check the returned mount point matches what we asked for
returnedMountPoint, err := out.GetString("mountPoint")
require.NoError(t, err)
assert.Equal(t, mountPointFlat, returnedMountPoint)
// check that the flat options were consumed and removed from parameter map
_, ok := in["file_perms"]
assert.False(t, ok, "file_perms flat option should have been deleted")
_, ok = in["volname"]
assert.False(t, ok, "volname flat option should have been deleted")
// unmount
_, err = unmount.Fn(ctx, rc.Params{
"mountPoint": mountPointFlat,
})
require.NoError(t, err)
// FIXME wait a moment for the OS to release the mount point
time.Sleep(100 * time.Millisecond)
}
func TestRcFlatOptionsNull(t *testing.T) {
ctx := context.Background()
configfile.Install()
mount := rc.Calls.Get("mount/mount")
assert.NotNil(t, mount)
in := rc.Params{
"fs": "some_fs",
"mountPoint": "some_mount_point",
"vfs_cache_mode": nil, // flat VFS option set to null
}
_, err := mount.Fn(ctx, in)
assert.Error(t, err)
assert.Contains(t, err.Error(), "interpreting <nil> as string failed")
}