1
0
mirror of https://github.com/rclone/rclone.git synced 2026-01-04 17:43:50 +00:00
Files
rclone/fs/config/config.go
Nick Craig-Wood 46078d391f config: make config file reads reload the config file if needed #4996
Before this change the config file needed to be explicitly reloaded.
This coupled the config file implementation with the backends
needlessly.

This change stats the config file to see if it needs to be reloaded on
every config file operation.

This allows us to remove calls to

- config.SaveConfig
- config.GetFresh

Which now makes the the only needed interface to the config file be
that provided by configmap.Map when rclone is not being configured.

This also adds tests for configfile
2021-03-14 16:03:35 +00:00

508 lines
14 KiB
Go

// Package config reads, writes and edits the config file and deals with command line flags
package config
import (
"context"
"encoding/json"
"fmt"
"log"
mathrand "math/rand"
"os"
"path/filepath"
"regexp"
"runtime"
"strings"
"time"
"github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/config/obscure"
"github.com/rclone/rclone/fs/fspath"
"github.com/rclone/rclone/fs/rc"
"github.com/rclone/rclone/lib/random"
)
const (
configFileName = "rclone.conf"
hiddenConfigFileName = "." + configFileName
// ConfigToken is the key used to store the token under
ConfigToken = "token"
// ConfigClientID is the config key used to store the client id
ConfigClientID = "client_id"
// ConfigClientSecret is the config key used to store the client secret
ConfigClientSecret = "client_secret"
// ConfigAuthURL is the config key used to store the auth server endpoint
ConfigAuthURL = "auth_url"
// ConfigTokenURL is the config key used to store the token server endpoint
ConfigTokenURL = "token_url"
// ConfigEncoding is the config key to change the encoding for a backend
ConfigEncoding = "encoding"
// ConfigEncodingHelp is the help for ConfigEncoding
ConfigEncodingHelp = "This sets the encoding for the backend.\n\nSee: the [encoding section in the overview](/overview/#encoding) for more info."
// ConfigAuthorize indicates that we just want "rclone authorize"
ConfigAuthorize = "config_authorize"
// ConfigAuthNoBrowser indicates that we do not want to open browser
ConfigAuthNoBrowser = "config_auth_no_browser"
)
// Storage defines an interface for loading and saving config to
// persistent storage. Rclone provides a default implementation to
// load and save to a config file when this is imported
//
// import "github.com/rclone/rclone/fs/config/configfile"
// configfile.LoadConfig(ctx)
type Storage interface {
// GetSectionList returns a slice of strings with names for all the
// sections
GetSectionList() []string
// HasSection returns true if section exists in the config file
HasSection(section string) bool
// DeleteSection removes the named section and all config from the
// config file
DeleteSection(section string)
// GetKeyList returns the keys in this section
GetKeyList(section string) []string
// GetValue returns the key in section with a found flag
GetValue(section string, key string) (value string, found bool)
// SetValue sets the value under key in section
SetValue(section string, key string, value string)
// DeleteKey removes the key under section
DeleteKey(section string, key string) bool
// Load the config from permanent storage
Load() error
// Save the config to permanent storage
Save() error
// Serialize the config into a string
Serialize() (string, error)
}
// Global
var (
// Data is the global config data structure
Data Storage = defaultStorage{}
// CacheDir points to the cache directory. Users of this
// should make a subdirectory and use MkdirAll() to create it
// and any parents.
CacheDir = makeCacheDir()
// ConfigPath points to the config file
ConfigPath = makeConfigPath()
// Password can be used to configure the random password generator
Password = random.Password
)
func init() {
// Set the function pointers up in fs
fs.ConfigFileGet = FileGetFlag
fs.ConfigFileSet = SetValueAndSave
}
// Return the path to the configuration file
func makeConfigPath() string {
// Use rclone.conf from rclone executable directory if already existing
exe, err := os.Executable()
if err == nil {
exedir := filepath.Dir(exe)
cfgpath := filepath.Join(exedir, configFileName)
_, err := os.Stat(cfgpath)
if err == nil {
return cfgpath
}
}
// Find user's home directory
homeDir, err := homedir.Dir()
// Find user's configuration directory.
// Prefer XDG config path, with fallback to $HOME/.config.
// See XDG Base Directory specification
// https://specifications.freedesktop.org/basedir-spec/latest/),
xdgdir := os.Getenv("XDG_CONFIG_HOME")
var cfgdir string
if xdgdir != "" {
// User's configuration directory for rclone is $XDG_CONFIG_HOME/rclone
cfgdir = filepath.Join(xdgdir, "rclone")
} else if homeDir != "" {
// User's configuration directory for rclone is $HOME/.config/rclone
cfgdir = filepath.Join(homeDir, ".config", "rclone")
}
// Use rclone.conf from user's configuration directory if already existing
var cfgpath string
if cfgdir != "" {
cfgpath = filepath.Join(cfgdir, configFileName)
_, err := os.Stat(cfgpath)
if err == nil {
return cfgpath
}
}
// Use .rclone.conf from user's home directory if already existing
var homeconf string
if homeDir != "" {
homeconf = filepath.Join(homeDir, hiddenConfigFileName)
_, err := os.Stat(homeconf)
if err == nil {
return homeconf
}
}
// Check to see if user supplied a --config variable or environment
// variable. We can't use pflag for this because it isn't initialised
// yet so we search the command line manually.
_, configSupplied := os.LookupEnv("RCLONE_CONFIG")
if !configSupplied {
for _, item := range os.Args {
if item == "--config" || strings.HasPrefix(item, "--config=") {
configSupplied = true
break
}
}
}
// If user's configuration directory was found, then try to create it
// and assume rclone.conf can be written there. If user supplied config
// then skip creating the directory since it will not be used.
if cfgpath != "" {
// cfgpath != "" implies cfgdir != ""
if configSupplied {
return cfgpath
}
err := os.MkdirAll(cfgdir, os.ModePerm)
if err == nil {
return cfgpath
}
}
// Assume .rclone.conf can be written to user's home directory.
if homeconf != "" {
return homeconf
}
// Default to ./.rclone.conf (current working directory) if everything else fails.
if !configSupplied {
fs.Errorf(nil, "Couldn't find home directory or read HOME or XDG_CONFIG_HOME environment variables.")
fs.Errorf(nil, "Defaulting to storing config in current directory.")
fs.Errorf(nil, "Use --config flag to workaround.")
fs.Errorf(nil, "Error was: %v", err)
}
return hiddenConfigFileName
}
// LoadConfig loads the config file
func LoadConfig(ctx context.Context) {
// Set RCLONE_CONFIG_DIR for backend config and subprocesses
_ = os.Setenv("RCLONE_CONFIG_DIR", filepath.Dir(ConfigPath))
// Load configuration file.
if err := Data.Load(); err == ErrorConfigFileNotFound {
fs.Logf(nil, "Config file %q not found - using defaults", ConfigPath)
} else if err != nil {
log.Fatalf("Failed to load config file %q: %v", ConfigPath, err)
} else {
fs.Debugf(nil, "Using config file from %q", ConfigPath)
}
}
// ErrorConfigFileNotFound is returned when the config file is not found
var ErrorConfigFileNotFound = errors.New("config file not found")
// SaveConfig calling function which saves configuration file.
// if SaveConfig returns error trying again after sleep.
func SaveConfig() {
ctx := context.Background()
ci := fs.GetConfig(ctx)
var err error
for i := 0; i < ci.LowLevelRetries+1; i++ {
if err = Data.Save(); err == nil {
return
}
waitingTimeMs := mathrand.Intn(1000)
time.Sleep(time.Duration(waitingTimeMs) * time.Millisecond)
}
log.Fatalf("Failed to save config after %d tries: %v", ci.LowLevelRetries, err)
return
}
// SetValueAndSave sets the key to the value and saves just that
// value in the config file. It loads the old config file in from
// disk first and overwrites the given value only.
func SetValueAndSave(name, key, value string) error {
// Set the value in config in case we fail to reload it
Data.SetValue(name, key, value)
// Save it again
SaveConfig()
return nil
}
// getWithDefault gets key out of section name returning defaultValue if not
// found.
func getWithDefault(name, key, defaultValue string) string {
value, found := Data.GetValue(name, key)
if !found {
return defaultValue
}
return value
}
// UpdateRemote adds the keyValues passed in to the remote of name.
// keyValues should be key, value pairs.
func UpdateRemote(ctx context.Context, name string, keyValues rc.Params, doObscure, noObscure bool) error {
if doObscure && noObscure {
return errors.New("can't use --obscure and --no-obscure together")
}
err := fspath.CheckConfigName(name)
if err != nil {
return err
}
ctx = suppressConfirm(ctx)
// Work out which options need to be obscured
needsObscure := map[string]struct{}{}
if !noObscure {
if fsType := FileGet(name, "type"); fsType != "" {
if ri, err := fs.Find(fsType); err != nil {
fs.Debugf(nil, "Couldn't find fs for type %q", fsType)
} else {
for _, opt := range ri.Options {
if opt.IsPassword {
needsObscure[opt.Name] = struct{}{}
}
}
}
} else {
fs.Debugf(nil, "UpdateRemote: Couldn't find fs type")
}
}
// Set the config
for k, v := range keyValues {
vStr := fmt.Sprint(v)
// Obscure parameter if necessary
if _, ok := needsObscure[k]; ok {
_, err := obscure.Reveal(vStr)
if err != nil || doObscure {
// If error => not already obscured, so obscure it
// or we are forced to obscure
vStr, err = obscure.Obscure(vStr)
if err != nil {
return errors.Wrap(err, "UpdateRemote: obscure failed")
}
}
}
Data.SetValue(name, k, vStr)
}
RemoteConfig(ctx, name)
SaveConfig()
return nil
}
// CreateRemote creates a new remote with name, provider and a list of
// parameters which are key, value pairs. If update is set then it
// adds the new keys rather than replacing all of them.
func CreateRemote(ctx context.Context, name string, provider string, keyValues rc.Params, doObscure, noObscure bool) error {
err := fspath.CheckConfigName(name)
if err != nil {
return err
}
// Delete the old config if it exists
Data.DeleteSection(name)
// Set the type
Data.SetValue(name, "type", provider)
// Set the remaining values
return UpdateRemote(ctx, name, keyValues, doObscure, noObscure)
}
// PasswordRemote adds the keyValues passed in to the remote of name.
// keyValues should be key, value pairs.
func PasswordRemote(ctx context.Context, name string, keyValues rc.Params) error {
ctx = suppressConfirm(ctx)
err := fspath.CheckConfigName(name)
if err != nil {
return err
}
for k, v := range keyValues {
keyValues[k] = obscure.MustObscure(fmt.Sprint(v))
}
return UpdateRemote(ctx, name, keyValues, false, true)
}
// JSONListProviders prints all the providers and options in JSON format
func JSONListProviders() error {
b, err := json.MarshalIndent(fs.Registry, "", " ")
if err != nil {
return errors.Wrap(err, "failed to marshal examples")
}
_, err = os.Stdout.Write(b)
if err != nil {
return errors.Wrap(err, "failed to write providers list")
}
return nil
}
// fsOption returns an Option describing the possible remotes
func fsOption() *fs.Option {
o := &fs.Option{
Name: "Storage",
Help: "Type of storage to configure.",
Default: "",
}
for _, item := range fs.Registry {
example := fs.OptionExample{
Value: item.Name,
Help: item.Description,
}
o.Examples = append(o.Examples, example)
}
o.Examples.Sort()
return o
}
// FileGetFlag gets the config key under section returning the
// the value and true if found and or ("", false) otherwise
func FileGetFlag(section, key string) (string, bool) {
return Data.GetValue(section, key)
}
// FileGet gets the config key under section returning the default if not set.
//
// It looks up defaults in the environment if they are present
func FileGet(section, key string) string {
var defaultVal string
envKey := fs.ConfigToEnv(section, key)
newValue, found := os.LookupEnv(envKey)
if found {
defaultVal = newValue
}
return getWithDefault(section, key, defaultVal)
}
// FileSet sets the key in section to value. It doesn't save
// the config file.
func FileSet(section, key, value string) {
if value != "" {
Data.SetValue(section, key, value)
} else {
FileDeleteKey(section, key)
}
}
// FileDeleteKey deletes the config key in the config file.
// It returns true if the key was deleted,
// or returns false if the section or key didn't exist.
func FileDeleteKey(section, key string) bool {
return Data.DeleteKey(section, key)
}
var matchEnv = regexp.MustCompile(`^RCLONE_CONFIG_(.*?)_TYPE=.*$`)
// FileSections returns the sections in the config file
// including any defined by environment variables.
func FileSections() []string {
sections := Data.GetSectionList()
for _, item := range os.Environ() {
matches := matchEnv.FindStringSubmatch(item)
if len(matches) == 2 {
sections = append(sections, strings.ToLower(matches[1]))
}
}
return sections
}
// DumpRcRemote dumps the config for a single remote
func DumpRcRemote(name string) (dump rc.Params) {
params := rc.Params{}
for _, key := range Data.GetKeyList(name) {
params[key] = FileGet(name, key)
}
return params
}
// DumpRcBlob dumps all the config as an unstructured blob suitable
// for the rc
func DumpRcBlob() (dump rc.Params) {
dump = rc.Params{}
for _, name := range Data.GetSectionList() {
dump[name] = DumpRcRemote(name)
}
return dump
}
// Dump dumps all the config as a JSON file
func Dump() error {
dump := DumpRcBlob()
b, err := json.MarshalIndent(dump, "", " ")
if err != nil {
return errors.Wrap(err, "failed to marshal config dump")
}
_, err = os.Stdout.Write(b)
if err != nil {
return errors.Wrap(err, "failed to write config dump")
}
return nil
}
// makeCacheDir returns a directory to use for caching.
//
// Code borrowed from go stdlib until it is made public
func makeCacheDir() (dir string) {
// Compute default location.
switch runtime.GOOS {
case "windows":
dir = os.Getenv("LocalAppData")
case "darwin":
dir = os.Getenv("HOME")
if dir != "" {
dir += "/Library/Caches"
}
case "plan9":
dir = os.Getenv("home")
if dir != "" {
// Plan 9 has no established per-user cache directory,
// but $home/lib/xyz is the usual equivalent of $HOME/.xyz on Unix.
dir += "/lib/cache"
}
default: // Unix
// https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
dir = os.Getenv("XDG_CACHE_HOME")
if dir == "" {
dir = os.Getenv("HOME")
if dir != "" {
dir += "/.cache"
}
}
}
// if no dir found then use TempDir - we will have a cachedir!
if dir == "" {
dir = os.TempDir()
}
return filepath.Join(dir, "rclone")
}