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