mountlib: support flat VFS and Mount options in mount RC command
This commit is contained in:
committed by
Nick Craig-Wood
parent
ab93058560
commit
71aef129cf
@@ -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
@@ -62,6 +62,10 @@ This takes the following parameters:
|
|||||||
- mountOpt: a JSON object with Mount options in.
|
- mountOpt: a JSON object with Mount options in.
|
||||||
- vfsOpt: a JSON object with VFS 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
|
On Windows mountPoint may be set to "*" to assign the next available
|
||||||
drive letter automatically, or a network share UNC path (e.g.
|
drive letter automatically, or a network share UNC path (e.g.
|
||||||
"\\server\share") to mount as a network drive. In these cases the
|
"\\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
|
||||||
rclone rc mount/mount fs=mydrive: mountPoint=/home/<user>/mountPoint mountType=mount
|
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 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
|
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
|
vfsOpt := vfscommon.Opt
|
||||||
err = in.GetStructMissingOK("vfsOpt", &vfsOpt)
|
err = parseVfsOptions(in, &vfsOpt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
mountOpt := Opt
|
mountOpt := Opt
|
||||||
err = in.GetStructMissingOK("mountOpt", &mountOpt)
|
err = parseMountOptions(in, &mountOpt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user