github.com/divyam234/rclone@v1.64.1/fs/backend_config.go (about) 1 // Structures and utilities for backend config 2 // 3 // 4 5 package fs 6 7 import ( 8 "context" 9 "errors" 10 "fmt" 11 "strconv" 12 "strings" 13 14 "github.com/divyam234/rclone/fs/config/configmap" 15 ) 16 17 const ( 18 // ConfigToken is the key used to store the token under 19 ConfigToken = "token" 20 21 // ConfigKeyEphemeralPrefix marks config keys which shouldn't be stored in the config file 22 ConfigKeyEphemeralPrefix = "config_" 23 ) 24 25 // ConfigOAuth should be called to do the OAuth 26 // 27 // set in lib/oauthutil to avoid a circular import 28 var ConfigOAuth func(ctx context.Context, name string, m configmap.Mapper, ri *RegInfo, in ConfigIn) (*ConfigOut, error) 29 30 // ConfigIn is passed to the Config function for an Fs 31 // 32 // The interactive config system for backends is state based. This is 33 // so that different frontends to the config can be attached, eg over 34 // the API or web page. 35 // 36 // Each call to the config system supplies ConfigIn which tells the 37 // system what to do. Each will return a ConfigOut which gives a 38 // question to ask the user and a state to return to. There is one 39 // special question which allows the backends to do OAuth. 40 // 41 // The ConfigIn contains a State which the backend should act upon and 42 // a Result from the previous question to the user. 43 // 44 // If ConfigOut is nil or ConfigOut.State == "" then the process is 45 // deemed to have finished. If there is no Option in ConfigOut then 46 // the next state will be called immediately. This is wrapped in 47 // ConfigGoto and ConfigResult. 48 // 49 // Backends should keep no state in memory - if they need to persist 50 // things between calls it should be persisted in the config file. 51 // Things can also be persisted in the state using the StatePush and 52 // StatePop utilities here. 53 // 54 // The utilities here are convenience methods for different kinds of 55 // questions and responses. 56 // 57 // Where the questions ask for a name then this should start with 58 // "config_" to show it is an ephemeral config input rather than the 59 // actual value stored in the config file. Names beginning with 60 // "config_fs_" are reserved for internal use. 61 // 62 // State names starting with "*" are reserved for internal use. 63 // 64 // Note that in the bin directory there is a python program called 65 // "config.py" which shows how this interface should be used. 66 type ConfigIn struct { 67 State string // State to run 68 Result string // Result from previous Option 69 } 70 71 // ConfigOut is returned from Config function for an Fs 72 // 73 // State is the state for the next call to Config 74 // OAuth is a special value set by oauthutil.ConfigOAuth 75 // Error is displayed to the user before asking a question 76 // Result is passed to the next call to Config if Option/OAuth isn't set 77 type ConfigOut struct { 78 State string // State to jump to after this 79 Option *Option // Option to query user about 80 OAuth interface{} `json:"-"` // Do OAuth if set 81 Error string // error to be displayed to the user 82 Result string // if Option/OAuth not set then this is passed to the next state 83 } 84 85 // ConfigInputOptional asks the user for a string which may be empty 86 // 87 // state should be the next state required 88 // name is the config name for this item 89 // help should be the help shown to the user 90 func ConfigInputOptional(state string, name string, help string) (*ConfigOut, error) { 91 return &ConfigOut{ 92 State: state, 93 Option: &Option{ 94 Name: name, 95 Help: help, 96 Default: "", 97 }, 98 }, nil 99 } 100 101 // ConfigInput asks the user for a non-empty string 102 // 103 // state should be the next state required 104 // name is the config name for this item 105 // help should be the help shown to the user 106 func ConfigInput(state string, name string, help string) (*ConfigOut, error) { 107 out, _ := ConfigInputOptional(state, name, help) 108 out.Option.Required = true 109 return out, nil 110 } 111 112 // ConfigPassword asks the user for a password 113 // 114 // state should be the next state required 115 // name is the config name for this item 116 // help should be the help shown to the user 117 func ConfigPassword(state string, name string, help string) (*ConfigOut, error) { 118 out, _ := ConfigInputOptional(state, name, help) 119 out.Option.IsPassword = true 120 return out, nil 121 } 122 123 // ConfigGoto goes to the next state with empty Result 124 // 125 // state should be the next state required 126 func ConfigGoto(state string) (*ConfigOut, error) { 127 return &ConfigOut{ 128 State: state, 129 }, nil 130 } 131 132 // ConfigResult goes to the next state with result given 133 // 134 // state should be the next state required 135 // result should be the result for the next state 136 func ConfigResult(state, result string) (*ConfigOut, error) { 137 return &ConfigOut{ 138 State: state, 139 Result: result, 140 }, nil 141 } 142 143 // ConfigError shows the error to the user and goes to the state passed in 144 // 145 // state should be the next state required 146 // Error should be the error shown to the user 147 func ConfigError(state string, Error string) (*ConfigOut, error) { 148 return &ConfigOut{ 149 State: state, 150 Error: Error, 151 }, nil 152 } 153 154 // ConfigConfirm returns a ConfigOut structure which asks a Yes/No question 155 // 156 // state should be the next state required 157 // Default should be the default state 158 // name is the config name for this item 159 // help should be the help shown to the user 160 func ConfigConfirm(state string, Default bool, name string, help string) (*ConfigOut, error) { 161 return &ConfigOut{ 162 State: state, 163 Option: &Option{ 164 Name: name, 165 Help: help, 166 Default: Default, 167 Examples: []OptionExample{{ 168 Value: "true", 169 Help: "Yes", 170 }, { 171 Value: "false", 172 Help: "No", 173 }}, 174 Exclusive: true, 175 }, 176 }, nil 177 } 178 179 // ConfigChooseExclusiveFixed returns a ConfigOut structure which has a list of 180 // items to choose from. 181 // 182 // Possible items must be supplied as a fixed list. 183 // 184 // User is required to supply a value, and is restricted to the specified list, 185 // i.e. free text input is not allowed. 186 // 187 // state should be the next state required 188 // name is the config name for this item 189 // help should be the help shown to the user 190 // items should be the items in the list 191 // 192 // It chooses the first item to be the default. 193 // If there are no items then it will return an error. 194 // If there is only one item it will short cut to the next state. 195 func ConfigChooseExclusiveFixed(state string, name string, help string, items []OptionExample) (*ConfigOut, error) { 196 if len(items) == 0 { 197 return nil, fmt.Errorf("no items found in: %s", help) 198 } 199 choose := &ConfigOut{ 200 State: state, 201 Option: &Option{ 202 Name: name, 203 Help: help, 204 Examples: items, 205 Exclusive: true, 206 }, 207 } 208 choose.Option.Default = choose.Option.Examples[0].Value 209 if len(items) == 1 { 210 // short circuit asking the question if only one entry 211 choose.Result = choose.Option.Examples[0].Value 212 choose.Option = nil 213 } 214 return choose, nil 215 } 216 217 // ConfigChooseExclusive returns a ConfigOut structure which has a list of 218 // items to choose from. 219 // 220 // Possible items are retrieved from a supplied function. 221 // 222 // User is required to supply a value, and is restricted to the specified list, 223 // i.e. free text input is not allowed. 224 // 225 // state should be the next state required 226 // name is the config name for this item 227 // help should be the help shown to the user 228 // n should be the number of items in the list 229 // getItem should return the items (value, help) 230 // 231 // It chooses the first item to be the default. 232 // If there are no items then it will return an error. 233 // If there is only one item it will short cut to the next state. 234 func ConfigChooseExclusive(state string, name string, help string, n int, getItem func(i int) (itemValue string, itemHelp string)) (*ConfigOut, error) { 235 items := make(OptionExamples, n) 236 for i := range items { 237 items[i].Value, items[i].Help = getItem(i) 238 } 239 return ConfigChooseExclusiveFixed(state, name, help, items) 240 } 241 242 // ConfigChooseFixed returns a ConfigOut structure which has a list of 243 // suggested items. 244 // 245 // Suggested items must be supplied as a fixed list. 246 // 247 // User is required to supply a value, but is not restricted to the specified 248 // list, i.e. free text input is accepted. 249 // 250 // state should be the next state required 251 // name is the config name for this item 252 // help should be the help shown to the user 253 // items should be the items in the list 254 // 255 // It chooses the first item to be the default. 256 func ConfigChooseFixed(state string, name string, help string, items []OptionExample) (*ConfigOut, error) { 257 choose := &ConfigOut{ 258 State: state, 259 Option: &Option{ 260 Name: name, 261 Help: help, 262 Examples: items, 263 Required: true, 264 }, 265 } 266 if len(choose.Option.Examples) > 0 { 267 choose.Option.Default = choose.Option.Examples[0].Value 268 } 269 return choose, nil 270 } 271 272 // ConfigChoose returns a ConfigOut structure which has a list of suggested 273 // items. 274 // 275 // Suggested items are retrieved from a supplied function. 276 // 277 // User is required to supply a value, but is not restricted to the specified 278 // list, i.e. free text input is accepted. 279 // 280 // state should be the next state required 281 // name is the config name for this item 282 // help should be the help shown to the user 283 // n should be the number of items in the list 284 // getItem should return the items (value, help) 285 // 286 // It chooses the first item to be the default. 287 func ConfigChoose(state string, name string, help string, n int, getItem func(i int) (itemValue string, itemHelp string)) (*ConfigOut, error) { 288 items := make(OptionExamples, n) 289 for i := range items { 290 items[i].Value, items[i].Help = getItem(i) 291 } 292 return ConfigChooseFixed(state, name, help, items) 293 } 294 295 // StatePush pushes a new values onto the front of the config string 296 func StatePush(state string, values ...string) string { 297 for i := range values { 298 values[i] = strings.ReplaceAll(values[i], ",", ",") // replace comma with unicode wide version 299 } 300 if state != "" { 301 values = append(values[:len(values):len(values)], state) 302 } 303 return strings.Join(values, ",") 304 } 305 306 type configOAuthKeyType struct{} 307 308 // OAuth key for config 309 var configOAuthKey = configOAuthKeyType{} 310 311 // ConfigOAuthOnly marks the ctx so that the Config will stop after 312 // finding an OAuth 313 func ConfigOAuthOnly(ctx context.Context) context.Context { 314 return context.WithValue(ctx, configOAuthKey, struct{}{}) 315 } 316 317 // Return true if ctx is marked as ConfigOAuthOnly 318 func isConfigOAuthOnly(ctx context.Context) bool { 319 return ctx.Value(configOAuthKey) != nil 320 } 321 322 // StatePop pops a state from the front of the config string 323 // It returns the new state and the value popped 324 func StatePop(state string) (newState string, value string) { 325 comma := strings.IndexRune(state, ',') 326 if comma < 0 { 327 return "", state 328 } 329 value, newState = state[:comma], state[comma+1:] 330 value = strings.ReplaceAll(value, ",", ",") // replace unicode wide comma with comma 331 return newState, value 332 } 333 334 // BackendConfig calls the config for the backend in ri 335 // 336 // It wraps any OAuth transactions as necessary so only straight 337 // forward config questions are emitted 338 func BackendConfig(ctx context.Context, name string, m configmap.Mapper, ri *RegInfo, choices configmap.Getter, in ConfigIn) (out *ConfigOut, err error) { 339 for { 340 out, err = backendConfigStep(ctx, name, m, ri, choices, in) 341 if err != nil { 342 break 343 } 344 if out == nil || out.State == "" { 345 // finished 346 break 347 } 348 if out.Option != nil { 349 // question to ask user 350 break 351 } 352 if out.Error != "" { 353 // error to show user 354 break 355 } 356 // non terminal state, but no question to ask or error to show - loop here 357 in = ConfigIn{ 358 State: out.State, 359 Result: out.Result, 360 } 361 } 362 return out, err 363 } 364 365 // ConfigAll should be passed in as the initial state to run the 366 // entire config 367 const ConfigAll = "*all" 368 369 // Run the config state machine for the normal config 370 func configAll(ctx context.Context, name string, m configmap.Mapper, ri *RegInfo, in ConfigIn) (out *ConfigOut, err error) { 371 if len(ri.Options) == 0 { 372 return ConfigGoto("*postconfig") 373 } 374 375 // States are encoded 376 // 377 // *all-ACTION,NUMBER,ADVANCED 378 // 379 // Where NUMBER is the current state, ADVANCED is a flag true or false 380 // to say whether we are asking about advanced config and 381 // ACTION is what the state should be doing next. 382 stateParams, state := StatePop(in.State) 383 stateParams, stateNumber := StatePop(stateParams) 384 _, stateAdvanced := StatePop(stateParams) 385 386 optionNumber := 0 387 advanced := stateAdvanced == "true" 388 if stateNumber != "" { 389 optionNumber, err = strconv.Atoi(stateNumber) 390 if err != nil { 391 return nil, fmt.Errorf("internal error: bad state number: %w", err) 392 } 393 } 394 395 // Detect if reached the end of the questions 396 if optionNumber == len(ri.Options) { 397 if ri.Options.HasAdvanced() { 398 return ConfigConfirm("*all-advanced", false, "config_fs_advanced", "Edit advanced config?") 399 } 400 return ConfigGoto("*postconfig") 401 } else if optionNumber < 0 || optionNumber > len(ri.Options) { 402 return nil, errors.New("internal error: option out of range") 403 } 404 405 // Make the next state 406 newState := func(state string, i int, advanced bool) string { 407 return StatePush("", state, fmt.Sprint(i), fmt.Sprint(advanced)) 408 } 409 410 // Find the current option 411 option := &ri.Options[optionNumber] 412 413 switch state { 414 case "*all": 415 // If option is hidden or doesn't match advanced setting then skip it 416 if option.Hide&OptionHideConfigurator != 0 || option.Advanced != advanced { 417 return ConfigGoto(newState("*all", optionNumber+1, advanced)) 418 } 419 420 // Skip this question if it isn't the correct provider 421 provider, _ := m.Get(ConfigProvider) 422 if !MatchProvider(option.Provider, provider) { 423 return ConfigGoto(newState("*all", optionNumber+1, advanced)) 424 } 425 426 out = &ConfigOut{ 427 State: newState("*all-set", optionNumber, advanced), 428 Option: option, 429 } 430 431 // Filter examples by provider if necessary 432 if provider != "" && len(option.Examples) > 0 { 433 optionCopy := option.Copy() 434 optionCopy.Examples = OptionExamples{} 435 for _, example := range option.Examples { 436 if MatchProvider(example.Provider, provider) { 437 optionCopy.Examples = append(optionCopy.Examples, example) 438 } 439 } 440 out.Option = optionCopy 441 } 442 443 return out, nil 444 case "*all-set": 445 // Set the value if not different to current 446 // Note this won't set blank values in the config file 447 // if the default is blank 448 currentValue, _ := m.Get(option.Name) 449 if currentValue != in.Result { 450 m.Set(option.Name, in.Result) 451 } 452 // Find the next question 453 return ConfigGoto(newState("*all", optionNumber+1, advanced)) 454 case "*all-advanced": 455 // Reply to edit advanced question 456 if in.Result == "true" { 457 return ConfigGoto(newState("*all", 0, true)) 458 } 459 return ConfigGoto("*postconfig") 460 } 461 return nil, fmt.Errorf("internal error: bad state %q", state) 462 } 463 464 func backendConfigStep(ctx context.Context, name string, m configmap.Mapper, ri *RegInfo, choices configmap.Getter, in ConfigIn) (out *ConfigOut, err error) { 465 ci := GetConfig(ctx) 466 Debugf(name, "config in: state=%q, result=%q", in.State, in.Result) 467 defer func() { 468 Debugf(name, "config out: out=%+v, err=%v", out, err) 469 }() 470 471 switch { 472 case strings.HasPrefix(in.State, ConfigAll): 473 // Do all config 474 out, err = configAll(ctx, name, m, ri, in) 475 case strings.HasPrefix(in.State, "*oauth"): 476 // Do internal oauth states 477 out, err = ConfigOAuth(ctx, name, m, ri, in) 478 case strings.HasPrefix(in.State, "*postconfig"): 479 // Do the post config starting from state "" 480 in.State = "" 481 return backendConfigStep(ctx, name, m, ri, choices, in) 482 case strings.HasPrefix(in.State, "*"): 483 err = fmt.Errorf("unknown internal state %q", in.State) 484 default: 485 // Otherwise pass to backend 486 if ri.Config == nil { 487 return nil, nil 488 } 489 out, err = ri.Config(ctx, name, m, in) 490 } 491 if err != nil { 492 return nil, err 493 } 494 switch { 495 case out == nil: 496 case out.OAuth != nil: 497 // If this is an OAuth state the deal with it here 498 returnState := out.State 499 // If rclone authorize, stop after doing oauth 500 if isConfigOAuthOnly(ctx) { 501 Debugf(nil, "OAuth only is set - overriding return state") 502 returnState = "" 503 } 504 // Run internal state, saving the input so we can recall the state 505 return ConfigGoto(StatePush("", "*oauth", returnState, in.State, in.Result)) 506 case out.Option != nil: 507 if out.Option.Name == "" { 508 return nil, errors.New("internal error: no name set in Option") 509 } 510 // If override value is set in the choices then use that 511 if result, ok := choices.Get(out.Option.Name); ok { 512 Debugf(nil, "Override value found, choosing value %q for state %q", result, out.State) 513 return ConfigResult(out.State, result) 514 } 515 // If AutoConfirm is set, choose the default value 516 if ci.AutoConfirm { 517 result := fmt.Sprint(out.Option.Default) 518 Debugf(nil, "Auto confirm is set, choosing default %q for state %q, override by setting config parameter %q", result, out.State, out.Option.Name) 519 return ConfigResult(out.State, result) 520 } 521 // If fs.ConfigEdit is set then make the default value 522 // in the config the current value. 523 if result, ok := choices.Get(ConfigEdit); ok && result == "true" { 524 if value, ok := m.Get(out.Option.Name); ok { 525 newOption := out.Option.Copy() 526 oldValue := newOption.Value 527 err = newOption.Set(value) 528 if err != nil { 529 Errorf(nil, "Failed to set %q from %q - using default: %v", out.Option.Name, value, err) 530 } else { 531 newOption.Default = newOption.Value 532 newOption.Value = oldValue 533 out.Option = newOption 534 } 535 } 536 } 537 } 538 return out, nil 539 } 540 541 // MatchProvider returns true if provider matches the providerConfig string. 542 // 543 // The providerConfig string can either be a list of providers to 544 // match, or if it starts with "!" it will be a list of providers not 545 // to match. 546 // 547 // If either providerConfig or provider is blank then it will return true 548 func MatchProvider(providerConfig, provider string) bool { 549 if providerConfig == "" || provider == "" { 550 return true 551 } 552 negate := false 553 if strings.HasPrefix(providerConfig, "!") { 554 providerConfig = providerConfig[1:] 555 negate = true 556 } 557 providers := strings.Split(providerConfig, ",") 558 matched := false 559 for _, p := range providers { 560 if p == provider { 561 matched = true 562 break 563 } 564 } 565 if negate { 566 return !matched 567 } 568 return matched 569 }