This commit is contained in:
bel
2020-01-13 03:37:51 +00:00
commit c8eb52f9ba
2023 changed files with 702080 additions and 0 deletions

1346
.rclone_repo/fs/config/config.go Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,10 @@
// Read, write and edit the config file
// Non-unix specific functions.
// +build !darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris
package config
// attemptCopyGroups tries to keep the group the same, which only makes sense
// for system with user-group-world permission model.
func attemptCopyGroup(fromPath, toPath string) {}

View File

@@ -0,0 +1,29 @@
// ReadPassword for OSes which are supported by golang.org/x/crypto/ssh/terminal
// See https://github.com/golang/go/issues/14441 - plan9
// https://github.com/golang/go/issues/13085 - solaris
// +build !solaris,!plan9
package config
import (
"fmt"
"log"
"os"
"golang.org/x/crypto/ssh/terminal"
)
// ReadPassword reads a password without echoing it to the terminal.
func ReadPassword() string {
stdin := int(os.Stdin.Fd())
if !terminal.IsTerminal(stdin) {
return ReadLine()
}
line, err := terminal.ReadPassword(stdin)
_, _ = fmt.Fprintln(os.Stderr)
if err != nil {
log.Fatalf("Failed to read password: %v", err)
}
return string(line)
}

View File

@@ -0,0 +1,12 @@
// ReadPassword for OSes which are not supported by golang.org/x/crypto/ssh/terminal
// See https://github.com/golang/go/issues/14441 - plan9
// https://github.com/golang/go/issues/13085 - solaris
// +build solaris plan9
package config
// ReadPassword reads a password with echoing it to the terminal.
func ReadPassword() string {
return ReadLine()
}

View File

@@ -0,0 +1,229 @@
package config
import (
"fmt"
"io/ioutil"
"os"
"testing"
"github.com/ncw/rclone/fs"
"github.com/ncw/rclone/fs/config/obscure"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCRUD(t *testing.T) {
configKey = nil // reset password
// create temp config file
tempFile, err := ioutil.TempFile("", "crud.conf")
assert.NoError(t, err)
path := tempFile.Name()
defer func() {
err := os.Remove(path)
assert.NoError(t, err)
}()
assert.NoError(t, tempFile.Close())
// temporarily adapt configuration
oldOsStdout := os.Stdout
oldConfigPath := ConfigPath
oldConfig := fs.Config
oldConfigFile := configFile
oldReadLine := ReadLine
os.Stdout = nil
ConfigPath = path
fs.Config = &fs.ConfigInfo{}
configFile = nil
defer func() {
os.Stdout = oldOsStdout
ConfigPath = oldConfigPath
ReadLine = oldReadLine
fs.Config = oldConfig
configFile = oldConfigFile
}()
LoadConfig()
assert.Equal(t, []string{}, getConfigData().GetSectionList())
// Fake a remote
fs.Register(&fs.RegInfo{Name: "config_test_remote"})
// add new remote
i := 0
ReadLine = func() string {
answers := []string{
"config_test_remote", // type
"y", // looks good, save
}
i = i + 1
return answers[i-1]
}
NewRemote("test")
assert.Equal(t, []string{"test"}, configFile.GetSectionList())
// Reload the config file to workaround this bug
// https://github.com/Unknwon/goconfig/issues/39
configFile, err = loadConfigFile()
require.NoError(t, err)
// normal rename, test → asdf
ReadLine = func() string { return "asdf" }
RenameRemote("test")
assert.Equal(t, []string{"asdf"}, configFile.GetSectionList())
// no-op rename, asdf → asdf
RenameRemote("asdf")
assert.Equal(t, []string{"asdf"}, configFile.GetSectionList())
// delete remote
DeleteRemote("asdf")
assert.Equal(t, []string{}, configFile.GetSectionList())
}
// Test some error cases
func TestReveal(t *testing.T) {
for _, test := range []struct {
in string
wantErr string
}{
{"YmJiYmJiYmJiYmJiYmJiYp*gcEWbAw", "base64 decode failed when revealing password - is it obscured?: illegal base64 data at input byte 22"},
{"aGVsbG8", "input too short when revealing password - is it obscured?"},
{"", "input too short when revealing password - is it obscured?"},
} {
gotString, gotErr := obscure.Reveal(test.in)
assert.Equal(t, "", gotString)
assert.Equal(t, test.wantErr, gotErr.Error())
}
}
func TestConfigLoad(t *testing.T) {
oldConfigPath := ConfigPath
ConfigPath = "./testdata/plain.conf"
defer func() {
ConfigPath = oldConfigPath
}()
configKey = nil // reset password
c, err := loadConfigFile()
if err != nil {
t.Fatal(err)
}
sections := c.GetSectionList()
var expect = []string{"RCLONE_ENCRYPT_V0", "nounc", "unc"}
assert.Equal(t, expect, sections)
keys := c.GetKeyList("nounc")
expect = []string{"type", "nounc"}
assert.Equal(t, expect, keys)
}
func TestConfigLoadEncrypted(t *testing.T) {
var err error
oldConfigPath := ConfigPath
ConfigPath = "./testdata/encrypted.conf"
defer func() {
ConfigPath = oldConfigPath
configKey = nil // reset password
}()
// Set correct password
err = setConfigPassword("asdf")
require.NoError(t, err)
c, err := loadConfigFile()
require.NoError(t, err)
sections := c.GetSectionList()
var expect = []string{"nounc", "unc"}
assert.Equal(t, expect, sections)
keys := c.GetKeyList("nounc")
expect = []string{"type", "nounc"}
assert.Equal(t, expect, keys)
}
func TestConfigLoadEncryptedFailures(t *testing.T) {
var err error
// This file should be too short to be decoded.
oldConfigPath := ConfigPath
ConfigPath = "./testdata/enc-short.conf"
defer func() { ConfigPath = oldConfigPath }()
_, err = loadConfigFile()
require.Error(t, err)
// This file contains invalid base64 characters.
ConfigPath = "./testdata/enc-invalid.conf"
_, err = loadConfigFile()
require.Error(t, err)
// This file contains invalid base64 characters.
ConfigPath = "./testdata/enc-too-new.conf"
_, err = loadConfigFile()
require.Error(t, err)
// This file does not exist.
ConfigPath = "./testdata/filenotfound.conf"
c, err := loadConfigFile()
assert.Equal(t, errorConfigFileNotFound, err)
assert.Nil(t, c)
}
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)
}
}

View File

@@ -0,0 +1,37 @@
// Read, write and edit the config file
// Unix specific functions.
// +build darwin dragonfly freebsd linux netbsd openbsd solaris
package config
import (
"os"
"os/user"
"strconv"
"syscall"
"github.com/ncw/rclone/fs"
)
// attemptCopyGroups tries to keep the group the same. User will be the one
// who is currently running this process.
func attemptCopyGroup(fromPath, toPath string) {
info, err := os.Stat(fromPath)
if err != nil || info.Sys() == nil {
return
}
if stat, ok := info.Sys().(*syscall.Stat_t); ok {
uid := int(stat.Uid)
// prefer self over previous owner of file, because it has a higher chance
// of success
if user, err := user.Current(); err == nil {
if tmpUID, err := strconv.Atoi(user.Uid); err == nil {
uid = tmpUID
}
}
if err = os.Chown(toPath, uid, int(stat.Gid)); err != nil {
fs.Debugf(nil, "Failed to keep previous owner of config file: %v", err)
}
}
}

View File

@@ -0,0 +1,172 @@
// Package configflags defines the flags used by rclone. It is
// decoupled into a separate package so it can be replaced.
package configflags
// Options set by command line flags
import (
"log"
"net"
"path/filepath"
"strings"
"github.com/ncw/rclone/fs"
"github.com/ncw/rclone/fs/config"
"github.com/ncw/rclone/fs/config/flags"
"github.com/spf13/pflag"
)
var (
// these will get interpreted into fs.Config via SetFlags() below
verbose int
quiet bool
dumpHeaders bool
dumpBodies bool
deleteBefore bool
deleteDuring bool
deleteAfter bool
bindAddr string
disableFeatures string
noTraverse bool
)
// AddFlags adds the non filing system specific flags to the command
func AddFlags(flagSet *pflag.FlagSet) {
// NB defaults which aren't the zero for the type should be set in fs/config.go NewConfig
flags.CountVarP(flagSet, &verbose, "verbose", "v", "Print lots more stuff (repeat for more)")
flags.BoolVarP(flagSet, &quiet, "quiet", "q", false, "Print as little stuff as possible")
flags.DurationVarP(flagSet, &fs.Config.ModifyWindow, "modify-window", "", fs.Config.ModifyWindow, "Max time diff to be considered the same")
flags.IntVarP(flagSet, &fs.Config.Checkers, "checkers", "", fs.Config.Checkers, "Number of checkers to run in parallel.")
flags.IntVarP(flagSet, &fs.Config.Transfers, "transfers", "", fs.Config.Transfers, "Number of file transfers to run in parallel.")
flags.StringVarP(flagSet, &config.ConfigPath, "config", "", config.ConfigPath, "Config file.")
flags.StringVarP(flagSet, &config.CacheDir, "cache-dir", "", config.CacheDir, "Directory rclone will use for caching.")
flags.BoolVarP(flagSet, &fs.Config.CheckSum, "checksum", "c", fs.Config.CheckSum, "Skip based on checksum & size, not mod-time & size")
flags.BoolVarP(flagSet, &fs.Config.SizeOnly, "size-only", "", fs.Config.SizeOnly, "Skip based on size only, not mod-time or checksum")
flags.BoolVarP(flagSet, &fs.Config.IgnoreTimes, "ignore-times", "I", fs.Config.IgnoreTimes, "Don't skip files that match size and time - transfer all files")
flags.BoolVarP(flagSet, &fs.Config.IgnoreExisting, "ignore-existing", "", fs.Config.IgnoreExisting, "Skip all files that exist on destination")
flags.BoolVarP(flagSet, &fs.Config.IgnoreErrors, "ignore-errors", "", fs.Config.IgnoreErrors, "delete even if there are I/O errors")
flags.BoolVarP(flagSet, &fs.Config.DryRun, "dry-run", "n", fs.Config.DryRun, "Do a trial run with no permanent changes")
flags.DurationVarP(flagSet, &fs.Config.ConnectTimeout, "contimeout", "", fs.Config.ConnectTimeout, "Connect timeout")
flags.DurationVarP(flagSet, &fs.Config.Timeout, "timeout", "", fs.Config.Timeout, "IO idle timeout")
flags.BoolVarP(flagSet, &dumpHeaders, "dump-headers", "", false, "Dump HTTP bodies - may contain sensitive info")
flags.BoolVarP(flagSet, &dumpBodies, "dump-bodies", "", false, "Dump HTTP headers and bodies - may contain sensitive info")
flags.BoolVarP(flagSet, &fs.Config.InsecureSkipVerify, "no-check-certificate", "", fs.Config.InsecureSkipVerify, "Do not verify the server SSL certificate. Insecure.")
flags.BoolVarP(flagSet, &fs.Config.AskPassword, "ask-password", "", fs.Config.AskPassword, "Allow prompt for password for encrypted configuration.")
flags.BoolVarP(flagSet, &deleteBefore, "delete-before", "", false, "When synchronizing, delete files on destination before transfering")
flags.BoolVarP(flagSet, &deleteDuring, "delete-during", "", false, "When synchronizing, delete files during transfer")
flags.BoolVarP(flagSet, &deleteAfter, "delete-after", "", false, "When synchronizing, delete files on destination after transfering (default)")
flags.IntVar64P(flagSet, &fs.Config.MaxDelete, "max-delete", "", -1, "When synchronizing, limit the number of deletes")
flags.BoolVarP(flagSet, &fs.Config.TrackRenames, "track-renames", "", fs.Config.TrackRenames, "When synchronizing, track file renames and do a server side move if possible")
flags.IntVarP(flagSet, &fs.Config.LowLevelRetries, "low-level-retries", "", fs.Config.LowLevelRetries, "Number of low level retries to do.")
flags.BoolVarP(flagSet, &fs.Config.UpdateOlder, "update", "u", fs.Config.UpdateOlder, "Skip files that are newer on the destination.")
flags.BoolVarP(flagSet, &fs.Config.UseServerModTime, "use-server-modtime", "", fs.Config.UseServerModTime, "Use server modified time instead of object metadata")
flags.BoolVarP(flagSet, &fs.Config.NoGzip, "no-gzip-encoding", "", fs.Config.NoGzip, "Don't set Accept-Encoding: gzip.")
flags.IntVarP(flagSet, &fs.Config.MaxDepth, "max-depth", "", fs.Config.MaxDepth, "If set limits the recursion depth to this.")
flags.BoolVarP(flagSet, &fs.Config.IgnoreSize, "ignore-size", "", false, "Ignore size when skipping use mod-time or checksum.")
flags.BoolVarP(flagSet, &fs.Config.IgnoreChecksum, "ignore-checksum", "", fs.Config.IgnoreChecksum, "Skip post copy check of checksums.")
flags.BoolVarP(flagSet, &noTraverse, "no-traverse", "", noTraverse, "Obsolete - does nothing.")
flags.BoolVarP(flagSet, &fs.Config.NoUpdateModTime, "no-update-modtime", "", fs.Config.NoUpdateModTime, "Don't update destination mod-time if files identical.")
flags.StringVarP(flagSet, &fs.Config.BackupDir, "backup-dir", "", fs.Config.BackupDir, "Make backups into hierarchy based in DIR.")
flags.StringVarP(flagSet, &fs.Config.Suffix, "suffix", "", fs.Config.Suffix, "Suffix for use with --backup-dir.")
flags.BoolVarP(flagSet, &fs.Config.UseListR, "fast-list", "", fs.Config.UseListR, "Use recursive list if available. Uses more memory but fewer transactions.")
flags.Float64VarP(flagSet, &fs.Config.TPSLimit, "tpslimit", "", fs.Config.TPSLimit, "Limit HTTP transactions per second to this.")
flags.IntVarP(flagSet, &fs.Config.TPSLimitBurst, "tpslimit-burst", "", fs.Config.TPSLimitBurst, "Max burst of transactions for --tpslimit.")
flags.StringVarP(flagSet, &bindAddr, "bind", "", "", "Local address to bind to for outgoing connections, IPv4, IPv6 or name.")
flags.StringVarP(flagSet, &disableFeatures, "disable", "", "", "Disable a comma separated list of features. Use help to see a list.")
flags.StringVarP(flagSet, &fs.Config.UserAgent, "user-agent", "", fs.Config.UserAgent, "Set the user-agent to a specified string. The default is rclone/ version")
flags.BoolVarP(flagSet, &fs.Config.Immutable, "immutable", "", fs.Config.Immutable, "Do not modify files. Fail if existing files have been modified.")
flags.BoolVarP(flagSet, &fs.Config.AutoConfirm, "auto-confirm", "", fs.Config.AutoConfirm, "If enabled, do not request console confirmation.")
flags.IntVarP(flagSet, &fs.Config.StatsFileNameLength, "stats-file-name-length", "", fs.Config.StatsFileNameLength, "Max file name length in stats. 0 for no limit")
flags.FVarP(flagSet, &fs.Config.LogLevel, "log-level", "", "Log level DEBUG|INFO|NOTICE|ERROR")
flags.FVarP(flagSet, &fs.Config.StatsLogLevel, "stats-log-level", "", "Log level to show --stats output DEBUG|INFO|NOTICE|ERROR")
flags.FVarP(flagSet, &fs.Config.BwLimit, "bwlimit", "", "Bandwidth limit in kBytes/s, or use suffix b|k|M|G or a full timetable.")
flags.FVarP(flagSet, &fs.Config.BufferSize, "buffer-size", "", "In memory buffer size when reading files for each --transfer.")
flags.FVarP(flagSet, &fs.Config.StreamingUploadCutoff, "streaming-upload-cutoff", "", "Cutoff for switching to chunked upload if file size is unknown. Upload starts after reaching cutoff or when file ends.")
flags.FVarP(flagSet, &fs.Config.Dump, "dump", "", "List of items to dump from: "+fs.DumpFlagsList)
flags.FVarP(flagSet, &fs.Config.MaxTransfer, "max-transfer", "", "Maximum size of data to transfer.")
flags.IntVarP(flagSet, &fs.Config.MaxBacklog, "max-backlog", "", fs.Config.MaxBacklog, "Maximum number of objects in sync or check backlog.")
flags.BoolVarP(flagSet, &fs.Config.StatsOneLine, "stats-one-line", "", fs.Config.StatsOneLine, "Make the stats fit on one line.")
flags.BoolVarP(flagSet, &fs.Config.Progress, "progress", "P", fs.Config.Progress, "Show progress during transfer.")
}
// SetFlags converts any flags into config which weren't straight foward
func SetFlags() {
if verbose >= 2 {
fs.Config.LogLevel = fs.LogLevelDebug
} else if verbose >= 1 {
fs.Config.LogLevel = fs.LogLevelInfo
}
if quiet {
if verbose > 0 {
log.Fatalf("Can't set -v and -q")
}
fs.Config.LogLevel = fs.LogLevelError
}
logLevelFlag := pflag.Lookup("log-level")
if logLevelFlag != nil && logLevelFlag.Changed {
if verbose > 0 {
log.Fatalf("Can't set -v and --log-level")
}
if quiet {
log.Fatalf("Can't set -q and --log-level")
}
}
if noTraverse {
fs.Logf(nil, "--no-traverse is obsolete and no longer needed - please remove")
}
if dumpHeaders {
fs.Config.Dump |= fs.DumpHeaders
fs.Logf(nil, "--dump-headers is obsolete - please use --dump headers instead")
}
if dumpBodies {
fs.Config.Dump |= fs.DumpBodies
fs.Logf(nil, "--dump-bodies is obsolete - please use --dump bodies instead")
}
switch {
case deleteBefore && (deleteDuring || deleteAfter),
deleteDuring && deleteAfter:
log.Fatalf(`Only one of --delete-before, --delete-during or --delete-after can be used.`)
case deleteBefore:
fs.Config.DeleteMode = fs.DeleteModeBefore
case deleteDuring:
fs.Config.DeleteMode = fs.DeleteModeDuring
case deleteAfter:
fs.Config.DeleteMode = fs.DeleteModeAfter
default:
fs.Config.DeleteMode = fs.DeleteModeDefault
}
if fs.Config.IgnoreSize && fs.Config.SizeOnly {
log.Fatalf(`Can't use --size-only and --ignore-size together.`)
}
if fs.Config.Suffix != "" && fs.Config.BackupDir == "" {
log.Fatalf(`Can only use --suffix with --backup-dir.`)
}
if bindAddr != "" {
addrs, err := net.LookupIP(bindAddr)
if err != nil {
log.Fatalf("--bind: Failed to parse %q as IP address: %v", bindAddr, err)
}
if len(addrs) != 1 {
log.Fatalf("--bind: Expecting 1 IP address for %q but got %d", bindAddr, len(addrs))
}
fs.Config.BindAddr = addrs[0]
}
if disableFeatures != "" {
if disableFeatures == "help" {
log.Fatalf("Possible backend features are: %s\n", strings.Join(new(fs.Features).List(), ", "))
}
fs.Config.DisableFeatures = strings.Split(disableFeatures, ",")
}
// Make the config file absolute
configPath, err := filepath.Abs(config.ConfigPath)
if err == nil {
config.ConfigPath = configPath
}
}

View File

@@ -0,0 +1,86 @@
// Package configmap provides an abstraction for reading and writing config
package configmap
// Getter provides an interface to get config items
type Getter interface {
// Get should get an item with the key passed in and return
// the value. If the item is found then it should return true,
// otherwise false.
Get(key string) (value string, ok bool)
}
// Setter provides an interface to set config items
type Setter interface {
// Set should set an item into persistent config store.
Set(key, value string)
}
// Mapper provides an interface to read and write config
type Mapper interface {
Getter
Setter
}
// Map provides a wrapper around multiple Setter and
// Getter interfaces.
type Map struct {
setters []Setter
getters []Getter
}
// New returns an empty Map
func New() *Map {
return &Map{}
}
// AddGetter appends a getter onto the end of the getters
func (c *Map) AddGetter(getter Getter) *Map {
c.getters = append(c.getters, getter)
return c
}
// AddGetters appends multiple getters onto the end of the getters
func (c *Map) AddGetters(getters ...Getter) *Map {
c.getters = append(c.getters, getters...)
return c
}
// AddSetter appends a setter onto the end of the setters
func (c *Map) AddSetter(setter Setter) *Map {
c.setters = append(c.setters, setter)
return c
}
// Get gets an item with the key passed in and return the value from
// the first getter. If the item is found then it returns true,
// otherwise false.
func (c *Map) Get(key string) (value string, ok bool) {
for _, do := range c.getters {
value, ok = do.Get(key)
if ok {
return value, ok
}
}
return "", false
}
// Set sets an item into all the stored setters.
func (c *Map) Set(key, value string) {
for _, do := range c.setters {
do.Set(key, value)
}
}
// Simple is a simple Mapper for testing
type Simple map[string]string
// Get the value
func (c Simple) Get(key string) (value string, ok bool) {
value, ok = c[key]
return value, ok
}
// Set the value
func (c Simple) Set(key, value string) {
c[key] = value
}

View File

@@ -0,0 +1,91 @@
package configmap
import (
"testing"
"github.com/stretchr/testify/assert"
)
var (
_ Mapper = Simple(nil)
_ Getter = Simple(nil)
_ Setter = Simple(nil)
)
func TestConfigMapGet(t *testing.T) {
m := New()
value, found := m.Get("config1")
assert.Equal(t, "", value)
assert.Equal(t, false, found)
value, found = m.Get("config2")
assert.Equal(t, "", value)
assert.Equal(t, false, found)
m1 := Simple{
"config1": "one",
}
m.AddGetter(m1)
value, found = m.Get("config1")
assert.Equal(t, "one", value)
assert.Equal(t, true, found)
value, found = m.Get("config2")
assert.Equal(t, "", value)
assert.Equal(t, false, found)
m2 := Simple{
"config1": "one2",
"config2": "two2",
}
m.AddGetter(m2)
value, found = m.Get("config1")
assert.Equal(t, "one", value)
assert.Equal(t, true, found)
value, found = m.Get("config2")
assert.Equal(t, "two2", value)
assert.Equal(t, true, found)
}
func TestConfigMapSet(t *testing.T) {
m := New()
m1 := Simple{
"config1": "one",
}
m2 := Simple{
"config1": "one2",
"config2": "two2",
}
m.AddSetter(m1).AddSetter(m2)
m.Set("config2", "potato")
assert.Equal(t, Simple{
"config1": "one",
"config2": "potato",
}, m1)
assert.Equal(t, Simple{
"config1": "one2",
"config2": "potato",
}, m2)
m.Set("config1", "beetroot")
assert.Equal(t, Simple{
"config1": "beetroot",
"config2": "potato",
}, m1)
assert.Equal(t, Simple{
"config1": "beetroot",
"config2": "potato",
}, m2)
}

View File

@@ -0,0 +1,127 @@
// Package configstruct parses unstructured maps into structures
package configstruct
import (
"fmt"
"reflect"
"regexp"
"strings"
"github.com/ncw/rclone/fs/config/configmap"
"github.com/pkg/errors"
)
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
func StringToInterface(def interface{}, in string) (newValue interface{}, err error) {
typ := reflect.TypeOf(def)
switch typ.Kind() {
case reflect.String:
// Pass strings unmodified
return in, nil
}
// Otherwise parse with Sscanln
//
// This means any types we use here must implement fmt.Scanner
o := reflect.New(typ)
n, err := fmt.Sscanln(in, o.Interface())
if err != nil {
return newValue, errors.Wrapf(err, "parsing %q as %T failed", in, def)
}
if n != 1 {
return newValue, errors.New("no items parsed")
}
return o.Elem().Interface(), nil
}
// Item descripts a single entry in the options structure
type Item struct {
Name string // snake_case
Field string // CamelCase
Num int // number of the field in the struct
Value interface{}
}
// 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.
func Items(opt interface{}) (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 := 0; i < def.NumField(); i++ {
field := defType.Field(i)
fieldName := field.Name
configName, ok := field.Tag.Lookup("config")
if !ok {
configName = camelToSnake(fieldName)
}
defaultItem := Item{
Name: configName,
Field: fieldName,
Num: i,
Value: def.Field(i).Interface(),
}
items = append(items, defaultItem)
}
return items, 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 interface{}) (err error) {
defaultItems, err := Items(opt)
if err != nil {
return err
}
defStruct := reflect.ValueOf(opt).Elem()
for _, defaultItem := range defaultItems {
newValue := defaultItem.Value
if configValue, ok := config.Get(defaultItem.Name); ok {
var newNewValue interface{}
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 errors.Wrapf(err, "couldn't parse config item %q = %q as %T", defaultItem.Name, configValue, defaultItem.Value)
}
} else {
newValue = newNewValue
}
}
defStruct.Field(defaultItem.Num).Set(reflect.ValueOf(newValue))
}
return nil
}

View File

@@ -0,0 +1,116 @@
package configstruct_test
import (
"testing"
"time"
"github.com/ncw/rclone/fs"
"github.com/ncw/rclone/fs/config/configstruct"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type conf struct {
A string
B string
}
type conf2 struct {
PotatoPie string `config:"spud_pie"`
BeanStew bool
RaisinRoll int
SausageOnStick int64
ForbiddenFruit uint
CookingTime fs.Duration
TotalWeight fs.SizeSuffix
}
func TestItemsError(t *testing.T) {
_, err := configstruct.Items(nil)
assert.EqualError(t, err, "argument must be a pointer")
_, err = configstruct.Items(new(int))
assert.EqualError(t, err, "argument must be a pointer to a struct")
}
func TestItems(t *testing.T) {
in := &conf2{
PotatoPie: "yum",
BeanStew: true,
RaisinRoll: 42,
SausageOnStick: 101,
ForbiddenFruit: 6,
CookingTime: fs.Duration(42 * time.Second),
TotalWeight: fs.SizeSuffix(17 << 20),
}
got, err := configstruct.Items(in)
require.NoError(t, err)
want := []configstruct.Item{
{Name: "spud_pie", Field: "PotatoPie", Num: 0, Value: string("yum")},
{Name: "bean_stew", Field: "BeanStew", Num: 1, Value: true},
{Name: "raisin_roll", Field: "RaisinRoll", Num: 2, Value: int(42)},
{Name: "sausage_on_stick", Field: "SausageOnStick", Num: 3, Value: int64(101)},
{Name: "forbidden_fruit", Field: "ForbiddenFruit", Num: 4, Value: uint(6)},
{Name: "cooking_time", Field: "CookingTime", Num: 5, Value: fs.Duration(42 * time.Second)},
{Name: "total_weight", Field: "TotalWeight", Num: 6, Value: fs.SizeSuffix(17 << 20)},
}
assert.Equal(t, want, got)
}
func TestSetBasics(t *testing.T) {
c := &conf{A: "one", B: "two"}
err := configstruct.Set(configMap{}, c)
require.NoError(t, err)
assert.Equal(t, &conf{A: "one", B: "two"}, c)
}
// a simple configmap.Getter for testing
type configMap map[string]string
// Get the value
func (c configMap) Get(key string) (value string, ok bool) {
value, ok = c[key]
return value, ok
}
func TestSetMore(t *testing.T) {
c := &conf{A: "one", B: "two"}
m := configMap{
"a": "ONE",
}
err := configstruct.Set(m, c)
require.NoError(t, err)
assert.Equal(t, &conf{A: "ONE", B: "two"}, c)
}
func TestSetFull(t *testing.T) {
in := &conf2{
PotatoPie: "yum",
BeanStew: true,
RaisinRoll: 42,
SausageOnStick: 101,
ForbiddenFruit: 6,
CookingTime: fs.Duration(42 * time.Second),
TotalWeight: fs.SizeSuffix(17 << 20),
}
m := configMap{
"spud_pie": "YUM",
"bean_stew": "FALSE",
"raisin_roll": "43 ",
"sausage_on_stick": " 102 ",
"forbidden_fruit": "0x7",
"cooking_time": "43s",
"total_weight": "18M",
}
want := &conf2{
PotatoPie: "YUM",
BeanStew: false,
RaisinRoll: 43,
SausageOnStick: 102,
ForbiddenFruit: 7,
CookingTime: fs.Duration(43 * time.Second),
TotalWeight: fs.SizeSuffix(18 << 20),
}
err := configstruct.Set(m, in)
require.NoError(t, err)
assert.Equal(t, want, in)
}

View File

@@ -0,0 +1,60 @@
package configstruct
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCamelToSnake(t *testing.T) {
for _, test := range []struct {
in string
want string
}{
{"", ""},
{"Type", "type"},
{"AuthVersion", "auth_version"},
{"AccessKeyID", "access_key_id"},
} {
got := camelToSnake(test.in)
assert.Equal(t, test.want, got, test.in)
}
}
func TestStringToInterface(t *testing.T) {
item := struct{ A int }{2}
for _, test := range []struct {
in string
def interface{}
want interface{}
err string
}{
{"", string(""), "", ""},
{" string ", string(""), " string ", ""},
{"123", int(0), int(123), ""},
{"0x123", int(0), int(0x123), ""},
{" 0x123 ", int(0), int(0x123), ""},
{"-123", int(0), int(-123), ""},
{"0", false, false, ""},
{"1", false, true, ""},
{"FALSE", false, false, ""},
{"true", false, true, ""},
{"123", uint(0), uint(123), ""},
{"123", int64(0), int64(123), ""},
{"123x", int64(0), nil, "parsing \"123x\" as int64 failed: expected newline"},
{"truth", false, nil, "parsing \"truth\" as bool failed: syntax error scanning boolean"},
{"struct", item, nil, "parsing \"struct\" as struct { A int } failed: can't scan type: *struct { A int }"},
} {
what := fmt.Sprintf("parse %q as %T", test.in, test.def)
got, err := StringToInterface(test.def, test.in)
if test.err == "" {
require.NoError(t, err, what)
assert.Equal(t, test.want, got, what)
} else {
assert.Nil(t, got)
assert.EqualError(t, err, test.err, what)
}
}
}

View File

@@ -0,0 +1,195 @@
// Package flags contains enahnced versions of spf13/pflag flag
// routines which will read from the environment also.
package flags
import (
"log"
"os"
"time"
"github.com/ncw/rclone/fs"
"github.com/spf13/pflag"
)
// setDefaultFromEnv constructs a name from the flag passed in and
// sets the default from the environment if possible.
func setDefaultFromEnv(name string) {
key := fs.OptionToEnv(name)
newValue, found := os.LookupEnv(key)
if found {
flag := pflag.Lookup(name)
if flag == nil {
log.Fatalf("Couldn't find flag %q", name)
}
err := flag.Value.Set(newValue)
if err != nil {
log.Fatalf("Invalid value for environment variable %q: %v", key, err)
}
fs.Debugf(nil, "Set default for %q from %q to %q (%v)", name, key, newValue, flag.Value)
flag.DefValue = newValue
}
}
// StringP defines a flag which can be overridden by an environment variable
//
// It is a thin wrapper around pflag.StringP
func StringP(name, shorthand string, value string, usage string) (out *string) {
out = pflag.StringP(name, shorthand, value, usage)
setDefaultFromEnv(name)
return out
}
// StringVarP defines a flag which can be overridden by an environment variable
//
// It is a thin wrapper around pflag.StringVarP
func StringVarP(flags *pflag.FlagSet, p *string, name, shorthand string, value string, usage string) {
flags.StringVarP(p, name, shorthand, value, usage)
setDefaultFromEnv(name)
}
// BoolP defines a flag which can be overridden by an environment variable
//
// It is a thin wrapper around pflag.BoolP
func BoolP(name, shorthand string, value bool, usage string) (out *bool) {
out = pflag.BoolP(name, shorthand, value, usage)
setDefaultFromEnv(name)
return out
}
// BoolVarP defines a flag which can be overridden by an environment variable
//
// It is a thin wrapper around pflag.BoolVarP
func BoolVarP(flags *pflag.FlagSet, p *bool, name, shorthand string, value bool, usage string) {
flags.BoolVarP(p, name, shorthand, value, usage)
setDefaultFromEnv(name)
}
// IntP defines a flag which can be overridden by an environment variable
//
// It is a thin wrapper around pflag.IntP
func IntP(name, shorthand string, value int, usage string) (out *int) {
out = pflag.IntP(name, shorthand, value, usage)
setDefaultFromEnv(name)
return out
}
// Int64P defines a flag which can be overridden by an environment variable
//
// It is a thin wrapper around pflag.IntP
func Int64P(name, shorthand string, value int64, usage string) (out *int64) {
out = pflag.Int64P(name, shorthand, value, usage)
setDefaultFromEnv(name)
return out
}
// IntVar64P defines a flag which can be overridden by an environment variable
//
// It is a thin wrapper around pflag.Int64VarP
func IntVar64P(flags *pflag.FlagSet, p *int64, name, shorthand string, value int64, usage string) {
flags.Int64VarP(p, name, shorthand, value, usage)
setDefaultFromEnv(name)
}
// IntVarP defines a flag which can be overridden by an environment variable
//
// It is a thin wrapper around pflag.IntVarP
func IntVarP(flags *pflag.FlagSet, p *int, name, shorthand string, value int, usage string) {
flags.IntVarP(p, name, shorthand, value, usage)
setDefaultFromEnv(name)
}
// Uint32VarP defines a flag which can be overridden by an environment variable
//
// It is a thin wrapper around pflag.Uint32VarP
func Uint32VarP(flags *pflag.FlagSet, p *uint32, name, shorthand string, value uint32, usage string) {
flags.Uint32VarP(p, name, shorthand, value, usage)
setDefaultFromEnv(name)
}
// Float64P defines a flag which can be overridden by an environment variable
//
// It is a thin wrapper around pflag.Float64P
func Float64P(name, shorthand string, value float64, usage string) (out *float64) {
out = pflag.Float64P(name, shorthand, value, usage)
setDefaultFromEnv(name)
return out
}
// Float64VarP defines a flag which can be overridden by an environment variable
//
// It is a thin wrapper around pflag.Float64VarP
func Float64VarP(flags *pflag.FlagSet, p *float64, name, shorthand string, value float64, usage string) {
flags.Float64VarP(p, name, shorthand, value, usage)
setDefaultFromEnv(name)
}
// DurationP defines a flag which can be overridden by an environment variable
//
// It is a thin wrapper around pflag.DurationP
func DurationP(name, shorthand string, value time.Duration, usage string) (out *time.Duration) {
out = pflag.DurationP(name, shorthand, value, usage)
setDefaultFromEnv(name)
return out
}
// DurationVarP defines a flag which can be overridden by an environment variable
//
// It is a thin wrapper around pflag.DurationVarP
func DurationVarP(flags *pflag.FlagSet, p *time.Duration, name, shorthand string, value time.Duration, usage string) {
flags.DurationVarP(p, name, shorthand, value, usage)
setDefaultFromEnv(name)
}
// VarP defines a flag which can be overridden by an environment variable
//
// It is a thin wrapper around pflag.VarP
func VarP(value pflag.Value, name, shorthand, usage string) {
pflag.VarP(value, name, shorthand, usage)
setDefaultFromEnv(name)
}
// FVarP defines a flag which can be overridden by an environment variable
//
// It is a thin wrapper around pflag.VarP
func FVarP(flags *pflag.FlagSet, value pflag.Value, name, shorthand, usage string) {
flags.VarP(value, name, shorthand, usage)
setDefaultFromEnv(name)
}
// StringArrayP defines a flag which can be overridden by an environment variable
//
// It sets one value only - command line flags can be used to set more.
//
// It is a thin wrapper around pflag.StringArrayP
func StringArrayP(name, shorthand string, value []string, usage string) (out *[]string) {
out = pflag.StringArrayP(name, shorthand, value, usage)
setDefaultFromEnv(name)
return out
}
// StringArrayVarP defines a flag which can be overridden by an environment variable
//
// It sets one value only - command line flags can be used to set more.
//
// It is a thin wrapper around pflag.StringArrayVarP
func StringArrayVarP(flags *pflag.FlagSet, p *[]string, name, shorthand string, value []string, usage string) {
flags.StringArrayVarP(p, name, shorthand, value, usage)
setDefaultFromEnv(name)
}
// CountP defines a flag which can be overridden by an environment variable
//
// It is a thin wrapper around pflag.CountP
func CountP(name, shorthand string, usage string) (out *int) {
out = pflag.CountP(name, shorthand, usage)
setDefaultFromEnv(name)
return out
}
// CountVarP defines a flag which can be overridden by an environment variable
//
// It is a thin wrapper around pflag.CountVarP
func CountVarP(flags *pflag.FlagSet, p *int, name, shorthand string, usage string) {
flags.CountVarP(p, name, shorthand, usage)
setDefaultFromEnv(name)
}

View File

@@ -0,0 +1,94 @@
// Package obscure contains the Obscure and Reveal commands
package obscure
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"io"
"log"
"github.com/pkg/errors"
)
// crypt internals
var (
cryptKey = []byte{
0x9c, 0x93, 0x5b, 0x48, 0x73, 0x0a, 0x55, 0x4d,
0x6b, 0xfd, 0x7c, 0x63, 0xc8, 0x86, 0xa9, 0x2b,
0xd3, 0x90, 0x19, 0x8e, 0xb8, 0x12, 0x8a, 0xfb,
0xf4, 0xde, 0x16, 0x2b, 0x8b, 0x95, 0xf6, 0x38,
}
cryptBlock cipher.Block
cryptRand = rand.Reader
)
// crypt transforms in to out using iv under AES-CTR.
//
// in and out may be the same buffer.
//
// Note encryption and decryption are the same operation
func crypt(out, in, iv []byte) error {
if cryptBlock == nil {
var err error
cryptBlock, err = aes.NewCipher(cryptKey)
if err != nil {
return err
}
}
stream := cipher.NewCTR(cryptBlock, iv)
stream.XORKeyStream(out, in)
return nil
}
// Obscure a value
//
// This is done by encrypting with AES-CTR
func Obscure(x string) (string, error) {
plaintext := []byte(x)
ciphertext := make([]byte, aes.BlockSize+len(plaintext))
iv := ciphertext[:aes.BlockSize]
if _, err := io.ReadFull(cryptRand, iv); err != nil {
return "", errors.Wrap(err, "failed to read iv")
}
if err := crypt(ciphertext[aes.BlockSize:], plaintext, iv); err != nil {
return "", errors.Wrap(err, "encrypt failed")
}
return base64.RawURLEncoding.EncodeToString(ciphertext), nil
}
// MustObscure obscures a value, exiting with a fatal error if it failed
func MustObscure(x string) string {
out, err := Obscure(x)
if err != nil {
log.Fatalf("Obscure failed: %v", err)
}
return out
}
// Reveal an obscured value
func Reveal(x string) (string, error) {
ciphertext, err := base64.RawURLEncoding.DecodeString(x)
if err != nil {
return "", errors.Wrap(err, "base64 decode failed when revealing password - is it obscured?")
}
if len(ciphertext) < aes.BlockSize {
return "", errors.New("input too short when revealing password - is it obscured?")
}
buf := ciphertext[aes.BlockSize:]
iv := ciphertext[:aes.BlockSize]
if err := crypt(buf, buf, iv); err != nil {
return "", errors.Wrap(err, "decrypt failed when revealing password - is it obscured?")
}
return string(buf), nil
}
// MustReveal reveals an obscured value, exiting with a fatal error if it failed
func MustReveal(x string) string {
out, err := Reveal(x)
if err != nil {
log.Fatalf("Reveal failed: %v", err)
}
return out
}

View File

@@ -0,0 +1,60 @@
package obscure
import (
"bytes"
"crypto/rand"
"testing"
"github.com/stretchr/testify/assert"
)
func TestObscure(t *testing.T) {
for _, test := range []struct {
in string
want string
iv string
}{
{"", "YWFhYWFhYWFhYWFhYWFhYQ", "aaaaaaaaaaaaaaaa"},
{"potato", "YWFhYWFhYWFhYWFhYWFhYXMaGgIlEQ", "aaaaaaaaaaaaaaaa"},
{"potato", "YmJiYmJiYmJiYmJiYmJiYp3gcEWbAw", "bbbbbbbbbbbbbbbb"},
} {
cryptRand = bytes.NewBufferString(test.iv)
got, err := Obscure(test.in)
cryptRand = rand.Reader
assert.NoError(t, err)
assert.Equal(t, test.want, got)
recoveredIn, err := Reveal(got)
assert.NoError(t, err)
assert.Equal(t, test.in, recoveredIn, "not bidirectional")
// Now the Must variants
cryptRand = bytes.NewBufferString(test.iv)
got = MustObscure(test.in)
cryptRand = rand.Reader
assert.Equal(t, test.want, got)
recoveredIn = MustReveal(got)
assert.Equal(t, test.in, recoveredIn, "not bidirectional")
}
}
func TestReveal(t *testing.T) {
for _, test := range []struct {
in string
want string
iv string
}{
{"YWFhYWFhYWFhYWFhYWFhYQ", "", "aaaaaaaaaaaaaaaa"},
{"YWFhYWFhYWFhYWFhYWFhYXMaGgIlEQ", "potato", "aaaaaaaaaaaaaaaa"},
{"YmJiYmJiYmJiYmJiYmJiYp3gcEWbAw", "potato", "bbbbbbbbbbbbbbbb"},
} {
cryptRand = bytes.NewBufferString(test.iv)
got, err := Reveal(test.in)
assert.NoError(t, err)
assert.Equal(t, test.want, got)
// Now the Must variants
cryptRand = bytes.NewBufferString(test.iv)
got = MustReveal(test.in)
assert.Equal(t, test.want, got)
}
}

View File

@@ -0,0 +1,4 @@
# Encrypted rclone configuration File
RCLONE_ENCRYPT_V0:
b5Uk6mE3cUn5Wb8xiWYnVBAxXUirAaEG1PO/GIDiO9274AOæøå+Yj790BwJA4d2y7lNkmHt4nJwIsoueFvUYmm7RDyzER8IA3XOCrjzl3OUcczZqcplk5JfBdhxMZpt1aGYWUdle1IgO/kAFne6sLD6IuxPySEb

View File

@@ -0,0 +1,4 @@
# Encrypted rclone configuration File
RCLONE_ENCRYPT_V0:
b5Uk6mE3cUn5Wb8xi

View File

@@ -0,0 +1,4 @@
# Encrypted rclone configuration File
RCLONE_ENCRYPT_V1:
b5Uk6mE3cUn5Wb8xiWYnVBAxXUirAaEG1PO/GIDiO9274AO+Yj790BwJA4d2y7lNkmHt4nJwIsoueFvUYmm7RDyzER8IA3XOCrjzl3OUcczZqcplk5JfBdhxMZpt1aGYWUdle1IgO/kAFne6sLD6IuxPySEb

View File

@@ -0,0 +1,4 @@
# Encrypted rclone configuration File
RCLONE_ENCRYPT_V0:
b5Uk6mE3cUn5Wb8xiWYnVBAxXUirAaEG1PO/GIDiO9274AO+Yj790BwJA4d2y7lNkmHt4nJwIsoueFvUYmm7RDyzER8IA3XOCrjzl3OUcczZqcplk5JfBdhxMZpt1aGYWUdle1IgO/kAFne6sLD6IuxPySEb

12
.rclone_repo/fs/config/testdata/plain.conf vendored Executable file
View File

@@ -0,0 +1,12 @@
[RCLONE_ENCRYPT_V0]
type = local
nounc = true
[nounc]
type = local
nounc = true
[unc]
type = local
nounc = false