rclone/fs/registry.go
Nick Craig-Wood fef1b61585 fs: fix --dump filters not always appearing
Before this fix, we initialised the options blocks in a random order.
This meant that there was a 50/50 chance whether --dump filters would
show the filters or not as it was depending on the "main" block having
being read first to set the Dump flags.

This initialises the options blocks in a defined order which is
alphabetically but with main first which fixes the problem.
2024-09-13 15:52:51 +01:00

586 lines
16 KiB
Go

// Filesystem registry and backend options
package fs
import (
"context"
"encoding/json"
"fmt"
"reflect"
"regexp"
"sort"
"strings"
"sync"
"github.com/rclone/rclone/fs/config/configmap"
"github.com/rclone/rclone/fs/config/configstruct"
"github.com/rclone/rclone/lib/errcount"
)
// Registry of filesystems
var Registry []*RegInfo
// optDescription is a basic description option
var optDescription = Option{
Name: "description",
Help: "Description of the remote.",
Default: "",
Advanced: true,
}
// RegInfo provides information about a filesystem
type RegInfo struct {
// Name of this fs
Name string
// Description of this fs - defaults to Name
Description string
// Prefix for command line flags for this fs - defaults to Name if not set
Prefix string
// Create a new file system. If root refers to an existing
// object, then it should return an Fs which points to
// the parent of that object and ErrorIsFile.
NewFs func(ctx context.Context, name string, root string, config configmap.Mapper) (Fs, error) `json:"-"`
// Function to call to help with config - see docs for ConfigIn for more info
Config func(ctx context.Context, name string, m configmap.Mapper, configIn ConfigIn) (*ConfigOut, error) `json:"-"`
// Options for the Fs configuration
Options Options
// The command help, if any
CommandHelp []CommandHelp
// Aliases - other names this backend is known by
Aliases []string
// Hide - if set don't show in the configurator
Hide bool
// MetadataInfo help about the metadata in use in this backend
MetadataInfo *MetadataInfo
}
// FileName returns the on disk file name for this backend
func (ri *RegInfo) FileName() string {
return strings.ReplaceAll(ri.Name, " ", "")
}
// Options is a slice of configuration Option for a backend
type Options []Option
// Add more options returning a new options slice
func (os Options) Add(newOptions Options) Options {
return append(os, newOptions...)
}
// AddPrefix adds more options with a prefix returning a new options slice
func (os Options) AddPrefix(newOptions Options, prefix string, groups string) Options {
for _, opt := range newOptions {
// opt is a copy so can modify
opt.Name = prefix + "_" + opt.Name
opt.Groups = groups
os = append(os, opt)
}
return os
}
// Set the default values for the options
func (os Options) setValues() {
for i := range os {
o := &os[i]
if o.Default == nil {
o.Default = ""
}
// Create options for Enums
if do, ok := o.Default.(Choices); ok && len(o.Examples) == 0 {
o.Exclusive = true
o.Required = true
o.Examples = make(OptionExamples, len(do.Choices()))
for i, choice := range do.Choices() {
o.Examples[i].Value = choice
}
}
}
}
// Get the Option corresponding to name or return nil if not found
func (os Options) Get(name string) *Option {
for i := range os {
opt := &os[i]
if opt.Name == name {
return opt
}
}
return nil
}
// SetDefault sets the default for the Option corresponding to name
//
// Writes an ERROR level log if the option is not found
func (os Options) SetDefault(name string, def any) Options {
opt := os.Get(name)
if opt == nil {
Errorf(nil, "Couldn't find option %q to SetDefault on", name)
} else {
opt.Default = def
}
return os
}
// Overridden discovers which config items have been overridden in the
// configmap passed in, either by the config string, command line
// flags or environment variables
func (os Options) Overridden(m *configmap.Map) configmap.Simple {
var overridden = configmap.Simple{}
for i := range os {
opt := &os[i]
value, isSet := m.GetPriority(opt.Name, configmap.PriorityNormal)
if isSet {
overridden.Set(opt.Name, value)
}
}
return overridden
}
// NonDefault discovers which config values aren't at their default
func (os Options) NonDefault(m configmap.Getter) configmap.Simple {
var nonDefault = configmap.Simple{}
for i := range os {
opt := &os[i]
value, isSet := m.Get(opt.Name)
if !isSet {
continue
}
defaultValue := fmt.Sprint(opt.Default)
if value != defaultValue {
nonDefault.Set(opt.Name, value)
}
}
return nonDefault
}
// HasAdvanced discovers if any options have an Advanced setting
func (os Options) HasAdvanced() bool {
for i := range os {
opt := &os[i]
if opt.Advanced {
return true
}
}
return false
}
// OptionVisibility controls whether the options are visible in the
// configurator or the command line.
type OptionVisibility byte
// Constants Option.Hide
const (
OptionHideCommandLine OptionVisibility = 1 << iota
OptionHideConfigurator
OptionHideBoth = OptionHideCommandLine | OptionHideConfigurator
)
// Option is describes an option for the config wizard
//
// This also describes command line options and environment variables.
//
// It is also used to describe options for the API.
//
// To create a multiple-choice option, specify the possible values
// in the Examples property. Whether the option's value is required
// to be one of these depends on other properties:
// - Default is to allow any value, either from specified examples,
// or any other value. To restrict exclusively to the specified
// examples, also set Exclusive=true.
// - If empty string should not be allowed then set Required=true,
// and do not set Default.
type Option struct {
Name string // name of the option in snake_case
FieldName string // name of the field used in the rc JSON - will be auto filled normally
Help string // help, start with a single sentence on a single line that will be extracted for command line help
Groups string `json:",omitempty"` // groups this option belongs to - comma separated string for options classification
Provider string `json:",omitempty"` // set to filter on provider
Default interface{} // default value, nil => "", if set (and not to nil or "") then Required does nothing
Value interface{} // value to be set by flags
Examples OptionExamples `json:",omitempty"` // predefined values that can be selected from list (multiple-choice option)
ShortOpt string `json:",omitempty"` // the short option for this if required
Hide OptionVisibility // set this to hide the config from the configurator or the command line
Required bool // this option is required, meaning value cannot be empty unless there is a default
IsPassword bool // set if the option is a password
NoPrefix bool // set if the option for this should not use the backend prefix
Advanced bool // set if this is an advanced config option
Exclusive bool // set if the answer can only be one of the examples (empty string allowed unless Required or Default is set)
Sensitive bool // set if this option should be redacted when using rclone config redacted
}
// BaseOption is an alias for Option used internally
type BaseOption Option
// MarshalJSON turns an Option into JSON
//
// It adds some generated fields for ease of use
// - DefaultStr - a string rendering of Default
// - ValueStr - a string rendering of Value
// - Type - the type of the option
func (o *Option) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
BaseOption
DefaultStr string
ValueStr string
Type string
}{
BaseOption: BaseOption(*o),
DefaultStr: fmt.Sprint(o.Default),
ValueStr: o.String(),
Type: o.Type(),
})
}
// GetValue gets the current value which is the default if not set
func (o *Option) GetValue() interface{} {
val := o.Value
if val == nil {
val = o.Default
if val == nil {
val = ""
}
}
return val
}
// IsDefault returns true if the value is the default value
func (o *Option) IsDefault() bool {
if o.Value == nil {
return true
}
Default := o.Default
if Default == nil {
Default = ""
}
return reflect.DeepEqual(o.Value, Default)
}
// String turns Option into a string
func (o *Option) String() string {
v := o.GetValue()
if stringArray, isStringArray := v.([]string); isStringArray {
// Treat empty string array as empty string
// This is to make the default value of the option help nice
if len(stringArray) == 0 {
return ""
}
// Encode string arrays as JSON
// The default Go encoding can't be decoded uniquely
buf, err := json.Marshal(stringArray)
if err != nil {
Errorf(nil, "Can't encode default value for %q key - ignoring: %v", o.Name, err)
return "[]"
}
return string(buf)
}
return fmt.Sprint(v)
}
// Set an Option from a string
func (o *Option) Set(s string) (err error) {
v := o.GetValue()
if stringArray, isStringArray := v.([]string); isStringArray {
if stringArray == nil {
stringArray = []string{}
}
// If this is still the default value then overwrite the defaults
if reflect.ValueOf(o.Default).Pointer() == reflect.ValueOf(v).Pointer() {
stringArray = []string{}
}
o.Value = append(stringArray, s)
return nil
}
newValue, err := configstruct.StringToInterface(v, s)
if err != nil {
return err
}
o.Value = newValue
return nil
}
type typer interface {
Type() string
}
// Type of the value
func (o *Option) Type() string {
v := o.GetValue()
// Try to call Type method on non-pointer
if do, ok := v.(typer); ok {
return do.Type()
}
// Special case []string
if _, isStringArray := v.([]string); isStringArray {
return "stringArray"
}
return reflect.TypeOf(v).Name()
}
// FlagName for the option
func (o *Option) FlagName(prefix string) string {
name := strings.ReplaceAll(o.Name, "_", "-") // convert snake_case to kebab-case
if !o.NoPrefix {
name = prefix + "-" + name
}
return name
}
// EnvVarName for the option
func (o *Option) EnvVarName(prefix string) string {
return OptionToEnv(prefix + "-" + o.Name)
}
// Copy makes a shallow copy of the option
func (o *Option) Copy() *Option {
copy := new(Option)
*copy = *o
return copy
}
// OptionExamples is a slice of examples
type OptionExamples []OptionExample
// Len is part of sort.Interface.
func (os OptionExamples) Len() int { return len(os) }
// Swap is part of sort.Interface.
func (os OptionExamples) Swap(i, j int) { os[i], os[j] = os[j], os[i] }
// Less is part of sort.Interface.
func (os OptionExamples) Less(i, j int) bool { return os[i].Help < os[j].Help }
// Sort sorts an OptionExamples
func (os OptionExamples) Sort() { sort.Sort(os) }
// OptionExample describes an example for an Option
type OptionExample struct {
Value string
Help string
Provider string `json:",omitempty"`
}
// Register a filesystem
//
// Fs modules should use this in an init() function
func Register(info *RegInfo) {
info.Options.setValues()
if info.Prefix == "" {
info.Prefix = info.Name
}
info.Options = append(info.Options, optDescription)
Registry = append(Registry, info)
for _, alias := range info.Aliases {
// Copy the info block and rename and hide the alias and options
aliasInfo := *info
aliasInfo.Name = alias
aliasInfo.Prefix = alias
aliasInfo.Hide = true
aliasInfo.Options = append(Options(nil), info.Options...)
for i := range aliasInfo.Options {
aliasInfo.Options[i].Hide = OptionHideBoth
}
Registry = append(Registry, &aliasInfo)
}
}
// Find looks for a RegInfo object for the name passed in. The name
// can be either the Name or the Prefix.
//
// Services are looked up in the config file
func Find(name string) (*RegInfo, error) {
for _, item := range Registry {
if item.Name == name || item.Prefix == name || item.FileName() == name {
return item, nil
}
}
return nil, fmt.Errorf("didn't find backend called %q", name)
}
// MustFind looks for an Info object for the type name passed in
//
// Services are looked up in the config file.
//
// Exits with a fatal error if not found
func MustFind(name string) *RegInfo {
fs, err := Find(name)
if err != nil {
Fatalf(nil, "Failed to find remote: %v", err)
}
return fs
}
// OptionsInfo holds info about an block of options
type OptionsInfo struct {
Name string // name of this options block for the rc
Opt interface{} // pointer to a struct to set the options in
Options Options // description of the options
Reload func(context.Context) error // if not nil, call when options changed and on init
}
// OptionsRegistry is a registry of global options
var OptionsRegistry = map[string]OptionsInfo{}
// RegisterGlobalOptions registers global options to be made into
// command line options, rc options and environment variable reading.
//
// Packages which need global options should use this in an init() function
func RegisterGlobalOptions(oi OptionsInfo) {
oi.Options.setValues()
OptionsRegistry[oi.Name] = oi
if oi.Opt != nil && oi.Options != nil {
err := oi.Check()
if err != nil {
Fatalf(nil, "%v", err)
}
}
// Load the default values into the options.
//
// These will be from the ultimate defaults or environment
// variables.
//
// The flags haven't been processed yet so this will be run
// again when the flags are ready.
err := oi.load()
if err != nil {
Fatalf(nil, "Failed to load %q default values: %v", oi.Name, err)
}
}
var optionName = regexp.MustCompile(`^[a-z0-9_]+$`)
// Check ensures that for every element of oi.Options there is a field
// in oi.Opt that matches it.
//
// It also sets Option.FieldName to be the name of the field for use
// in JSON.
func (oi *OptionsInfo) Check() error {
errCount := errcount.New()
items, err := configstruct.Items(oi.Opt)
if err != nil {
return err
}
itemsByName := map[string]*configstruct.Item{}
for i := range items {
item := &items[i]
itemsByName[item.Name] = item
if !optionName.MatchString(item.Name) {
err = fmt.Errorf("invalid name in `config:%q` in Options struct", item.Name)
errCount.Add(err)
Errorf(nil, "%s", err)
}
}
for i := range oi.Options {
option := &oi.Options[i]
// Check name is correct
if !optionName.MatchString(option.Name) {
err = fmt.Errorf("invalid Name: %q", option.Name)
errCount.Add(err)
Errorf(nil, "%s", err)
continue
}
// Check item exists
item, found := itemsByName[option.Name]
if !found {
err = fmt.Errorf("key %q in OptionsInfo not found in Options struct", option.Name)
errCount.Add(err)
Errorf(nil, "%s", err)
continue
}
// Check type
optType := fmt.Sprintf("%T", option.Default)
itemType := fmt.Sprintf("%T", item.Value)
if optType != itemType {
err = fmt.Errorf("key %q in has type %q in OptionsInfo.Default but type %q in Options struct", option.Name, optType, itemType)
//errCount.Add(err)
Errorf(nil, "%s", err)
}
// Set FieldName
option.FieldName = item.Field
}
return errCount.Err(fmt.Sprintf("internal error: options block %q", oi.Name))
}
// load the defaults from the options
//
// Reload the options if required
func (oi *OptionsInfo) load() error {
if oi.Options == nil {
Errorf(nil, "No options defined for config block %q", oi.Name)
return nil
}
m := ConfigMap("", oi.Options, "", nil)
err := configstruct.Set(m, oi.Opt)
if err != nil {
return fmt.Errorf("failed to initialise %q options: %w", oi.Name, err)
}
if oi.Reload != nil {
err = oi.Reload(context.Background())
if err != nil {
return fmt.Errorf("failed to reload %q options: %w", oi.Name, err)
}
}
return nil
}
// GlobalOptionsInit initialises the defaults of global options to
// their values read from the options, environment variables and
// command line parameters.
func GlobalOptionsInit() error {
var keys []string
for key := range OptionsRegistry {
keys = append(keys, key)
}
sort.Slice(keys, func(i, j int) bool {
// Sort alphabetically, but with "main" first
if keys[i] == "main" {
return true
}
if keys[j] == "main" {
return false
}
return keys[i] < keys[j]
})
for _, key := range keys {
opt := OptionsRegistry[key]
err := opt.load()
if err != nil {
return err
}
}
return nil
}
// Type returns a textual string to identify the type of the remote
func Type(f Fs) string {
typeName := fmt.Sprintf("%T", f)
typeName = strings.TrimPrefix(typeName, "*")
typeName = strings.TrimSuffix(typeName, ".Fs")
return typeName
}
var (
typeToRegInfoMu sync.Mutex
typeToRegInfo = map[string]*RegInfo{}
)
// Add the RegInfo to the reverse map
func addReverse(f Fs, fsInfo *RegInfo) {
typeToRegInfoMu.Lock()
defer typeToRegInfoMu.Unlock()
typeToRegInfo[Type(f)] = fsInfo
}
// FindFromFs finds the *RegInfo used to create this Fs, provided
// it was created by fs.NewFs or cache.Get
//
// It returns nil if not found
func FindFromFs(f Fs) *RegInfo {
typeToRegInfoMu.Lock()
defer typeToRegInfoMu.Unlock()
return typeToRegInfo[Type(f)]
}