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.
|
||||
- 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
|
||||
}
|
||||
|
||||
@@ -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