mirror of
https://github.com/rclone/rclone.git
synced 2025-01-10 16:28:30 +01:00
config: split up main file more and move tests into correct packages
This splits config.go into ui.go for the user interface functions and authorize.go for the implementation of `rclone authorize`. It also moves the tests into the correct places (including one from obscure which was in the wrong place).
This commit is contained in:
parent
a7fd65bf2d
commit
6a9ae32012
47
fs/config/authorize.go
Normal file
47
fs/config/authorize.go
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/rclone/rclone/fs"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Authorize is for remote authorization of headless machines.
|
||||||
|
//
|
||||||
|
// It expects 1 or 3 arguments
|
||||||
|
//
|
||||||
|
// rclone authorize "fs name"
|
||||||
|
// rclone authorize "fs name" "client id" "client secret"
|
||||||
|
func Authorize(ctx context.Context, args []string, noAutoBrowser bool) {
|
||||||
|
ctx = suppressConfirm(ctx)
|
||||||
|
switch len(args) {
|
||||||
|
case 1, 3:
|
||||||
|
default:
|
||||||
|
log.Fatalf("Invalid number of arguments: %d", len(args))
|
||||||
|
}
|
||||||
|
newType := args[0]
|
||||||
|
f := fs.MustFind(newType)
|
||||||
|
if f.Config == nil {
|
||||||
|
log.Fatalf("Can't authorize fs %q", newType)
|
||||||
|
}
|
||||||
|
// Name used for temporary fs
|
||||||
|
name := "**temp-fs**"
|
||||||
|
|
||||||
|
// Make sure we delete it
|
||||||
|
defer DeleteRemote(name)
|
||||||
|
|
||||||
|
// Indicate that we are running rclone authorize
|
||||||
|
Data.SetValue(name, ConfigAuthorize, "true")
|
||||||
|
if noAutoBrowser {
|
||||||
|
Data.SetValue(name, ConfigAuthNoBrowser, "true")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) == 3 {
|
||||||
|
Data.SetValue(name, ConfigClientID, args[1])
|
||||||
|
Data.SetValue(name, ConfigClientSecret, args[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
m := fs.ConfigMap(f, name)
|
||||||
|
f.Config(ctx, name, m)
|
||||||
|
}
|
@ -2,7 +2,6 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -12,8 +11,6 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -21,14 +18,10 @@ import (
|
|||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/rclone/rclone/fs"
|
"github.com/rclone/rclone/fs"
|
||||||
"github.com/rclone/rclone/fs/config/configmap"
|
|
||||||
"github.com/rclone/rclone/fs/config/configstruct"
|
|
||||||
"github.com/rclone/rclone/fs/config/obscure"
|
"github.com/rclone/rclone/fs/config/obscure"
|
||||||
"github.com/rclone/rclone/fs/driveletter"
|
|
||||||
"github.com/rclone/rclone/fs/fspath"
|
"github.com/rclone/rclone/fs/fspath"
|
||||||
"github.com/rclone/rclone/fs/rc"
|
"github.com/rclone/rclone/fs/rc"
|
||||||
"github.com/rclone/rclone/lib/random"
|
"github.com/rclone/rclone/lib/random"
|
||||||
"github.com/rclone/rclone/lib/terminal"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -302,385 +295,6 @@ func FileGetFresh(section, key string) (value string, err error) {
|
|||||||
return value, nil
|
return value, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ShowRemotes shows an overview of the config file
|
|
||||||
func ShowRemotes() {
|
|
||||||
remotes := Data.GetSectionList()
|
|
||||||
if len(remotes) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
sort.Strings(remotes)
|
|
||||||
fmt.Printf("%-20s %s\n", "Name", "Type")
|
|
||||||
fmt.Printf("%-20s %s\n", "====", "====")
|
|
||||||
for _, remote := range remotes {
|
|
||||||
fmt.Printf("%-20s %s\n", remote, FileGet(remote, "type"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ChooseRemote chooses a remote name
|
|
||||||
func ChooseRemote() string {
|
|
||||||
remotes := Data.GetSectionList()
|
|
||||||
sort.Strings(remotes)
|
|
||||||
return Choose("remote", remotes, nil, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadLine reads some input
|
|
||||||
var ReadLine = func() string {
|
|
||||||
buf := bufio.NewReader(os.Stdin)
|
|
||||||
line, err := buf.ReadString('\n')
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to read line: %v", err)
|
|
||||||
}
|
|
||||||
return strings.TrimSpace(line)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadNonEmptyLine prints prompt and calls Readline until non empty
|
|
||||||
func ReadNonEmptyLine(prompt string) string {
|
|
||||||
result := ""
|
|
||||||
for result == "" {
|
|
||||||
fmt.Print(prompt)
|
|
||||||
result = strings.TrimSpace(ReadLine())
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// CommandDefault - choose one. If return is pressed then it will
|
|
||||||
// chose the defaultIndex if it is >= 0
|
|
||||||
func CommandDefault(commands []string, defaultIndex int) byte {
|
|
||||||
opts := []string{}
|
|
||||||
for i, text := range commands {
|
|
||||||
def := ""
|
|
||||||
if i == defaultIndex {
|
|
||||||
def = " (default)"
|
|
||||||
}
|
|
||||||
fmt.Printf("%c) %s%s\n", text[0], text[1:], def)
|
|
||||||
opts = append(opts, text[:1])
|
|
||||||
}
|
|
||||||
optString := strings.Join(opts, "")
|
|
||||||
optHelp := strings.Join(opts, "/")
|
|
||||||
for {
|
|
||||||
fmt.Printf("%s> ", optHelp)
|
|
||||||
result := strings.ToLower(ReadLine())
|
|
||||||
if len(result) == 0 && defaultIndex >= 0 {
|
|
||||||
return optString[defaultIndex]
|
|
||||||
}
|
|
||||||
if len(result) != 1 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
i := strings.Index(optString, string(result[0]))
|
|
||||||
if i >= 0 {
|
|
||||||
return result[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Command - choose one
|
|
||||||
func Command(commands []string) byte {
|
|
||||||
return CommandDefault(commands, -1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Confirm asks the user for Yes or No and returns true or false
|
|
||||||
//
|
|
||||||
// If the user presses enter then the Default will be used
|
|
||||||
func Confirm(Default bool) bool {
|
|
||||||
defaultIndex := 0
|
|
||||||
if !Default {
|
|
||||||
defaultIndex = 1
|
|
||||||
}
|
|
||||||
return CommandDefault([]string{"yYes", "nNo"}, defaultIndex) == 'y'
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConfirmWithConfig asks the user for Yes or No and returns true or
|
|
||||||
// false.
|
|
||||||
//
|
|
||||||
// If AutoConfirm is set, it will look up the value in m and return
|
|
||||||
// that, but if it isn't set then it will return the Default value
|
|
||||||
// passed in
|
|
||||||
func ConfirmWithConfig(ctx context.Context, m configmap.Getter, configName string, Default bool) bool {
|
|
||||||
ci := fs.GetConfig(ctx)
|
|
||||||
if ci.AutoConfirm {
|
|
||||||
configString, ok := m.Get(configName)
|
|
||||||
if ok {
|
|
||||||
configValue, err := strconv.ParseBool(configString)
|
|
||||||
if err != nil {
|
|
||||||
fs.Errorf(nil, "Failed to parse config parameter %s=%q as boolean - using default %v: %v", configName, configString, Default, err)
|
|
||||||
} else {
|
|
||||||
Default = configValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
answer := "No"
|
|
||||||
if Default {
|
|
||||||
answer = "Yes"
|
|
||||||
}
|
|
||||||
fmt.Printf("Auto confirm is set: answering %s, override by setting config parameter %s=%v\n", answer, configName, !Default)
|
|
||||||
return Default
|
|
||||||
}
|
|
||||||
return Confirm(Default)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Choose one of the defaults or type a new string if newOk is set
|
|
||||||
func Choose(what string, defaults, help []string, newOk bool) string {
|
|
||||||
valueDescription := "an existing"
|
|
||||||
if newOk {
|
|
||||||
valueDescription = "your own"
|
|
||||||
}
|
|
||||||
fmt.Printf("Choose a number from below, or type in %s value\n", valueDescription)
|
|
||||||
attributes := []string{terminal.HiRedFg, terminal.HiGreenFg}
|
|
||||||
for i, text := range defaults {
|
|
||||||
var lines []string
|
|
||||||
if help != nil {
|
|
||||||
parts := strings.Split(help[i], "\n")
|
|
||||||
lines = append(lines, parts...)
|
|
||||||
}
|
|
||||||
lines = append(lines, fmt.Sprintf("%q", text))
|
|
||||||
pos := i + 1
|
|
||||||
terminal.WriteString(attributes[i%len(attributes)])
|
|
||||||
if len(lines) == 1 {
|
|
||||||
fmt.Printf("%2d > %s\n", pos, text)
|
|
||||||
} else {
|
|
||||||
mid := (len(lines) - 1) / 2
|
|
||||||
for i, line := range lines {
|
|
||||||
var sep rune
|
|
||||||
switch i {
|
|
||||||
case 0:
|
|
||||||
sep = '/'
|
|
||||||
case len(lines) - 1:
|
|
||||||
sep = '\\'
|
|
||||||
default:
|
|
||||||
sep = '|'
|
|
||||||
}
|
|
||||||
number := " "
|
|
||||||
if i == mid {
|
|
||||||
number = fmt.Sprintf("%2d", pos)
|
|
||||||
}
|
|
||||||
fmt.Printf("%s %c %s\n", number, sep, line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
terminal.WriteString(terminal.Reset)
|
|
||||||
}
|
|
||||||
for {
|
|
||||||
fmt.Printf("%s> ", what)
|
|
||||||
result := ReadLine()
|
|
||||||
i, err := strconv.Atoi(result)
|
|
||||||
if err != nil {
|
|
||||||
if newOk {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
for _, v := range defaults {
|
|
||||||
if result == v {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if i >= 1 && i <= len(defaults) {
|
|
||||||
return defaults[i-1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ChooseNumber asks the user to enter a number between min and max
|
|
||||||
// inclusive prompting them with what.
|
|
||||||
func ChooseNumber(what string, min, max int) int {
|
|
||||||
for {
|
|
||||||
fmt.Printf("%s> ", what)
|
|
||||||
result := ReadLine()
|
|
||||||
i, err := strconv.Atoi(result)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Bad number: %v\n", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if i < min || i > max {
|
|
||||||
fmt.Printf("Out of range - %d to %d inclusive\n", min, max)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ShowRemote shows the contents of the remote
|
|
||||||
func ShowRemote(name string) {
|
|
||||||
fmt.Printf("--------------------\n")
|
|
||||||
fmt.Printf("[%s]\n", name)
|
|
||||||
fs := MustFindByName(name)
|
|
||||||
for _, key := range Data.GetKeyList(name) {
|
|
||||||
isPassword := false
|
|
||||||
for _, option := range fs.Options {
|
|
||||||
if option.Name == key && option.IsPassword {
|
|
||||||
isPassword = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
value := FileGet(name, key)
|
|
||||||
if isPassword && value != "" {
|
|
||||||
fmt.Printf("%s = *** ENCRYPTED ***\n", key)
|
|
||||||
} else {
|
|
||||||
fmt.Printf("%s = %s\n", key, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fmt.Printf("--------------------\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
// OkRemote prints the contents of the remote and ask if it is OK
|
|
||||||
func OkRemote(name string) bool {
|
|
||||||
ShowRemote(name)
|
|
||||||
switch i := CommandDefault([]string{"yYes this is OK", "eEdit this remote", "dDelete this remote"}, 0); i {
|
|
||||||
case 'y':
|
|
||||||
return true
|
|
||||||
case 'e':
|
|
||||||
return false
|
|
||||||
case 'd':
|
|
||||||
Data.DeleteSection(name)
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
fs.Errorf(nil, "Bad choice %c", i)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// MustFindByName finds the RegInfo for the remote name passed in or
|
|
||||||
// exits with a fatal error.
|
|
||||||
func MustFindByName(name string) *fs.RegInfo {
|
|
||||||
fsType := FileGet(name, "type")
|
|
||||||
if fsType == "" {
|
|
||||||
log.Fatalf("Couldn't find type of fs for %q", name)
|
|
||||||
}
|
|
||||||
return fs.MustFind(fsType)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoteConfig runs the config helper for the remote if needed
|
|
||||||
func RemoteConfig(ctx context.Context, name string) {
|
|
||||||
fmt.Printf("Remote config\n")
|
|
||||||
f := MustFindByName(name)
|
|
||||||
if f.Config != nil {
|
|
||||||
m := fs.ConfigMap(f, name)
|
|
||||||
f.Config(ctx, name, m)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// matchProvider returns true if provider matches the providerConfig string.
|
|
||||||
//
|
|
||||||
// The providerConfig string can either be a list of providers to
|
|
||||||
// match, or if it starts with "!" it will be a list of providers not
|
|
||||||
// to match.
|
|
||||||
//
|
|
||||||
// If either providerConfig or provider is blank then it will return true
|
|
||||||
func matchProvider(providerConfig, provider string) bool {
|
|
||||||
if providerConfig == "" || provider == "" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
negate := false
|
|
||||||
if strings.HasPrefix(providerConfig, "!") {
|
|
||||||
providerConfig = providerConfig[1:]
|
|
||||||
negate = true
|
|
||||||
}
|
|
||||||
providers := strings.Split(providerConfig, ",")
|
|
||||||
matched := false
|
|
||||||
for _, p := range providers {
|
|
||||||
if p == provider {
|
|
||||||
matched = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if negate {
|
|
||||||
return !matched
|
|
||||||
}
|
|
||||||
return matched
|
|
||||||
}
|
|
||||||
|
|
||||||
// ChooseOption asks the user to choose an option
|
|
||||||
func ChooseOption(o *fs.Option, name string) string {
|
|
||||||
var subProvider = getWithDefault(name, fs.ConfigProvider, "")
|
|
||||||
fmt.Println(o.Help)
|
|
||||||
if o.IsPassword {
|
|
||||||
actions := []string{"yYes type in my own password", "gGenerate random password"}
|
|
||||||
defaultAction := -1
|
|
||||||
if !o.Required {
|
|
||||||
defaultAction = len(actions)
|
|
||||||
actions = append(actions, "nNo leave this optional password blank")
|
|
||||||
}
|
|
||||||
var password string
|
|
||||||
var err error
|
|
||||||
switch i := CommandDefault(actions, defaultAction); i {
|
|
||||||
case 'y':
|
|
||||||
password = ChangePassword("the")
|
|
||||||
case 'g':
|
|
||||||
for {
|
|
||||||
fmt.Printf("Password strength in bits.\n64 is just about memorable\n128 is secure\n1024 is the maximum\n")
|
|
||||||
bits := ChooseNumber("Bits", 64, 1024)
|
|
||||||
password, err = Password(bits)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to make password: %v", err)
|
|
||||||
}
|
|
||||||
fmt.Printf("Your password is: %s\n", password)
|
|
||||||
fmt.Printf("Use this password? Please note that an obscured version of this \npassword (and not the " +
|
|
||||||
"password itself) will be stored under your \nconfiguration file, so keep this generated password " +
|
|
||||||
"in a safe place.\n")
|
|
||||||
if Confirm(true) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case 'n':
|
|
||||||
return ""
|
|
||||||
default:
|
|
||||||
fs.Errorf(nil, "Bad choice %c", i)
|
|
||||||
}
|
|
||||||
return obscure.MustObscure(password)
|
|
||||||
}
|
|
||||||
what := fmt.Sprintf("%T value", o.Default)
|
|
||||||
switch o.Default.(type) {
|
|
||||||
case bool:
|
|
||||||
what = "boolean value (true or false)"
|
|
||||||
case fs.SizeSuffix:
|
|
||||||
what = "size with suffix k,M,G,T"
|
|
||||||
case fs.Duration:
|
|
||||||
what = "duration s,m,h,d,w,M,y"
|
|
||||||
case int, int8, int16, int32, int64:
|
|
||||||
what = "signed integer"
|
|
||||||
case uint, byte, uint16, uint32, uint64:
|
|
||||||
what = "unsigned integer"
|
|
||||||
}
|
|
||||||
var in string
|
|
||||||
for {
|
|
||||||
fmt.Printf("Enter a %s. Press Enter for the default (%q).\n", what, fmt.Sprint(o.Default))
|
|
||||||
if len(o.Examples) > 0 {
|
|
||||||
var values []string
|
|
||||||
var help []string
|
|
||||||
for _, example := range o.Examples {
|
|
||||||
if matchProvider(example.Provider, subProvider) {
|
|
||||||
values = append(values, example.Value)
|
|
||||||
help = append(help, example.Help)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
in = Choose(o.Name, values, help, true)
|
|
||||||
} else {
|
|
||||||
fmt.Printf("%s> ", o.Name)
|
|
||||||
in = ReadLine()
|
|
||||||
}
|
|
||||||
if in == "" {
|
|
||||||
if o.Required && fmt.Sprint(o.Default) == "" {
|
|
||||||
fmt.Printf("This value is required and it has no default.\n")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
newIn, err := configstruct.StringToInterface(o.Default, in)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Failed to parse %q: %v\n", in, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
in = fmt.Sprint(newIn) // canonicalise
|
|
||||||
break
|
|
||||||
}
|
|
||||||
return in
|
|
||||||
}
|
|
||||||
|
|
||||||
// Suppress the confirm prompts by altering the context config
|
|
||||||
func suppressConfirm(ctx context.Context) context.Context {
|
|
||||||
newCtx, ci := fs.AddConfig(ctx)
|
|
||||||
ci.AutoConfirm = true
|
|
||||||
return newCtx
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateRemote adds the keyValues passed in to the remote of name.
|
// UpdateRemote adds the keyValues passed in to the remote of name.
|
||||||
// keyValues should be key, value pairs.
|
// keyValues should be key, value pairs.
|
||||||
func UpdateRemote(ctx context.Context, name string, keyValues rc.Params, doObscure, noObscure bool) error {
|
func UpdateRemote(ctx context.Context, name string, keyValues rc.Params, doObscure, noObscure bool) error {
|
||||||
@ -794,241 +408,6 @@ func fsOption() *fs.Option {
|
|||||||
return o
|
return o
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRemoteName asks the user for a name for a new remote
|
|
||||||
func NewRemoteName() (name string) {
|
|
||||||
for {
|
|
||||||
fmt.Printf("name> ")
|
|
||||||
name = ReadLine()
|
|
||||||
if Data.HasSection(name) {
|
|
||||||
fmt.Printf("Remote %q already exists.\n", name)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
err := fspath.CheckConfigName(name)
|
|
||||||
switch {
|
|
||||||
case name == "":
|
|
||||||
fmt.Printf("Can't use empty name.\n")
|
|
||||||
case driveletter.IsDriveLetter(name):
|
|
||||||
fmt.Printf("Can't use %q as it can be confused with a drive letter.\n", name)
|
|
||||||
case err != nil:
|
|
||||||
fmt.Printf("Can't use %q as %v.\n", name, err)
|
|
||||||
default:
|
|
||||||
return name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// editOptions edits the options. If new is true then it just allows
|
|
||||||
// entry and doesn't show any old values.
|
|
||||||
func editOptions(ri *fs.RegInfo, name string, isNew bool) {
|
|
||||||
fmt.Printf("** See help for %s backend at: https://rclone.org/%s/ **\n\n", ri.Name, ri.FileName())
|
|
||||||
hasAdvanced := false
|
|
||||||
for _, advanced := range []bool{false, true} {
|
|
||||||
if advanced {
|
|
||||||
if !hasAdvanced {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
fmt.Printf("Edit advanced config? (y/n)\n")
|
|
||||||
if !Confirm(false) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, option := range ri.Options {
|
|
||||||
isVisible := option.Hide&fs.OptionHideConfigurator == 0
|
|
||||||
hasAdvanced = hasAdvanced || (option.Advanced && isVisible)
|
|
||||||
if option.Advanced != advanced {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
subProvider := getWithDefault(name, fs.ConfigProvider, "")
|
|
||||||
if matchProvider(option.Provider, subProvider) && isVisible {
|
|
||||||
if !isNew {
|
|
||||||
fmt.Printf("Value %q = %q\n", option.Name, FileGet(name, option.Name))
|
|
||||||
fmt.Printf("Edit? (y/n)>\n")
|
|
||||||
if !Confirm(false) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
FileSet(name, option.Name, ChooseOption(&option, name))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewRemote make a new remote from its name
|
|
||||||
func NewRemote(ctx context.Context, name string) {
|
|
||||||
var (
|
|
||||||
newType string
|
|
||||||
ri *fs.RegInfo
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
// Set the type first
|
|
||||||
for {
|
|
||||||
newType = ChooseOption(fsOption(), name)
|
|
||||||
ri, err = fs.Find(newType)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Bad remote %q: %v\n", newType, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
Data.SetValue(name, "type", newType)
|
|
||||||
|
|
||||||
editOptions(ri, name, true)
|
|
||||||
RemoteConfig(ctx, name)
|
|
||||||
if OkRemote(name) {
|
|
||||||
SaveConfig()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
EditRemote(ctx, ri, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// EditRemote gets the user to edit a remote
|
|
||||||
func EditRemote(ctx context.Context, ri *fs.RegInfo, name string) {
|
|
||||||
ShowRemote(name)
|
|
||||||
fmt.Printf("Edit remote\n")
|
|
||||||
for {
|
|
||||||
editOptions(ri, name, false)
|
|
||||||
if OkRemote(name) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SaveConfig()
|
|
||||||
RemoteConfig(ctx, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteRemote gets the user to delete a remote
|
|
||||||
func DeleteRemote(name string) {
|
|
||||||
Data.DeleteSection(name)
|
|
||||||
SaveConfig()
|
|
||||||
}
|
|
||||||
|
|
||||||
// copyRemote asks the user for a new remote name and copies name into
|
|
||||||
// it. Returns the new name.
|
|
||||||
func copyRemote(name string) string {
|
|
||||||
newName := NewRemoteName()
|
|
||||||
// Copy the keys
|
|
||||||
for _, key := range Data.GetKeyList(name) {
|
|
||||||
value := getWithDefault(name, key, "")
|
|
||||||
Data.SetValue(newName, key, value)
|
|
||||||
}
|
|
||||||
return newName
|
|
||||||
}
|
|
||||||
|
|
||||||
// RenameRemote renames a config section
|
|
||||||
func RenameRemote(name string) {
|
|
||||||
fmt.Printf("Enter new name for %q remote.\n", name)
|
|
||||||
newName := copyRemote(name)
|
|
||||||
if name != newName {
|
|
||||||
Data.DeleteSection(name)
|
|
||||||
SaveConfig()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CopyRemote copies a config section
|
|
||||||
func CopyRemote(name string) {
|
|
||||||
fmt.Printf("Enter name for copy of %q remote.\n", name)
|
|
||||||
copyRemote(name)
|
|
||||||
SaveConfig()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ShowConfigLocation prints the location of the config file in use
|
|
||||||
func ShowConfigLocation() {
|
|
||||||
if _, err := os.Stat(ConfigPath); os.IsNotExist(err) {
|
|
||||||
fmt.Println("Configuration file doesn't exist, but rclone will use this path:")
|
|
||||||
} else {
|
|
||||||
fmt.Println("Configuration file is stored at:")
|
|
||||||
}
|
|
||||||
fmt.Printf("%s\n", ConfigPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ShowConfig prints the (unencrypted) config options
|
|
||||||
func ShowConfig() {
|
|
||||||
str, err := Data.Serialize()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to serialize config: %v", err)
|
|
||||||
}
|
|
||||||
if str == "" {
|
|
||||||
str = "; empty config\n"
|
|
||||||
}
|
|
||||||
fmt.Printf("%s", str)
|
|
||||||
}
|
|
||||||
|
|
||||||
// EditConfig edits the config file interactively
|
|
||||||
func EditConfig(ctx context.Context) {
|
|
||||||
for {
|
|
||||||
haveRemotes := len(Data.GetSectionList()) != 0
|
|
||||||
what := []string{"eEdit existing remote", "nNew remote", "dDelete remote", "rRename remote", "cCopy remote", "sSet configuration password", "qQuit config"}
|
|
||||||
if haveRemotes {
|
|
||||||
fmt.Printf("Current remotes:\n\n")
|
|
||||||
ShowRemotes()
|
|
||||||
fmt.Printf("\n")
|
|
||||||
} else {
|
|
||||||
fmt.Printf("No remotes found - make a new one\n")
|
|
||||||
// take 2nd item and last 2 items of menu list
|
|
||||||
what = append(what[1:2], what[len(what)-2:]...)
|
|
||||||
}
|
|
||||||
switch i := Command(what); i {
|
|
||||||
case 'e':
|
|
||||||
name := ChooseRemote()
|
|
||||||
fs := MustFindByName(name)
|
|
||||||
EditRemote(ctx, fs, name)
|
|
||||||
case 'n':
|
|
||||||
NewRemote(ctx, NewRemoteName())
|
|
||||||
case 'd':
|
|
||||||
name := ChooseRemote()
|
|
||||||
DeleteRemote(name)
|
|
||||||
case 'r':
|
|
||||||
RenameRemote(ChooseRemote())
|
|
||||||
case 'c':
|
|
||||||
CopyRemote(ChooseRemote())
|
|
||||||
case 's':
|
|
||||||
SetPassword()
|
|
||||||
case 'q':
|
|
||||||
return
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Authorize is for remote authorization of headless machines.
|
|
||||||
//
|
|
||||||
// It expects 1 or 3 arguments
|
|
||||||
//
|
|
||||||
// rclone authorize "fs name"
|
|
||||||
// rclone authorize "fs name" "client id" "client secret"
|
|
||||||
func Authorize(ctx context.Context, args []string, noAutoBrowser bool) {
|
|
||||||
ctx = suppressConfirm(ctx)
|
|
||||||
switch len(args) {
|
|
||||||
case 1, 3:
|
|
||||||
default:
|
|
||||||
log.Fatalf("Invalid number of arguments: %d", len(args))
|
|
||||||
}
|
|
||||||
newType := args[0]
|
|
||||||
f := fs.MustFind(newType)
|
|
||||||
if f.Config == nil {
|
|
||||||
log.Fatalf("Can't authorize fs %q", newType)
|
|
||||||
}
|
|
||||||
// Name used for temporary fs
|
|
||||||
name := "**temp-fs**"
|
|
||||||
|
|
||||||
// Make sure we delete it
|
|
||||||
defer DeleteRemote(name)
|
|
||||||
|
|
||||||
// Indicate that we are running rclone authorize
|
|
||||||
Data.SetValue(name, ConfigAuthorize, "true")
|
|
||||||
if noAutoBrowser {
|
|
||||||
Data.SetValue(name, ConfigAuthNoBrowser, "true")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(args) == 3 {
|
|
||||||
Data.SetValue(name, ConfigClientID, args[1])
|
|
||||||
Data.SetValue(name, ConfigClientSecret, args[2])
|
|
||||||
}
|
|
||||||
|
|
||||||
m := fs.ConfigMap(f, name)
|
|
||||||
f.Config(ctx, name, m)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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) {
|
||||||
|
@ -1,259 +1,20 @@
|
|||||||
|
// These are in an external package because we need to import configfile
|
||||||
|
|
||||||
package config_test
|
package config_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"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/configfile"
|
||||||
"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"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func testConfigFile(t *testing.T, configFileName string) func() {
|
|
||||||
ctx := context.Background()
|
|
||||||
ci := fs.GetConfig(ctx)
|
|
||||||
config.ClearConfigPassword()
|
|
||||||
_ = os.Unsetenv("_RCLONE_CONFIG_KEY_FILE")
|
|
||||||
_ = os.Unsetenv("RCLONE_CONFIG_PASS")
|
|
||||||
// create temp config file
|
|
||||||
tempFile, err := ioutil.TempFile("", configFileName)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
path := tempFile.Name()
|
|
||||||
assert.NoError(t, tempFile.Close())
|
|
||||||
|
|
||||||
// temporarily adapt configuration
|
|
||||||
oldOsStdout := os.Stdout
|
|
||||||
oldConfigPath := config.ConfigPath
|
|
||||||
oldConfig := *ci
|
|
||||||
oldConfigFile := config.Data
|
|
||||||
oldReadLine := config.ReadLine
|
|
||||||
oldPassword := config.Password
|
|
||||||
os.Stdout = nil
|
|
||||||
config.ConfigPath = path
|
|
||||||
ci = &fs.ConfigInfo{}
|
|
||||||
|
|
||||||
configfile.LoadConfig(ctx)
|
|
||||||
assert.Equal(t, []string{}, config.Data.GetSectionList())
|
|
||||||
|
|
||||||
// Fake a remote
|
|
||||||
fs.Register(&fs.RegInfo{
|
|
||||||
Name: "config_test_remote",
|
|
||||||
Options: fs.Options{
|
|
||||||
{
|
|
||||||
Name: "bool",
|
|
||||||
Default: false,
|
|
||||||
IsPassword: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "pass",
|
|
||||||
Default: "",
|
|
||||||
IsPassword: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// Undo the above
|
|
||||||
return func() {
|
|
||||||
err := os.Remove(path)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
os.Stdout = oldOsStdout
|
|
||||||
config.ConfigPath = oldConfigPath
|
|
||||||
config.ReadLine = oldReadLine
|
|
||||||
config.Password = oldPassword
|
|
||||||
*ci = oldConfig
|
|
||||||
config.Data = oldConfigFile
|
|
||||||
|
|
||||||
_ = os.Unsetenv("_RCLONE_CONFIG_KEY_FILE")
|
|
||||||
_ = os.Unsetenv("RCLONE_CONFIG_PASS")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// makeReadLine makes a simple readLine which returns a fixed list of
|
|
||||||
// strings
|
|
||||||
func makeReadLine(answers []string) func() string {
|
|
||||||
i := 0
|
|
||||||
return func() string {
|
|
||||||
i = i + 1
|
|
||||||
return answers[i-1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCRUD(t *testing.T) {
|
|
||||||
defer testConfigFile(t, "crud.conf")()
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
// script for creating remote
|
|
||||||
config.ReadLine = makeReadLine([]string{
|
|
||||||
"config_test_remote", // type
|
|
||||||
"true", // bool value
|
|
||||||
"y", // type my own password
|
|
||||||
"secret", // password
|
|
||||||
"secret", // repeat
|
|
||||||
"y", // looks good, save
|
|
||||||
})
|
|
||||||
config.NewRemote(ctx, "test")
|
|
||||||
|
|
||||||
assert.Equal(t, []string{"test"}, config.Data.GetSectionList())
|
|
||||||
assert.Equal(t, "config_test_remote", config.FileGet("test", "type"))
|
|
||||||
assert.Equal(t, "true", config.FileGet("test", "bool"))
|
|
||||||
assert.Equal(t, "secret", obscure.MustReveal(config.FileGet("test", "pass")))
|
|
||||||
|
|
||||||
// normal rename, test → asdf
|
|
||||||
config.ReadLine = makeReadLine([]string{
|
|
||||||
"asdf",
|
|
||||||
"asdf",
|
|
||||||
"asdf",
|
|
||||||
})
|
|
||||||
config.RenameRemote("test")
|
|
||||||
|
|
||||||
assert.Equal(t, []string{"asdf"}, config.Data.GetSectionList())
|
|
||||||
assert.Equal(t, "config_test_remote", config.FileGet("asdf", "type"))
|
|
||||||
assert.Equal(t, "true", config.FileGet("asdf", "bool"))
|
|
||||||
assert.Equal(t, "secret", obscure.MustReveal(config.FileGet("asdf", "pass")))
|
|
||||||
|
|
||||||
// delete remote
|
|
||||||
config.DeleteRemote("asdf")
|
|
||||||
assert.Equal(t, []string{}, config.Data.GetSectionList())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestChooseOption(t *testing.T) {
|
|
||||||
defer testConfigFile(t, "crud.conf")()
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
// script for creating remote
|
|
||||||
config.ReadLine = makeReadLine([]string{
|
|
||||||
"config_test_remote", // type
|
|
||||||
"false", // bool value
|
|
||||||
"x", // bad choice
|
|
||||||
"g", // generate password
|
|
||||||
"1024", // very big
|
|
||||||
"y", // password OK
|
|
||||||
"y", // looks good, save
|
|
||||||
})
|
|
||||||
config.Password = func(bits int) (string, error) {
|
|
||||||
assert.Equal(t, 1024, bits)
|
|
||||||
return "not very random password", nil
|
|
||||||
}
|
|
||||||
config.NewRemote(ctx, "test")
|
|
||||||
|
|
||||||
assert.Equal(t, "false", config.FileGet("test", "bool"))
|
|
||||||
assert.Equal(t, "not very random password", obscure.MustReveal(config.FileGet("test", "pass")))
|
|
||||||
|
|
||||||
// script for creating remote
|
|
||||||
config.ReadLine = makeReadLine([]string{
|
|
||||||
"config_test_remote", // type
|
|
||||||
"true", // bool value
|
|
||||||
"n", // not required
|
|
||||||
"y", // looks good, save
|
|
||||||
})
|
|
||||||
config.NewRemote(ctx, "test")
|
|
||||||
|
|
||||||
assert.Equal(t, "true", config.FileGet("test", "bool"))
|
|
||||||
assert.Equal(t, "", config.FileGet("test", "pass"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewRemoteName(t *testing.T) {
|
|
||||||
defer testConfigFile(t, "crud.conf")()
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
// script for creating remote
|
|
||||||
config.ReadLine = makeReadLine([]string{
|
|
||||||
"config_test_remote", // type
|
|
||||||
"true", // bool value
|
|
||||||
"n", // not required
|
|
||||||
"y", // looks good, save
|
|
||||||
})
|
|
||||||
config.NewRemote(ctx, "test")
|
|
||||||
|
|
||||||
config.ReadLine = makeReadLine([]string{
|
|
||||||
"test", // already exists
|
|
||||||
"", // empty string not allowed
|
|
||||||
"bad@characters", // bad characters
|
|
||||||
"newname", // OK
|
|
||||||
})
|
|
||||||
|
|
||||||
assert.Equal(t, "newname", config.NewRemoteName())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCreateUpdatePasswordRemote(t *testing.T) {
|
|
||||||
ctx := context.Background()
|
|
||||||
defer testConfigFile(t, "update.conf")()
|
|
||||||
|
|
||||||
for _, doObscure := range []bool{false, true} {
|
|
||||||
for _, noObscure := range []bool{false, true} {
|
|
||||||
if doObscure && noObscure {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
t.Run(fmt.Sprintf("doObscure=%v,noObscure=%v", doObscure, noObscure), func(t *testing.T) {
|
|
||||||
require.NoError(t, config.CreateRemote(ctx, "test2", "config_test_remote", rc.Params{
|
|
||||||
"bool": true,
|
|
||||||
"pass": "potato",
|
|
||||||
}, doObscure, noObscure))
|
|
||||||
|
|
||||||
assert.Equal(t, []string{"test2"}, config.Data.GetSectionList())
|
|
||||||
assert.Equal(t, "config_test_remote", config.FileGet("test2", "type"))
|
|
||||||
assert.Equal(t, "true", config.FileGet("test2", "bool"))
|
|
||||||
gotPw := config.FileGet("test2", "pass")
|
|
||||||
if !noObscure {
|
|
||||||
gotPw = obscure.MustReveal(gotPw)
|
|
||||||
}
|
|
||||||
assert.Equal(t, "potato", gotPw)
|
|
||||||
|
|
||||||
wantPw := obscure.MustObscure("potato2")
|
|
||||||
require.NoError(t, config.UpdateRemote(ctx, "test2", rc.Params{
|
|
||||||
"bool": false,
|
|
||||||
"pass": wantPw,
|
|
||||||
"spare": "spare",
|
|
||||||
}, doObscure, noObscure))
|
|
||||||
|
|
||||||
assert.Equal(t, []string{"test2"}, config.Data.GetSectionList())
|
|
||||||
assert.Equal(t, "config_test_remote", config.FileGet("test2", "type"))
|
|
||||||
assert.Equal(t, "false", config.FileGet("test2", "bool"))
|
|
||||||
gotPw = config.FileGet("test2", "pass")
|
|
||||||
if doObscure {
|
|
||||||
gotPw = obscure.MustReveal(gotPw)
|
|
||||||
}
|
|
||||||
assert.Equal(t, wantPw, gotPw)
|
|
||||||
|
|
||||||
require.NoError(t, config.PasswordRemote(ctx, "test2", rc.Params{
|
|
||||||
"pass": "potato3",
|
|
||||||
}))
|
|
||||||
|
|
||||||
assert.Equal(t, []string{"test2"}, config.Data.GetSectionList())
|
|
||||||
assert.Equal(t, "config_test_remote", config.FileGet("test2", "type"))
|
|
||||||
assert.Equal(t, "false", config.FileGet("test2", "bool"))
|
|
||||||
assert.Equal(t, "potato3", obscure.MustReveal(config.FileGet("test2", "pass")))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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) {
|
func TestConfigLoad(t *testing.T) {
|
||||||
oldConfigPath := config.ConfigPath
|
oldConfigPath := config.ConfigPath
|
||||||
config.ConfigPath = "./testdata/plain.conf"
|
config.ConfigPath = "./testdata/plain.conf"
|
||||||
@ -271,106 +32,6 @@ func TestConfigLoad(t *testing.T) {
|
|||||||
assert.Equal(t, expect, keys)
|
assert.Equal(t, expect, keys)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfigLoadEncrypted(t *testing.T) {
|
|
||||||
var err error
|
|
||||||
oldConfigPath := config.ConfigPath
|
|
||||||
config.ConfigPath = "./testdata/encrypted.conf"
|
|
||||||
defer func() {
|
|
||||||
config.ConfigPath = oldConfigPath
|
|
||||||
config.ClearConfigPassword()
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Set correct password
|
|
||||||
err = config.SetConfigPassword("asdf")
|
|
||||||
require.NoError(t, err)
|
|
||||||
err = config.Data.Load()
|
|
||||||
require.NoError(t, err)
|
|
||||||
sections := config.Data.GetSectionList()
|
|
||||||
var expect = []string{"nounc", "unc"}
|
|
||||||
assert.Equal(t, expect, sections)
|
|
||||||
|
|
||||||
keys := config.Data.GetKeyList("nounc")
|
|
||||||
expect = []string{"type", "nounc"}
|
|
||||||
assert.Equal(t, expect, keys)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfigLoadEncryptedWithValidPassCommand(t *testing.T) {
|
|
||||||
ctx := context.Background()
|
|
||||||
ci := fs.GetConfig(ctx)
|
|
||||||
oldConfigPath := config.ConfigPath
|
|
||||||
oldConfig := *ci
|
|
||||||
config.ConfigPath = "./testdata/encrypted.conf"
|
|
||||||
// using ci.PasswordCommand, correct password
|
|
||||||
ci.PasswordCommand = fs.SpaceSepList{"echo", "asdf"}
|
|
||||||
defer func() {
|
|
||||||
config.ConfigPath = oldConfigPath
|
|
||||||
config.ClearConfigPassword()
|
|
||||||
*ci = oldConfig
|
|
||||||
ci.PasswordCommand = nil
|
|
||||||
}()
|
|
||||||
|
|
||||||
config.ClearConfigPassword()
|
|
||||||
|
|
||||||
err := config.Data.Load()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
sections := config.Data.GetSectionList()
|
|
||||||
var expect = []string{"nounc", "unc"}
|
|
||||||
assert.Equal(t, expect, sections)
|
|
||||||
|
|
||||||
keys := config.Data.GetKeyList("nounc")
|
|
||||||
expect = []string{"type", "nounc"}
|
|
||||||
assert.Equal(t, expect, keys)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfigLoadEncryptedWithInvalidPassCommand(t *testing.T) {
|
|
||||||
ctx := context.Background()
|
|
||||||
ci := fs.GetConfig(ctx)
|
|
||||||
oldConfigPath := config.ConfigPath
|
|
||||||
oldConfig := *ci
|
|
||||||
config.ConfigPath = "./testdata/encrypted.conf"
|
|
||||||
// using ci.PasswordCommand, incorrect password
|
|
||||||
ci.PasswordCommand = fs.SpaceSepList{"echo", "asdf-blurfl"}
|
|
||||||
defer func() {
|
|
||||||
config.ConfigPath = oldConfigPath
|
|
||||||
config.ClearConfigPassword()
|
|
||||||
*ci = oldConfig
|
|
||||||
ci.PasswordCommand = nil
|
|
||||||
}()
|
|
||||||
|
|
||||||
config.ClearConfigPassword()
|
|
||||||
|
|
||||||
err := config.Data.Load()
|
|
||||||
require.Error(t, err)
|
|
||||||
assert.Contains(t, err.Error(), "using --password-command derived password")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfigLoadEncryptedFailures(t *testing.T) {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
// This file should be too short to be decoded.
|
|
||||||
oldConfigPath := config.ConfigPath
|
|
||||||
config.ConfigPath = "./testdata/enc-short.conf"
|
|
||||||
defer func() { config.ConfigPath = oldConfigPath }()
|
|
||||||
err = config.Data.Load()
|
|
||||||
require.Error(t, err)
|
|
||||||
|
|
||||||
// This file contains invalid base64 characters.
|
|
||||||
config.ConfigPath = "./testdata/enc-invalid.conf"
|
|
||||||
err = config.Data.Load()
|
|
||||||
require.Error(t, err)
|
|
||||||
|
|
||||||
// This file contains invalid base64 characters.
|
|
||||||
config.ConfigPath = "./testdata/enc-too-new.conf"
|
|
||||||
err = config.Data.Load()
|
|
||||||
require.Error(t, err)
|
|
||||||
|
|
||||||
// This file does not exist.
|
|
||||||
config.ConfigPath = "./testdata/filenotfound.conf"
|
|
||||||
err = config.Data.Load()
|
|
||||||
assert.Equal(t, config.ErrorConfigFileNotFound, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
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")()
|
||||||
|
@ -14,11 +14,9 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode/utf8"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"golang.org/x/crypto/nacl/secretbox"
|
"golang.org/x/crypto/nacl/secretbox"
|
||||||
"golang.org/x/text/unicode/norm"
|
|
||||||
|
|
||||||
"github.com/rclone/rclone/fs"
|
"github.com/rclone/rclone/fs"
|
||||||
"github.com/rclone/rclone/fs/config/obscure"
|
"github.com/rclone/rclone/fs/config/obscure"
|
||||||
@ -227,52 +225,6 @@ func Encrypt(src io.Reader, dst io.Writer) error {
|
|||||||
return enc.Close()
|
return enc.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkPassword normalises and validates the password
|
|
||||||
func checkPassword(password string) (string, error) {
|
|
||||||
if !utf8.ValidString(password) {
|
|
||||||
return "", errors.New("password contains invalid utf8 characters")
|
|
||||||
}
|
|
||||||
// Check for leading/trailing whitespace
|
|
||||||
trimmedPassword := strings.TrimSpace(password)
|
|
||||||
// Warn user if password has leading+trailing whitespace
|
|
||||||
if len(password) != len(trimmedPassword) {
|
|
||||||
_, _ = fmt.Fprintln(os.Stderr, "Your password contains leading/trailing whitespace - in previous versions of rclone this was stripped")
|
|
||||||
}
|
|
||||||
// Normalize to reduce weird variations.
|
|
||||||
password = norm.NFKC.String(password)
|
|
||||||
if len(password) == 0 || len(trimmedPassword) == 0 {
|
|
||||||
return "", errors.New("no characters in password")
|
|
||||||
}
|
|
||||||
return password, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPassword asks the user for a password with the prompt given.
|
|
||||||
func GetPassword(prompt string) string {
|
|
||||||
_, _ = fmt.Fprintln(PasswordPromptOutput, prompt)
|
|
||||||
for {
|
|
||||||
_, _ = fmt.Fprint(PasswordPromptOutput, "password:")
|
|
||||||
password := ReadPassword()
|
|
||||||
password, err := checkPassword(password)
|
|
||||||
if err == nil {
|
|
||||||
return password
|
|
||||||
}
|
|
||||||
_, _ = fmt.Fprintf(os.Stderr, "Bad password: %v\n", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ChangePassword will query the user twice for the named password. If
|
|
||||||
// the same password is entered it is returned.
|
|
||||||
func ChangePassword(name string) string {
|
|
||||||
for {
|
|
||||||
a := GetPassword(fmt.Sprintf("Enter %s password:", name))
|
|
||||||
b := GetPassword(fmt.Sprintf("Confirm %s password:", name))
|
|
||||||
if a == b {
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
fmt.Println("Passwords do not match!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// getConfigPassword will query the user for a password the
|
// getConfigPassword will query the user for a password the
|
||||||
// first time it is required.
|
// first time it is required.
|
||||||
func getConfigPassword(q string) {
|
func getConfigPassword(q string) {
|
||||||
@ -353,41 +305,3 @@ func changeConfigPassword() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetPassword will allow the user to modify the current
|
|
||||||
// configuration encryption settings.
|
|
||||||
func SetPassword() {
|
|
||||||
for {
|
|
||||||
if len(configKey) > 0 {
|
|
||||||
fmt.Println("Your configuration is encrypted.")
|
|
||||||
what := []string{"cChange Password", "uUnencrypt configuration", "qQuit to main menu"}
|
|
||||||
switch i := Command(what); i {
|
|
||||||
case 'c':
|
|
||||||
changeConfigPassword()
|
|
||||||
SaveConfig()
|
|
||||||
fmt.Println("Password changed")
|
|
||||||
continue
|
|
||||||
case 'u':
|
|
||||||
configKey = nil
|
|
||||||
SaveConfig()
|
|
||||||
continue
|
|
||||||
case 'q':
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
fmt.Println("Your configuration is not encrypted.")
|
|
||||||
fmt.Println("If you add a password, you will protect your login information to cloud services.")
|
|
||||||
what := []string{"aAdd Password", "qQuit to main menu"}
|
|
||||||
switch i := Command(what); i {
|
|
||||||
case 'a':
|
|
||||||
changeConfigPassword()
|
|
||||||
SaveConfig()
|
|
||||||
fmt.Println("Password set")
|
|
||||||
continue
|
|
||||||
case 'q':
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
48
fs/config/crypt_internal_test.go
Normal file
48
fs/config/crypt_internal_test.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
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 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)
|
||||||
|
|
||||||
|
}
|
@ -1,48 +1,115 @@
|
|||||||
package config
|
// These are in an external package because we need to import configfile
|
||||||
|
//
|
||||||
|
// Internal tests are in crypt_internal_test.go
|
||||||
|
|
||||||
|
package config_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPassword(t *testing.T) {
|
func TestConfigLoadEncrypted(t *testing.T) {
|
||||||
defer func() {
|
|
||||||
configKey = nil // reset password
|
|
||||||
}()
|
|
||||||
var err error
|
var err error
|
||||||
// Empty password should give error
|
oldConfigPath := config.ConfigPath
|
||||||
err = SetConfigPassword(" \t ")
|
config.ConfigPath = "./testdata/encrypted.conf"
|
||||||
require.Error(t, err)
|
defer func() {
|
||||||
|
config.ConfigPath = oldConfigPath
|
||||||
|
config.ClearConfigPassword()
|
||||||
|
}()
|
||||||
|
|
||||||
// Test invalid utf8 sequence
|
// Set correct password
|
||||||
err = SetConfigPassword(string([]byte{0xff, 0xfe, 0xfd}) + "abc")
|
err = config.SetConfigPassword("asdf")
|
||||||
require.Error(t, err)
|
require.NoError(t, err)
|
||||||
|
err = config.Data.Load()
|
||||||
// Simple check of wrong passwords
|
require.NoError(t, err)
|
||||||
hashedKeyCompare(t, "mis", "match", false)
|
sections := config.Data.GetSectionList()
|
||||||
|
var expect = []string{"nounc", "unc"}
|
||||||
// Check that passwords match after unicode normalization
|
assert.Equal(t, expect, sections)
|
||||||
hashedKeyCompare(t, "ff\u0041\u030A", "ffÅ", true)
|
|
||||||
|
|
||||||
// Check that passwords preserves case
|
|
||||||
hashedKeyCompare(t, "abcdef", "ABCDEF", false)
|
|
||||||
|
|
||||||
|
keys := config.Data.GetKeyList("nounc")
|
||||||
|
expect = []string{"type", "nounc"}
|
||||||
|
assert.Equal(t, expect, keys)
|
||||||
}
|
}
|
||||||
|
|
||||||
func hashedKeyCompare(t *testing.T, a, b string, shouldMatch bool) {
|
func TestConfigLoadEncryptedWithValidPassCommand(t *testing.T) {
|
||||||
err := SetConfigPassword(a)
|
ctx := context.Background()
|
||||||
require.NoError(t, err)
|
ci := fs.GetConfig(ctx)
|
||||||
k1 := configKey
|
oldConfigPath := config.ConfigPath
|
||||||
|
oldConfig := *ci
|
||||||
|
config.ConfigPath = "./testdata/encrypted.conf"
|
||||||
|
// using ci.PasswordCommand, correct password
|
||||||
|
ci.PasswordCommand = fs.SpaceSepList{"echo", "asdf"}
|
||||||
|
defer func() {
|
||||||
|
config.ConfigPath = oldConfigPath
|
||||||
|
config.ClearConfigPassword()
|
||||||
|
*ci = oldConfig
|
||||||
|
ci.PasswordCommand = nil
|
||||||
|
}()
|
||||||
|
|
||||||
err = SetConfigPassword(b)
|
config.ClearConfigPassword()
|
||||||
require.NoError(t, err)
|
|
||||||
k2 := configKey
|
|
||||||
|
|
||||||
if shouldMatch {
|
err := config.Data.Load()
|
||||||
assert.Equal(t, k1, k2)
|
require.NoError(t, err)
|
||||||
} else {
|
|
||||||
assert.NotEqual(t, k1, k2)
|
sections := config.Data.GetSectionList()
|
||||||
}
|
var expect = []string{"nounc", "unc"}
|
||||||
|
assert.Equal(t, expect, sections)
|
||||||
|
|
||||||
|
keys := config.Data.GetKeyList("nounc")
|
||||||
|
expect = []string{"type", "nounc"}
|
||||||
|
assert.Equal(t, expect, keys)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigLoadEncryptedWithInvalidPassCommand(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
ci := fs.GetConfig(ctx)
|
||||||
|
oldConfigPath := config.ConfigPath
|
||||||
|
oldConfig := *ci
|
||||||
|
config.ConfigPath = "./testdata/encrypted.conf"
|
||||||
|
// using ci.PasswordCommand, incorrect password
|
||||||
|
ci.PasswordCommand = fs.SpaceSepList{"echo", "asdf-blurfl"}
|
||||||
|
defer func() {
|
||||||
|
config.ConfigPath = oldConfigPath
|
||||||
|
config.ClearConfigPassword()
|
||||||
|
*ci = oldConfig
|
||||||
|
ci.PasswordCommand = nil
|
||||||
|
}()
|
||||||
|
|
||||||
|
config.ClearConfigPassword()
|
||||||
|
|
||||||
|
err := config.Data.Load()
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "using --password-command derived password")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigLoadEncryptedFailures(t *testing.T) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// This file should be too short to be decoded.
|
||||||
|
oldConfigPath := config.ConfigPath
|
||||||
|
config.ConfigPath = "./testdata/enc-short.conf"
|
||||||
|
defer func() { config.ConfigPath = oldConfigPath }()
|
||||||
|
err = config.Data.Load()
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
// This file contains invalid base64 characters.
|
||||||
|
config.ConfigPath = "./testdata/enc-invalid.conf"
|
||||||
|
err = config.Data.Load()
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
// This file contains invalid base64 characters.
|
||||||
|
config.ConfigPath = "./testdata/enc-too-new.conf"
|
||||||
|
err = config.Data.Load()
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
// This file does not exist.
|
||||||
|
config.ConfigPath = "./testdata/filenotfound.conf"
|
||||||
|
err = config.Data.Load()
|
||||||
|
assert.Equal(t, config.ErrorConfigFileNotFound, err)
|
||||||
}
|
}
|
||||||
|
@ -58,3 +58,19 @@ func TestReveal(t *testing.T) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test some error cases
|
||||||
|
func TestRevealErrors(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 := Reveal(test.in)
|
||||||
|
assert.Equal(t, "", gotString)
|
||||||
|
assert.Equal(t, test.wantErr, gotErr.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
684
fs/config/ui.go
Normal file
684
fs/config/ui.go
Normal file
@ -0,0 +1,684 @@
|
|||||||
|
// Textual user interface parts of the config system
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/rclone/rclone/fs"
|
||||||
|
"github.com/rclone/rclone/fs/config/configmap"
|
||||||
|
"github.com/rclone/rclone/fs/config/configstruct"
|
||||||
|
"github.com/rclone/rclone/fs/config/obscure"
|
||||||
|
"github.com/rclone/rclone/fs/driveletter"
|
||||||
|
"github.com/rclone/rclone/fs/fspath"
|
||||||
|
"github.com/rclone/rclone/lib/terminal"
|
||||||
|
"golang.org/x/text/unicode/norm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReadLine reads some input
|
||||||
|
var ReadLine = func() string {
|
||||||
|
buf := bufio.NewReader(os.Stdin)
|
||||||
|
line, err := buf.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to read line: %v", err)
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(line)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadNonEmptyLine prints prompt and calls Readline until non empty
|
||||||
|
func ReadNonEmptyLine(prompt string) string {
|
||||||
|
result := ""
|
||||||
|
for result == "" {
|
||||||
|
fmt.Print(prompt)
|
||||||
|
result = strings.TrimSpace(ReadLine())
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommandDefault - choose one. If return is pressed then it will
|
||||||
|
// chose the defaultIndex if it is >= 0
|
||||||
|
func CommandDefault(commands []string, defaultIndex int) byte {
|
||||||
|
opts := []string{}
|
||||||
|
for i, text := range commands {
|
||||||
|
def := ""
|
||||||
|
if i == defaultIndex {
|
||||||
|
def = " (default)"
|
||||||
|
}
|
||||||
|
fmt.Printf("%c) %s%s\n", text[0], text[1:], def)
|
||||||
|
opts = append(opts, text[:1])
|
||||||
|
}
|
||||||
|
optString := strings.Join(opts, "")
|
||||||
|
optHelp := strings.Join(opts, "/")
|
||||||
|
for {
|
||||||
|
fmt.Printf("%s> ", optHelp)
|
||||||
|
result := strings.ToLower(ReadLine())
|
||||||
|
if len(result) == 0 && defaultIndex >= 0 {
|
||||||
|
return optString[defaultIndex]
|
||||||
|
}
|
||||||
|
if len(result) != 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
i := strings.Index(optString, string(result[0]))
|
||||||
|
if i >= 0 {
|
||||||
|
return result[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Command - choose one
|
||||||
|
func Command(commands []string) byte {
|
||||||
|
return CommandDefault(commands, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm asks the user for Yes or No and returns true or false
|
||||||
|
//
|
||||||
|
// If the user presses enter then the Default will be used
|
||||||
|
func Confirm(Default bool) bool {
|
||||||
|
defaultIndex := 0
|
||||||
|
if !Default {
|
||||||
|
defaultIndex = 1
|
||||||
|
}
|
||||||
|
return CommandDefault([]string{"yYes", "nNo"}, defaultIndex) == 'y'
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfirmWithConfig asks the user for Yes or No and returns true or
|
||||||
|
// false.
|
||||||
|
//
|
||||||
|
// If AutoConfirm is set, it will look up the value in m and return
|
||||||
|
// that, but if it isn't set then it will return the Default value
|
||||||
|
// passed in
|
||||||
|
func ConfirmWithConfig(ctx context.Context, m configmap.Getter, configName string, Default bool) bool {
|
||||||
|
ci := fs.GetConfig(ctx)
|
||||||
|
if ci.AutoConfirm {
|
||||||
|
configString, ok := m.Get(configName)
|
||||||
|
if ok {
|
||||||
|
configValue, err := strconv.ParseBool(configString)
|
||||||
|
if err != nil {
|
||||||
|
fs.Errorf(nil, "Failed to parse config parameter %s=%q as boolean - using default %v: %v", configName, configString, Default, err)
|
||||||
|
} else {
|
||||||
|
Default = configValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
answer := "No"
|
||||||
|
if Default {
|
||||||
|
answer = "Yes"
|
||||||
|
}
|
||||||
|
fmt.Printf("Auto confirm is set: answering %s, override by setting config parameter %s=%v\n", answer, configName, !Default)
|
||||||
|
return Default
|
||||||
|
}
|
||||||
|
return Confirm(Default)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Choose one of the defaults or type a new string if newOk is set
|
||||||
|
func Choose(what string, defaults, help []string, newOk bool) string {
|
||||||
|
valueDescription := "an existing"
|
||||||
|
if newOk {
|
||||||
|
valueDescription = "your own"
|
||||||
|
}
|
||||||
|
fmt.Printf("Choose a number from below, or type in %s value\n", valueDescription)
|
||||||
|
attributes := []string{terminal.HiRedFg, terminal.HiGreenFg}
|
||||||
|
for i, text := range defaults {
|
||||||
|
var lines []string
|
||||||
|
if help != nil {
|
||||||
|
parts := strings.Split(help[i], "\n")
|
||||||
|
lines = append(lines, parts...)
|
||||||
|
}
|
||||||
|
lines = append(lines, fmt.Sprintf("%q", text))
|
||||||
|
pos := i + 1
|
||||||
|
terminal.WriteString(attributes[i%len(attributes)])
|
||||||
|
if len(lines) == 1 {
|
||||||
|
fmt.Printf("%2d > %s\n", pos, text)
|
||||||
|
} else {
|
||||||
|
mid := (len(lines) - 1) / 2
|
||||||
|
for i, line := range lines {
|
||||||
|
var sep rune
|
||||||
|
switch i {
|
||||||
|
case 0:
|
||||||
|
sep = '/'
|
||||||
|
case len(lines) - 1:
|
||||||
|
sep = '\\'
|
||||||
|
default:
|
||||||
|
sep = '|'
|
||||||
|
}
|
||||||
|
number := " "
|
||||||
|
if i == mid {
|
||||||
|
number = fmt.Sprintf("%2d", pos)
|
||||||
|
}
|
||||||
|
fmt.Printf("%s %c %s\n", number, sep, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
terminal.WriteString(terminal.Reset)
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
fmt.Printf("%s> ", what)
|
||||||
|
result := ReadLine()
|
||||||
|
i, err := strconv.Atoi(result)
|
||||||
|
if err != nil {
|
||||||
|
if newOk {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
for _, v := range defaults {
|
||||||
|
if result == v {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if i >= 1 && i <= len(defaults) {
|
||||||
|
return defaults[i-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChooseNumber asks the user to enter a number between min and max
|
||||||
|
// inclusive prompting them with what.
|
||||||
|
func ChooseNumber(what string, min, max int) int {
|
||||||
|
for {
|
||||||
|
fmt.Printf("%s> ", what)
|
||||||
|
result := ReadLine()
|
||||||
|
i, err := strconv.Atoi(result)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Bad number: %v\n", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if i < min || i > max {
|
||||||
|
fmt.Printf("Out of range - %d to %d inclusive\n", min, max)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShowRemotes shows an overview of the config file
|
||||||
|
func ShowRemotes() {
|
||||||
|
remotes := Data.GetSectionList()
|
||||||
|
if len(remotes) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sort.Strings(remotes)
|
||||||
|
fmt.Printf("%-20s %s\n", "Name", "Type")
|
||||||
|
fmt.Printf("%-20s %s\n", "====", "====")
|
||||||
|
for _, remote := range remotes {
|
||||||
|
fmt.Printf("%-20s %s\n", remote, FileGet(remote, "type"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChooseRemote chooses a remote name
|
||||||
|
func ChooseRemote() string {
|
||||||
|
remotes := Data.GetSectionList()
|
||||||
|
sort.Strings(remotes)
|
||||||
|
return Choose("remote", remotes, nil, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// mustFindByName finds the RegInfo for the remote name passed in or
|
||||||
|
// exits with a fatal error.
|
||||||
|
func mustFindByName(name string) *fs.RegInfo {
|
||||||
|
fsType := FileGet(name, "type")
|
||||||
|
if fsType == "" {
|
||||||
|
log.Fatalf("Couldn't find type of fs for %q", name)
|
||||||
|
}
|
||||||
|
return fs.MustFind(fsType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShowRemote shows the contents of the remote
|
||||||
|
func ShowRemote(name string) {
|
||||||
|
fmt.Printf("--------------------\n")
|
||||||
|
fmt.Printf("[%s]\n", name)
|
||||||
|
fs := mustFindByName(name)
|
||||||
|
for _, key := range Data.GetKeyList(name) {
|
||||||
|
isPassword := false
|
||||||
|
for _, option := range fs.Options {
|
||||||
|
if option.Name == key && option.IsPassword {
|
||||||
|
isPassword = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value := FileGet(name, key)
|
||||||
|
if isPassword && value != "" {
|
||||||
|
fmt.Printf("%s = *** ENCRYPTED ***\n", key)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("%s = %s\n", key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Printf("--------------------\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// OkRemote prints the contents of the remote and ask if it is OK
|
||||||
|
func OkRemote(name string) bool {
|
||||||
|
ShowRemote(name)
|
||||||
|
switch i := CommandDefault([]string{"yYes this is OK", "eEdit this remote", "dDelete this remote"}, 0); i {
|
||||||
|
case 'y':
|
||||||
|
return true
|
||||||
|
case 'e':
|
||||||
|
return false
|
||||||
|
case 'd':
|
||||||
|
Data.DeleteSection(name)
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
fs.Errorf(nil, "Bad choice %c", i)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoteConfig runs the config helper for the remote if needed
|
||||||
|
func RemoteConfig(ctx context.Context, name string) {
|
||||||
|
fmt.Printf("Remote config\n")
|
||||||
|
f := mustFindByName(name)
|
||||||
|
if f.Config != nil {
|
||||||
|
m := fs.ConfigMap(f, name)
|
||||||
|
f.Config(ctx, name, m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// matchProvider returns true if provider matches the providerConfig string.
|
||||||
|
//
|
||||||
|
// The providerConfig string can either be a list of providers to
|
||||||
|
// match, or if it starts with "!" it will be a list of providers not
|
||||||
|
// to match.
|
||||||
|
//
|
||||||
|
// If either providerConfig or provider is blank then it will return true
|
||||||
|
func matchProvider(providerConfig, provider string) bool {
|
||||||
|
if providerConfig == "" || provider == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
negate := false
|
||||||
|
if strings.HasPrefix(providerConfig, "!") {
|
||||||
|
providerConfig = providerConfig[1:]
|
||||||
|
negate = true
|
||||||
|
}
|
||||||
|
providers := strings.Split(providerConfig, ",")
|
||||||
|
matched := false
|
||||||
|
for _, p := range providers {
|
||||||
|
if p == provider {
|
||||||
|
matched = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if negate {
|
||||||
|
return !matched
|
||||||
|
}
|
||||||
|
return matched
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChooseOption asks the user to choose an option
|
||||||
|
func ChooseOption(o *fs.Option, name string) string {
|
||||||
|
var subProvider = getWithDefault(name, fs.ConfigProvider, "")
|
||||||
|
fmt.Println(o.Help)
|
||||||
|
if o.IsPassword {
|
||||||
|
actions := []string{"yYes type in my own password", "gGenerate random password"}
|
||||||
|
defaultAction := -1
|
||||||
|
if !o.Required {
|
||||||
|
defaultAction = len(actions)
|
||||||
|
actions = append(actions, "nNo leave this optional password blank")
|
||||||
|
}
|
||||||
|
var password string
|
||||||
|
var err error
|
||||||
|
switch i := CommandDefault(actions, defaultAction); i {
|
||||||
|
case 'y':
|
||||||
|
password = ChangePassword("the")
|
||||||
|
case 'g':
|
||||||
|
for {
|
||||||
|
fmt.Printf("Password strength in bits.\n64 is just about memorable\n128 is secure\n1024 is the maximum\n")
|
||||||
|
bits := ChooseNumber("Bits", 64, 1024)
|
||||||
|
password, err = Password(bits)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to make password: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Your password is: %s\n", password)
|
||||||
|
fmt.Printf("Use this password? Please note that an obscured version of this \npassword (and not the " +
|
||||||
|
"password itself) will be stored under your \nconfiguration file, so keep this generated password " +
|
||||||
|
"in a safe place.\n")
|
||||||
|
if Confirm(true) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'n':
|
||||||
|
return ""
|
||||||
|
default:
|
||||||
|
fs.Errorf(nil, "Bad choice %c", i)
|
||||||
|
}
|
||||||
|
return obscure.MustObscure(password)
|
||||||
|
}
|
||||||
|
what := fmt.Sprintf("%T value", o.Default)
|
||||||
|
switch o.Default.(type) {
|
||||||
|
case bool:
|
||||||
|
what = "boolean value (true or false)"
|
||||||
|
case fs.SizeSuffix:
|
||||||
|
what = "size with suffix k,M,G,T"
|
||||||
|
case fs.Duration:
|
||||||
|
what = "duration s,m,h,d,w,M,y"
|
||||||
|
case int, int8, int16, int32, int64:
|
||||||
|
what = "signed integer"
|
||||||
|
case uint, byte, uint16, uint32, uint64:
|
||||||
|
what = "unsigned integer"
|
||||||
|
}
|
||||||
|
var in string
|
||||||
|
for {
|
||||||
|
fmt.Printf("Enter a %s. Press Enter for the default (%q).\n", what, fmt.Sprint(o.Default))
|
||||||
|
if len(o.Examples) > 0 {
|
||||||
|
var values []string
|
||||||
|
var help []string
|
||||||
|
for _, example := range o.Examples {
|
||||||
|
if matchProvider(example.Provider, subProvider) {
|
||||||
|
values = append(values, example.Value)
|
||||||
|
help = append(help, example.Help)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
in = Choose(o.Name, values, help, true)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("%s> ", o.Name)
|
||||||
|
in = ReadLine()
|
||||||
|
}
|
||||||
|
if in == "" {
|
||||||
|
if o.Required && fmt.Sprint(o.Default) == "" {
|
||||||
|
fmt.Printf("This value is required and it has no default.\n")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
newIn, err := configstruct.StringToInterface(o.Default, in)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Failed to parse %q: %v\n", in, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
in = fmt.Sprint(newIn) // canonicalise
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return in
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRemoteName asks the user for a name for a new remote
|
||||||
|
func NewRemoteName() (name string) {
|
||||||
|
for {
|
||||||
|
fmt.Printf("name> ")
|
||||||
|
name = ReadLine()
|
||||||
|
if Data.HasSection(name) {
|
||||||
|
fmt.Printf("Remote %q already exists.\n", name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err := fspath.CheckConfigName(name)
|
||||||
|
switch {
|
||||||
|
case name == "":
|
||||||
|
fmt.Printf("Can't use empty name.\n")
|
||||||
|
case driveletter.IsDriveLetter(name):
|
||||||
|
fmt.Printf("Can't use %q as it can be confused with a drive letter.\n", name)
|
||||||
|
case err != nil:
|
||||||
|
fmt.Printf("Can't use %q as %v.\n", name, err)
|
||||||
|
default:
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// editOptions edits the options. If new is true then it just allows
|
||||||
|
// entry and doesn't show any old values.
|
||||||
|
func editOptions(ri *fs.RegInfo, name string, isNew bool) {
|
||||||
|
fmt.Printf("** See help for %s backend at: https://rclone.org/%s/ **\n\n", ri.Name, ri.FileName())
|
||||||
|
hasAdvanced := false
|
||||||
|
for _, advanced := range []bool{false, true} {
|
||||||
|
if advanced {
|
||||||
|
if !hasAdvanced {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
fmt.Printf("Edit advanced config? (y/n)\n")
|
||||||
|
if !Confirm(false) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, option := range ri.Options {
|
||||||
|
isVisible := option.Hide&fs.OptionHideConfigurator == 0
|
||||||
|
hasAdvanced = hasAdvanced || (option.Advanced && isVisible)
|
||||||
|
if option.Advanced != advanced {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
subProvider := getWithDefault(name, fs.ConfigProvider, "")
|
||||||
|
if matchProvider(option.Provider, subProvider) && isVisible {
|
||||||
|
if !isNew {
|
||||||
|
fmt.Printf("Value %q = %q\n", option.Name, FileGet(name, option.Name))
|
||||||
|
fmt.Printf("Edit? (y/n)>\n")
|
||||||
|
if !Confirm(false) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FileSet(name, option.Name, ChooseOption(&option, name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRemote make a new remote from its name
|
||||||
|
func NewRemote(ctx context.Context, name string) {
|
||||||
|
var (
|
||||||
|
newType string
|
||||||
|
ri *fs.RegInfo
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
// Set the type first
|
||||||
|
for {
|
||||||
|
newType = ChooseOption(fsOption(), name)
|
||||||
|
ri, err = fs.Find(newType)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Bad remote %q: %v\n", newType, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
Data.SetValue(name, "type", newType)
|
||||||
|
|
||||||
|
editOptions(ri, name, true)
|
||||||
|
RemoteConfig(ctx, name)
|
||||||
|
if OkRemote(name) {
|
||||||
|
SaveConfig()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
EditRemote(ctx, ri, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EditRemote gets the user to edit a remote
|
||||||
|
func EditRemote(ctx context.Context, ri *fs.RegInfo, name string) {
|
||||||
|
ShowRemote(name)
|
||||||
|
fmt.Printf("Edit remote\n")
|
||||||
|
for {
|
||||||
|
editOptions(ri, name, false)
|
||||||
|
if OkRemote(name) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SaveConfig()
|
||||||
|
RemoteConfig(ctx, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteRemote gets the user to delete a remote
|
||||||
|
func DeleteRemote(name string) {
|
||||||
|
Data.DeleteSection(name)
|
||||||
|
SaveConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
// copyRemote asks the user for a new remote name and copies name into
|
||||||
|
// it. Returns the new name.
|
||||||
|
func copyRemote(name string) string {
|
||||||
|
newName := NewRemoteName()
|
||||||
|
// Copy the keys
|
||||||
|
for _, key := range Data.GetKeyList(name) {
|
||||||
|
value := getWithDefault(name, key, "")
|
||||||
|
Data.SetValue(newName, key, value)
|
||||||
|
}
|
||||||
|
return newName
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenameRemote renames a config section
|
||||||
|
func RenameRemote(name string) {
|
||||||
|
fmt.Printf("Enter new name for %q remote.\n", name)
|
||||||
|
newName := copyRemote(name)
|
||||||
|
if name != newName {
|
||||||
|
Data.DeleteSection(name)
|
||||||
|
SaveConfig()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CopyRemote copies a config section
|
||||||
|
func CopyRemote(name string) {
|
||||||
|
fmt.Printf("Enter name for copy of %q remote.\n", name)
|
||||||
|
copyRemote(name)
|
||||||
|
SaveConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShowConfigLocation prints the location of the config file in use
|
||||||
|
func ShowConfigLocation() {
|
||||||
|
if _, err := os.Stat(ConfigPath); os.IsNotExist(err) {
|
||||||
|
fmt.Println("Configuration file doesn't exist, but rclone will use this path:")
|
||||||
|
} else {
|
||||||
|
fmt.Println("Configuration file is stored at:")
|
||||||
|
}
|
||||||
|
fmt.Printf("%s\n", ConfigPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShowConfig prints the (unencrypted) config options
|
||||||
|
func ShowConfig() {
|
||||||
|
str, err := Data.Serialize()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to serialize config: %v", err)
|
||||||
|
}
|
||||||
|
if str == "" {
|
||||||
|
str = "; empty config\n"
|
||||||
|
}
|
||||||
|
fmt.Printf("%s", str)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EditConfig edits the config file interactively
|
||||||
|
func EditConfig(ctx context.Context) {
|
||||||
|
for {
|
||||||
|
haveRemotes := len(Data.GetSectionList()) != 0
|
||||||
|
what := []string{"eEdit existing remote", "nNew remote", "dDelete remote", "rRename remote", "cCopy remote", "sSet configuration password", "qQuit config"}
|
||||||
|
if haveRemotes {
|
||||||
|
fmt.Printf("Current remotes:\n\n")
|
||||||
|
ShowRemotes()
|
||||||
|
fmt.Printf("\n")
|
||||||
|
} else {
|
||||||
|
fmt.Printf("No remotes found - make a new one\n")
|
||||||
|
// take 2nd item and last 2 items of menu list
|
||||||
|
what = append(what[1:2], what[len(what)-2:]...)
|
||||||
|
}
|
||||||
|
switch i := Command(what); i {
|
||||||
|
case 'e':
|
||||||
|
name := ChooseRemote()
|
||||||
|
fs := mustFindByName(name)
|
||||||
|
EditRemote(ctx, fs, name)
|
||||||
|
case 'n':
|
||||||
|
NewRemote(ctx, NewRemoteName())
|
||||||
|
case 'd':
|
||||||
|
name := ChooseRemote()
|
||||||
|
DeleteRemote(name)
|
||||||
|
case 'r':
|
||||||
|
RenameRemote(ChooseRemote())
|
||||||
|
case 'c':
|
||||||
|
CopyRemote(ChooseRemote())
|
||||||
|
case 's':
|
||||||
|
SetPassword()
|
||||||
|
case 'q':
|
||||||
|
return
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Suppress the confirm prompts by altering the context config
|
||||||
|
func suppressConfirm(ctx context.Context) context.Context {
|
||||||
|
newCtx, ci := fs.AddConfig(ctx)
|
||||||
|
ci.AutoConfirm = true
|
||||||
|
return newCtx
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkPassword normalises and validates the password
|
||||||
|
func checkPassword(password string) (string, error) {
|
||||||
|
if !utf8.ValidString(password) {
|
||||||
|
return "", errors.New("password contains invalid utf8 characters")
|
||||||
|
}
|
||||||
|
// Check for leading/trailing whitespace
|
||||||
|
trimmedPassword := strings.TrimSpace(password)
|
||||||
|
// Warn user if password has leading+trailing whitespace
|
||||||
|
if len(password) != len(trimmedPassword) {
|
||||||
|
_, _ = fmt.Fprintln(os.Stderr, "Your password contains leading/trailing whitespace - in previous versions of rclone this was stripped")
|
||||||
|
}
|
||||||
|
// Normalize to reduce weird variations.
|
||||||
|
password = norm.NFKC.String(password)
|
||||||
|
if len(password) == 0 || len(trimmedPassword) == 0 {
|
||||||
|
return "", errors.New("no characters in password")
|
||||||
|
}
|
||||||
|
return password, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPassword asks the user for a password with the prompt given.
|
||||||
|
func GetPassword(prompt string) string {
|
||||||
|
_, _ = fmt.Fprintln(PasswordPromptOutput, prompt)
|
||||||
|
for {
|
||||||
|
_, _ = fmt.Fprint(PasswordPromptOutput, "password:")
|
||||||
|
password := ReadPassword()
|
||||||
|
password, err := checkPassword(password)
|
||||||
|
if err == nil {
|
||||||
|
return password
|
||||||
|
}
|
||||||
|
_, _ = fmt.Fprintf(os.Stderr, "Bad password: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChangePassword will query the user twice for the named password. If
|
||||||
|
// the same password is entered it is returned.
|
||||||
|
func ChangePassword(name string) string {
|
||||||
|
for {
|
||||||
|
a := GetPassword(fmt.Sprintf("Enter %s password:", name))
|
||||||
|
b := GetPassword(fmt.Sprintf("Confirm %s password:", name))
|
||||||
|
if a == b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
fmt.Println("Passwords do not match!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetPassword will allow the user to modify the current
|
||||||
|
// configuration encryption settings.
|
||||||
|
func SetPassword() {
|
||||||
|
for {
|
||||||
|
if len(configKey) > 0 {
|
||||||
|
fmt.Println("Your configuration is encrypted.")
|
||||||
|
what := []string{"cChange Password", "uUnencrypt configuration", "qQuit to main menu"}
|
||||||
|
switch i := Command(what); i {
|
||||||
|
case 'c':
|
||||||
|
changeConfigPassword()
|
||||||
|
SaveConfig()
|
||||||
|
fmt.Println("Password changed")
|
||||||
|
continue
|
||||||
|
case 'u':
|
||||||
|
configKey = nil
|
||||||
|
SaveConfig()
|
||||||
|
continue
|
||||||
|
case 'q':
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
fmt.Println("Your configuration is not encrypted.")
|
||||||
|
fmt.Println("If you add a password, you will protect your login information to cloud services.")
|
||||||
|
what := []string{"aAdd Password", "qQuit to main menu"}
|
||||||
|
switch i := Command(what); i {
|
||||||
|
case 'a':
|
||||||
|
changeConfigPassword()
|
||||||
|
SaveConfig()
|
||||||
|
fmt.Println("Password set")
|
||||||
|
continue
|
||||||
|
case 'q':
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
242
fs/config/ui_test.go
Normal file
242
fs/config/ui_test.go
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
// These are in an external package because we need to import configfile
|
||||||
|
//
|
||||||
|
// Internal tests are in ui_internal_test.go
|
||||||
|
|
||||||
|
package config_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"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/rc"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testConfigFile(t *testing.T, configFileName string) func() {
|
||||||
|
ctx := context.Background()
|
||||||
|
ci := fs.GetConfig(ctx)
|
||||||
|
config.ClearConfigPassword()
|
||||||
|
_ = os.Unsetenv("_RCLONE_CONFIG_KEY_FILE")
|
||||||
|
_ = os.Unsetenv("RCLONE_CONFIG_PASS")
|
||||||
|
// create temp config file
|
||||||
|
tempFile, err := ioutil.TempFile("", configFileName)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
path := tempFile.Name()
|
||||||
|
assert.NoError(t, tempFile.Close())
|
||||||
|
|
||||||
|
// temporarily adapt configuration
|
||||||
|
oldOsStdout := os.Stdout
|
||||||
|
oldConfigPath := config.ConfigPath
|
||||||
|
oldConfig := *ci
|
||||||
|
oldConfigFile := config.Data
|
||||||
|
oldReadLine := config.ReadLine
|
||||||
|
oldPassword := config.Password
|
||||||
|
os.Stdout = nil
|
||||||
|
config.ConfigPath = path
|
||||||
|
ci = &fs.ConfigInfo{}
|
||||||
|
|
||||||
|
configfile.LoadConfig(ctx)
|
||||||
|
assert.Equal(t, []string{}, config.Data.GetSectionList())
|
||||||
|
|
||||||
|
// Fake a remote
|
||||||
|
fs.Register(&fs.RegInfo{
|
||||||
|
Name: "config_test_remote",
|
||||||
|
Options: fs.Options{
|
||||||
|
{
|
||||||
|
Name: "bool",
|
||||||
|
Default: false,
|
||||||
|
IsPassword: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "pass",
|
||||||
|
Default: "",
|
||||||
|
IsPassword: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Undo the above
|
||||||
|
return func() {
|
||||||
|
err := os.Remove(path)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
os.Stdout = oldOsStdout
|
||||||
|
config.ConfigPath = oldConfigPath
|
||||||
|
config.ReadLine = oldReadLine
|
||||||
|
config.Password = oldPassword
|
||||||
|
*ci = oldConfig
|
||||||
|
config.Data = oldConfigFile
|
||||||
|
|
||||||
|
_ = os.Unsetenv("_RCLONE_CONFIG_KEY_FILE")
|
||||||
|
_ = os.Unsetenv("RCLONE_CONFIG_PASS")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// makeReadLine makes a simple readLine which returns a fixed list of
|
||||||
|
// strings
|
||||||
|
func makeReadLine(answers []string) func() string {
|
||||||
|
i := 0
|
||||||
|
return func() string {
|
||||||
|
i = i + 1
|
||||||
|
return answers[i-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCRUD(t *testing.T) {
|
||||||
|
defer testConfigFile(t, "crud.conf")()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// script for creating remote
|
||||||
|
config.ReadLine = makeReadLine([]string{
|
||||||
|
"config_test_remote", // type
|
||||||
|
"true", // bool value
|
||||||
|
"y", // type my own password
|
||||||
|
"secret", // password
|
||||||
|
"secret", // repeat
|
||||||
|
"y", // looks good, save
|
||||||
|
})
|
||||||
|
config.NewRemote(ctx, "test")
|
||||||
|
|
||||||
|
assert.Equal(t, []string{"test"}, config.Data.GetSectionList())
|
||||||
|
assert.Equal(t, "config_test_remote", config.FileGet("test", "type"))
|
||||||
|
assert.Equal(t, "true", config.FileGet("test", "bool"))
|
||||||
|
assert.Equal(t, "secret", obscure.MustReveal(config.FileGet("test", "pass")))
|
||||||
|
|
||||||
|
// normal rename, test → asdf
|
||||||
|
config.ReadLine = makeReadLine([]string{
|
||||||
|
"asdf",
|
||||||
|
"asdf",
|
||||||
|
"asdf",
|
||||||
|
})
|
||||||
|
config.RenameRemote("test")
|
||||||
|
|
||||||
|
assert.Equal(t, []string{"asdf"}, config.Data.GetSectionList())
|
||||||
|
assert.Equal(t, "config_test_remote", config.FileGet("asdf", "type"))
|
||||||
|
assert.Equal(t, "true", config.FileGet("asdf", "bool"))
|
||||||
|
assert.Equal(t, "secret", obscure.MustReveal(config.FileGet("asdf", "pass")))
|
||||||
|
|
||||||
|
// delete remote
|
||||||
|
config.DeleteRemote("asdf")
|
||||||
|
assert.Equal(t, []string{}, config.Data.GetSectionList())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChooseOption(t *testing.T) {
|
||||||
|
defer testConfigFile(t, "crud.conf")()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// script for creating remote
|
||||||
|
config.ReadLine = makeReadLine([]string{
|
||||||
|
"config_test_remote", // type
|
||||||
|
"false", // bool value
|
||||||
|
"x", // bad choice
|
||||||
|
"g", // generate password
|
||||||
|
"1024", // very big
|
||||||
|
"y", // password OK
|
||||||
|
"y", // looks good, save
|
||||||
|
})
|
||||||
|
config.Password = func(bits int) (string, error) {
|
||||||
|
assert.Equal(t, 1024, bits)
|
||||||
|
return "not very random password", nil
|
||||||
|
}
|
||||||
|
config.NewRemote(ctx, "test")
|
||||||
|
|
||||||
|
assert.Equal(t, "false", config.FileGet("test", "bool"))
|
||||||
|
assert.Equal(t, "not very random password", obscure.MustReveal(config.FileGet("test", "pass")))
|
||||||
|
|
||||||
|
// script for creating remote
|
||||||
|
config.ReadLine = makeReadLine([]string{
|
||||||
|
"config_test_remote", // type
|
||||||
|
"true", // bool value
|
||||||
|
"n", // not required
|
||||||
|
"y", // looks good, save
|
||||||
|
})
|
||||||
|
config.NewRemote(ctx, "test")
|
||||||
|
|
||||||
|
assert.Equal(t, "true", config.FileGet("test", "bool"))
|
||||||
|
assert.Equal(t, "", config.FileGet("test", "pass"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewRemoteName(t *testing.T) {
|
||||||
|
defer testConfigFile(t, "crud.conf")()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// script for creating remote
|
||||||
|
config.ReadLine = makeReadLine([]string{
|
||||||
|
"config_test_remote", // type
|
||||||
|
"true", // bool value
|
||||||
|
"n", // not required
|
||||||
|
"y", // looks good, save
|
||||||
|
})
|
||||||
|
config.NewRemote(ctx, "test")
|
||||||
|
|
||||||
|
config.ReadLine = makeReadLine([]string{
|
||||||
|
"test", // already exists
|
||||||
|
"", // empty string not allowed
|
||||||
|
"bad@characters", // bad characters
|
||||||
|
"newname", // OK
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.Equal(t, "newname", config.NewRemoteName())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateUpdatePasswordRemote(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
defer testConfigFile(t, "update.conf")()
|
||||||
|
|
||||||
|
for _, doObscure := range []bool{false, true} {
|
||||||
|
for _, noObscure := range []bool{false, true} {
|
||||||
|
if doObscure && noObscure {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
t.Run(fmt.Sprintf("doObscure=%v,noObscure=%v", doObscure, noObscure), func(t *testing.T) {
|
||||||
|
require.NoError(t, config.CreateRemote(ctx, "test2", "config_test_remote", rc.Params{
|
||||||
|
"bool": true,
|
||||||
|
"pass": "potato",
|
||||||
|
}, doObscure, noObscure))
|
||||||
|
|
||||||
|
assert.Equal(t, []string{"test2"}, config.Data.GetSectionList())
|
||||||
|
assert.Equal(t, "config_test_remote", config.FileGet("test2", "type"))
|
||||||
|
assert.Equal(t, "true", config.FileGet("test2", "bool"))
|
||||||
|
gotPw := config.FileGet("test2", "pass")
|
||||||
|
if !noObscure {
|
||||||
|
gotPw = obscure.MustReveal(gotPw)
|
||||||
|
}
|
||||||
|
assert.Equal(t, "potato", gotPw)
|
||||||
|
|
||||||
|
wantPw := obscure.MustObscure("potato2")
|
||||||
|
require.NoError(t, config.UpdateRemote(ctx, "test2", rc.Params{
|
||||||
|
"bool": false,
|
||||||
|
"pass": wantPw,
|
||||||
|
"spare": "spare",
|
||||||
|
}, doObscure, noObscure))
|
||||||
|
|
||||||
|
assert.Equal(t, []string{"test2"}, config.Data.GetSectionList())
|
||||||
|
assert.Equal(t, "config_test_remote", config.FileGet("test2", "type"))
|
||||||
|
assert.Equal(t, "false", config.FileGet("test2", "bool"))
|
||||||
|
gotPw = config.FileGet("test2", "pass")
|
||||||
|
if doObscure {
|
||||||
|
gotPw = obscure.MustReveal(gotPw)
|
||||||
|
}
|
||||||
|
assert.Equal(t, wantPw, gotPw)
|
||||||
|
|
||||||
|
require.NoError(t, config.PasswordRemote(ctx, "test2", rc.Params{
|
||||||
|
"pass": "potato3",
|
||||||
|
}))
|
||||||
|
|
||||||
|
assert.Equal(t, []string{"test2"}, config.Data.GetSectionList())
|
||||||
|
assert.Equal(t, "config_test_remote", config.FileGet("test2", "type"))
|
||||||
|
assert.Equal(t, "false", config.FileGet("test2", "bool"))
|
||||||
|
assert.Equal(t, "potato3", obscure.MustReveal(config.FileGet("test2", "pass")))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user