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  }