diff --git a/cmd/mountlib/context.go b/cmd/mountlib/context.go new file mode 100644 index 000000000..f2ca4c77a --- /dev/null +++ b/cmd/mountlib/context.go @@ -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 +} diff --git a/cmd/mountlib/rc.go b/cmd/mountlib/rc.go index 56170a354..362c33760 100644 --- a/cmd/mountlib/rc.go +++ b/cmd/mountlib/rc.go @@ -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//mountPoint rclone rc mount/mount fs=mydrive: mountPoint=/home//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 } diff --git a/cmd/mountlib/rc_test.go b/cmd/mountlib/rc_test.go index daa44ed72..4d316998b 100644 --- a/cmd/mountlib/rc_test.go +++ b/cmd/mountlib/rc_test.go @@ -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 as string failed") +}