rclone/fs/config/ui.go

677 lines
17 KiB
Go
Raw Normal View History

// 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'
}
// 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 := LoadedData().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 := LoadedData().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 LoadedData().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':
LoadedData().DeleteSection(name)
return true
default:
fs.Errorf(nil, "Bad choice %c", i)
}
return false
}
// backendConfig configures the backend starting from the state passed in
//
// The is the user interface loop that drives the post configuration backend config.
func backendConfig(ctx context.Context, name string, m configmap.Mapper, ri *fs.RegInfo, choices configmap.Getter, startState string) error {
in := fs.ConfigIn{
State: startState,
}
for {
out, err := fs.BackendConfig(ctx, name, m, ri, choices, in)
if err != nil {
return err
}
if out == nil {
break
}
if out.Error != "" {
fmt.Println(out.Error)
}
in.State = out.State
in.Result = out.Result
if out.Option != nil {
fs.Debugf(name, "config: reading config parameter %q", out.Option.Name)
if out.Option.Default == nil {
out.Option.Default = ""
}
if Default, isBool := out.Option.Default.(bool); isBool &&
len(out.Option.Examples) == 2 &&
out.Option.Examples[0].Help == "Yes" &&
out.Option.Examples[0].Value == "true" &&
out.Option.Examples[1].Help == "No" &&
out.Option.Examples[1].Value == "false" &&
out.Option.Exclusive {
// Use Confirm for Yes/No questions as it has a nicer interface=
fmt.Println(out.Option.Help)
in.Result = fmt.Sprint(Confirm(Default))
} else {
value := ChooseOption(out.Option, "")
if value != "" {
err := out.Option.Set(value)
if err != nil {
return errors.Wrap(err, "failed to set option")
}
}
in.Result = out.Option.String()
}
}
if out.State == "" {
break
}
}
return nil
}
// PostConfig configures the backend after the main config has been done
//
// The is the user interface loop that drives the post configuration backend config.
func PostConfig(ctx context.Context, name string, m configmap.Mapper, ri *fs.RegInfo) error {
if ri.Config == nil {
return errors.New("backend doesn't support reconnect or authorize")
}
return backendConfig(ctx, name, m, ri, configmap.Simple{}, "")
}
// RemoteConfig runs the config helper for the remote if needed
func RemoteConfig(ctx context.Context, name string) error {
fmt.Printf("Remote config\n")
ri := mustFindByName(name)
m := fs.ConfigMap(ri, name, nil)
if ri.Config == nil {
return nil
}
return PostConfig(ctx, name, m, ri)
}
// ChooseOption asks the user to choose an option
func ChooseOption(o *fs.Option, name string) string {
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 {
values = append(values, example.Value)
help = append(help, example.Help)
}
in = Choose(o.Name, values, help, !o.Exclusive)
} 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 LoadedData().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(ctx context.Context, ri *fs.RegInfo, name string, isNew bool) error {
fmt.Printf("** See help for %s backend at: https://rclone.org/%s/ **\n\n", ri.Name, ri.FileName())
m := fs.ConfigMap(ri, name, nil)
choices := configmap.Simple{
fs.ConfigEdit: fmt.Sprint(isNew),
}
return backendConfig(ctx, name, m, ri, choices, fs.ConfigAll)
}
// NewRemote make a new remote from its name
func NewRemote(ctx context.Context, name string) error {
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
}
LoadedData().SetValue(name, "type", newType)
err = editOptions(ctx, ri, name, true)
if err != nil {
return err
}
if OkRemote(name) {
SaveConfig()
return nil
}
return EditRemote(ctx, ri, name)
}
// EditRemote gets the user to edit a remote
func EditRemote(ctx context.Context, ri *fs.RegInfo, name string) error {
ShowRemote(name)
fmt.Printf("Edit remote\n")
for {
err := editOptions(ctx, ri, name, true)
if err != nil {
return err
}
if OkRemote(name) {
break
}
}
SaveConfig()
return nil
}
// DeleteRemote gets the user to delete a remote
func DeleteRemote(name string) {
LoadedData().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 LoadedData().GetKeyList(name) {
value := getWithDefault(name, key, "")
LoadedData().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 {
LoadedData().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 configPath := GetConfigPath(); configPath == "" {
fmt.Println("Configuration is in memory only")
} else {
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 := LoadedData().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) (err error) {
for {
haveRemotes := len(LoadedData().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)
err = EditRemote(ctx, fs, name)
if err != nil {
return err
}
case 'n':
err = NewRemote(ctx, NewRemoteName())
if err != nil {
return err
}
case 'd':
name := ChooseRemote()
DeleteRemote(name)
case 'r':
RenameRemote(ChooseRemote())
case 'c':
CopyRemote(ChooseRemote())
case 's':
SetPassword()
case 'q':
return nil
}
}
}
// 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
}
}
}
}