mirror of
https://github.com/rclone/rclone.git
synced 2025-08-07 21:18:43 +02:00
Now that we have unified the config, we can make a much more convenient rc interface which mirrors the command line exactly, rather than using the structure of the internal Go structs.
313 lines
9.3 KiB
Go
313 lines
9.3 KiB
Go
// Package configstruct parses unstructured maps into structures
|
|
package configstruct
|
|
|
|
import (
|
|
"encoding/csv"
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/rclone/rclone/fs/config/configmap"
|
|
)
|
|
|
|
var matchUpper = regexp.MustCompile("([A-Z]+)")
|
|
|
|
// camelToSnake converts CamelCase to snake_case
|
|
func camelToSnake(in string) string {
|
|
out := matchUpper.ReplaceAllString(in, "_$1")
|
|
out = strings.ToLower(out)
|
|
out = strings.Trim(out, "_")
|
|
return out
|
|
}
|
|
|
|
// StringToInterface turns in into an interface{} the same type as def
|
|
//
|
|
// This supports a subset of builtin types, string, integer types,
|
|
// bool, time.Duration and []string.
|
|
//
|
|
// Builtin types are expected to be encoding as their natural
|
|
// stringificatons as produced by fmt.Sprint except for []string which
|
|
// is expected to be encoded a a CSV with empty array encoded as "".
|
|
//
|
|
// Any other types are expected to be encoded by their String()
|
|
// methods and decoded by their `Set(s string) error` methods.
|
|
func StringToInterface(def any, in string) (newValue any, err error) {
|
|
typ := reflect.TypeOf(def)
|
|
o := reflect.New(typ)
|
|
switch def.(type) {
|
|
case string:
|
|
// return strings unmodified
|
|
newValue = in
|
|
case int, int8, int16, int32, int64,
|
|
uint, uint8, uint16, uint32, uint64, uintptr,
|
|
float32, float64:
|
|
// As per Rob Pike's advice in https://github.com/golang/go/issues/43306
|
|
// we only use Sscan for numbers
|
|
var n int
|
|
n, err = fmt.Sscanln(in, o.Interface())
|
|
if err == nil && n != 1 {
|
|
err = errors.New("no items parsed")
|
|
}
|
|
newValue = o.Elem().Interface()
|
|
case bool:
|
|
newValue, err = strconv.ParseBool(in)
|
|
case time.Duration:
|
|
newValue, err = time.ParseDuration(in)
|
|
case []string:
|
|
// CSV decode arrays of strings - ideally we would use
|
|
// fs.CommaSepList here but we can't as it would cause
|
|
// a circular import.
|
|
if len(in) == 0 {
|
|
newValue = []string{}
|
|
} else {
|
|
r := csv.NewReader(strings.NewReader(in))
|
|
newValue, err = r.Read()
|
|
switch _err := err.(type) {
|
|
case *csv.ParseError:
|
|
err = _err.Err // remove line numbers from the error message
|
|
}
|
|
}
|
|
default:
|
|
// Try using a Set method
|
|
if do, ok := o.Interface().(interface{ Set(s string) error }); ok {
|
|
err = do.Set(in)
|
|
} else {
|
|
err = errors.New("don't know how to parse this type")
|
|
}
|
|
newValue = o.Elem().Interface()
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parsing %q as %T failed: %w", in, def, err)
|
|
}
|
|
return newValue, nil
|
|
}
|
|
|
|
// InterfaceToString turns in into a string
|
|
//
|
|
// This supports a subset of builtin types, string, integer types,
|
|
// bool, time.Duration and []string.
|
|
//
|
|
// Builtin types are expected to be encoding as their natural
|
|
// stringificatons as produced by fmt.Sprint except for []string which
|
|
// is expected to be encoded a a CSV with empty array encoded as "".
|
|
//
|
|
// Any other types are expected to be encoded by their String()
|
|
// methods and decoded by their `Set(s string) error` methods.
|
|
func InterfaceToString(in any) (strValue string, err error) {
|
|
switch x := in.(type) {
|
|
case string:
|
|
// return strings unmodified
|
|
strValue = x
|
|
case int, int8, int16, int32, int64,
|
|
uint, uint8, uint16, uint32, uint64, uintptr,
|
|
float32, float64:
|
|
strValue = fmt.Sprint(in)
|
|
case bool:
|
|
strValue = fmt.Sprint(in)
|
|
case time.Duration:
|
|
strValue = fmt.Sprint(in)
|
|
case []string:
|
|
// CSV encode arrays of strings - ideally we would use
|
|
// fs.CommaSepList here but we can't as it would cause
|
|
// a circular import.
|
|
if len(x) == 0 {
|
|
strValue = ""
|
|
} else if len(x) == 1 && len(x[0]) == 0 {
|
|
strValue = `""`
|
|
} else {
|
|
var buf strings.Builder
|
|
w := csv.NewWriter(&buf)
|
|
err := w.Write(x)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
w.Flush()
|
|
strValue = strings.TrimSpace(buf.String())
|
|
}
|
|
default:
|
|
// Try using a String method
|
|
if do, ok := in.(fmt.Stringer); ok {
|
|
strValue = do.String()
|
|
} else {
|
|
err = errors.New("don't know how to convert this")
|
|
}
|
|
}
|
|
if err != nil {
|
|
return "", fmt.Errorf("interpreting %T as string failed: %w", in, err)
|
|
}
|
|
return strValue, nil
|
|
}
|
|
|
|
// Item describes a single entry in the options structure
|
|
type Item struct {
|
|
Name string // snake_case
|
|
Field string // CamelCase
|
|
Set func(any) // set this field
|
|
Value any
|
|
}
|
|
|
|
// Items parses the opt struct and returns a slice of Item objects.
|
|
//
|
|
// opt must be a pointer to a struct. The struct should have entirely
|
|
// public fields.
|
|
//
|
|
// The config_name is looked up in a struct tag called "config" or if
|
|
// not found is the field name converted from CamelCase to snake_case.
|
|
//
|
|
// Nested structs are looked up too. If the parent struct has a struct
|
|
// tag, this will be used as a prefix for the values in the sub
|
|
// struct, otherwise they will be embedded as they are.
|
|
func Items(opt any) (items []Item, err error) {
|
|
def := reflect.ValueOf(opt)
|
|
if def.Kind() != reflect.Ptr {
|
|
return nil, errors.New("argument must be a pointer")
|
|
}
|
|
def = def.Elem() // indirect the pointer
|
|
if def.Kind() != reflect.Struct {
|
|
return nil, errors.New("argument must be a pointer to a struct")
|
|
}
|
|
defType := def.Type()
|
|
for i := range def.NumField() {
|
|
field := def.Field(i)
|
|
fieldType := defType.Field(i)
|
|
fieldName := fieldType.Name
|
|
configName, hasTag := fieldType.Tag.Lookup("config")
|
|
if hasTag && configName == "-" {
|
|
// Skip items with config:"-"
|
|
continue
|
|
}
|
|
if !hasTag {
|
|
configName = camelToSnake(fieldName)
|
|
}
|
|
valuePtr := field.Addr().Interface() // pointer to the value as an interface
|
|
_, canSet := valuePtr.(interface{ Set(string) error }) // can we set this with the Option Set protocol
|
|
// If we have a nested struct that isn't a config item then recurse
|
|
if fieldType.Type.Kind() == reflect.Struct && !canSet {
|
|
newItems, err := Items(valuePtr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error parsing field %q: %w", fieldName, err)
|
|
}
|
|
for _, newItem := range newItems {
|
|
if hasTag {
|
|
newItem.Name = configName + "_" + newItem.Name
|
|
}
|
|
newItem.Field = fieldName + "." + newItem.Field
|
|
items = append(items, newItem)
|
|
}
|
|
} else {
|
|
defaultItem := Item{
|
|
Name: configName,
|
|
Field: fieldName,
|
|
Set: func(newValue any) {
|
|
field.Set(reflect.ValueOf(newValue))
|
|
},
|
|
Value: field.Interface(),
|
|
}
|
|
items = append(items, defaultItem)
|
|
}
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
// setValue sets newValue to configValue returning an updated newValue
|
|
func setValue(newValue any, configValue string) (any, error) {
|
|
newNewValue, err := StringToInterface(newValue, configValue)
|
|
if err != nil {
|
|
// Mask errors if setting an empty string as
|
|
// it isn't valid for all types. This makes
|
|
// empty string be the equivalent of unset.
|
|
if configValue != "" {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
newValue = newNewValue
|
|
}
|
|
return newValue, nil
|
|
}
|
|
|
|
// Set interprets the field names in defaults and looks up config
|
|
// values in the config passed in. Any values found in config will be
|
|
// set in the opt structure.
|
|
//
|
|
// opt must be a pointer to a struct. The struct should have entirely
|
|
// public fields. The field names are converted from CamelCase to
|
|
// snake_case and looked up in the config supplied or a
|
|
// `config:"field_name"` is looked up.
|
|
//
|
|
// If items are found then they are converted from string to native
|
|
// types and set in opt.
|
|
//
|
|
// All the field types in the struct must implement fmt.Scanner.
|
|
func Set(config configmap.Getter, opt any) (err error) {
|
|
defaultItems, err := Items(opt)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, defaultItem := range defaultItems {
|
|
newValue := defaultItem.Value
|
|
if configValue, ok := config.Get(defaultItem.Name); ok {
|
|
newValue, err = setValue(newValue, configValue)
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't parse config item %q = %q as %T: %w", defaultItem.Name, configValue, defaultItem.Value, err)
|
|
}
|
|
}
|
|
defaultItem.Set(newValue)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// setIfSameType set aPtr with b if they are the same type or returns false.
|
|
func setIfSameType(aPtr interface{}, b interface{}) bool {
|
|
aVal := reflect.ValueOf(aPtr).Elem()
|
|
bVal := reflect.ValueOf(b)
|
|
|
|
if aVal.Type() != bVal.Type() {
|
|
return false
|
|
}
|
|
aVal.Set(bVal)
|
|
return true
|
|
}
|
|
|
|
// SetAny interprets the field names in defaults and looks up config
|
|
// values in the config passed in. Any values found in config will be
|
|
// set in the opt structure.
|
|
//
|
|
// opt must be a pointer to a struct. The struct should have entirely
|
|
// public fields. The field names are converted from CamelCase to
|
|
// snake_case and looked up in the config supplied or a
|
|
// `config:"field_name"` is looked up.
|
|
//
|
|
// If items are found then they are set directly if the correct type,
|
|
// otherwise they are converted to string and then converted from
|
|
// string to native types and set in opt.
|
|
//
|
|
// All the field types in the struct must implement fmt.Scanner.
|
|
func SetAny(config map[string]any, opt any) (err error) {
|
|
defaultItems, err := Items(opt)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, defaultItem := range defaultItems {
|
|
newValue := defaultItem.Value
|
|
if configValue, ok := config[defaultItem.Name]; ok {
|
|
if !setIfSameType(&newValue, configValue) {
|
|
// Convert the config value to be a string
|
|
stringConfigValue, err := InterfaceToString(configValue)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
newValue, err = setValue(newValue, stringConfigValue)
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't parse config item %q = %q as %T: %w", defaultItem.Name, stringConfigValue, defaultItem.Value, err)
|
|
}
|
|
}
|
|
}
|
|
defaultItem.Set(newValue)
|
|
}
|
|
return nil
|
|
}
|