diff --git a/cmd/config/config.go b/cmd/config/config.go index 09216481b..659b23d4d 100644 --- a/cmd/config/config.go +++ b/cmd/config/config.go @@ -1,8 +1,11 @@ package config import ( + "errors" + "github.com/ncw/rclone/cmd" "github.com/ncw/rclone/fs/config" + "github.com/ncw/rclone/fs/rc" "github.com/spf13/cobra" ) @@ -93,7 +96,16 @@ you would do: `, RunE: func(command *cobra.Command, args []string) error { cmd.CheckArgs(2, 256, command, args) - return config.CreateRemote(args[0], args[1], args[2:]) + in, err := argsToMap(args[2:]) + if err != nil { + return err + } + err = config.CreateRemote(args[0], args[1], in) + if err != nil { + return err + } + config.ShowRemote(args[0]) + return nil }, } @@ -110,7 +122,16 @@ For example to update the env_auth field of a remote of name myremote you would `, RunE: func(command *cobra.Command, args []string) error { cmd.CheckArgs(3, 256, command, args) - return config.UpdateRemote(args[0], args[1:]) + in, err := argsToMap(args[1:]) + if err != nil { + return err + } + err = config.UpdateRemote(args[0], in) + if err != nil { + return err + } + config.ShowRemote(args[0]) + return nil }, } @@ -136,6 +157,29 @@ For example to set password of a remote of name myremote you would do: `, RunE: func(command *cobra.Command, args []string) error { cmd.CheckArgs(3, 256, command, args) - return config.PasswordRemote(args[0], args[1:]) + in, err := argsToMap(args[1:]) + if err != nil { + return err + } + err = config.PasswordRemote(args[0], in) + if err != nil { + return err + } + config.ShowRemote(args[0]) + return nil }, } + +// This takes a list of arguments in key value key value form and +// converts it into a map +func argsToMap(args []string) (out rc.Params, err error) { + if len(args)%2 != 0 { + return nil, errors.New("found key without value") + } + out = rc.Params{} + // Set the config + for i := 0; i < len(args); i += 2 { + out[args[i]] = args[i+1] + } + return out, nil +} diff --git a/fs/config/config.go b/fs/config/config.go index 21cbec463..94c3a5e7d 100644 --- a/fs/config/config.go +++ b/fs/config/config.go @@ -32,6 +32,7 @@ import ( "github.com/ncw/rclone/fs/driveletter" "github.com/ncw/rclone/fs/fshttp" "github.com/ncw/rclone/fs/fspath" + "github.com/ncw/rclone/fs/rc" "github.com/pkg/errors" "golang.org/x/crypto/nacl/secretbox" "golang.org/x/text/unicode/norm" @@ -901,18 +902,24 @@ func ChooseOption(o *fs.Option, name string) string { return in } +// Suppress the confirm prompts and return a function to undo that +func suppressConfirm() func() { + old := fs.Config.AutoConfirm + fs.Config.AutoConfirm = true + return func() { + fs.Config.AutoConfirm = old + } +} + // UpdateRemote adds the keyValues passed in to the remote of name. // keyValues should be key, value pairs. -func UpdateRemote(name string, keyValues []string) error { - if len(keyValues)%2 != 0 { - return errors.New("found key without value") - } +func UpdateRemote(name string, keyValues rc.Params) error { + defer suppressConfirm()() // Set the config - for i := 0; i < len(keyValues); i += 2 { - getConfigData().SetValue(name, keyValues[i], keyValues[i+1]) + for k, v := range keyValues { + getConfigData().SetValue(name, k, fmt.Sprint(v)) } RemoteConfig(name) - ShowRemote(name) SaveConfig() return nil } @@ -920,9 +927,7 @@ func UpdateRemote(name string, keyValues []string) error { // CreateRemote creates a new remote with name, provider and a list of // parameters which are key, value pairs. If update is set then it // adds the new keys rather than replacing all of them. -func CreateRemote(name string, provider string, keyValues []string) error { - // Suppress Confirm - fs.Config.AutoConfirm = true +func CreateRemote(name string, provider string, keyValues rc.Params) error { // Delete the old config if it exists getConfigData().DeleteSection(name) // Set the type @@ -935,20 +940,12 @@ func CreateRemote(name string, provider string, keyValues []string) error { // PasswordRemote adds the keyValues passed in to the remote of name. // keyValues should be key, value pairs. -func PasswordRemote(name string, keyValues []string) error { - if len(keyValues) != 2 { - return errors.New("found key without value") +func PasswordRemote(name string, keyValues rc.Params) error { + defer suppressConfirm()() + for k, v := range keyValues { + keyValues[k] = obscure.MustObscure(fmt.Sprint(v)) } - // Suppress Confirm - fs.Config.AutoConfirm = true - passwd := obscure.MustObscure(keyValues[1]) - if passwd != "" { - getConfigData().SetValue(name, keyValues[0], passwd) - RemoteConfig(name) - ShowRemote(name) - SaveConfig() - } - return nil + return UpdateRemote(name, keyValues) } // JSONListProviders prints all the providers and options in JSON format @@ -1297,16 +1294,28 @@ func FileSections() []string { return sections } +// DumpRcRemote dumps the config for a single remote +func DumpRcRemote(name string) (dump rc.Params) { + params := rc.Params{} + for _, key := range getConfigData().GetKeyList(name) { + params[key] = FileGet(name, key) + } + return params +} + +// DumpRcBlob dumps all the config as an unstructured blob suitable +// for the rc +func DumpRcBlob() (dump rc.Params) { + dump = rc.Params{} + for _, name := range getConfigData().GetSectionList() { + dump[name] = DumpRcRemote(name) + } + return dump +} + // Dump dumps all the config as a JSON file func Dump() error { - dump := make(map[string]map[string]string) - for _, name := range getConfigData().GetSectionList() { - params := make(map[string]string) - for _, key := range getConfigData().GetKeyList(name) { - params[key] = FileGet(name, key) - } - dump[name] = params - } + dump := DumpRcBlob() b, err := json.MarshalIndent(dump, "", " ") if err != nil { return errors.Wrap(err, "failed to marshal config dump") diff --git a/fs/config/rc.go b/fs/config/rc.go new file mode 100644 index 000000000..99856e627 --- /dev/null +++ b/fs/config/rc.go @@ -0,0 +1,178 @@ +package config + +import ( + "github.com/ncw/rclone/fs" + "github.com/ncw/rclone/fs/rc" +) + +func init() { + rc.Add(rc.Call{ + Path: "config/dump", + Fn: rcDump, + Title: "Dumps the config file.", + AuthRequired: true, + Help: ` +Returns a JSON object: +- key: value + +Where keys are remote names and values are the config parameters. + +See the [config dump command](/commands/rclone_config_dump/) command for more information on the above. +`, + }) +} + +// Return the config file dump +func rcDump(in rc.Params) (out rc.Params, err error) { + return DumpRcBlob(), nil +} + +func init() { + rc.Add(rc.Call{ + Path: "config/get", + Fn: rcGet, + Title: "Get a remote in the config file.", + AuthRequired: true, + Help: ` +Parameters: +- name - name of remote to get + +See the [config dump command](/commands/rclone_config_dump/) command for more information on the above. +`, + }) +} + +// Return the config file get +func rcGet(in rc.Params) (out rc.Params, err error) { + name, err := in.GetString("name") + if err != nil { + return nil, err + } + return DumpRcRemote(name), nil +} + +func init() { + rc.Add(rc.Call{ + Path: "config/listremotes", + Fn: rcListRemotes, + Title: "Lists the remotes in the config file.", + AuthRequired: true, + Help: ` +Returns +- remotes - array of remote names + +See the [listremotes command](/commands/rclone_listremotes/) command for more information on the above. +`, + }) +} + +// Return the a list of remotes in the config file +func rcListRemotes(in rc.Params) (out rc.Params, err error) { + var remotes = []string{} + for _, remote := range getConfigData().GetSectionList() { + remotes = append(remotes, remote) + } + out = rc.Params{ + "remotes": remotes, + } + return out, nil +} + +func init() { + rc.Add(rc.Call{ + Path: "config/providers", + Fn: rcProviders, + Title: "Shows how providers are configured in the config file.", + AuthRequired: true, + Help: ` +Returns a JSON object: +- providers - array of objects + +See the [config providers command](/commands/rclone_config_providers/) command for more information on the above. +`, + }) +} + +// Return the config file providers +func rcProviders(in rc.Params) (out rc.Params, err error) { + out = rc.Params{ + "providers": fs.Registry, + } + return out, nil +} + +func init() { + for _, name := range []string{"create", "update", "password"} { + name := name + extraHelp := "" + if name == "create" { + extraHelp = "- type - type of the new remote\n" + } + rc.Add(rc.Call{ + Path: "config/" + name, + AuthRequired: true, + Fn: func(in rc.Params) (rc.Params, error) { + return rcConfig(in, name) + }, + Title: name + " the config for a remote.", + Help: `This takes the following parameters + +- name - name of remote +- type - type of new remote +` + extraHelp + ` + +See the [config ` + name + ` command](/commands/rclone_config_` + name + `/) command for more information on the above.`, + }) + } +} + +// Manipulate the config file +func rcConfig(in rc.Params, what string) (out rc.Params, err error) { + name, err := in.GetString("name") + if err != nil { + return nil, err + } + parameters := rc.Params{} + err = in.GetStruct("parameters", ¶meters) + if err != nil { + return nil, err + } + switch what { + case "create": + remoteType, err := in.GetString("type") + if err != nil { + return nil, err + } + return nil, CreateRemote(name, remoteType, parameters) + case "update": + return nil, UpdateRemote(name, parameters) + case "password": + return nil, PasswordRemote(name, parameters) + } + panic("unknown rcConfig type") +} + +func init() { + rc.Add(rc.Call{ + Path: "config/delete", + Fn: rcDelete, + Title: "Delete a remote in the config file.", + AuthRequired: true, + Help: ` +Parameters: +- name - name of remote to delete + +See the [config delete command](/commands/rclone_config_delete/) command for more information on the above. +`, + }) +} + +// Return the config file delete +func rcDelete(in rc.Params) (out rc.Params, err error) { + name, err := in.GetString("name") + if err != nil { + return nil, err + } + DeleteRemote(name) + return nil, nil +} diff --git a/fs/config/rc_test.go b/fs/config/rc_test.go new file mode 100644 index 000000000..bfecbd29f --- /dev/null +++ b/fs/config/rc_test.go @@ -0,0 +1,149 @@ +package config + +import ( + "testing" + + _ "github.com/ncw/rclone/backend/local" + "github.com/ncw/rclone/fs" + "github.com/ncw/rclone/fs/config/obscure" + "github.com/ncw/rclone/fs/rc" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const testName = "configTestNameForRc" + +func TestRc(t *testing.T) { + // Create the test remote + call := rc.Calls.Get("config/create") + assert.NotNil(t, call) + in := rc.Params{ + "name": testName, + "type": "local", + "parameters": rc.Params{ + "test_key": "sausage", + }, + } + out, err := call.Fn(in) + require.NoError(t, err) + require.Nil(t, out) + assert.Equal(t, "local", FileGet(testName, "type")) + assert.Equal(t, "sausage", FileGet(testName, "test_key")) + + // The sub tests rely on the remote created above but they can + // all be run independently + + t.Run("Dump", func(t *testing.T) { + call := rc.Calls.Get("config/dump") + assert.NotNil(t, call) + in := rc.Params{} + out, err := call.Fn(in) + require.NoError(t, err) + require.NotNil(t, out) + + require.NotNil(t, out[testName]) + config := out[testName].(rc.Params) + + assert.Equal(t, "local", config["type"]) + assert.Equal(t, "sausage", config["test_key"]) + }) + + t.Run("Get", func(t *testing.T) { + call := rc.Calls.Get("config/get") + assert.NotNil(t, call) + in := rc.Params{ + "name": testName, + } + out, err := call.Fn(in) + require.NoError(t, err) + require.NotNil(t, out) + + assert.Equal(t, "local", out["type"]) + assert.Equal(t, "sausage", out["test_key"]) + }) + + t.Run("ListRemotes", func(t *testing.T) { + call := rc.Calls.Get("config/listremotes") + assert.NotNil(t, call) + in := rc.Params{} + out, err := call.Fn(in) + require.NoError(t, err) + require.NotNil(t, out) + + var remotes []string + err = out.GetStruct("remotes", &remotes) + require.NoError(t, err) + + assert.Contains(t, remotes, testName) + }) + + t.Run("Update", func(t *testing.T) { + call := rc.Calls.Get("config/update") + assert.NotNil(t, call) + in := rc.Params{ + "name": testName, + "parameters": rc.Params{ + "test_key": "rutabaga", + "test_key2": "cabbage", + }, + } + out, err := call.Fn(in) + require.NoError(t, err) + assert.Nil(t, out) + + assert.Equal(t, "local", FileGet(testName, "type")) + assert.Equal(t, "rutabaga", FileGet(testName, "test_key")) + assert.Equal(t, "cabbage", FileGet(testName, "test_key2")) + }) + + t.Run("Password", func(t *testing.T) { + call := rc.Calls.Get("config/password") + assert.NotNil(t, call) + in := rc.Params{ + "name": testName, + "parameters": rc.Params{ + "test_key": "rutabaga", + "test_key2": "cabbage", + }, + } + out, err := call.Fn(in) + require.NoError(t, err) + assert.Nil(t, out) + + assert.Equal(t, "local", FileGet(testName, "type")) + assert.Equal(t, "rutabaga", obscure.MustReveal(FileGet(testName, "test_key"))) + assert.Equal(t, "cabbage", obscure.MustReveal(FileGet(testName, "test_key2"))) + }) + + // Delete the test remote + call = rc.Calls.Get("config/delete") + assert.NotNil(t, call) + in = rc.Params{ + "name": testName, + } + out, err = call.Fn(in) + require.NoError(t, err) + assert.Nil(t, out) + assert.Equal(t, "", FileGet(testName, "type")) + assert.Equal(t, "", FileGet(testName, "test_key")) +} + +func TestRcProviders(t *testing.T) { + call := rc.Calls.Get("config/providers") + assert.NotNil(t, call) + in := rc.Params{} + out, err := call.Fn(in) + require.NoError(t, err) + require.NotNil(t, out) + var registry []*fs.RegInfo + err = out.GetStruct("providers", ®istry) + require.NoError(t, err) + foundLocal := false + for _, provider := range registry { + if provider.Name == "local" { + foundLocal = true + break + } + } + assert.True(t, foundLocal, "didn't find local provider") +}