config: add config unset command to remove options from a remote - fixes #9541
Previously the only way to remove an option from a remote was to set it to an empty string, which is not the same as deleting it - a present but empty value overrides the option's default whereas a deleted key restores it. Editing the file by hand isn't an option for an encrypted config either. This adds a "config unset" command and a "config/unset" rc endpoint to remove one or more keys from an existing remote.
This commit is contained in:
@@ -32,6 +32,7 @@ func init() {
|
|||||||
configCommand.AddCommand(configCreateCommand)
|
configCommand.AddCommand(configCreateCommand)
|
||||||
configCommand.AddCommand(configUpdateCommand)
|
configCommand.AddCommand(configUpdateCommand)
|
||||||
configCommand.AddCommand(configDeleteCommand)
|
configCommand.AddCommand(configDeleteCommand)
|
||||||
|
configCommand.AddCommand(configUnsetCommand)
|
||||||
configCommand.AddCommand(configPasswordCommand)
|
configCommand.AddCommand(configPasswordCommand)
|
||||||
configCommand.AddCommand(configReconnectCommand)
|
configCommand.AddCommand(configReconnectCommand)
|
||||||
configCommand.AddCommand(configDisconnectCommand)
|
configCommand.AddCommand(configDisconnectCommand)
|
||||||
@@ -388,6 +389,40 @@ var configDeleteCommand = &cobra.Command{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var configUnsetCommand = &cobra.Command{
|
||||||
|
Use: "unset name [key]+",
|
||||||
|
Short: `Unset options in an existing remote.`,
|
||||||
|
Long: strings.ReplaceAll(`Remove one or more options from an existing remote. The options to
|
||||||
|
remove should be passed in as a list of key names.
|
||||||
|
|
||||||
|
For example, to remove the |client_id| and |client_secret| options from
|
||||||
|
a remote of name myremote you would do:
|
||||||
|
|
||||||
|
|||sh
|
||||||
|
rclone config unset myremote client_id client_secret
|
||||||
|
|||
|
||||||
|
|
||||||
|
This removes the keys from the config file entirely, which is different
|
||||||
|
from setting them to an empty string with |config update|. Removing a
|
||||||
|
key restores rclone's default behaviour for that option, whereas setting
|
||||||
|
it to an empty string overrides the default with an empty value.
|
||||||
|
|
||||||
|
You can't unset the |type| of a remote - use |config delete| to remove
|
||||||
|
the whole remote instead.`, "|", "`"),
|
||||||
|
Annotations: map[string]string{
|
||||||
|
"versionIntroduced": "v1.75",
|
||||||
|
},
|
||||||
|
RunE: func(command *cobra.Command, args []string) error {
|
||||||
|
cmd.CheckArgs(2, 256, command, args)
|
||||||
|
_, err := config.UnsetRemote(args[0], args[1:]...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
config.ShowRemote(args[0])
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
var configPasswordCommand = &cobra.Command{
|
var configPasswordCommand = &cobra.Command{
|
||||||
Use: "password name [key value]+",
|
Use: "password name [key value]+",
|
||||||
Short: `Update password in an existing remote.`,
|
Short: `Update password in an existing remote.`,
|
||||||
|
|||||||
@@ -668,6 +668,34 @@ func PasswordRemote(ctx context.Context, name string, keyValues rc.Params) error
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UnsetRemote removes the named keys from the remote of name.
|
||||||
|
//
|
||||||
|
// It returns the keys that were actually removed - keys that didn't
|
||||||
|
// exist are silently ignored. It returns an error if the remote
|
||||||
|
// doesn't exist or if an attempt is made to unset the "type" key.
|
||||||
|
func UnsetRemote(name string, keys ...string) (removed []string, err error) {
|
||||||
|
err = fspath.CheckConfigName(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if GetValue(name, "type") == "" {
|
||||||
|
return nil, fmt.Errorf("remote %q doesn't exist", name)
|
||||||
|
}
|
||||||
|
for _, key := range keys {
|
||||||
|
if key == "type" {
|
||||||
|
return nil, errors.New(`can't unset the "type" of a remote - use "config delete" to remove the whole remote`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, key := range keys {
|
||||||
|
if FileDeleteKey(name, key) {
|
||||||
|
removed = append(removed, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SaveConfig()
|
||||||
|
cache.ClearConfig(name) // remove any remotes based on this config from the cache
|
||||||
|
return removed, nil
|
||||||
|
}
|
||||||
|
|
||||||
// JSONListProviders prints all the providers and options in JSON format
|
// JSONListProviders prints all the providers and options in JSON format
|
||||||
func JSONListProviders() error {
|
func JSONListProviders() error {
|
||||||
b, err := json.MarshalIndent(fs.Registry, "", " ")
|
b, err := json.MarshalIndent(fs.Registry, "", " ")
|
||||||
|
|||||||
@@ -256,6 +256,44 @@ func rcDelete(ctx context.Context, in rc.Params) (out rc.Params, err error) {
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rc.Add(rc.Call{
|
||||||
|
Path: "config/unset",
|
||||||
|
Fn: rcUnset,
|
||||||
|
Title: "Unset keys in a remote in the config file.",
|
||||||
|
Help: `
|
||||||
|
Parameters:
|
||||||
|
|
||||||
|
- name - name of remote
|
||||||
|
- keys - a list of key names to remove
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
- removed - a list of the keys that were actually removed
|
||||||
|
|
||||||
|
See the [config unset](/commands/rclone_config_unset/) command for more information on the above.
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove keys from a remote in the config file
|
||||||
|
func rcUnset(ctx context.Context, in rc.Params) (out rc.Params, err error) {
|
||||||
|
name, err := in.GetString("name")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var keys []string
|
||||||
|
err = in.GetStruct("keys", &keys)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
removed, err := UnsetRemote(name, keys...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return rc.Params{"removed": removed}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rc.Add(rc.Call{
|
rc.Add(rc.Call{
|
||||||
Path: "config/setpath",
|
Path: "config/setpath",
|
||||||
|
|||||||
@@ -134,6 +134,29 @@ func TestRc(t *testing.T) {
|
|||||||
assert.Equal(t, pw2, obscure.MustReveal(config.GetValue(testName, "test_key2")))
|
assert.Equal(t, pw2, obscure.MustReveal(config.GetValue(testName, "test_key2")))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("Unset", func(t *testing.T) {
|
||||||
|
config.FileSetValue(testName, "unset_key", "to be removed")
|
||||||
|
call := rc.Calls.Get("config/unset")
|
||||||
|
assert.NotNil(t, call)
|
||||||
|
in := rc.Params{
|
||||||
|
"name": testName,
|
||||||
|
"keys": []string{"unset_key", "missing_key"},
|
||||||
|
}
|
||||||
|
out, err := call.Fn(context.Background(), in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, out)
|
||||||
|
|
||||||
|
// Only the key that existed is reported as removed
|
||||||
|
var removed []string
|
||||||
|
err = out.GetStruct("removed", &removed)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, []string{"unset_key"}, removed)
|
||||||
|
|
||||||
|
// The key is gone from the config file entirely
|
||||||
|
_, found := config.FileGetValue(testName, "unset_key")
|
||||||
|
assert.False(t, found)
|
||||||
|
})
|
||||||
|
|
||||||
// Delete the test remote
|
// Delete the test remote
|
||||||
call = rc.Calls.Get("config/delete")
|
call = rc.Calls.Get("config/delete")
|
||||||
assert.NotNil(t, call)
|
assert.NotNil(t, call)
|
||||||
|
|||||||
@@ -250,6 +250,40 @@ func TestCreateUpdatePasswordRemote(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUnsetRemote(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
defer testConfigFile(t, simpleOptions, "unset.conf")()
|
||||||
|
|
||||||
|
_, err := config.CreateRemote(ctx, "test", "config_test_remote", rc.Params{
|
||||||
|
"bool": true,
|
||||||
|
"spare": "spare",
|
||||||
|
}, config.UpdateRemoteOpt{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, found := config.FileGetValue("test", "spare")
|
||||||
|
require.True(t, found)
|
||||||
|
|
||||||
|
// Unsetting a missing remote is an error
|
||||||
|
_, err = config.UnsetRemote("notfound", "spare")
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
// Unsetting the type is an error and removes nothing
|
||||||
|
_, err = config.UnsetRemote("test", "type")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Equal(t, "config_test_remote", config.GetValue("test", "type"))
|
||||||
|
|
||||||
|
// Unset an existing key and a missing one - only the existing one
|
||||||
|
// is reported as removed
|
||||||
|
removed, err := config.UnsetRemote("test", "spare", "missing")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, []string{"spare"}, removed)
|
||||||
|
|
||||||
|
// The key is gone entirely, not just set to empty
|
||||||
|
_, found = config.FileGetValue("test", "spare")
|
||||||
|
assert.False(t, found)
|
||||||
|
// Other keys are untouched
|
||||||
|
assert.Equal(t, "true", config.GetValue("test", "bool"))
|
||||||
|
}
|
||||||
|
|
||||||
func TestDefaultRequired(t *testing.T) {
|
func TestDefaultRequired(t *testing.T) {
|
||||||
// By default options are optional (sic), regardless if a default value is defined.
|
// By default options are optional (sic), regardless if a default value is defined.
|
||||||
// Setting Required=true means empty string is no longer allowed, except when
|
// Setting Required=true means empty string is no longer allowed, except when
|
||||||
|
|||||||
Reference in New Issue
Block a user