config: add sub commands for full config file management

Previously config sub commands were manually parsed rather than using
cobra.

Make config command have the following sub commands:

 * create    Create a new remote with name, type and options.
 * delete    Delete an existing remote <name>.
 * dump      Dump the config file as JSON.
 * edit      Enter an interactive configuration session.
 * file      Show path of configuration file in use.
 * providers List in JSON format all the providers and options.
 * show      Print (decrypted) config file, or the config for a single remote.
 * update    Update options in an existing remote.

The following changes were made to existing commands

 * listproviders was renamed to providers
 * listoptions was removed in favour of providing the output in providers
 * jsonconfig was renamed to create
 * an optional parameter was added to the show command
This commit is contained in:
Nick Craig-Wood 2017-10-14 11:50:41 +01:00
parent 0575623dff
commit edfab09eb9
3 changed files with 163 additions and 94 deletions

View file

@ -1,65 +1,123 @@
package config package config
import ( import (
"fmt"
"os"
"github.com/ncw/rclone/cmd" "github.com/ncw/rclone/cmd"
"github.com/ncw/rclone/fs" "github.com/ncw/rclone/fs"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
func init() { func init() {
cmd.Root.AddCommand(commandDefintion) cmd.Root.AddCommand(configCommand)
configCommand.AddCommand(configEditCommand)
configCommand.AddCommand(configFileCommand)
configCommand.AddCommand(configShowCommand)
configCommand.AddCommand(configDumpCommand)
configCommand.AddCommand(configProvidersCommand)
configCommand.AddCommand(configCreateCommand)
configCommand.AddCommand(configUpdateCommand)
configCommand.AddCommand(configDeleteCommand)
} }
var commandDefintion = &cobra.Command{ var configCommand = &cobra.Command{
Use: "config [function]", Use: "config",
Short: `Enter an interactive configuration session.`, Short: `Enter an interactive configuration session.`,
Long: "`rclone config`" + ` Long: `Enter an interactive configuration session where you can setup new
enters an interactive configuration sessions where you can setup remotes and manage existing ones. You may also set or remove a
new remotes and manage existing ones. You may also set or remove a password to password to protect your configuration.
protect your configuration.
Additional functions:
* ` + "`rclone config edit`" + ` same as above
* ` + "`rclone config file`" + ` show path of configuration file in use
* ` + "`rclone config show`" + ` print (decrypted) config file
* ` + "`rclone config listproviders`" + ` List, in json format, the protocols supported by sync
* ` + "`rclone config listoptions type`" + ` Lists all the options needed to connect to a protocol
* ` + "`rclone config jsonconfig`name type jsonoptions" + ` Created a new remote type X with parameters Y
`, `,
Run: func(command *cobra.Command, args []string) { Run: func(command *cobra.Command, args []string) {
cmd.CheckArgs(0, 4, command, args) cmd.CheckArgs(0, 0, command, args)
if len(args) == 0 {
fs.EditConfig() fs.EditConfig()
} else if (len(args) == 1) { },
switch args[0] { }
case "edit":
fs.EditConfig() var configEditCommand = &cobra.Command{
case "show": Use: "edit",
fs.ShowConfig() Short: configCommand.Short,
case "file": Long: configCommand.Long,
fs.ShowConfigLocation() Run: configCommand.Run,
case "listproviders": }
fs.ListProviders()
default: var configFileCommand = &cobra.Command{
fmt.Fprintf(os.Stderr, "Unknown subcommand %q, %s only supports edit, show and file.\n", args[0], command.Name()) Use: "file",
} Short: `Show path of configuration file in use.`,
} else if (len(args) == 2) { Run: func(command *cobra.Command, args []string) {
if ((args[0] == "listoptions") && (args[1] != "")) { cmd.CheckArgs(0, 0, command, args)
fs.ListOptions(args[1]) fs.ShowConfigLocation()
} else { },
fmt.Fprintf(os.Stderr, "Unknown subcommand %q %q, %s only supports optionsprovider <type>.\n", args[0], args[1], command.Name()) }
}
} else if (len(args) == 4) { var configShowCommand = &cobra.Command{
if ((args[0] == "jsonconfig") && (args[1] != "") && (args[2] != "") && (args[3]!= "")) { Use: "show [<remote>]",
fs.JsonConfig(args[1], args[2], args[3]) Short: `Print (decrypted) config file, or the config for a single remote.`,
} else { Run: func(command *cobra.Command, args []string) {
fmt.Fprintf(os.Stderr, "Unknown subcommand %q %q %q %q, %s only supports jsonconfig <name> <type> <json:options>.\n", args[0], args[1], args[2], args[3], command.Name()) cmd.CheckArgs(0, 1, command, args)
} if len(args) == 0 {
} fs.ShowConfig()
return } else {
fs.ShowRemote(args[0])
}
},
}
var configDumpCommand = &cobra.Command{
Use: "dump",
Short: `Dump the config file as JSON.`,
RunE: func(command *cobra.Command, args []string) error {
cmd.CheckArgs(0, 0, command, args)
return fs.ConfigDump()
},
}
var configProvidersCommand = &cobra.Command{
Use: "providers",
Short: `List in JSON format all the providers and options.`,
RunE: func(command *cobra.Command, args []string) error {
cmd.CheckArgs(0, 0, command, args)
return fs.JSONListProviders()
},
}
var configCreateCommand = &cobra.Command{
Use: "create <name> <type> [<key> <value>]*",
Short: `Create a new remote with name, type and options.`,
Long: `
Create a new remote of <name> with <type> and options. The options
should be passed in in pairs of <key> <value>.
For example to make a swift remote of name myremote using auto config
you would do:
rclone config create myremote swift env_auth true
`,
RunE: func(command *cobra.Command, args []string) error {
cmd.CheckArgs(2, 256, command, args)
return fs.CreateRemote(args[0], args[1], args[2:])
},
}
var configUpdateCommand = &cobra.Command{
Use: "update <name> [<key> <value>]+",
Short: `Update options in an existing remote.`,
Long: `
Update an existing remote's options. The options should be passed in
in pairs of <key> <value>.
For example to update the env_auth field of a remote of name myremote you would do:
rclone config update myremote swift env_auth true
`,
RunE: func(command *cobra.Command, args []string) error {
cmd.CheckArgs(3, 256, command, args)
return fs.UpdateRemote(args[0], args[1:])
},
}
var configDeleteCommand = &cobra.Command{
Use: "delete <name>",
Short: `Delete an existing remote <name>.`,
Run: func(command *cobra.Command, args []string) {
cmd.CheckArgs(1, 1, command, args)
fs.DeleteRemote(args[0])
}, },
} }

View file

@ -992,56 +992,47 @@ func ChooseOption(o *Option) string {
return ReadLine() return ReadLine()
} }
// JsonConfig Created a new remote type X with parameters Y // UpdateRemote adds the keyValues passed in to the remote of name.
func JsonConfig(name string, provider string, jsonstr string) { // keyValues should be key, value pairs.
bytes := []byte(jsonstr) func UpdateRemote(name string, keyValues []string) error {
if len(keyValues)%2 != 0 {
// Unmarshal string into structs. return errors.New("found key without value")
var options []Option }
json.Unmarshal(bytes, &options) // Set the config
for i := 0; i < len(keyValues); i += 2 {
configData.SetValue(name, "type", provider) configData.SetValue(name, keyValues[i], keyValues[i+1])
// Loop over structs and display them.
for op := range options {
configData.SetValue(name, options[op].Name, options[op].Value)
} }
configData.SetValue(name, ConfigAutomatic, "yes")
RemoteConfig(name) RemoteConfig(name)
ShowRemote(name) ShowRemote(name)
SaveConfig() SaveConfig()
return nil
} }
// ListOptions Lists all the options needed to connect to a protocol // CreateRemote creates a new remote with name, provider and a list of
func ListOptions(provider string) { // parameters which are key, value pairs. If update is set then it
fs := MustFind(provider) // adds the new keys rather than replacing all of them.
b, err := json.Marshal(fs.Options) func CreateRemote(name string, provider string, keyValues []string) error {
if err != nil { // Delete the old config if it exists
fmt.Println("error:", err) configData.DeleteSection(name)
} // Set the type
os.Stdout.Write(b) configData.SetValue(name, "type", provider)
// Show this is automatically configured
configData.SetValue(name, ConfigAutomatic, "yes")
// Set the remaining values
return UpdateRemote(name, keyValues)
} }
// ListProviders print all providersList, in json format, the protocols supported by sync // JSONListProviders prints all the providers and options in JSON format
func ListProviders() { func JSONListProviders() error {
o := &Option{ b, err := json.MarshalIndent(fsRegistry, "", " ")
Name: "Storage",
Help: "Type of storage to configure.",
}
for _, item := range fsRegistry {
example := OptionExample{
Value: item.Name,
Help: item.Description,
}
o.Examples = append(o.Examples, example)
}
if len(o.Examples) > 0 {
o.Examples.Sort()
b, err := json.Marshal(o.Examples)
if err != nil { if err != nil {
fmt.Println("error:", err) return errors.Wrap(err, "failed to marshal examples")
} }
os.Stdout.Write(b) _, err = os.Stdout.Write(b)
if err != nil {
return errors.Wrap(err, "failed to write providers list")
} }
return nil
} }
// fsOption returns an Option describing the possible remotes // fsOption returns an Option describing the possible remotes
@ -1367,3 +1358,24 @@ func ConfigFileSections() []string {
} }
return sections return sections
} }
// ConfigDump dumps all the config as a JSON file
func ConfigDump() error {
dump := make(map[string]map[string]string)
for _, name := range configData.GetSectionList() {
params := make(map[string]string)
for _, key := range configData.GetKeyList(name) {
params[key] = ConfigFileGet(name, key)
}
dump[name] = params
}
b, err := json.MarshalIndent(dump, "", " ")
if err != nil {
return errors.Wrap(err, "failed to marshal config dump")
}
_, err = os.Stdout.Write(b)
if err != nil {
return errors.Wrap(err, "failed to write config dump")
}
return nil
}

View file

@ -62,9 +62,9 @@ type RegInfo struct {
// Create a new file system. If root refers to an existing // Create a new file system. If root refers to an existing
// object, then it should return a Fs which which points to // object, then it should return a Fs which which points to
// the parent of that object and ErrorIsFile. // the parent of that object and ErrorIsFile.
NewFs func(name string, root string) (Fs, error) NewFs func(name string, root string) (Fs, error) `json:"-"`
// Function to call to help with config // Function to call to help with config
Config func(string) Config func(string) `json:"-"`
// Options for the Fs configuration // Options for the Fs configuration
Options []Option Options []Option
} }
@ -75,8 +75,7 @@ type Option struct {
Help string Help string
Optional bool Optional bool
IsPassword bool IsPassword bool
Examples OptionExamples Examples OptionExamples `json:",omitempty"`
Value string
} }
// OptionExamples is a slice of examples // OptionExamples is a slice of examples