config: add rc operations for config

This commit is contained in:
Nick Craig-Wood 2018-11-04 18:23:12 +00:00
parent e600217666
commit e6dd121f52
4 changed files with 414 additions and 34 deletions

View File

@ -1,8 +1,11 @@
package config package config
import ( import (
"errors"
"github.com/ncw/rclone/cmd" "github.com/ncw/rclone/cmd"
"github.com/ncw/rclone/fs/config" "github.com/ncw/rclone/fs/config"
"github.com/ncw/rclone/fs/rc"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -93,7 +96,16 @@ you would do:
`, `,
RunE: func(command *cobra.Command, args []string) error { RunE: func(command *cobra.Command, args []string) error {
cmd.CheckArgs(2, 256, command, args) 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 { RunE: func(command *cobra.Command, args []string) error {
cmd.CheckArgs(3, 256, command, args) 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 { RunE: func(command *cobra.Command, args []string) error {
cmd.CheckArgs(3, 256, command, args) 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
}

View File

@ -32,6 +32,7 @@ import (
"github.com/ncw/rclone/fs/driveletter" "github.com/ncw/rclone/fs/driveletter"
"github.com/ncw/rclone/fs/fshttp" "github.com/ncw/rclone/fs/fshttp"
"github.com/ncw/rclone/fs/fspath" "github.com/ncw/rclone/fs/fspath"
"github.com/ncw/rclone/fs/rc"
"github.com/pkg/errors" "github.com/pkg/errors"
"golang.org/x/crypto/nacl/secretbox" "golang.org/x/crypto/nacl/secretbox"
"golang.org/x/text/unicode/norm" "golang.org/x/text/unicode/norm"
@ -901,18 +902,24 @@ func ChooseOption(o *fs.Option, name string) string {
return in 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. // UpdateRemote adds the keyValues passed in to the remote of name.
// keyValues should be key, value pairs. // keyValues should be key, value pairs.
func UpdateRemote(name string, keyValues []string) error { func UpdateRemote(name string, keyValues rc.Params) error {
if len(keyValues)%2 != 0 { defer suppressConfirm()()
return errors.New("found key without value")
}
// Set the config // Set the config
for i := 0; i < len(keyValues); i += 2 { for k, v := range keyValues {
getConfigData().SetValue(name, keyValues[i], keyValues[i+1]) getConfigData().SetValue(name, k, fmt.Sprint(v))
} }
RemoteConfig(name) RemoteConfig(name)
ShowRemote(name)
SaveConfig() SaveConfig()
return nil 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 // CreateRemote creates a new remote with name, provider and a list of
// parameters which are key, value pairs. If update is set then it // parameters which are key, value pairs. If update is set then it
// adds the new keys rather than replacing all of them. // adds the new keys rather than replacing all of them.
func CreateRemote(name string, provider string, keyValues []string) error { func CreateRemote(name string, provider string, keyValues rc.Params) error {
// Suppress Confirm
fs.Config.AutoConfirm = true
// Delete the old config if it exists // Delete the old config if it exists
getConfigData().DeleteSection(name) getConfigData().DeleteSection(name)
// Set the type // 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. // PasswordRemote adds the keyValues passed in to the remote of name.
// keyValues should be key, value pairs. // keyValues should be key, value pairs.
func PasswordRemote(name string, keyValues []string) error { func PasswordRemote(name string, keyValues rc.Params) error {
if len(keyValues) != 2 { defer suppressConfirm()()
return errors.New("found key without value") for k, v := range keyValues {
keyValues[k] = obscure.MustObscure(fmt.Sprint(v))
} }
// Suppress Confirm return UpdateRemote(name, keyValues)
fs.Config.AutoConfirm = true
passwd := obscure.MustObscure(keyValues[1])
if passwd != "" {
getConfigData().SetValue(name, keyValues[0], passwd)
RemoteConfig(name)
ShowRemote(name)
SaveConfig()
}
return nil
} }
// JSONListProviders prints all the providers and options in JSON format // JSONListProviders prints all the providers and options in JSON format
@ -1297,16 +1294,28 @@ func FileSections() []string {
return sections return sections
} }
// Dump dumps all the config as a JSON file // DumpRcRemote dumps the config for a single remote
func Dump() error { func DumpRcRemote(name string) (dump rc.Params) {
dump := make(map[string]map[string]string) params := rc.Params{}
for _, name := range getConfigData().GetSectionList() {
params := make(map[string]string)
for _, key := range getConfigData().GetKeyList(name) { for _, key := range getConfigData().GetKeyList(name) {
params[key] = FileGet(name, key) params[key] = FileGet(name, key)
} }
dump[name] = params 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 := DumpRcBlob()
b, err := json.MarshalIndent(dump, "", " ") b, err := json.MarshalIndent(dump, "", " ")
if err != nil { if err != nil {
return errors.Wrap(err, "failed to marshal config dump") return errors.Wrap(err, "failed to marshal config dump")

178
fs/config/rc.go Normal file
View File

@ -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
}

149
fs/config/rc_test.go Normal file
View File

@ -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")
}