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