docker serve: parse all remaining mount and VFS options

Before this change, this code implemented an ad-hoc parser for a
subset of vfs and mount options.

After the config re-organization it can use the same parsing code as
the rest of rclone which simplifies the code and exposes all the VFS
and mount options.
This commit is contained in:
Nick Craig-Wood 2024-09-09 10:53:00 +01:00
parent d8bc542ffc
commit 455b673a9f
3 changed files with 104 additions and 160 deletions

View File

@ -2,17 +2,15 @@ package docker
import ( import (
"fmt" "fmt"
"math"
"strings" "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/fs/config/configmap" "github.com/rclone/rclone/fs/config/configmap"
"github.com/rclone/rclone/fs/config/configstruct"
"github.com/rclone/rclone/fs/fspath" "github.com/rclone/rclone/fs/fspath"
"github.com/rclone/rclone/fs/rc" "github.com/rclone/rclone/fs/rc"
"github.com/rclone/rclone/vfs/vfscommon" "github.com/rclone/rclone/vfs/vfscommon"
"github.com/spf13/pflag"
) )
// applyOptions configures volume from request options. // applyOptions configures volume from request options.
@ -112,11 +110,15 @@ func (vol *Volume) applyOptions(volOpt VolOpts) error {
for key, val := range vol.Options { for key, val := range vol.Options {
opt[key] = val opt[key] = val
} }
mntMap := configmap.Simple{}
vfsMap := configmap.Simple{}
for key := range opt { for key := range opt {
var ok bool var ok bool
var err error var err error
normalKey := normalOptName(key)
underscoreKey := strings.ReplaceAll(normalKey, "-", "_")
switch normalOptName(key) { switch normalKey {
case "persist": case "persist":
vol.persist, err = opt.GetBool(key) vol.persist, err = opt.GetBool(key)
ok = true ok = true
@ -129,25 +131,24 @@ func (vol *Volume) applyOptions(volOpt VolOpts) error {
} }
if !ok { if !ok {
// try to use as a mount option in mntOpt // try to use as a mount option in mntMap
ok, err = getMountOption(mntOpt, opt, key) if mountlib.OptionsInfo.Get(underscoreKey) != nil {
if ok && err != nil { mntMap[underscoreKey] = vol.Options[key]
return fmt.Errorf("cannot parse mount option %q: %w", key, err) ok = true
} }
} }
if !ok { if !ok {
// try as a vfs option in vfsOpt // try as a vfs option in vfsMap
ok, err = getVFSOption(vfsOpt, opt, key) if vfscommon.OptionsInfo.Get(underscoreKey) != nil {
if ok && err != nil { vfsMap[underscoreKey] = vol.Options[key]
return fmt.Errorf("cannot parse vfs option %q: %w", key, err) ok = true
} }
} }
if !ok { if !ok {
// try as a backend option in fsOpt (backends use "_" instead of "-") // try as a backend option in fsOpt (backends use "_" instead of "-")
optWithPrefix := strings.ReplaceAll(normalOptName(key), "-", "_") fsOptName := strings.TrimPrefix(underscoreKey, fsType+"_")
fsOptName := strings.TrimPrefix(optWithPrefix, fsType+"_") hasFsPrefix := underscoreKey != fsOptName
hasFsPrefix := optWithPrefix != fsOptName
if !hasFsPrefix || fsInfo.Options.Get(fsOptName) == nil { if !hasFsPrefix || fsInfo.Options.Get(fsOptName) == nil {
fs.Logf(nil, "Option %q is not supported by backend %q", key, fsType) fs.Logf(nil, "Option %q is not supported by backend %q", key, fsType)
return fmt.Errorf("unsupported backend option %q", key) return fmt.Errorf("unsupported backend option %q", key)
@ -159,6 +160,18 @@ func (vol *Volume) applyOptions(volOpt VolOpts) error {
} }
} }
// Parse VFS options
err = configstruct.Set(vfsMap, vfsOpt)
if err != nil {
return fmt.Errorf("cannot parse vfs options: %w", err)
}
// Parse Mount options
err = configstruct.Set(mntMap, mntOpt)
if err != nil {
return fmt.Errorf("cannot parse mount options: %w", err)
}
// build remote string from fsName, fsType, fsOpt, fsPath // build remote string from fsName, fsType, fsOpt, fsPath
colon := ":" colon := ":"
comma := "," comma := ","
@ -178,150 +191,6 @@ func (vol *Volume) applyOptions(volOpt VolOpts) error {
return vol.validate() return vol.validate()
} }
func getMountOption(mntOpt *mountlib.Options, opt rc.Params, key string) (ok bool, err error) {
ok = true
switch normalOptName(key) {
case "debug-fuse":
mntOpt.DebugFUSE, err = opt.GetBool(key)
case "attr-timeout":
mntOpt.AttrTimeout, err = opt.GetFsDuration(key)
case "option":
mntOpt.ExtraOptions, err = getStringArray(opt, key)
case "fuse-flag":
mntOpt.ExtraFlags, err = getStringArray(opt, key)
case "daemon":
mntOpt.Daemon, err = opt.GetBool(key)
case "daemon-timeout":
mntOpt.DaemonTimeout, err = opt.GetFsDuration(key)
case "default-permissions":
mntOpt.DefaultPermissions, err = opt.GetBool(key)
case "allow-non-empty":
mntOpt.AllowNonEmpty, err = opt.GetBool(key)
case "allow-root":
mntOpt.AllowRoot, err = opt.GetBool(key)
case "allow-other":
mntOpt.AllowOther, err = opt.GetBool(key)
case "async-read":
mntOpt.AsyncRead, err = opt.GetBool(key)
case "max-read-ahead":
err = getFVarP(&mntOpt.MaxReadAhead, opt, key)
case "write-back-cache":
mntOpt.WritebackCache, err = opt.GetBool(key)
case "volname":
mntOpt.VolumeName, err = opt.GetString(key)
case "noappledouble":
mntOpt.NoAppleDouble, err = opt.GetBool(key)
case "noapplexattr":
mntOpt.NoAppleXattr, err = opt.GetBool(key)
case "network-mode":
mntOpt.NetworkMode, err = opt.GetBool(key)
default:
ok = false
}
return
}
func getVFSOption(vfsOpt *vfscommon.Options, opt rc.Params, key string) (ok bool, err error) {
var intVal int64
ok = true
switch normalOptName(key) {
// options prefixed with "vfs-"
case "vfs-cache-mode":
err = getFVarP(&vfsOpt.CacheMode, opt, key)
case "vfs-cache-poll-interval":
vfsOpt.CachePollInterval, err = opt.GetFsDuration(key)
case "vfs-cache-max-age":
vfsOpt.CacheMaxAge, err = opt.GetFsDuration(key)
case "vfs-cache-max-size":
err = getFVarP(&vfsOpt.CacheMaxSize, opt, key)
case "vfs-read-chunk-size":
err = getFVarP(&vfsOpt.ChunkSize, opt, key)
case "vfs-read-chunk-size-limit":
err = getFVarP(&vfsOpt.ChunkSizeLimit, opt, key)
case "vfs-case-insensitive":
vfsOpt.CaseInsensitive, err = opt.GetBool(key)
case "vfs-write-wait":
vfsOpt.WriteWait, err = opt.GetFsDuration(key)
case "vfs-read-wait":
vfsOpt.ReadWait, err = opt.GetFsDuration(key)
case "vfs-write-back":
vfsOpt.WriteBack, err = opt.GetFsDuration(key)
case "vfs-read-ahead":
err = getFVarP(&vfsOpt.ReadAhead, opt, key)
case "vfs-used-is-size":
vfsOpt.UsedIsSize, err = opt.GetBool(key)
case "vfs-read-chunk-streams":
intVal, err = opt.GetInt64(key)
if err == nil {
if intVal >= 0 && intVal <= math.MaxInt {
vfsOpt.ChunkStreams = int(intVal)
} else {
err = fmt.Errorf("key %q (%v) overflows int", key, intVal)
}
}
// unprefixed vfs options
case "no-modtime":
vfsOpt.NoModTime, err = opt.GetBool(key)
case "no-checksum":
vfsOpt.NoChecksum, err = opt.GetBool(key)
case "dir-cache-time":
vfsOpt.DirCacheTime, err = opt.GetFsDuration(key)
case "poll-interval":
vfsOpt.PollInterval, err = opt.GetFsDuration(key)
case "read-only":
vfsOpt.ReadOnly, err = opt.GetBool(key)
case "dir-perms":
err = getFVarP(&vfsOpt.DirPerms, opt, key)
case "file-perms":
err = getFVarP(&vfsOpt.FilePerms, opt, key)
// unprefixed unix-only vfs options
case "umask":
err = getFVarP(&vfsOpt.Umask, opt, key)
case "uid":
intVal, err = opt.GetInt64(key)
if err == nil {
if intVal >= 0 && intVal <= math.MaxUint32 {
vfsOpt.UID = uint32(intVal)
} else {
err = fmt.Errorf("key %q (%v) overflows uint32", key, intVal)
}
}
case "gid":
intVal, err = opt.GetInt64(key)
if err == nil {
if intVal >= 0 && intVal <= math.MaxUint32 {
vfsOpt.UID = uint32(intVal)
} else {
err = fmt.Errorf("key %q (%v) overflows uint32", key, intVal)
}
}
// non-vfs options
default:
ok = false
}
return
}
func getFVarP(pvalue pflag.Value, opt rc.Params, key string) error {
str, err := opt.GetString(key)
if err != nil {
return err
}
return pvalue.Set(str)
}
func getStringArray(opt rc.Params, key string) ([]string, error) {
str, err := opt.GetString(key)
if err != nil {
return nil, err
}
return strings.Split(str, ","), nil
}
func normalOptName(key string) string { func normalOptName(key string) string {
return strings.ReplaceAll(strings.TrimPrefix(strings.ToLower(key), "--"), "_", "-") return strings.ReplaceAll(strings.TrimPrefix(strings.ToLower(key), "--"), "_", "-")
} }

View File

@ -0,0 +1,75 @@
package docker
import (
"testing"
"time"
"github.com/rclone/rclone/cmd/mountlib"
"github.com/rclone/rclone/fs"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
_ "github.com/rclone/rclone/backend/local"
)
func TestApplyOptions(t *testing.T) {
vol := &Volume{
Name: "testName",
MountPoint: "testPath",
drv: &Driver{
root: "testRoot",
},
mnt: &mountlib.MountPoint{
MountPoint: "testPath",
},
mountReqs: make(map[string]interface{}),
}
// Happy path
volOpt := VolOpts{
"remote": "/tmp/docker",
"persist": "FALSE",
"mount_type": "potato",
// backend options
"--local-case-sensitive": "true",
"local_no_check_updated": "1",
// mount options
"debug-fuse": "true",
"attr_timeout": "100s",
"--async-read": "TRUE",
// vfs options
"no-modtime": "1",
"no_checksum": "true",
"--no-seek": "true",
}
err := vol.applyOptions(volOpt)
require.NoError(t, err)
// normal options
assert.Equal(t, ":local,case_sensitive='true',no_check_updated='1':/tmp/docker", vol.fsString)
assert.Equal(t, false, vol.persist)
assert.Equal(t, "potato", vol.mountType)
// mount options
assert.Equal(t, true, vol.mnt.MountOpt.DebugFUSE)
assert.Equal(t, fs.Duration(100*time.Second), vol.mnt.MountOpt.AttrTimeout)
assert.Equal(t, true, vol.mnt.MountOpt.AsyncRead)
// vfs options
assert.Equal(t, true, vol.mnt.VFSOpt.NoModTime)
assert.Equal(t, true, vol.mnt.VFSOpt.NoChecksum)
assert.Equal(t, true, vol.mnt.VFSOpt.NoSeek)
// Check errors
err = vol.applyOptions(VolOpts{
"debug-fuse": "POTATO",
})
require.ErrorContains(t, err, "cannot parse mount options")
err = vol.applyOptions(VolOpts{
"no-modtime": "POTATO",
})
require.ErrorContains(t, err, "cannot parse vfs options")
err = vol.applyOptions(VolOpts{
"remote": "/tmp/docker",
"local_not_found": "POTATO",
})
require.ErrorContains(t, err, "unsupported backend option")
}

View File

@ -209,7 +209,7 @@ but is arguably easier to parameterize in scripts.
The `path` part is optional. The `path` part is optional.
[Mount and VFS options](/commands/rclone_serve_docker/#options) [Mount and VFS options](/commands/rclone_serve_docker/#options)
as well as [backend parameters](/flags/#backend-flags) are named as well as [backend parameters](/flags/#backend) are named
like their twin command-line flags without the `--` CLI prefix. like their twin command-line flags without the `--` CLI prefix.
Optionally you can use underscores instead of dashes in option names. Optionally you can use underscores instead of dashes in option names.
For example, `--vfs-cache-mode full` becomes For example, `--vfs-cache-mode full` becomes