github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/cmd/config/config.go (about)

     1  // Package config provides the config command.
     2  package config
     3  
     4  import (
     5  	"context"
     6  	"encoding/json"
     7  	"errors"
     8  	"fmt"
     9  	"os"
    10  	"sort"
    11  	"strings"
    12  
    13  	"github.com/rclone/rclone/cmd"
    14  	"github.com/rclone/rclone/fs"
    15  	"github.com/rclone/rclone/fs/config"
    16  	"github.com/rclone/rclone/fs/config/flags"
    17  	"github.com/rclone/rclone/fs/rc"
    18  	"github.com/spf13/cobra"
    19  	"github.com/spf13/pflag"
    20  )
    21  
    22  func init() {
    23  	cmd.Root.AddCommand(configCommand)
    24  	configCommand.AddCommand(configEditCommand)
    25  	configCommand.AddCommand(configFileCommand)
    26  	configCommand.AddCommand(configTouchCommand)
    27  	configCommand.AddCommand(configPathsCommand)
    28  	configCommand.AddCommand(configShowCommand)
    29  	configCommand.AddCommand(configRedactedCommand)
    30  	configCommand.AddCommand(configDumpCommand)
    31  	configCommand.AddCommand(configProvidersCommand)
    32  	configCommand.AddCommand(configCreateCommand)
    33  	configCommand.AddCommand(configUpdateCommand)
    34  	configCommand.AddCommand(configDeleteCommand)
    35  	configCommand.AddCommand(configPasswordCommand)
    36  	configCommand.AddCommand(configReconnectCommand)
    37  	configCommand.AddCommand(configDisconnectCommand)
    38  	configCommand.AddCommand(configUserInfoCommand)
    39  }
    40  
    41  var configCommand = &cobra.Command{
    42  	Use:   "config",
    43  	Short: `Enter an interactive configuration session.`,
    44  	Long: `Enter an interactive configuration session where you can setup new
    45  remotes and manage existing ones. You may also set or remove a
    46  password to protect your configuration.
    47  `,
    48  	Annotations: map[string]string{
    49  		"versionIntroduced": "v1.39",
    50  	},
    51  	RunE: func(command *cobra.Command, args []string) error {
    52  		cmd.CheckArgs(0, 0, command, args)
    53  		return config.EditConfig(context.Background())
    54  	},
    55  }
    56  
    57  var configEditCommand = &cobra.Command{
    58  	Use:   "edit",
    59  	Short: configCommand.Short,
    60  	Long:  configCommand.Long,
    61  	Annotations: map[string]string{
    62  		"versionIntroduced": "v1.39",
    63  	},
    64  	RunE: func(command *cobra.Command, args []string) error {
    65  		cmd.CheckArgs(0, 0, command, args)
    66  		return config.EditConfig(context.Background())
    67  	},
    68  }
    69  
    70  var configFileCommand = &cobra.Command{
    71  	Use:   "file",
    72  	Short: `Show path of configuration file in use.`,
    73  	Annotations: map[string]string{
    74  		"versionIntroduced": "v1.38",
    75  	},
    76  	Run: func(command *cobra.Command, args []string) {
    77  		cmd.CheckArgs(0, 0, command, args)
    78  		config.ShowConfigLocation()
    79  	},
    80  }
    81  
    82  var configTouchCommand = &cobra.Command{
    83  	Use:   "touch",
    84  	Short: `Ensure configuration file exists.`,
    85  	Annotations: map[string]string{
    86  		"versionIntroduced": "v1.56",
    87  	},
    88  	Run: func(command *cobra.Command, args []string) {
    89  		cmd.CheckArgs(0, 0, command, args)
    90  		config.SaveConfig()
    91  	},
    92  }
    93  
    94  var configPathsCommand = &cobra.Command{
    95  	Use:   "paths",
    96  	Short: `Show paths used for configuration, cache, temp etc.`,
    97  	Annotations: map[string]string{
    98  		"versionIntroduced": "v1.57",
    99  	},
   100  	Run: func(command *cobra.Command, args []string) {
   101  		cmd.CheckArgs(0, 0, command, args)
   102  		fmt.Printf("Config file: %s\n", config.GetConfigPath())
   103  		fmt.Printf("Cache dir:   %s\n", config.GetCacheDir())
   104  		fmt.Printf("Temp dir:    %s\n", os.TempDir())
   105  	},
   106  }
   107  
   108  var configShowCommand = &cobra.Command{
   109  	Use:   "show [<remote>]",
   110  	Short: `Print (decrypted) config file, or the config for a single remote.`,
   111  	Annotations: map[string]string{
   112  		"versionIntroduced": "v1.38",
   113  	},
   114  	Run: func(command *cobra.Command, args []string) {
   115  		cmd.CheckArgs(0, 1, command, args)
   116  		if len(args) == 0 {
   117  			config.ShowConfig()
   118  		} else {
   119  			name := strings.TrimRight(args[0], ":")
   120  			config.ShowRemote(name)
   121  		}
   122  	},
   123  }
   124  
   125  var configRedactedCommand = &cobra.Command{
   126  	Use:   "redacted [<remote>]",
   127  	Short: `Print redacted (decrypted) config file, or the redacted config for a single remote.`,
   128  	Long: `This prints a redacted copy of the config file, either the
   129  whole config file or for a given remote.
   130  
   131  The config file will be redacted by replacing all passwords and other
   132  sensitive info with XXX.
   133  
   134  This makes the config file suitable for posting online for support.
   135  
   136  It should be double checked before posting as the redaction may not be perfect.
   137  
   138  `,
   139  	Annotations: map[string]string{
   140  		"versionIntroduced": "v1.64",
   141  	},
   142  	Run: func(command *cobra.Command, args []string) {
   143  		cmd.CheckArgs(0, 1, command, args)
   144  		if len(args) == 0 {
   145  			config.ShowRedactedConfig()
   146  		} else {
   147  			name := strings.TrimRight(args[0], ":")
   148  			config.ShowRedactedRemote(name)
   149  		}
   150  		fmt.Println("### Double check the config for sensitive info before posting publicly")
   151  	},
   152  }
   153  
   154  var configDumpCommand = &cobra.Command{
   155  	Use:   "dump",
   156  	Short: `Dump the config file as JSON.`,
   157  	Annotations: map[string]string{
   158  		"versionIntroduced": "v1.39",
   159  	},
   160  	RunE: func(command *cobra.Command, args []string) error {
   161  		cmd.CheckArgs(0, 0, command, args)
   162  		return config.Dump()
   163  	},
   164  }
   165  
   166  var configProvidersCommand = &cobra.Command{
   167  	Use:   "providers",
   168  	Short: `List in JSON format all the providers and options.`,
   169  	Annotations: map[string]string{
   170  		"versionIntroduced": "v1.39",
   171  	},
   172  	RunE: func(command *cobra.Command, args []string) error {
   173  		cmd.CheckArgs(0, 0, command, args)
   174  		return config.JSONListProviders()
   175  	},
   176  }
   177  
   178  var updateRemoteOpt config.UpdateRemoteOpt
   179  
   180  var configPasswordHelp = strings.ReplaceAll(`
   181  Note that if the config process would normally ask a question the
   182  default is taken (unless |--non-interactive| is used).  Each time
   183  that happens rclone will print or DEBUG a message saying how to
   184  affect the value taken.
   185  
   186  If any of the parameters passed is a password field, then rclone will
   187  automatically obscure them if they aren't already obscured before
   188  putting them in the config file.
   189  
   190  **NB** If the password parameter is 22 characters or longer and
   191  consists only of base64 characters then rclone can get confused about
   192  whether the password is already obscured or not and put unobscured
   193  passwords into the config file. If you want to be 100% certain that
   194  the passwords get obscured then use the |--obscure| flag, or if you
   195  are 100% certain you are already passing obscured passwords then use
   196  |--no-obscure|.  You can also set obscured passwords using the
   197  |rclone config password| command.
   198  
   199  The flag |--non-interactive| is for use by applications that wish to
   200  configure rclone themselves, rather than using rclone's text based
   201  configuration questions. If this flag is set, and rclone needs to ask
   202  the user a question, a JSON blob will be returned with the question in
   203  it.
   204  
   205  This will look something like (some irrelevant detail removed):
   206  
   207  |||
   208  {
   209      "State": "*oauth-islocal,teamdrive,,",
   210      "Option": {
   211          "Name": "config_is_local",
   212          "Help": "Use web browser to automatically authenticate rclone with remote?\n * Say Y if the machine running rclone has a web browser you can use\n * Say N if running rclone on a (remote) machine without web browser access\nIf not sure try Y. If Y failed, try N.\n",
   213          "Default": true,
   214          "Examples": [
   215              {
   216                  "Value": "true",
   217                  "Help": "Yes"
   218              },
   219              {
   220                  "Value": "false",
   221                  "Help": "No"
   222              }
   223          ],
   224          "Required": false,
   225          "IsPassword": false,
   226          "Type": "bool",
   227          "Exclusive": true,
   228      },
   229      "Error": "",
   230  }
   231  |||
   232  
   233  The format of |Option| is the same as returned by |rclone config
   234  providers|. The question should be asked to the user and returned to
   235  rclone as the |--result| option along with the |--state| parameter.
   236  
   237  The keys of |Option| are used as follows:
   238  
   239  - |Name| - name of variable - show to user
   240  - |Help| - help text. Hard wrapped at 80 chars. Any URLs should be clicky.
   241  - |Default| - default value - return this if the user just wants the default.
   242  - |Examples| - the user should be able to choose one of these
   243  - |Required| - the value should be non-empty
   244  - |IsPassword| - the value is a password and should be edited as such
   245  - |Type| - type of value, eg |bool|, |string|, |int| and others
   246  - |Exclusive| - if set no free-form entry allowed only the |Examples|
   247  - Irrelevant keys |Provider|, |ShortOpt|, |Hide|, |NoPrefix|, |Advanced|
   248  
   249  If |Error| is set then it should be shown to the user at the same
   250  time as the question.
   251  
   252      rclone config update name --continue --state "*oauth-islocal,teamdrive,," --result "true"
   253  
   254  Note that when using |--continue| all passwords should be passed in
   255  the clear (not obscured). Any default config values should be passed
   256  in with each invocation of |--continue|.
   257  
   258  At the end of the non interactive process, rclone will return a result
   259  with |State| as empty string.
   260  
   261  If |--all| is passed then rclone will ask all the config questions,
   262  not just the post config questions. Any parameters are used as
   263  defaults for questions as usual.
   264  
   265  Note that |bin/config.py| in the rclone source implements this protocol
   266  as a readable demonstration.
   267  `, "|", "`")
   268  var configCreateCommand = &cobra.Command{
   269  	Use:   "create name type [key value]*",
   270  	Short: `Create a new remote with name, type and options.`,
   271  	Long: strings.ReplaceAll(`
   272  Create a new remote of |name| with |type| and options.  The options
   273  should be passed in pairs of |key| |value| or as |key=value|.
   274  
   275  For example, to make a swift remote of name myremote using auto config
   276  you would do:
   277  
   278      rclone config create myremote swift env_auth true
   279      rclone config create myremote swift env_auth=true
   280  
   281  So for example if you wanted to configure a Google Drive remote but
   282  using remote authorization you would do this:
   283  
   284      rclone config create mydrive drive config_is_local=false
   285  `, "|", "`") + configPasswordHelp,
   286  	Annotations: map[string]string{
   287  		"versionIntroduced": "v1.39",
   288  	},
   289  	RunE: func(command *cobra.Command, args []string) error {
   290  		cmd.CheckArgs(2, 256, command, args)
   291  		in, err := argsToMap(args[2:])
   292  		if err != nil {
   293  			return err
   294  		}
   295  		return doConfig(args[0], in, func(opts config.UpdateRemoteOpt) (*fs.ConfigOut, error) {
   296  			return config.CreateRemote(context.Background(), args[0], args[1], in, opts)
   297  		})
   298  	},
   299  }
   300  
   301  func doConfig(name string, in rc.Params, do func(config.UpdateRemoteOpt) (*fs.ConfigOut, error)) error {
   302  	out, err := do(updateRemoteOpt)
   303  	if err != nil {
   304  		return err
   305  	}
   306  	if !(updateRemoteOpt.NonInteractive || updateRemoteOpt.Continue) {
   307  		config.ShowRemote(name)
   308  	} else {
   309  		if out == nil {
   310  			out = &fs.ConfigOut{}
   311  		}
   312  		outBytes, err := json.MarshalIndent(out, "", "\t")
   313  		if err != nil {
   314  			return err
   315  		}
   316  		_, _ = os.Stdout.Write(outBytes)
   317  		_, _ = os.Stdout.WriteString("\n")
   318  	}
   319  	return nil
   320  }
   321  
   322  func init() {
   323  	for _, cmdFlags := range []*pflag.FlagSet{configCreateCommand.Flags(), configUpdateCommand.Flags()} {
   324  		flags.BoolVarP(cmdFlags, &updateRemoteOpt.Obscure, "obscure", "", false, "Force any passwords to be obscured", "Config")
   325  		flags.BoolVarP(cmdFlags, &updateRemoteOpt.NoObscure, "no-obscure", "", false, "Force any passwords not to be obscured", "Config")
   326  		flags.BoolVarP(cmdFlags, &updateRemoteOpt.NonInteractive, "non-interactive", "", false, "Don't interact with user and return questions", "Config")
   327  		flags.BoolVarP(cmdFlags, &updateRemoteOpt.Continue, "continue", "", false, "Continue the configuration process with an answer", "Config")
   328  		flags.BoolVarP(cmdFlags, &updateRemoteOpt.All, "all", "", false, "Ask the full set of config questions", "Config")
   329  		flags.StringVarP(cmdFlags, &updateRemoteOpt.State, "state", "", "", "State - use with --continue", "Config")
   330  		flags.StringVarP(cmdFlags, &updateRemoteOpt.Result, "result", "", "", "Result - use with --continue", "Config")
   331  	}
   332  }
   333  
   334  var configUpdateCommand = &cobra.Command{
   335  	Use:   "update name [key value]+",
   336  	Short: `Update options in an existing remote.`,
   337  	Long: strings.ReplaceAll(`
   338  Update an existing remote's options. The options should be passed in
   339  pairs of |key| |value| or as |key=value|.
   340  
   341  For example, to update the env_auth field of a remote of name myremote
   342  you would do:
   343  
   344      rclone config update myremote env_auth true
   345      rclone config update myremote env_auth=true
   346  
   347  If the remote uses OAuth the token will be updated, if you don't
   348  require this add an extra parameter thus:
   349  
   350      rclone config update myremote env_auth=true config_refresh_token=false
   351  `, "|", "`") + configPasswordHelp,
   352  	Annotations: map[string]string{
   353  		"versionIntroduced": "v1.39",
   354  	},
   355  	RunE: func(command *cobra.Command, args []string) error {
   356  		cmd.CheckArgs(1, 256, command, args)
   357  		in, err := argsToMap(args[1:])
   358  		if err != nil {
   359  			return err
   360  		}
   361  		return doConfig(args[0], in, func(opts config.UpdateRemoteOpt) (*fs.ConfigOut, error) {
   362  			return config.UpdateRemote(context.Background(), args[0], in, opts)
   363  		})
   364  	},
   365  }
   366  
   367  var configDeleteCommand = &cobra.Command{
   368  	Use:   "delete name",
   369  	Short: "Delete an existing remote.",
   370  	Annotations: map[string]string{
   371  		"versionIntroduced": "v1.39",
   372  	},
   373  	Run: func(command *cobra.Command, args []string) {
   374  		cmd.CheckArgs(1, 1, command, args)
   375  		config.DeleteRemote(args[0])
   376  	},
   377  }
   378  
   379  var configPasswordCommand = &cobra.Command{
   380  	Use:   "password name [key value]+",
   381  	Short: `Update password in an existing remote.`,
   382  	Long: strings.ReplaceAll(`
   383  Update an existing remote's password. The password
   384  should be passed in pairs of |key| |password| or as |key=password|.
   385  The |password| should be passed in in clear (unobscured).
   386  
   387  For example, to set password of a remote of name myremote you would do:
   388  
   389      rclone config password myremote fieldname mypassword
   390      rclone config password myremote fieldname=mypassword
   391  
   392  This command is obsolete now that "config update" and "config create"
   393  both support obscuring passwords directly.
   394  `, "|", "`"),
   395  	Annotations: map[string]string{
   396  		"versionIntroduced": "v1.39",
   397  	},
   398  	RunE: func(command *cobra.Command, args []string) error {
   399  		cmd.CheckArgs(1, 256, command, args)
   400  		in, err := argsToMap(args[1:])
   401  		if err != nil {
   402  			return err
   403  		}
   404  		err = config.PasswordRemote(context.Background(), args[0], in)
   405  		if err != nil {
   406  			return err
   407  		}
   408  		config.ShowRemote(args[0])
   409  		return nil
   410  	},
   411  }
   412  
   413  // This takes a list of arguments in key value key value form, or
   414  // key=value key=value form and converts it into a map
   415  func argsToMap(args []string) (out rc.Params, err error) {
   416  	out = rc.Params{}
   417  	for i := 0; i < len(args); i++ {
   418  		key := args[i]
   419  		equals := strings.IndexRune(key, '=')
   420  		var value string
   421  		if equals >= 0 {
   422  			key, value = key[:equals], key[equals+1:]
   423  		} else {
   424  			i++
   425  			if i >= len(args) {
   426  				return nil, errors.New("found key without value")
   427  			}
   428  			value = args[i]
   429  		}
   430  		out[key] = value
   431  	}
   432  	return out, nil
   433  }
   434  
   435  var configReconnectCommand = &cobra.Command{
   436  	Use:   "reconnect remote:",
   437  	Short: `Re-authenticates user with remote.`,
   438  	Long: `
   439  This reconnects remote: passed in to the cloud storage system.
   440  
   441  To disconnect the remote use "rclone config disconnect".
   442  
   443  This normally means going through the interactive oauth flow again.
   444  `,
   445  	RunE: func(command *cobra.Command, args []string) error {
   446  		ctx := context.Background()
   447  		cmd.CheckArgs(1, 1, command, args)
   448  		fsInfo, configName, _, m, err := fs.ConfigFs(args[0])
   449  		if err != nil {
   450  			return err
   451  		}
   452  		return config.PostConfig(ctx, configName, m, fsInfo)
   453  	},
   454  }
   455  
   456  var configDisconnectCommand = &cobra.Command{
   457  	Use:   "disconnect remote:",
   458  	Short: `Disconnects user from remote`,
   459  	Long: `
   460  This disconnects the remote: passed in to the cloud storage system.
   461  
   462  This normally means revoking the oauth token.
   463  
   464  To reconnect use "rclone config reconnect".
   465  `,
   466  	RunE: func(command *cobra.Command, args []string) error {
   467  		cmd.CheckArgs(1, 1, command, args)
   468  		f := cmd.NewFsSrc(args)
   469  		doDisconnect := f.Features().Disconnect
   470  		if doDisconnect == nil {
   471  			return fmt.Errorf("%v doesn't support Disconnect", f)
   472  		}
   473  		err := doDisconnect(context.Background())
   474  		if err != nil {
   475  			return fmt.Errorf("disconnect call failed: %w", err)
   476  		}
   477  		return nil
   478  	},
   479  }
   480  
   481  var (
   482  	jsonOutput bool
   483  )
   484  
   485  func init() {
   486  	flags.BoolVarP(configUserInfoCommand.Flags(), &jsonOutput, "json", "", false, "Format output as JSON", "")
   487  }
   488  
   489  var configUserInfoCommand = &cobra.Command{
   490  	Use:   "userinfo remote:",
   491  	Short: `Prints info about logged in user of remote.`,
   492  	Long: `
   493  This prints the details of the person logged in to the cloud storage
   494  system.
   495  `,
   496  	RunE: func(command *cobra.Command, args []string) error {
   497  		cmd.CheckArgs(1, 1, command, args)
   498  		f := cmd.NewFsSrc(args)
   499  		doUserInfo := f.Features().UserInfo
   500  		if doUserInfo == nil {
   501  			return fmt.Errorf("%v doesn't support UserInfo", f)
   502  		}
   503  		u, err := doUserInfo(context.Background())
   504  		if err != nil {
   505  			return fmt.Errorf("UserInfo call failed: %w", err)
   506  		}
   507  		if jsonOutput {
   508  			out := json.NewEncoder(os.Stdout)
   509  			out.SetIndent("", "\t")
   510  			return out.Encode(u)
   511  		}
   512  		var keys []string
   513  		var maxKeyLen int
   514  		for key := range u {
   515  			keys = append(keys, key)
   516  			if len(key) > maxKeyLen {
   517  				maxKeyLen = len(key)
   518  			}
   519  		}
   520  		sort.Strings(keys)
   521  		for _, key := range keys {
   522  			fmt.Printf("%*s: %s\n", maxKeyLen, key, u[key])
   523  		}
   524  		return nil
   525  	},
   526  }