From e6dd121f529dfa6d7d71831bc41253c7575dbb85 Mon Sep 17 00:00:00 2001
From: Nick Craig-Wood <nick@craig-wood.com>
Date: Sun, 4 Nov 2018 18:23:12 +0000
Subject: [PATCH] config: add rc operations for config

---
 cmd/config/config.go |  50 +++++++++++-
 fs/config/config.go  |  71 +++++++++--------
 fs/config/rc.go      | 178 +++++++++++++++++++++++++++++++++++++++++++
 fs/config/rc_test.go | 149 ++++++++++++++++++++++++++++++++++++
 4 files changed, 414 insertions(+), 34 deletions(-)
 create mode 100644 fs/config/rc.go
 create mode 100644 fs/config/rc_test.go

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", &parameters)
+	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", &registry)
+	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")
+}