1
mirror of https://github.com/rclone/rclone synced 2024-11-21 22:50:16 +01:00

rc: add options/info call to enumerate options

This also makes some fields in the Options block optional - these are
documented in rc.md
This commit is contained in:
Nick Craig-Wood 2024-07-10 18:24:13 +01:00
parent 4d2bc190cc
commit 8fbb259091
6 changed files with 163 additions and 31 deletions

View File

@ -400,6 +400,76 @@ call and taken by the [options/set](#options-set) calls as well as the
- `BandwidthSpec` - this will be set and returned as a string, eg - `BandwidthSpec` - this will be set and returned as a string, eg
"1M". "1M".
### Option blocks {#option-blocks}
The calls [options/info](#options-info) (for the main config) and
[config/providers](#config-providers) (for the backend config) may be
used to get information on the rclone configuration options. This can
be used to build user interfaces for displaying and setting any rclone
option.
These consist of arrays of `Option` blocks. These have the following
format. Each block describes a single option.
| Field | Type | Optional | Description |
|-------|------|----------|-------------|
| Name | string | N | name of the option in snake_case |
| FieldName | string | N | name of the field used in the rc - if blank use Name |
| Help | string | N | help, started with a single sentence on a single line |
| Groups | string | Y | groups this option belongs to - comma separated string for options classification |
| Provider | string | Y | set to filter on provider |
| Default | any | N | default value, if set (and not to nil or "") then Required does nothing |
| Value | any | N | value to be set by flags |
| Examples | Examples | Y | predefined values that can be selected from list (multiple-choice option) |
| ShortOpt | string | Y | the short command line option for this |
| Hide | Visibility | N | if non zero, this option is hidden from the configurator or the command line |
| Required | bool | N | this option is required, meaning value cannot be empty unless there is a default |
| IsPassword | bool | N | set if the option is a password |
| NoPrefix | bool | N | set if the option for this should not use the backend prefix |
| Advanced | bool | N | set if this is an advanced config option |
| Exclusive | bool | N | set if the answer can only be one of the examples (empty string allowed unless Required or Default is set) |
| Sensitive | bool | N | set if this option should be redacted when using `rclone config redacted` |
An example of this might be the `--log-level` flag. Note that the
`Name` of the option becomes the command line flag with `_` replaced
with `-`.
```
{
"Advanced": false,
"Default": 5,
"DefaultStr": "NOTICE",
"Examples": [
{
"Help": "",
"Value": "EMERGENCY"
},
{
"Help": "",
"Value": "ALERT"
},
...
],
"Exclusive": true,
"FieldName": "LogLevel",
"Groups": "Logging",
"Help": "Log level DEBUG|INFO|NOTICE|ERROR",
"Hide": 0,
"IsPassword": false,
"Name": "log_level",
"NoPrefix": true,
"Required": true,
"Sensitive": false,
"Type": "LogLevel",
"Value": null,
"ValueStr": "NOTICE"
},
```
Note that the `Help` may be multiple lines separated by `\n`. The
first line will always be a short sentence and this is the sentence
shown when running `rclone help flags`.
## Specifying remotes to work on ## Specifying remotes to work on
Remotes are specified with the `fs=`, `srcFs=`, `dstFs=` Remotes are specified with the `fs=`, `srcFs=`, `dstFs=`
@ -638,7 +708,12 @@ See the [config paths](/commands/rclone_config_paths/) command for more informat
Returns a JSON object: Returns a JSON object:
- providers - array of objects - providers - array of objects
See the [config providers](/commands/rclone_config_providers/) command for more information on the above. See the [config providers](/commands/rclone_config_providers/) command
for more information on the above.
Note that the Options blocks are in the same format as returned by
"options/info". They are described in the
[option blocks](#option-blocks) section.
**Authentication is required for this call.** **Authentication is required for this call.**
@ -1647,6 +1722,14 @@ set in _config then use options/config and for _filter use options/filter.
This shows the internal names of the option within rclone which should This shows the internal names of the option within rclone which should
map to the external options very easily with a few exceptions. map to the external options very easily with a few exceptions.
### options/info: Get info about all the global options {#options-info}
Returns an object where keys are option block names and values are an
array of objects with info about each options.
These objects are in the same format as returned by "config/providers". They are
described in the [option blocks](#option-blocks) section.
### options/local: Get the currently active config for this call {#options-local} ### options/local: Get the currently active config for this call {#options-local}
Returns an object with the keys "config" and "filter". Returns an object with the keys "config" and "filter".

View File

@ -91,7 +91,12 @@ func init() {
Returns a JSON object: Returns a JSON object:
- providers - array of objects - providers - array of objects
See the [config providers](/commands/rclone_config_providers/) command for more information on the above. See the [config providers](/commands/rclone_config_providers/) command
for more information on the above.
Note that the Options blocks are in the same format as returned by
"options/info". They are described in the
[option blocks](#option-blocks) section.
`, `,
}) })
} }

View File

@ -234,10 +234,8 @@ func TestOptionMarshalJSON(t *testing.T) {
"Name": "case_insensitive", "Name": "case_insensitive",
"FieldName": "", "FieldName": "",
"Help": "", "Help": "",
"Provider": "",
"Default": false, "Default": false,
"Value": true, "Value": true,
"ShortOpt": "",
"Hide": 0, "Hide": 0,
"Required": false, "Required": false,
"IsPassword": false, "IsPassword": false,

View File

@ -12,19 +12,6 @@ import (
"github.com/rclone/rclone/fs/filter" "github.com/rclone/rclone/fs/filter"
) )
// AddOption adds an option set
func AddOption(name string, option interface{}) {
// FIXME remove this function when conversion to options is complete
fs.RegisterGlobalOptions(fs.OptionsInfo{Name: name, Opt: option})
}
// AddOptionReload adds an option set with a reload function to be
// called when options are changed
func AddOptionReload(name string, option interface{}, reload func(context.Context) error) {
// FIXME remove this function when conversion to options is complete
fs.RegisterGlobalOptions(fs.OptionsInfo{Name: name, Opt: option, Reload: reload})
}
func init() { func init() {
Add(Call{ Add(Call{
Path: "options/blocks", Path: "options/blocks",
@ -73,6 +60,29 @@ func rcOptionsGet(ctx context.Context, in Params) (out Params, err error) {
return out, nil return out, nil
} }
func init() {
Add(Call{
Path: "options/info",
Fn: rcOptionsInfo,
Title: "Get info about all the global options",
Help: `Returns an object where keys are option block names and values are an
array of objects with info about each options.
These objects are in the same format as returned by "config/providers". They are
described in the [option blocks](#option-blocks) section.
`,
})
}
// Show the info of all the option blocks
func rcOptionsInfo(ctx context.Context, in Params) (out Params, err error) {
out = make(Params)
for _, opt := range fs.OptionsRegistry {
out[opt.Name] = opt.Options
}
return out, nil
}
func init() { func init() {
Add(Call{ Add(Call{
Path: "options/local", Path: "options/local",

View File

@ -20,6 +20,16 @@ func clearOptionBlock() func() {
} }
} }
var testInfo = fs.Options{{
Name: "string",
Default: "str",
Help: "It is a string",
}, {
Name: "int",
Default: 17,
Help: "It is an int",
}}
var testOptions = struct { var testOptions = struct {
String string String string
Int int Int int
@ -28,10 +38,18 @@ var testOptions = struct {
Int: 42, Int: 42,
} }
func registerTestOptions() {
fs.RegisterGlobalOptions(fs.OptionsInfo{Name: "potato", Opt: &testOptions, Options: testInfo})
}
func registerTestOptionsReload(reload func(context.Context) error) {
fs.RegisterGlobalOptions(fs.OptionsInfo{Name: "potato", Opt: &testOptions, Options: testInfo, Reload: reload})
}
func TestAddOption(t *testing.T) { func TestAddOption(t *testing.T) {
defer clearOptionBlock()() defer clearOptionBlock()()
assert.Equal(t, len(fs.OptionsRegistry), 0) assert.Equal(t, len(fs.OptionsRegistry), 0)
AddOption("potato", &testOptions) registerTestOptions()
assert.Equal(t, len(fs.OptionsRegistry), 1) assert.Equal(t, len(fs.OptionsRegistry), 1)
assert.Equal(t, &testOptions, fs.OptionsRegistry["potato"].Opt) assert.Equal(t, &testOptions, fs.OptionsRegistry["potato"].Opt)
} }
@ -40,7 +58,7 @@ func TestAddOptionReload(t *testing.T) {
defer clearOptionBlock()() defer clearOptionBlock()()
assert.Equal(t, len(fs.OptionsRegistry), 0) assert.Equal(t, len(fs.OptionsRegistry), 0)
reload := func(ctx context.Context) error { return nil } reload := func(ctx context.Context) error { return nil }
AddOptionReload("potato", &testOptions, reload) registerTestOptionsReload(reload)
assert.Equal(t, len(fs.OptionsRegistry), 1) assert.Equal(t, len(fs.OptionsRegistry), 1)
assert.Equal(t, &testOptions, fs.OptionsRegistry["potato"].Opt) assert.Equal(t, &testOptions, fs.OptionsRegistry["potato"].Opt)
assert.Equal(t, fmt.Sprintf("%p", reload), fmt.Sprintf("%p", fs.OptionsRegistry["potato"].Reload)) assert.Equal(t, fmt.Sprintf("%p", reload), fmt.Sprintf("%p", fs.OptionsRegistry["potato"].Reload))
@ -48,7 +66,7 @@ func TestAddOptionReload(t *testing.T) {
func TestOptionsBlocks(t *testing.T) { func TestOptionsBlocks(t *testing.T) {
defer clearOptionBlock()() defer clearOptionBlock()()
AddOption("potato", &testOptions) registerTestOptions()
call := Calls.Get("options/blocks") call := Calls.Get("options/blocks")
require.NotNil(t, call) require.NotNil(t, call)
in := Params{} in := Params{}
@ -60,7 +78,7 @@ func TestOptionsBlocks(t *testing.T) {
func TestOptionsGet(t *testing.T) { func TestOptionsGet(t *testing.T) {
defer clearOptionBlock()() defer clearOptionBlock()()
AddOption("potato", &testOptions) registerTestOptions()
call := Calls.Get("options/get") call := Calls.Get("options/get")
require.NotNil(t, call) require.NotNil(t, call)
in := Params{} in := Params{}
@ -76,8 +94,8 @@ func TestOptionsGetMarshal(t *testing.T) {
ci := fs.GetConfig(ctx) ci := fs.GetConfig(ctx)
// Add some real options // Add some real options
AddOption("main", ci) fs.RegisterGlobalOptions(fs.OptionsInfo{Name: "main", Opt: ci, Options: nil})
AddOption("rc", &Opt) fs.RegisterGlobalOptions(fs.OptionsInfo{Name: "rc", Opt: &Opt, Options: nil})
// get them // get them
call := Calls.Get("options/get") call := Calls.Get("options/get")
@ -92,11 +110,23 @@ func TestOptionsGetMarshal(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
} }
func TestOptionsInfo(t *testing.T) {
defer clearOptionBlock()()
registerTestOptions()
call := Calls.Get("options/info")
require.NotNil(t, call)
in := Params{}
out, err := call.Fn(context.Background(), in)
require.NoError(t, err)
require.NotNil(t, out)
assert.Equal(t, Params{"potato": testInfo}, out)
}
func TestOptionsSet(t *testing.T) { func TestOptionsSet(t *testing.T) {
defer clearOptionBlock()() defer clearOptionBlock()()
var reloaded int var reloaded int
AddOptionReload("potato", &testOptions, func(ctx context.Context) error { registerTestOptionsReload(func(ctx context.Context) error {
if reloaded > 0 { if reloaded > 1 {
return errors.New("error while reloading") return errors.New("error while reloading")
} }
reloaded++ reloaded++
@ -114,8 +144,8 @@ func TestOptionsSet(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Nil(t, out) require.Nil(t, out)
assert.Equal(t, 50, testOptions.Int) assert.Equal(t, 50, testOptions.Int)
assert.Equal(t, "hello", testOptions.String) assert.Equal(t, "str", testOptions.String)
assert.Equal(t, 1, reloaded) assert.Equal(t, 2, reloaded)
// error from reload // error from reload
_, err = call.Fn(context.Background(), in) _, err = call.Fn(context.Background(), in)

View File

@ -195,11 +195,11 @@ type Option struct {
FieldName string // name of the field used in the rc JSON - will be auto filled normally FieldName string // name of the field used in the rc JSON - will be auto filled normally
Help string // help, start with a single sentence on a single line that will be extracted for command line help Help string // help, start with a single sentence on a single line that will be extracted for command line help
Groups string `json:",omitempty"` // groups this option belongs to - comma separated string for options classification Groups string `json:",omitempty"` // groups this option belongs to - comma separated string for options classification
Provider string // set to filter on provider Provider string `json:",omitempty"` // set to filter on provider
Default interface{} // default value, nil => "", if set (and not to nil or "") then Required does nothing Default interface{} // default value, nil => "", if set (and not to nil or "") then Required does nothing
Value interface{} // value to be set by flags Value interface{} // value to be set by flags
Examples OptionExamples `json:",omitempty"` // predefined values that can be selected from list (multiple-choice option) Examples OptionExamples `json:",omitempty"` // predefined values that can be selected from list (multiple-choice option)
ShortOpt string // the short option for this if required ShortOpt string `json:",omitempty"` // the short option for this if required
Hide OptionVisibility // set this to hide the config from the configurator or the command line Hide OptionVisibility // set this to hide the config from the configurator or the command line
Required bool // this option is required, meaning value cannot be empty unless there is a default Required bool // this option is required, meaning value cannot be empty unless there is a default
IsPassword bool // set if the option is a password IsPassword bool // set if the option is a password
@ -348,7 +348,7 @@ func (os OptionExamples) Sort() { sort.Sort(os) }
type OptionExample struct { type OptionExample struct {
Value string Value string
Help string Help string
Provider string Provider string `json:",omitempty"`
} }
// Register a filesystem // Register a filesystem
@ -417,6 +417,7 @@ var OptionsRegistry = map[string]OptionsInfo{}
// //
// Packages which need global options should use this in an init() function // Packages which need global options should use this in an init() function
func RegisterGlobalOptions(oi OptionsInfo) { func RegisterGlobalOptions(oi OptionsInfo) {
oi.Options.setValues()
OptionsRegistry[oi.Name] = oi OptionsRegistry[oi.Name] = oi
if oi.Opt != nil && oi.Options != nil { if oi.Opt != nil && oi.Options != nil {
err := oi.Check() err := oi.Check()
@ -429,7 +430,10 @@ func RegisterGlobalOptions(oi OptionsInfo) {
var optionName = regexp.MustCompile(`^[a-z0-9_]+$`) var optionName = regexp.MustCompile(`^[a-z0-9_]+$`)
// Check ensures that for every element of oi.Options there is a field // Check ensures that for every element of oi.Options there is a field
// in oi.Opt that matches it // in oi.Opt that matches it.
//
// It also sets Option.FieldName to be the name of the field for use
// in JSON.
func (oi *OptionsInfo) Check() error { func (oi *OptionsInfo) Check() error {
errCount := errcount.New() errCount := errcount.New()
items, err := configstruct.Items(oi.Opt) items, err := configstruct.Items(oi.Opt)
@ -471,6 +475,8 @@ func (oi *OptionsInfo) Check() error {
//errCount.Add(err) //errCount.Add(err)
Errorf(nil, "%s", err) Errorf(nil, "%s", err)
} }
// Set FieldName
option.FieldName = item.Field
} }
return errCount.Err(fmt.Sprintf("internal error: options block %q", oi.Name)) return errCount.Err(fmt.Sprintf("internal error: options block %q", oi.Name))
} }