serve: update serve remote control to use ParseOptions

This refactors all 8 serve protocols to use `rc.ParseOptions` for VFS
and protocol options decoding. It also implements parameter validation in the
main runner using `rc.CheckParamsUsed`.

- Update dlna, ftp, http, nfs, restic, s3, sftp, and webdav to call
  rc.ParseOptions, enabling nested option block support.
- Remove unused configstruct imports.
- Update startRc in cmd/serve/rc.go to copy input parameters and
  call rc.CheckParamsUsed to reject unknown parameters.
- Add TestRcStartFlatNestedAndUnknownRejection to cmd/serve/rc_test.go.
- Update inline documentation in serve/start command help to include
  nested blocks information (vfsOpt, proxyOpt, opt) and a new WebDAV example.
This commit is contained in:
Hakan İSMAİL
2026-07-29 19:42:45 +01:00
committed by Nick Craig-Wood
parent a50d1137a3
commit 01495c8ded
10 changed files with 98 additions and 31 deletions
+2 -3
View File
@@ -24,7 +24,6 @@ import (
"github.com/rclone/rclone/cmd/serve"
"github.com/rclone/rclone/cmd/serve/dlna/data"
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/config/configstruct"
"github.com/rclone/rclone/fs/config/flags"
"github.com/rclone/rclone/fs/rc"
"github.com/rclone/rclone/lib/systemd"
@@ -78,13 +77,13 @@ func init() {
serve.AddRc("dlna", func(ctx context.Context, f fs.Fs, in rc.Params) (serve.Handle, error) {
// Read VFS Opts
var vfsOpt = vfscommon.Opt // set default opts
err := configstruct.SetAny(in, &vfsOpt)
err := rc.ParseOptions(in, "vfsOpt", &vfsOpt)
if err != nil {
return nil, err
}
// Read opts
var opt = Opt // set default opts
err = configstruct.SetAny(in, &opt)
err = rc.ParseOptions(in, "opt", &opt)
if err != nil {
return nil, err
}
+3 -4
View File
@@ -24,7 +24,6 @@ import (
"github.com/rclone/rclone/cmd/serve/proxy/proxyflags"
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/accounting"
"github.com/rclone/rclone/fs/config/configstruct"
"github.com/rclone/rclone/fs/config/flags"
"github.com/rclone/rclone/fs/config/obscure"
"github.com/rclone/rclone/fs/log"
@@ -96,19 +95,19 @@ func init() {
serve.AddRc("ftp", func(ctx context.Context, f fs.Fs, in rc.Params) (serve.Handle, error) {
// Read VFS Opts
var vfsOpt = vfscommon.Opt // set default opts
err := configstruct.SetAny(in, &vfsOpt)
err := rc.ParseOptions(in, "vfsOpt", &vfsOpt)
if err != nil {
return nil, err
}
// Read Proxy Opts
var proxyOpt = proxy.Opt // set default opts
err = configstruct.SetAny(in, &proxyOpt)
err = rc.ParseOptions(in, "proxyOpt", &proxyOpt)
if err != nil {
return nil, err
}
// Read opts
var opt = Opt // set default opts
err = configstruct.SetAny(in, &opt)
err = rc.ParseOptions(in, "opt", &opt)
if err != nil {
return nil, err
}
+3 -4
View File
@@ -22,7 +22,6 @@ import (
"github.com/rclone/rclone/cmd/serve/proxy/proxyflags"
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/accounting"
"github.com/rclone/rclone/fs/config/configstruct"
"github.com/rclone/rclone/fs/config/flags"
"github.com/rclone/rclone/fs/rc"
libhttp "github.com/rclone/rclone/lib/http"
@@ -82,19 +81,19 @@ func init() {
cmdserve.AddRc("http", func(ctx context.Context, f fs.Fs, in rc.Params) (cmdserve.Handle, error) {
// Read VFS Opts
var vfsOpt = vfscommon.Opt // set default opts
err := configstruct.SetAny(in, &vfsOpt)
err := rc.ParseOptions(in, "vfsOpt", &vfsOpt)
if err != nil {
return nil, err
}
// Read Proxy Opts
var proxyOpt = proxy.Opt // set default opts
err = configstruct.SetAny(in, &proxyOpt)
err = rc.ParseOptions(in, "proxyOpt", &proxyOpt)
if err != nil {
return nil, err
}
// Read opts
var opt = Opt // set default opts
err = configstruct.SetAny(in, &opt)
err = rc.ParseOptions(in, "opt", &opt)
if err != nil {
return nil, err
}
+2 -3
View File
@@ -16,7 +16,6 @@ import (
"github.com/rclone/rclone/cmd"
"github.com/rclone/rclone/cmd/serve"
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/config/configstruct"
"github.com/rclone/rclone/fs/config/flags"
"github.com/rclone/rclone/fs/rc"
"github.com/rclone/rclone/vfs"
@@ -90,14 +89,14 @@ func init() {
serve.AddRc("nfs", func(ctx context.Context, f fs.Fs, in rc.Params) (serve.Handle, error) {
// Create VFS
var vfsOpt = vfscommon.Opt // set default opts
err := configstruct.SetAny(in, &vfsOpt)
err := rc.ParseOptions(in, "vfsOpt", &vfsOpt)
if err != nil {
return nil, err
}
VFS := vfs.New(ctx, f, &vfsOpt)
// Read opts
var opt = Opt // set default opts
err = configstruct.SetAny(in, &opt)
err = rc.ParseOptions(in, "opt", &opt)
if err != nil {
return nil, err
}
+16 -3
View File
@@ -82,13 +82,15 @@ Other parameters are as described in the documentation for the
relevant [rclone serve](/commands/rclone_serve/) command line options.
To translate a command line option to an rc parameter, remove the
leading |--| and replace |-| with |_|, so |--vfs-cache-mode| becomes
|vfs_cache_mode|. Note that global parameters must be set with
|_config| and |_filter| as described above.
|vfs_cache_mode|.
Option parameters (such as VFS, proxy, and protocol-specific options) can be passed flat at the top level of the parameter map or inside nested JSON objects under the |vfsOpt|, |proxyOpt|, and |opt| keys (e.g. |vfsOpt='{"CacheMode": 2}'|, |proxyOpt='{"AuthProxy": "http://127.0.0.1:8080"}'|). If both flat parameters and nested blocks are supplied, the parameters in the nested blocks will take precedence. Note that global parameters must be set with |_config| and |_filter| as described above.
Examples:
rclone rc serve/start type=nfs fs=remote: addr=:4321 vfs_cache_mode=full
rclone rc serve/start --json '{"type":"nfs","fs":"remote:","addr":":1234","vfs_cache_mode":"full"}'
rclone rc serve/start type=webdav fs=remote: vfsOpt='{"CacheMode": 2}' proxyOpt='{"AuthProxy": "http://127.0.0.1:8080"}'
This will give the reply
@@ -108,6 +110,9 @@ Stop the server with |serve/stop| and list the running servers with |serve/list|
// startRc allows the serve command to be run from rc
func startRc(ctx context.Context, in rc.Params) (out rc.Params, err error) {
// Make a copy of input parameters to store in runningServer before parsing/deletion
paramsCopy := in.Copy()
serveType, err := in.GetString("type")
serveMu.Lock()
@@ -135,6 +140,14 @@ func startRc(ctx context.Context, in rc.Params) (out rc.Params, err error) {
return nil, fmt.Errorf("could not start serve %q: %w", serveType, err)
}
delete(in, "type")
delete(in, "fs")
err = rc.CheckParamsUsed(in)
if err != nil {
_ = h.Shutdown()
return nil, err
}
// Start the server running in the background
errChan := make(chan error, 1)
go func() {
@@ -158,7 +171,7 @@ func startRc(ctx context.Context, in rc.Params) (out rc.Params, err error) {
// Store it for later
runningServer := server{
ID: fmt.Sprintf("%s-%08x", serveType, rand.Uint32()),
Params: in,
Params: paramsCopy,
Addr: h.Addr().String(),
h: h,
errChan: errChan,
+62
View File
@@ -178,3 +178,65 @@ func TestRcStopAll(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, 0, len(servers))
}
type mockServeOptions struct {
StringOpt string `config:"string_opt"`
IntOpt int `config:"int_opt"`
}
func newMockServer(ctx context.Context, f fs.Fs, in rc.Params) (Handle, error) {
var opt mockServeOptions
err := rc.ParseOptions(in, "serveOpt", &opt)
if err != nil {
return nil, err
}
return &dummyServer{
addr: &net.TCPAddr{
IP: net.IPv4(127, 0, 0, 1),
Port: 8080,
},
shutdownCh: make(chan struct{}),
}, nil
}
func TestRcStartFlatNestedAndUnknownRejection(t *testing.T) {
newTest(t)
serveStart := rc.Calls.Get("serve/start")
serveStop := rc.Calls.Get("serve/stop")
AddRc("mockserve", newMockServer)
t.Run("FlatAndNested", func(t *testing.T) {
in := rc.Params{
"fs": ":mockfs:",
"type": "mockserve",
"string_opt": "flat",
"serveOpt": rc.Params{
"IntOpt": 42,
},
}
out, err := serveStart.Fn(context.Background(), in)
require.NoError(t, err)
id := out["id"].(string)
// Verify the running server holds a copy of the original parameters
s := servers[id]
require.NotNil(t, s)
assert.Equal(t, "flat", s.Params["string_opt"])
assert.Equal(t, rc.Params{"IntOpt": 42}, s.Params["serveOpt"])
_, err = serveStop.Fn(context.Background(), rc.Params{"id": id})
require.NoError(t, err)
})
t.Run("UnknownRejection", func(t *testing.T) {
in := rc.Params{
"fs": ":mockfs:",
"type": "mockserve",
"unknown_param": "leftover",
}
_, err := serveStart.Fn(context.Background(), in)
assert.Error(t, err)
assert.ErrorContains(t, err, "unknown parameters: unknown_param")
})
}
+1 -2
View File
@@ -20,7 +20,6 @@ import (
cmdserve "github.com/rclone/rclone/cmd/serve"
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/accounting"
"github.com/rclone/rclone/fs/config/configstruct"
"github.com/rclone/rclone/fs/config/flags"
"github.com/rclone/rclone/fs/operations"
"github.com/rclone/rclone/fs/rc"
@@ -79,7 +78,7 @@ func init() {
cmdserve.AddRc("restic", func(ctx context.Context, f fs.Fs, in rc.Params) (cmdserve.Handle, error) {
// Read opts
var opt = Opt // set default opts
err := configstruct.SetAny(in, &opt)
err := rc.ParseOptions(in, "opt", &opt)
if err != nil {
return nil, err
}
+3 -4
View File
@@ -10,7 +10,6 @@ import (
"github.com/rclone/rclone/cmd/serve/proxy"
"github.com/rclone/rclone/cmd/serve/proxy/proxyflags"
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/config/configstruct"
"github.com/rclone/rclone/fs/config/flags"
"github.com/rclone/rclone/fs/rc"
httplib "github.com/rclone/rclone/lib/http"
@@ -77,19 +76,19 @@ func init() {
serve.AddRc("s3", func(ctx context.Context, f fs.Fs, in rc.Params) (serve.Handle, error) {
// Read VFS Opts
var vfsOpt = vfscommon.Opt // set default opts
err := configstruct.SetAny(in, &vfsOpt)
err := rc.ParseOptions(in, "vfsOpt", &vfsOpt)
if err != nil {
return nil, err
}
// Read Proxy Opts
var proxyOpt = proxy.Opt // set default opts
err = configstruct.SetAny(in, &proxyOpt)
err = rc.ParseOptions(in, "proxyOpt", &proxyOpt)
if err != nil {
return nil, err
}
// Read opts
var opt = Opt // set default opts
err = configstruct.SetAny(in, &opt)
err = rc.ParseOptions(in, "opt", &opt)
if err != nil {
return nil, err
}
+3 -4
View File
@@ -13,7 +13,6 @@ import (
"github.com/rclone/rclone/cmd/serve/proxy"
"github.com/rclone/rclone/cmd/serve/proxy/proxyflags"
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/config/configstruct"
"github.com/rclone/rclone/fs/config/flags"
"github.com/rclone/rclone/fs/rc"
"github.com/rclone/rclone/lib/systemd"
@@ -86,19 +85,19 @@ func init() {
serve.AddRc("sftp", func(ctx context.Context, f fs.Fs, in rc.Params) (serve.Handle, error) {
// Read VFS Opts
var vfsOpt = vfscommon.Opt // set default opts
err := configstruct.SetAny(in, &vfsOpt)
err := rc.ParseOptions(in, "vfsOpt", &vfsOpt)
if err != nil {
return nil, err
}
// Read Proxy Opts
var proxyOpt = proxy.Opt // set default opts
err = configstruct.SetAny(in, &proxyOpt)
err = rc.ParseOptions(in, "proxyOpt", &proxyOpt)
if err != nil {
return nil, err
}
// Read opts
var opt = Opt // set default opts
err = configstruct.SetAny(in, &opt)
err = rc.ParseOptions(in, "opt", &opt)
if err != nil {
return nil, err
}
+3 -4
View File
@@ -22,7 +22,6 @@ import (
"github.com/rclone/rclone/cmd/serve/proxy"
"github.com/rclone/rclone/cmd/serve/proxy/proxyflags"
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/config/configstruct"
"github.com/rclone/rclone/fs/config/flags"
"github.com/rclone/rclone/fs/hash"
"github.com/rclone/rclone/fs/rc"
@@ -81,19 +80,19 @@ func init() {
cmdserve.AddRc("webdav", func(ctx context.Context, f fs.Fs, in rc.Params) (cmdserve.Handle, error) {
// Read VFS Opts
var vfsOpt = vfscommon.Opt // set default opts
err := configstruct.SetAny(in, &vfsOpt)
err := rc.ParseOptions(in, "vfsOpt", &vfsOpt)
if err != nil {
return nil, err
}
// Read Proxy Opts
var proxyOpt = proxy.Opt // set default opts
err = configstruct.SetAny(in, &proxyOpt)
err = rc.ParseOptions(in, "proxyOpt", &proxyOpt)
if err != nil {
return nil, err
}
// Read opts
var opt = Opt // set default opts
err = configstruct.SetAny(in, &opt)
err = rc.ParseOptions(in, "opt", &opt)
if err != nil {
return nil, err
}