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 }