mirror of
https://github.com/rclone/rclone.git
synced 2026-06-30 02:45:04 -04:00
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(configUpdateCommand)
|
||||
configCommand.AddCommand(configDeleteCommand)
|
||||
configCommand.AddCommand(configUnsetCommand)
|
||||
configCommand.AddCommand(configPasswordCommand)
|
||||
configCommand.AddCommand(configReconnectCommand)
|
||||
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{
|
||||
Use: "password name [key value]+",
|
||||
Short: `Update password in an existing remote.`,
|
||||
|
||||
@@ -661,6 +661,34 @@ func PasswordRemote(ctx context.Context, name string, keyValues rc.Params) error
|
||||
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
|
||||
func JSONListProviders() error {
|
||||
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
|
||||
}
|
||||
|
||||
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() {
|
||||
rc.Add(rc.Call{
|
||||
Path: "config/setpath",
|
||||
|
||||
@@ -134,6 +134,29 @@ func TestRc(t *testing.T) {
|
||||
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
|
||||
call = rc.Calls.Get("config/delete")
|
||||
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) {
|
||||
// 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
|
||||
|
||||
Reference in New Issue
Block a user