config: make config file system pluggable

If you are using rclone a library you can decide to use the rclone
config file system or not by calling

    configfile.LoadConfig(ctx)

If you don't you will need to set `config.Data` to an implementation
of `config.Storage`.

Other changes
- change interface of config.FileGet to remove unused default
- remove MustValue from config.Storage interface
- change GetValue to return string or bool like elsewhere in rclone
- implement a default config file system which panics with helpful error
- implement getWithDefault to replace the removed MustValue
- don't embed goconfig.ConfigFile so we can change the methods
This commit is contained in:
Nick Craig-Wood 2021-03-10 15:40:34 +00:00
parent c95b580478
commit 1fed2d910c
19 changed files with 380 additions and 235 deletions

View file

@ -11,6 +11,7 @@ import (
_ "github.com/rclone/rclone/backend/local" // pull in test backend _ "github.com/rclone/rclone/backend/local" // pull in test backend
"github.com/rclone/rclone/fs" "github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/config" "github.com/rclone/rclone/fs/config"
"github.com/rclone/rclone/fs/config/configfile"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -19,7 +20,7 @@ var (
) )
func prepare(t *testing.T, root string) { func prepare(t *testing.T, root string) {
config.LoadConfig(context.Background()) configfile.LoadConfig(context.Background())
// Configure the remote // Configure the remote
config.FileSet(remoteName, "type", "alias") config.FileSet(remoteName, "type", "alias")

View file

@ -892,7 +892,7 @@ func (r *run) newCacheFs(t *testing.T, remote, id string, needRemote, purge bool
m.Set("type", "cache") m.Set("type", "cache")
m.Set("remote", localRemote+":"+filepath.Join(os.TempDir(), localRemote)) m.Set("remote", localRemote+":"+filepath.Join(os.TempDir(), localRemote))
} else { } else {
remoteType := config.FileGet(remote, "type", "") remoteType := config.FileGet(remote, "type")
if remoteType == "" { if remoteType == "" {
t.Skipf("skipped due to invalid remote type for %v", remote) t.Skipf("skipped due to invalid remote type for %v", remote)
return nil, nil return nil, nil
@ -903,14 +903,14 @@ func (r *run) newCacheFs(t *testing.T, remote, id string, needRemote, purge bool
m.Set("password", cryptPassword1) m.Set("password", cryptPassword1)
m.Set("password2", cryptPassword2) m.Set("password2", cryptPassword2)
} }
remoteRemote := config.FileGet(remote, "remote", "") remoteRemote := config.FileGet(remote, "remote")
if remoteRemote == "" { if remoteRemote == "" {
t.Skipf("skipped due to invalid remote wrapper for %v", remote) t.Skipf("skipped due to invalid remote wrapper for %v", remote)
return nil, nil return nil, nil
} }
remoteRemoteParts := strings.Split(remoteRemote, ":") remoteRemoteParts := strings.Split(remoteRemote, ":")
remoteWrapping := remoteRemoteParts[0] remoteWrapping := remoteRemoteParts[0]
remoteType := config.FileGet(remoteWrapping, "type", "") remoteType := config.FileGet(remoteWrapping, "type")
if remoteType != "cache" { if remoteType != "cache" {
t.Skipf("skipped due to invalid remote type for %v: '%v'", remoteWrapping, remoteType) t.Skipf("skipped due to invalid remote type for %v: '%v'", remoteWrapping, remoteType)
return nil, nil return nil, nil

View file

@ -15,7 +15,7 @@ import (
"time" "time"
"github.com/rclone/rclone/fs" "github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/config" "github.com/rclone/rclone/fs/config/configfile"
"github.com/rclone/rclone/fs/config/configmap" "github.com/rclone/rclone/fs/config/configmap"
"github.com/rclone/rclone/fstest" "github.com/rclone/rclone/fstest"
"github.com/rclone/rclone/lib/rest" "github.com/rclone/rclone/lib/rest"
@ -47,7 +47,7 @@ func prepareServer(t *testing.T) (configmap.Simple, func()) {
ts := httptest.NewServer(handler) ts := httptest.NewServer(handler)
// Configure the remote // Configure the remote
config.LoadConfig(context.Background()) configfile.LoadConfig(context.Background())
// fs.Config.LogLevel = fs.LogLevelDebug // fs.Config.LogLevel = fs.LogLevelDebug
// fs.Config.DumpHeaders = true // fs.Config.DumpHeaders = true
// fs.Config.DumpBodies = true // fs.Config.DumpBodies = true

View file

@ -25,6 +25,7 @@ import (
"github.com/rclone/rclone/fs" "github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/accounting" "github.com/rclone/rclone/fs/accounting"
"github.com/rclone/rclone/fs/cache" "github.com/rclone/rclone/fs/cache"
"github.com/rclone/rclone/fs/config/configfile"
"github.com/rclone/rclone/fs/config/configflags" "github.com/rclone/rclone/fs/config/configflags"
"github.com/rclone/rclone/fs/config/flags" "github.com/rclone/rclone/fs/config/flags"
"github.com/rclone/rclone/fs/filter" "github.com/rclone/rclone/fs/filter"
@ -382,6 +383,9 @@ func initConfig() {
// Finish parsing any command line flags // Finish parsing any command line flags
configflags.SetFlags(ci) configflags.SetFlags(ci)
// Load the config
configfile.LoadConfig(ctx)
// Hide console window // Hide console window
if ci.NoConsole { if ci.NoConsole {
terminal.HideConsole() terminal.HideConsole()

View file

@ -41,7 +41,7 @@ When uses with the -l flag it lists the types too.
} }
for _, remote := range remotes { for _, remote := range remotes {
if listLong { if listLong {
remoteType := config.FileGet(remote, "type", "UNKNOWN") remoteType := config.FileGet(remote, "type")
fmt.Printf("%-*s %s\n", maxlen+1, remote+":", remoteType) fmt.Printf("%-*s %s\n", maxlen+1, remote+":", remoteType)
} else { } else {
fmt.Printf("%s:\n", remote) fmt.Printf("%s:\n", remote)

View file

@ -13,6 +13,7 @@ import (
_ "github.com/rclone/rclone/cmd/cmount" _ "github.com/rclone/rclone/cmd/cmount"
_ "github.com/rclone/rclone/cmd/mount" _ "github.com/rclone/rclone/cmd/mount"
_ "github.com/rclone/rclone/cmd/mount2" _ "github.com/rclone/rclone/cmd/mount2"
"github.com/rclone/rclone/fs/config/configfile"
"github.com/rclone/rclone/fs/rc" "github.com/rclone/rclone/fs/rc"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -20,6 +21,7 @@ import (
func TestRc(t *testing.T) { func TestRc(t *testing.T) {
ctx := context.Background() ctx := context.Background()
configfile.LoadConfig(ctx)
mount := rc.Calls.Get("mount/mount") mount := rc.Calls.Get("mount/mount")
assert.NotNil(t, mount) assert.NotNil(t, mount)
unmount := rc.Calls.Get("mount/unmount") unmount := rc.Calls.Get("mount/unmount")

View file

@ -13,12 +13,12 @@ import (
"github.com/anacrolix/dms/soap" "github.com/anacrolix/dms/soap"
"github.com/rclone/rclone/fs/config/configfile"
"github.com/rclone/rclone/vfs" "github.com/rclone/rclone/vfs"
_ "github.com/rclone/rclone/backend/local" _ "github.com/rclone/rclone/backend/local"
"github.com/rclone/rclone/cmd/serve/dlna/dlnaflags" "github.com/rclone/rclone/cmd/serve/dlna/dlnaflags"
"github.com/rclone/rclone/fs" "github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/config"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -41,7 +41,7 @@ func startServer(t *testing.T, f fs.Fs) {
} }
func TestInit(t *testing.T) { func TestInit(t *testing.T) {
config.LoadConfig(context.Background()) configfile.LoadConfig(context.Background())
f, err := fs.NewFs(context.Background(), "testdata/files") f, err := fs.NewFs(context.Background(), "testdata/files")
l, _ := f.List(context.Background(), "") l, _ := f.List(context.Background(), "")

View file

@ -12,7 +12,7 @@ import (
_ "github.com/rclone/rclone/backend/local" _ "github.com/rclone/rclone/backend/local"
"github.com/rclone/rclone/cmd/serve/httplib" "github.com/rclone/rclone/cmd/serve/httplib"
"github.com/rclone/rclone/fs" "github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/config" "github.com/rclone/rclone/fs/config/configfile"
"github.com/rclone/rclone/fs/filter" "github.com/rclone/rclone/fs/filter"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -61,7 +61,7 @@ var (
func TestInit(t *testing.T) { func TestInit(t *testing.T) {
ctx := context.Background() ctx := context.Background()
// Configure the remote // Configure the remote
config.LoadConfig(context.Background()) configfile.LoadConfig(context.Background())
// fs.Config.LogLevel = fs.LogLevelDebug // fs.Config.LogLevel = fs.LogLevelDebug
// fs.Config.DumpHeaders = true // fs.Config.DumpHeaders = true
// fs.Config.DumpBodies = true // fs.Config.DumpBodies = true

View file

@ -1,6 +1,7 @@
package restic package restic
import ( import (
"context"
"crypto/rand" "crypto/rand"
"encoding/hex" "encoding/hex"
"io" "io"
@ -12,6 +13,7 @@ import (
"github.com/rclone/rclone/cmd" "github.com/rclone/rclone/cmd"
"github.com/rclone/rclone/cmd/serve/httplib/httpflags" "github.com/rclone/rclone/cmd/serve/httplib/httpflags"
"github.com/rclone/rclone/fs/config/configfile"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -63,6 +65,8 @@ func createOverwriteDeleteSeq(t testing.TB, path string) []TestRequest {
// TestResticHandler runs tests on the restic handler code, especially in append-only mode. // TestResticHandler runs tests on the restic handler code, especially in append-only mode.
func TestResticHandler(t *testing.T) { func TestResticHandler(t *testing.T) {
ctx := context.Background()
configfile.LoadConfig(ctx)
buf := make([]byte, 32) buf := make([]byte, 32)
_, err := io.ReadFull(rand.Reader, buf) _, err := io.ReadFull(rand.Reader, buf)
require.NoError(t, err) require.NoError(t, err)

View file

@ -64,7 +64,12 @@ const (
ConfigAuthNoBrowser = "config_auth_no_browser" ConfigAuthNoBrowser = "config_auth_no_browser"
) )
// Storage defines an interface for loading and saving the config file. // 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 { type Storage interface {
// GetSectionList returns a slice of strings with names for all the // GetSectionList returns a slice of strings with names for all the
// sections // sections
@ -80,11 +85,8 @@ type Storage interface {
// GetKeyList returns the keys in this section // GetKeyList returns the keys in this section
GetKeyList(section string) []string GetKeyList(section string) []string
// GetValue returns the key in section or an error if not found // GetValue returns the key in section with a found flag
GetValue(section string, key string) (string, error) GetValue(section string, key string) (value string, found bool)
// MustValue returns the key in section returning defaultValue if not set
MustValue(section string, key string, defaultValue ...string) string
// SetValue sets the value under key in section // SetValue sets the value under key in section
SetValue(section string, key string, value string) SetValue(section string, key string, value string)
@ -104,8 +106,8 @@ type Storage interface {
// Global // Global
var ( var (
// configFile is the global config data structure. Don't read it directly, use Data // Data is the global config data structure
Data Storage Data Storage = defaultStorage{}
// CacheDir points to the cache directory. Users of this // CacheDir points to the cache directory. Users of this
// should make a subdirectory and use MkdirAll() to create it // should make a subdirectory and use MkdirAll() to create it
@ -286,6 +288,16 @@ func SetValueAndSave(name, key, value string) error {
return nil 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
}
// FileGetFresh reads the config key under section return the value or // FileGetFresh reads the config key under section return the value or
// an error if the config file was not found or that value couldn't be // an error if the config file was not found or that value couldn't be
// read. // read.
@ -293,7 +305,11 @@ func FileGetFresh(section, key string) (value string, err error) {
if err := Data.Load(); err != nil { if err := Data.Load(); err != nil {
return "", err return "", err
} }
return Data.GetValue(section, key) value, found := Data.GetValue(section, key)
if !found {
return "", errors.New("value not found")
}
return value, nil
} }
// ShowRemotes shows an overview of the config file // ShowRemotes shows an overview of the config file
@ -583,7 +599,7 @@ func matchProvider(providerConfig, provider string) bool {
// ChooseOption asks the user to choose an option // ChooseOption asks the user to choose an option
func ChooseOption(o *fs.Option, name string) string { func ChooseOption(o *fs.Option, name string) string {
var subProvider = Data.MustValue(name, fs.ConfigProvider, "") var subProvider = getWithDefault(name, fs.ConfigProvider, "")
fmt.Println(o.Help) fmt.Println(o.Help)
if o.IsPassword { if o.IsPassword {
actions := []string{"yYes type in my own password", "gGenerate random password"} actions := []string{"yYes type in my own password", "gGenerate random password"}
@ -832,7 +848,7 @@ func editOptions(ri *fs.RegInfo, name string, isNew bool) {
if option.Advanced != advanced { if option.Advanced != advanced {
continue continue
} }
subProvider := Data.MustValue(name, fs.ConfigProvider, "") subProvider := getWithDefault(name, fs.ConfigProvider, "")
if matchProvider(option.Provider, subProvider) && isVisible { if matchProvider(option.Provider, subProvider) && isVisible {
if !isNew { if !isNew {
fmt.Printf("Value %q = %q\n", option.Name, FileGet(name, option.Name)) fmt.Printf("Value %q = %q\n", option.Name, FileGet(name, option.Name))
@ -902,7 +918,7 @@ func copyRemote(name string) string {
newName := NewRemoteName() newName := NewRemoteName()
// Copy the keys // Copy the keys
for _, key := range Data.GetKeyList(name) { for _, key := range Data.GetKeyList(name) {
value := Data.MustValue(name, key, "") value := getWithDefault(name, key, "")
Data.SetValue(newName, key, value) Data.SetValue(newName, key, value)
} }
return newName return newName
@ -1026,21 +1042,20 @@ func Authorize(ctx context.Context, args []string, noAutoBrowser bool) {
// FileGetFlag gets the config key under section returning the // FileGetFlag gets the config key under section returning the
// the value and true if found and or ("", false) otherwise // the value and true if found and or ("", false) otherwise
func FileGetFlag(section, key string) (string, bool) { func FileGetFlag(section, key string) (string, bool) {
newValue, err := Data.GetValue(section, key) return Data.GetValue(section, key)
return newValue, err == nil
} }
// FileGet gets the config key under section returning the // FileGet gets the config key under section returning the default if not set.
// default or empty string if not set.
// //
// It looks up defaults in the environment if they are present // It looks up defaults in the environment if they are present
func FileGet(section, key string, defaultVal ...string) string { func FileGet(section, key string) string {
var defaultVal string
envKey := fs.ConfigToEnv(section, key) envKey := fs.ConfigToEnv(section, key)
newValue, found := os.LookupEnv(envKey) newValue, found := os.LookupEnv(envKey)
if found { if found {
defaultVal = []string{newValue} defaultVal = newValue
} }
return Data.MustValue(section, key, defaultVal...) return getWithDefault(section, key, defaultVal)
} }
// FileSet sets the key in section to value. It doesn't save // FileSet sets the key in section to value. It doesn't save

View file

@ -0,0 +1,29 @@
package config
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func TestMatchProvider(t *testing.T) {
for _, test := range []struct {
config string
provider string
want bool
}{
{"", "", true},
{"one", "one", true},
{"one,two", "two", true},
{"one,two,three", "two", true},
{"one", "on", false},
{"one,two,three", "tw", false},
{"!one,two,three", "two", false},
{"!one,two,three", "four", true},
} {
what := fmt.Sprintf("%q,%q", test.config, test.provider)
got := matchProvider(test.config, test.provider)
assert.Equal(t, test.want, got, what)
}
}

View file

@ -1,4 +1,4 @@
package config package config_test
import ( import (
"bytes" "bytes"
@ -9,6 +9,8 @@ import (
"testing" "testing"
"github.com/rclone/rclone/fs" "github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/config"
"github.com/rclone/rclone/fs/config/configfile"
"github.com/rclone/rclone/fs/config/obscure" "github.com/rclone/rclone/fs/config/obscure"
"github.com/rclone/rclone/fs/rc" "github.com/rclone/rclone/fs/rc"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -18,7 +20,7 @@ import (
func testConfigFile(t *testing.T, configFileName string) func() { func testConfigFile(t *testing.T, configFileName string) func() {
ctx := context.Background() ctx := context.Background()
ci := fs.GetConfig(ctx) ci := fs.GetConfig(ctx)
configKey = nil // reset password config.ClearConfigPassword()
_ = os.Unsetenv("_RCLONE_CONFIG_KEY_FILE") _ = os.Unsetenv("_RCLONE_CONFIG_KEY_FILE")
_ = os.Unsetenv("RCLONE_CONFIG_PASS") _ = os.Unsetenv("RCLONE_CONFIG_PASS")
// create temp config file // create temp config file
@ -29,18 +31,17 @@ func testConfigFile(t *testing.T, configFileName string) func() {
// temporarily adapt configuration // temporarily adapt configuration
oldOsStdout := os.Stdout oldOsStdout := os.Stdout
oldConfigPath := ConfigPath oldConfigPath := config.ConfigPath
oldConfig := *ci oldConfig := *ci
oldConfigFile := Data oldConfigFile := config.Data
oldReadLine := ReadLine oldReadLine := config.ReadLine
oldPassword := Password oldPassword := config.Password
os.Stdout = nil os.Stdout = nil
ConfigPath = path config.ConfigPath = path
ci = &fs.ConfigInfo{} ci = &fs.ConfigInfo{}
Data = nil
LoadConfig(ctx) configfile.LoadConfig(ctx)
assert.Equal(t, []string{}, Data.GetSectionList()) assert.Equal(t, []string{}, config.Data.GetSectionList())
// Fake a remote // Fake a remote
fs.Register(&fs.RegInfo{ fs.Register(&fs.RegInfo{
@ -65,11 +66,11 @@ func testConfigFile(t *testing.T, configFileName string) func() {
assert.NoError(t, err) assert.NoError(t, err)
os.Stdout = oldOsStdout os.Stdout = oldOsStdout
ConfigPath = oldConfigPath config.ConfigPath = oldConfigPath
ReadLine = oldReadLine config.ReadLine = oldReadLine
Password = oldPassword config.Password = oldPassword
*ci = oldConfig *ci = oldConfig
Data = oldConfigFile config.Data = oldConfigFile
_ = os.Unsetenv("_RCLONE_CONFIG_KEY_FILE") _ = os.Unsetenv("_RCLONE_CONFIG_KEY_FILE")
_ = os.Unsetenv("RCLONE_CONFIG_PASS") _ = os.Unsetenv("RCLONE_CONFIG_PASS")
@ -91,7 +92,7 @@ func TestCRUD(t *testing.T) {
ctx := context.Background() ctx := context.Background()
// script for creating remote // script for creating remote
ReadLine = makeReadLine([]string{ config.ReadLine = makeReadLine([]string{
"config_test_remote", // type "config_test_remote", // type
"true", // bool value "true", // bool value
"y", // type my own password "y", // type my own password
@ -99,29 +100,29 @@ func TestCRUD(t *testing.T) {
"secret", // repeat "secret", // repeat
"y", // looks good, save "y", // looks good, save
}) })
NewRemote(ctx, "test") config.NewRemote(ctx, "test")
assert.Equal(t, []string{"test"}, Data.GetSectionList()) assert.Equal(t, []string{"test"}, config.Data.GetSectionList())
assert.Equal(t, "config_test_remote", FileGet("test", "type")) assert.Equal(t, "config_test_remote", config.FileGet("test", "type"))
assert.Equal(t, "true", FileGet("test", "bool")) assert.Equal(t, "true", config.FileGet("test", "bool"))
assert.Equal(t, "secret", obscure.MustReveal(FileGet("test", "pass"))) assert.Equal(t, "secret", obscure.MustReveal(config.FileGet("test", "pass")))
// normal rename, test → asdf // normal rename, test → asdf
ReadLine = makeReadLine([]string{ config.ReadLine = makeReadLine([]string{
"asdf", "asdf",
"asdf", "asdf",
"asdf", "asdf",
}) })
RenameRemote("test") config.RenameRemote("test")
assert.Equal(t, []string{"asdf"}, Data.GetSectionList()) assert.Equal(t, []string{"asdf"}, config.Data.GetSectionList())
assert.Equal(t, "config_test_remote", FileGet("asdf", "type")) assert.Equal(t, "config_test_remote", config.FileGet("asdf", "type"))
assert.Equal(t, "true", FileGet("asdf", "bool")) assert.Equal(t, "true", config.FileGet("asdf", "bool"))
assert.Equal(t, "secret", obscure.MustReveal(FileGet("asdf", "pass"))) assert.Equal(t, "secret", obscure.MustReveal(config.FileGet("asdf", "pass")))
// delete remote // delete remote
DeleteRemote("asdf") config.DeleteRemote("asdf")
assert.Equal(t, []string{}, Data.GetSectionList()) assert.Equal(t, []string{}, config.Data.GetSectionList())
} }
func TestChooseOption(t *testing.T) { func TestChooseOption(t *testing.T) {
@ -129,7 +130,7 @@ func TestChooseOption(t *testing.T) {
ctx := context.Background() ctx := context.Background()
// script for creating remote // script for creating remote
ReadLine = makeReadLine([]string{ config.ReadLine = makeReadLine([]string{
"config_test_remote", // type "config_test_remote", // type
"false", // bool value "false", // bool value
"x", // bad choice "x", // bad choice
@ -138,26 +139,26 @@ func TestChooseOption(t *testing.T) {
"y", // password OK "y", // password OK
"y", // looks good, save "y", // looks good, save
}) })
Password = func(bits int) (string, error) { config.Password = func(bits int) (string, error) {
assert.Equal(t, 1024, bits) assert.Equal(t, 1024, bits)
return "not very random password", nil return "not very random password", nil
} }
NewRemote(ctx, "test") config.NewRemote(ctx, "test")
assert.Equal(t, "false", FileGet("test", "bool")) assert.Equal(t, "false", config.FileGet("test", "bool"))
assert.Equal(t, "not very random password", obscure.MustReveal(FileGet("test", "pass"))) assert.Equal(t, "not very random password", obscure.MustReveal(config.FileGet("test", "pass")))
// script for creating remote // script for creating remote
ReadLine = makeReadLine([]string{ config.ReadLine = makeReadLine([]string{
"config_test_remote", // type "config_test_remote", // type
"true", // bool value "true", // bool value
"n", // not required "n", // not required
"y", // looks good, save "y", // looks good, save
}) })
NewRemote(ctx, "test") config.NewRemote(ctx, "test")
assert.Equal(t, "true", FileGet("test", "bool")) assert.Equal(t, "true", config.FileGet("test", "bool"))
assert.Equal(t, "", FileGet("test", "pass")) assert.Equal(t, "", config.FileGet("test", "pass"))
} }
func TestNewRemoteName(t *testing.T) { func TestNewRemoteName(t *testing.T) {
@ -165,22 +166,22 @@ func TestNewRemoteName(t *testing.T) {
ctx := context.Background() ctx := context.Background()
// script for creating remote // script for creating remote
ReadLine = makeReadLine([]string{ config.ReadLine = makeReadLine([]string{
"config_test_remote", // type "config_test_remote", // type
"true", // bool value "true", // bool value
"n", // not required "n", // not required
"y", // looks good, save "y", // looks good, save
}) })
NewRemote(ctx, "test") config.NewRemote(ctx, "test")
ReadLine = makeReadLine([]string{ config.ReadLine = makeReadLine([]string{
"test", // already exists "test", // already exists
"", // empty string not allowed "", // empty string not allowed
"bad@characters", // bad characters "bad@characters", // bad characters
"newname", // OK "newname", // OK
}) })
assert.Equal(t, "newname", NewRemoteName()) assert.Equal(t, "newname", config.NewRemoteName())
} }
func TestCreateUpdatePasswordRemote(t *testing.T) { func TestCreateUpdatePasswordRemote(t *testing.T) {
@ -193,44 +194,44 @@ func TestCreateUpdatePasswordRemote(t *testing.T) {
break break
} }
t.Run(fmt.Sprintf("doObscure=%v,noObscure=%v", doObscure, noObscure), func(t *testing.T) { t.Run(fmt.Sprintf("doObscure=%v,noObscure=%v", doObscure, noObscure), func(t *testing.T) {
require.NoError(t, CreateRemote(ctx, "test2", "config_test_remote", rc.Params{ require.NoError(t, config.CreateRemote(ctx, "test2", "config_test_remote", rc.Params{
"bool": true, "bool": true,
"pass": "potato", "pass": "potato",
}, doObscure, noObscure)) }, doObscure, noObscure))
assert.Equal(t, []string{"test2"}, Data.GetSectionList()) assert.Equal(t, []string{"test2"}, config.Data.GetSectionList())
assert.Equal(t, "config_test_remote", FileGet("test2", "type")) assert.Equal(t, "config_test_remote", config.FileGet("test2", "type"))
assert.Equal(t, "true", FileGet("test2", "bool")) assert.Equal(t, "true", config.FileGet("test2", "bool"))
gotPw := FileGet("test2", "pass") gotPw := config.FileGet("test2", "pass")
if !noObscure { if !noObscure {
gotPw = obscure.MustReveal(gotPw) gotPw = obscure.MustReveal(gotPw)
} }
assert.Equal(t, "potato", gotPw) assert.Equal(t, "potato", gotPw)
wantPw := obscure.MustObscure("potato2") wantPw := obscure.MustObscure("potato2")
require.NoError(t, UpdateRemote(ctx, "test2", rc.Params{ require.NoError(t, config.UpdateRemote(ctx, "test2", rc.Params{
"bool": false, "bool": false,
"pass": wantPw, "pass": wantPw,
"spare": "spare", "spare": "spare",
}, doObscure, noObscure)) }, doObscure, noObscure))
assert.Equal(t, []string{"test2"}, Data.GetSectionList()) assert.Equal(t, []string{"test2"}, config.Data.GetSectionList())
assert.Equal(t, "config_test_remote", FileGet("test2", "type")) assert.Equal(t, "config_test_remote", config.FileGet("test2", "type"))
assert.Equal(t, "false", FileGet("test2", "bool")) assert.Equal(t, "false", config.FileGet("test2", "bool"))
gotPw = FileGet("test2", "pass") gotPw = config.FileGet("test2", "pass")
if doObscure { if doObscure {
gotPw = obscure.MustReveal(gotPw) gotPw = obscure.MustReveal(gotPw)
} }
assert.Equal(t, wantPw, gotPw) assert.Equal(t, wantPw, gotPw)
require.NoError(t, PasswordRemote(ctx, "test2", rc.Params{ require.NoError(t, config.PasswordRemote(ctx, "test2", rc.Params{
"pass": "potato3", "pass": "potato3",
})) }))
assert.Equal(t, []string{"test2"}, Data.GetSectionList()) assert.Equal(t, []string{"test2"}, config.Data.GetSectionList())
assert.Equal(t, "config_test_remote", FileGet("test2", "type")) assert.Equal(t, "config_test_remote", config.FileGet("test2", "type"))
assert.Equal(t, "false", FileGet("test2", "bool")) assert.Equal(t, "false", config.FileGet("test2", "bool"))
assert.Equal(t, "potato3", obscure.MustReveal(FileGet("test2", "pass"))) assert.Equal(t, "potato3", obscure.MustReveal(config.FileGet("test2", "pass")))
}) })
} }
} }
@ -254,44 +255,41 @@ func TestReveal(t *testing.T) {
} }
func TestConfigLoad(t *testing.T) { func TestConfigLoad(t *testing.T) {
oldConfigPath := ConfigPath oldConfigPath := config.ConfigPath
ConfigPath = "./testdata/plain.conf" config.ConfigPath = "./testdata/plain.conf"
defer func() { defer func() {
ConfigPath = oldConfigPath config.ConfigPath = oldConfigPath
}() }()
configKey = nil // reset password config.ClearConfigPassword()
err := Data.Load() configfile.LoadConfig(context.Background())
if err != nil { sections := config.Data.GetSectionList()
t.Fatal(err)
}
sections := Data.GetSectionList()
var expect = []string{"RCLONE_ENCRYPT_V0", "nounc", "unc"} var expect = []string{"RCLONE_ENCRYPT_V0", "nounc", "unc"}
assert.Equal(t, expect, sections) assert.Equal(t, expect, sections)
keys := Data.GetKeyList("nounc") keys := config.Data.GetKeyList("nounc")
expect = []string{"type", "nounc"} expect = []string{"type", "nounc"}
assert.Equal(t, expect, keys) assert.Equal(t, expect, keys)
} }
func TestConfigLoadEncrypted(t *testing.T) { func TestConfigLoadEncrypted(t *testing.T) {
var err error var err error
oldConfigPath := ConfigPath oldConfigPath := config.ConfigPath
ConfigPath = "./testdata/encrypted.conf" config.ConfigPath = "./testdata/encrypted.conf"
defer func() { defer func() {
ConfigPath = oldConfigPath config.ConfigPath = oldConfigPath
configKey = nil // reset password config.ClearConfigPassword()
}() }()
// Set correct password // Set correct password
err = setConfigPassword("asdf") err = config.SetConfigPassword("asdf")
require.NoError(t, err) require.NoError(t, err)
err = Data.Load() err = config.Data.Load()
require.NoError(t, err) require.NoError(t, err)
sections := Data.GetSectionList() sections := config.Data.GetSectionList()
var expect = []string{"nounc", "unc"} var expect = []string{"nounc", "unc"}
assert.Equal(t, expect, sections) assert.Equal(t, expect, sections)
keys := Data.GetKeyList("nounc") keys := config.Data.GetKeyList("nounc")
expect = []string{"type", "nounc"} expect = []string{"type", "nounc"}
assert.Equal(t, expect, keys) assert.Equal(t, expect, keys)
} }
@ -299,28 +297,28 @@ func TestConfigLoadEncrypted(t *testing.T) {
func TestConfigLoadEncryptedWithValidPassCommand(t *testing.T) { func TestConfigLoadEncryptedWithValidPassCommand(t *testing.T) {
ctx := context.Background() ctx := context.Background()
ci := fs.GetConfig(ctx) ci := fs.GetConfig(ctx)
oldConfigPath := ConfigPath oldConfigPath := config.ConfigPath
oldConfig := *ci oldConfig := *ci
ConfigPath = "./testdata/encrypted.conf" config.ConfigPath = "./testdata/encrypted.conf"
// using ci.PasswordCommand, correct password // using ci.PasswordCommand, correct password
ci.PasswordCommand = fs.SpaceSepList{"echo", "asdf"} ci.PasswordCommand = fs.SpaceSepList{"echo", "asdf"}
defer func() { defer func() {
ConfigPath = oldConfigPath config.ConfigPath = oldConfigPath
configKey = nil // reset password config.ClearConfigPassword()
*ci = oldConfig *ci = oldConfig
ci.PasswordCommand = nil ci.PasswordCommand = nil
}() }()
configKey = nil // reset password config.ClearConfigPassword()
err := Data.Load() err := config.Data.Load()
require.NoError(t, err) require.NoError(t, err)
sections := Data.GetSectionList() sections := config.Data.GetSectionList()
var expect = []string{"nounc", "unc"} var expect = []string{"nounc", "unc"}
assert.Equal(t, expect, sections) assert.Equal(t, expect, sections)
keys := Data.GetKeyList("nounc") keys := config.Data.GetKeyList("nounc")
expect = []string{"type", "nounc"} expect = []string{"type", "nounc"}
assert.Equal(t, expect, keys) assert.Equal(t, expect, keys)
} }
@ -328,21 +326,21 @@ func TestConfigLoadEncryptedWithValidPassCommand(t *testing.T) {
func TestConfigLoadEncryptedWithInvalidPassCommand(t *testing.T) { func TestConfigLoadEncryptedWithInvalidPassCommand(t *testing.T) {
ctx := context.Background() ctx := context.Background()
ci := fs.GetConfig(ctx) ci := fs.GetConfig(ctx)
oldConfigPath := ConfigPath oldConfigPath := config.ConfigPath
oldConfig := *ci oldConfig := *ci
ConfigPath = "./testdata/encrypted.conf" config.ConfigPath = "./testdata/encrypted.conf"
// using ci.PasswordCommand, incorrect password // using ci.PasswordCommand, incorrect password
ci.PasswordCommand = fs.SpaceSepList{"echo", "asdf-blurfl"} ci.PasswordCommand = fs.SpaceSepList{"echo", "asdf-blurfl"}
defer func() { defer func() {
ConfigPath = oldConfigPath config.ConfigPath = oldConfigPath
configKey = nil // reset password config.ClearConfigPassword()
*ci = oldConfig *ci = oldConfig
ci.PasswordCommand = nil ci.PasswordCommand = nil
}() }()
configKey = nil // reset password config.ClearConfigPassword()
err := Data.Load() err := config.Data.Load()
require.Error(t, err) require.Error(t, err)
assert.Contains(t, err.Error(), "using --password-command derived password") assert.Contains(t, err.Error(), "using --password-command derived password")
} }
@ -351,104 +349,43 @@ func TestConfigLoadEncryptedFailures(t *testing.T) {
var err error var err error
// This file should be too short to be decoded. // This file should be too short to be decoded.
oldConfigPath := ConfigPath oldConfigPath := config.ConfigPath
ConfigPath = "./testdata/enc-short.conf" config.ConfigPath = "./testdata/enc-short.conf"
defer func() { ConfigPath = oldConfigPath }() defer func() { config.ConfigPath = oldConfigPath }()
err = Data.Load() err = config.Data.Load()
require.Error(t, err) require.Error(t, err)
// This file contains invalid base64 characters. // This file contains invalid base64 characters.
ConfigPath = "./testdata/enc-invalid.conf" config.ConfigPath = "./testdata/enc-invalid.conf"
err = Data.Load() err = config.Data.Load()
require.Error(t, err) require.Error(t, err)
// This file contains invalid base64 characters. // This file contains invalid base64 characters.
ConfigPath = "./testdata/enc-too-new.conf" config.ConfigPath = "./testdata/enc-too-new.conf"
err = Data.Load() err = config.Data.Load()
require.Error(t, err) require.Error(t, err)
// This file does not exist. // This file does not exist.
ConfigPath = "./testdata/filenotfound.conf" config.ConfigPath = "./testdata/filenotfound.conf"
err = Data.Load() err = config.Data.Load()
assert.Equal(t, ErrorConfigFileNotFound, err) assert.Equal(t, config.ErrorConfigFileNotFound, err)
}
func TestPassword(t *testing.T) {
defer func() {
configKey = nil // reset password
}()
var err error
// Empty password should give error
err = setConfigPassword(" \t ")
require.Error(t, err)
// Test invalid utf8 sequence
err = setConfigPassword(string([]byte{0xff, 0xfe, 0xfd}) + "abc")
require.Error(t, err)
// Simple check of wrong passwords
hashedKeyCompare(t, "mis", "match", false)
// Check that passwords match after unicode normalization
hashedKeyCompare(t, "ff\u0041\u030A", "ffÅ", true)
// Check that passwords preserves case
hashedKeyCompare(t, "abcdef", "ABCDEF", false)
}
func hashedKeyCompare(t *testing.T, a, b string, shouldMatch bool) {
err := setConfigPassword(a)
require.NoError(t, err)
k1 := configKey
err = setConfigPassword(b)
require.NoError(t, err)
k2 := configKey
if shouldMatch {
assert.Equal(t, k1, k2)
} else {
assert.NotEqual(t, k1, k2)
}
}
func TestMatchProvider(t *testing.T) {
for _, test := range []struct {
config string
provider string
want bool
}{
{"", "", true},
{"one", "one", true},
{"one,two", "two", true},
{"one,two,three", "two", true},
{"one", "on", false},
{"one,two,three", "tw", false},
{"!one,two,three", "two", false},
{"!one,two,three", "four", true},
} {
what := fmt.Sprintf("%q,%q", test.config, test.provider)
got := matchProvider(test.config, test.provider)
assert.Equal(t, test.want, got, what)
}
} }
func TestFileRefresh(t *testing.T) { func TestFileRefresh(t *testing.T) {
ctx := context.Background() ctx := context.Background()
defer testConfigFile(t, "refresh.conf")() defer testConfigFile(t, "refresh.conf")()
require.NoError(t, CreateRemote(ctx, "refresh_test", "config_test_remote", rc.Params{ require.NoError(t, config.CreateRemote(ctx, "refresh_test", "config_test_remote", rc.Params{
"bool": true, "bool": true,
}, false, false)) }, false, false))
b, err := ioutil.ReadFile(ConfigPath) b, err := ioutil.ReadFile(config.ConfigPath)
assert.NoError(t, err) assert.NoError(t, err)
b = bytes.Replace(b, []byte("refresh_test"), []byte("refreshed_test"), 1) b = bytes.Replace(b, []byte("refresh_test"), []byte("refreshed_test"), 1)
err = ioutil.WriteFile(ConfigPath, b, 0644) err = ioutil.WriteFile(config.ConfigPath, b, 0644)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotEqual(t, []string{"refreshed_test"}, Data.GetSectionList()) assert.NotEqual(t, []string{"refreshed_test"}, config.Data.GetSectionList())
err = FileRefresh() err = config.FileRefresh()
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, []string{"refreshed_test"}, Data.GetSectionList()) assert.Equal(t, []string{"refreshed_test"}, config.Data.GetSectionList())
} }

View file

@ -2,6 +2,7 @@ package configfile
import ( import (
"bytes" "bytes"
"context"
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
@ -12,14 +13,27 @@ import (
"github.com/rclone/rclone/fs/config" "github.com/rclone/rclone/fs/config"
) )
// GoConfig implements config file saving using a simple ini based // LoadConfig installs the config file handler and calls config.LoadConfig
// format. func LoadConfig(ctx context.Context) {
type GoConfig struct { config.Data = &Storage{}
*goconfig.ConfigFile config.LoadConfig(ctx)
} }
// Load the config from permanent storage // Storage implements config.Storage for saving and loading config
func (gc *GoConfig) Load() error { // data in a simple INI based file.
type Storage struct {
gc *goconfig.ConfigFile
}
// Load the config from permanent storage, decrypting if necessary
func (s *Storage) Load() (err error) {
// Make sure we have a sensible default even when we error
defer func() {
if err != nil {
s.gc, _ = goconfig.LoadFromReader(bytes.NewReader([]byte{}))
}
}()
b, err := os.Open(config.ConfigPath) b, err := os.Open(config.ConfigPath)
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
@ -34,22 +48,17 @@ func (gc *GoConfig) Load() error {
return err return err
} }
if gc.ConfigFile == nil { gc, err := goconfig.LoadFromReader(cryptReader)
c, err := goconfig.LoadFromReader(cryptReader) if err != nil {
if err != nil { return err
return err
}
gc.ConfigFile = c
} else {
return gc.ReloadData(cryptReader)
} }
s.gc = gc
return nil return nil
} }
// Save the config to permanent storage // Save the config to permanent storage, encrypting if necessary
func (gc *GoConfig) Save() error { func (s *Storage) Save() error {
dir, name := filepath.Split(config.ConfigPath) dir, name := filepath.Split(config.ConfigPath)
err := os.MkdirAll(dir, os.ModePerm) err := os.MkdirAll(dir, os.ModePerm)
if err != nil { if err != nil {
@ -67,7 +76,7 @@ func (gc *GoConfig) Save() error {
}() }()
var buf bytes.Buffer var buf bytes.Buffer
if err := goconfig.SaveConfigData(gc.ConfigFile, &buf); err != nil { if err := goconfig.SaveConfigData(s.gc, &buf); err != nil {
return errors.Errorf("Failed to save config file: %v", err) return errors.Errorf("Failed to save config file: %v", err)
} }
@ -111,9 +120,9 @@ func (gc *GoConfig) Save() error {
} }
// Serialize the config into a string // Serialize the config into a string
func (gc *GoConfig) Serialize() (string, error) { func (s *Storage) Serialize() (string, error) {
var buf bytes.Buffer var buf bytes.Buffer
if err := goconfig.SaveConfigData(gc.ConfigFile, &buf); err != nil { if err := goconfig.SaveConfigData(s.gc, &buf); err != nil {
return "", errors.Errorf("Failed to save config file: %v", err) return "", errors.Errorf("Failed to save config file: %v", err)
} }
@ -121,8 +130,8 @@ func (gc *GoConfig) Serialize() (string, error) {
} }
// HasSection returns true if section exists in the config file // HasSection returns true if section exists in the config file
func (gc *GoConfig) HasSection(section string) bool { func (s *Storage) HasSection(section string) bool {
_, err := gc.ConfigFile.GetSection(section) _, err := s.gc.GetSection(section)
if err != nil { if err != nil {
return false return false
} }
@ -131,16 +140,39 @@ func (gc *GoConfig) HasSection(section string) bool {
// DeleteSection removes the named section and all config from the // DeleteSection removes the named section and all config from the
// config file // config file
func (gc *GoConfig) DeleteSection(section string) { func (s *Storage) DeleteSection(section string) {
gc.ConfigFile.DeleteSection(section) s.gc.DeleteSection(section)
}
// GetSectionList returns a slice of strings with names for all the
// sections
func (s *Storage) GetSectionList() []string {
return s.gc.GetSectionList()
}
// GetKeyList returns the keys in this section
func (s *Storage) GetKeyList(section string) []string {
return s.gc.GetKeyList(section)
}
// GetValue returns the key in section with a found flag
func (s *Storage) GetValue(section string, key string) (value string, found bool) {
value, err := s.gc.GetValue(section, key)
if err != nil {
return "", false
}
return value, true
} }
// SetValue sets the value under key in section // SetValue sets the value under key in section
func (gc *GoConfig) SetValue(section string, key string, value string) { func (s *Storage) SetValue(section string, key string, value string) {
gc.ConfigFile.SetValue(section, key, value) s.gc.SetValue(section, key, value)
} }
// Check the interfaces are satisfied // DeleteKey removes the key under section
var ( func (s *Storage) DeleteKey(section string, key string) bool {
_ config.File = (*GoConfig)(nil) return s.gc.DeleteKey(section, key)
) }
// Check the interface is satisfied
var _ config.Storage = (*Storage)(nil)

View file

@ -101,7 +101,7 @@ func Decrypt(b io.ReadSeeker) (io.Reader, error) {
return nil, errors.Wrap(err, "password command failed") return nil, errors.Wrap(err, "password command failed")
} }
if pass := strings.Trim(stdout.String(), "\r\n"); pass != "" { if pass := strings.Trim(stdout.String(), "\r\n"); pass != "" {
err := setConfigPassword(pass) err := SetConfigPassword(pass)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "incorrect password") return nil, errors.Wrap(err, "incorrect password")
} }
@ -119,7 +119,7 @@ func Decrypt(b io.ReadSeeker) (io.Reader, error) {
envpw := os.Getenv("RCLONE_CONFIG_PASS") envpw := os.Getenv("RCLONE_CONFIG_PASS")
if envpw != "" { if envpw != "" {
err := setConfigPassword(envpw) err := SetConfigPassword(envpw)
if err != nil { if err != nil {
fs.Errorf(nil, "Using RCLONE_CONFIG_PASS returned: %v", err) fs.Errorf(nil, "Using RCLONE_CONFIG_PASS returned: %v", err)
} else { } else {
@ -281,7 +281,7 @@ func getConfigPassword(q string) {
} }
for { for {
password := GetPassword(q) password := GetPassword(q)
err := setConfigPassword(password) err := SetConfigPassword(password)
if err == nil { if err == nil {
return return
} }
@ -289,10 +289,10 @@ func getConfigPassword(q string) {
} }
} }
// setConfigPassword will set the configKey to the hash of // SetConfigPassword will set the configKey to the hash of
// the password. If the length of the password is // the password. If the length of the password is
// zero after trimming+normalization, an error is returned. // zero after trimming+normalization, an error is returned.
func setConfigPassword(password string) error { func SetConfigPassword(password string) error {
password, err := checkPassword(password) password, err := checkPassword(password)
if err != nil { if err != nil {
return err return err
@ -338,11 +338,16 @@ func setConfigPassword(password string) error {
return nil return nil
} }
// ClearConfigPassword sets the current the password to empty
func ClearConfigPassword() {
configKey = nil
}
// changeConfigPassword will query the user twice // changeConfigPassword will query the user twice
// for a password. If the same password is entered // for a password. If the same password is entered
// twice the key is updated. // twice the key is updated.
func changeConfigPassword() { func changeConfigPassword() {
err := setConfigPassword(ChangePassword("NEW configuration")) err := SetConfigPassword(ChangePassword("NEW configuration"))
if err != nil { if err != nil {
fmt.Printf("Failed to set config password: %v\n", err) fmt.Printf("Failed to set config password: %v\n", err)
return return

48
fs/config/crypt_test.go Normal file
View file

@ -0,0 +1,48 @@
package config
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPassword(t *testing.T) {
defer func() {
configKey = nil // reset password
}()
var err error
// Empty password should give error
err = SetConfigPassword(" \t ")
require.Error(t, err)
// Test invalid utf8 sequence
err = SetConfigPassword(string([]byte{0xff, 0xfe, 0xfd}) + "abc")
require.Error(t, err)
// Simple check of wrong passwords
hashedKeyCompare(t, "mis", "match", false)
// Check that passwords match after unicode normalization
hashedKeyCompare(t, "ff\u0041\u030A", "ffÅ", true)
// Check that passwords preserves case
hashedKeyCompare(t, "abcdef", "ABCDEF", false)
}
func hashedKeyCompare(t *testing.T, a, b string, shouldMatch bool) {
err := SetConfigPassword(a)
require.NoError(t, err)
k1 := configKey
err = SetConfigPassword(b)
require.NoError(t, err)
k2 := configKey
if shouldMatch {
assert.Equal(t, k1, k2)
} else {
assert.NotEqual(t, k1, k2)
}
}

View file

@ -0,0 +1,61 @@
package config
// Default config.Storage which panics with a useful error when used
type defaultStorage struct{}
var noConfigStorage = "internal error: no config file system found. Did you call configfile.LoadConfig(ctx)?"
// GetSectionList returns a slice of strings with names for all the
// sections
func (defaultStorage) GetSectionList() []string {
panic(noConfigStorage)
}
// HasSection returns true if section exists in the config file
func (defaultStorage) HasSection(section string) bool {
panic(noConfigStorage)
}
// DeleteSection removes the named section and all config from the
// config file
func (defaultStorage) DeleteSection(section string) {
panic(noConfigStorage)
}
// GetKeyList returns the keys in this section
func (defaultStorage) GetKeyList(section string) []string {
panic(noConfigStorage)
}
// GetValue returns the key in section with a found flag
func (defaultStorage) GetValue(section string, key string) (value string, found bool) {
panic(noConfigStorage)
}
// SetValue sets the value under key in section
func (defaultStorage) SetValue(section string, key string, value string) {
panic(noConfigStorage)
}
// DeleteKey removes the key under section
func (defaultStorage) DeleteKey(section string, key string) bool {
panic(noConfigStorage)
}
// Load the config from permanent storage
func (defaultStorage) Load() error {
panic(noConfigStorage)
}
// Save the config to permanent storage
func (defaultStorage) Save() error {
panic(noConfigStorage)
}
// Serialize the config into a string
func (defaultStorage) Serialize() (string, error) {
panic(noConfigStorage)
}
// Check the interface is satisfied
var _ Storage = defaultStorage{}

View file

@ -7,6 +7,7 @@ import (
_ "github.com/rclone/rclone/backend/local" _ "github.com/rclone/rclone/backend/local"
"github.com/rclone/rclone/fs" "github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/config" "github.com/rclone/rclone/fs/config"
"github.com/rclone/rclone/fs/config/configfile"
"github.com/rclone/rclone/fs/config/obscure" "github.com/rclone/rclone/fs/config/obscure"
"github.com/rclone/rclone/fs/rc" "github.com/rclone/rclone/fs/rc"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -16,6 +17,8 @@ import (
const testName = "configTestNameForRc" const testName = "configTestNameForRc"
func TestRc(t *testing.T) { func TestRc(t *testing.T) {
ctx := context.Background()
configfile.LoadConfig(ctx)
// Create the test remote // Create the test remote
call := rc.Calls.Get("config/create") call := rc.Calls.Get("config/create")
assert.NotNil(t, call) assert.NotNil(t, call)
@ -26,7 +29,7 @@ func TestRc(t *testing.T) {
"test_key": "sausage", "test_key": "sausage",
}, },
} }
out, err := call.Fn(context.Background(), in) out, err := call.Fn(ctx, in)
require.NoError(t, err) require.NoError(t, err)
require.Nil(t, out) require.Nil(t, out)
assert.Equal(t, "local", config.FileGet(testName, "type")) assert.Equal(t, "local", config.FileGet(testName, "type"))

View file

@ -20,6 +20,7 @@ import (
_ "github.com/rclone/rclone/backend/local" _ "github.com/rclone/rclone/backend/local"
"github.com/rclone/rclone/fs" "github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/accounting" "github.com/rclone/rclone/fs/accounting"
"github.com/rclone/rclone/fs/config/configfile"
"github.com/rclone/rclone/fs/rc" "github.com/rclone/rclone/fs/rc"
) )
@ -101,9 +102,11 @@ type testRun struct {
// Run a suite of tests // Run a suite of tests
func testServer(t *testing.T, tests []testRun, opt *rc.Options) { func testServer(t *testing.T, tests []testRun, opt *rc.Options) {
ctx := context.Background()
configfile.LoadConfig(ctx)
mux := http.NewServeMux() mux := http.NewServeMux()
opt.HTTPOptions.Template = testTemplate opt.HTTPOptions.Template = testTemplate
rcServer := newServer(context.Background(), opt, mux) rcServer := newServer(ctx, opt, mux)
for _, test := range tests { for _, test := range tests {
t.Run(test.Name, func(t *testing.T) { t.Run(test.Name, func(t *testing.T) {
method := test.Method method := test.Method

View file

@ -25,6 +25,7 @@ import (
"github.com/rclone/rclone/fs" "github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/accounting" "github.com/rclone/rclone/fs/accounting"
"github.com/rclone/rclone/fs/config" "github.com/rclone/rclone/fs/config"
"github.com/rclone/rclone/fs/config/configfile"
"github.com/rclone/rclone/fs/hash" "github.com/rclone/rclone/fs/hash"
"github.com/rclone/rclone/fs/walk" "github.com/rclone/rclone/fs/walk"
"github.com/rclone/rclone/lib/random" "github.com/rclone/rclone/lib/random"
@ -70,7 +71,7 @@ func Initialise() {
if envConfig := os.Getenv("RCLONE_CONFIG"); envConfig != "" { if envConfig := os.Getenv("RCLONE_CONFIG"); envConfig != "" {
config.ConfigPath = envConfig config.ConfigPath = envConfig
} }
config.LoadConfig(ctx) configfile.LoadConfig(ctx)
if *Verbose { if *Verbose {
ci.LogLevel = fs.LogLevelDebug ci.LogLevel = fs.LogLevelDebug
} }