mirror of
https://github.com/rclone/rclone
synced 2024-12-14 02:29:58 +01:00
955 lines
26 KiB
Go
955 lines
26 KiB
Go
// Read, write and edit the config file
|
|
|
|
package fs
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"crypto/tls"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"log"
|
|
"math"
|
|
"net/http"
|
|
"os"
|
|
"os/user"
|
|
"path"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
"unicode/utf8"
|
|
|
|
"github.com/Unknwon/goconfig"
|
|
"github.com/mreiferson/go-httpclient"
|
|
"github.com/spf13/pflag"
|
|
"golang.org/x/crypto/nacl/secretbox"
|
|
"golang.org/x/text/unicode/norm"
|
|
)
|
|
|
|
const (
|
|
configFileName = ".rclone.conf"
|
|
|
|
// 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"
|
|
|
|
// ConfigAutomatic indicates that we want non-interactive configuration
|
|
ConfigAutomatic = "config_automatic"
|
|
)
|
|
|
|
// SizeSuffix is parsed by flag with k/M/G suffixes
|
|
type SizeSuffix int64
|
|
|
|
// Global
|
|
var (
|
|
// ConfigFile is the config file data structure
|
|
ConfigFile *goconfig.ConfigFile
|
|
// HomeDir is the home directory of the user
|
|
HomeDir = configHome()
|
|
// ConfigPath points to the config file
|
|
ConfigPath = path.Join(HomeDir, configFileName)
|
|
// Config is the global config
|
|
Config = &ConfigInfo{}
|
|
// Flags
|
|
verbose = pflag.BoolP("verbose", "v", false, "Print lots more stuff")
|
|
quiet = pflag.BoolP("quiet", "q", false, "Print as little stuff as possible")
|
|
modifyWindow = pflag.DurationP("modify-window", "", time.Nanosecond, "Max time diff to be considered the same")
|
|
checkers = pflag.IntP("checkers", "", 8, "Number of checkers to run in parallel.")
|
|
transfers = pflag.IntP("transfers", "", 4, "Number of file transfers to run in parallel.")
|
|
configFile = pflag.StringP("config", "", ConfigPath, "Config file.")
|
|
checkSum = pflag.BoolP("checksum", "c", false, "Skip based on checksum & size, not mod-time & size")
|
|
sizeOnly = pflag.BoolP("size-only", "", false, "Skip based on size only, not mod-time or checksum")
|
|
ignoreTimes = pflag.BoolP("ignore-times", "I", false, "Don't skip files that match size and time - transfer all files")
|
|
ignoreExisting = pflag.BoolP("ignore-existing", "", false, "Skip all files that exist on destination")
|
|
dryRun = pflag.BoolP("dry-run", "n", false, "Do a trial run with no permanent changes")
|
|
connectTimeout = pflag.DurationP("contimeout", "", 60*time.Second, "Connect timeout")
|
|
timeout = pflag.DurationP("timeout", "", 5*60*time.Second, "IO idle timeout")
|
|
dumpHeaders = pflag.BoolP("dump-headers", "", false, "Dump HTTP headers - may contain sensitive info")
|
|
dumpBodies = pflag.BoolP("dump-bodies", "", false, "Dump HTTP headers and bodies - may contain sensitive info")
|
|
skipVerify = pflag.BoolP("no-check-certificate", "", false, "Do not verify the server SSL certificate. Insecure.")
|
|
AskPassword = pflag.BoolP("ask-password", "", true, "Allow prompt for password for encrypted configuration.")
|
|
deleteBefore = pflag.BoolP("delete-before", "", false, "When synchronizing, delete files on destination before transfering")
|
|
deleteDuring = pflag.BoolP("delete-during", "", false, "When synchronizing, delete files during transfer (default)")
|
|
deleteAfter = pflag.BoolP("delete-after", "", false, "When synchronizing, delete files on destination after transfering")
|
|
lowLevelRetries = pflag.IntP("low-level-retries", "", 10, "Number of low level retries to do.")
|
|
updateOlder = pflag.BoolP("update", "u", false, "Skip files that are newer on the destination.")
|
|
noGzip = pflag.BoolP("no-gzip-encoding", "", false, "Don't set Accept-Encoding: gzip.")
|
|
dedupeMode = pflag.StringP("dedupe-mode", "", "interactive", "Dedupe mode interactive|skip|first|newest|oldest|rename.")
|
|
maxDepth = pflag.IntP("max-depth", "", -1, "If set limits the recursion depth to this.")
|
|
bwLimit SizeSuffix
|
|
|
|
// Key to use for password en/decryption.
|
|
// When nil, no encryption will be used for saving.
|
|
configKey []byte
|
|
)
|
|
|
|
func init() {
|
|
pflag.VarP(&bwLimit, "bwlimit", "", "Bandwidth limit in kBytes/s, or use suffix b|k|M|G")
|
|
}
|
|
|
|
// Turn SizeSuffix into a string
|
|
func (x SizeSuffix) String() string {
|
|
scaled := float64(0)
|
|
suffix := ""
|
|
switch {
|
|
case x < 0:
|
|
return "off"
|
|
case x == 0:
|
|
return "0"
|
|
case x < 1024:
|
|
scaled = float64(x)
|
|
suffix = "b"
|
|
case x < 1024*1024:
|
|
scaled = float64(x) / 1024
|
|
suffix = "k"
|
|
case x < 1024*1024*1024:
|
|
scaled = float64(x) / 1024 / 1024
|
|
suffix = "M"
|
|
default:
|
|
scaled = float64(x) / 1024 / 1024 / 1024
|
|
suffix = "G"
|
|
}
|
|
if math.Floor(scaled) == scaled {
|
|
return fmt.Sprintf("%.0f%s", scaled, suffix)
|
|
}
|
|
return fmt.Sprintf("%.3f%s", scaled, suffix)
|
|
}
|
|
|
|
// Set a SizeSuffix
|
|
func (x *SizeSuffix) Set(s string) error {
|
|
if len(s) == 0 {
|
|
return fmt.Errorf("Empty string")
|
|
}
|
|
if strings.ToLower(s) == "off" {
|
|
*x = -1
|
|
return nil
|
|
}
|
|
suffix := s[len(s)-1]
|
|
suffixLen := 1
|
|
var multiplier float64
|
|
switch suffix {
|
|
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.':
|
|
suffixLen = 0
|
|
multiplier = 1 << 10
|
|
case 'b', 'B':
|
|
multiplier = 1
|
|
case 'k', 'K':
|
|
multiplier = 1 << 10
|
|
case 'm', 'M':
|
|
multiplier = 1 << 20
|
|
case 'g', 'G':
|
|
multiplier = 1 << 30
|
|
default:
|
|
return fmt.Errorf("Bad suffix %q", suffix)
|
|
}
|
|
s = s[:len(s)-suffixLen]
|
|
value, err := strconv.ParseFloat(s, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if value < 0 {
|
|
return fmt.Errorf("Size can't be negative %q", s)
|
|
}
|
|
value *= multiplier
|
|
*x = SizeSuffix(value)
|
|
return nil
|
|
}
|
|
|
|
// Type of the value
|
|
func (x *SizeSuffix) Type() string {
|
|
return "int64"
|
|
}
|
|
|
|
// Check it satisfies the interface
|
|
var _ pflag.Value = (*SizeSuffix)(nil)
|
|
|
|
// Obscure a config value
|
|
func Obscure(x string) string {
|
|
y := []byte(x)
|
|
for i := range y {
|
|
y[i] ^= byte(i) ^ 0xAA
|
|
}
|
|
return base64.StdEncoding.EncodeToString(y)
|
|
}
|
|
|
|
// Reveal a config value
|
|
func Reveal(y string) string {
|
|
x, err := base64.StdEncoding.DecodeString(y)
|
|
if err != nil {
|
|
log.Fatalf("Failed to reveal %q: %v", y, err)
|
|
}
|
|
for i := range x {
|
|
x[i] ^= byte(i) ^ 0xAA
|
|
}
|
|
return string(x)
|
|
}
|
|
|
|
// ConfigInfo is filesystem config options
|
|
type ConfigInfo struct {
|
|
Verbose bool
|
|
Quiet bool
|
|
DryRun bool
|
|
CheckSum bool
|
|
SizeOnly bool
|
|
IgnoreTimes bool
|
|
IgnoreExisting bool
|
|
ModifyWindow time.Duration
|
|
Checkers int
|
|
Transfers int
|
|
ConnectTimeout time.Duration // Connect timeout
|
|
Timeout time.Duration // Data channel timeout
|
|
DumpHeaders bool
|
|
DumpBodies bool
|
|
Filter *Filter
|
|
InsecureSkipVerify bool // Skip server certificate verification
|
|
DeleteBefore bool // Delete before checking
|
|
DeleteDuring bool // Delete during checking/transfer
|
|
DeleteAfter bool // Delete after successful transfer.
|
|
LowLevelRetries int
|
|
UpdateOlder bool // Skip files that are newer on the destination
|
|
NoGzip bool // Disable compression
|
|
DedupeMode DeduplicateMode
|
|
MaxDepth int
|
|
}
|
|
|
|
// Transport returns an http.RoundTripper with the correct timeouts
|
|
func (ci *ConfigInfo) Transport() http.RoundTripper {
|
|
t := &httpclient.Transport{
|
|
Proxy: http.ProxyFromEnvironment,
|
|
MaxIdleConnsPerHost: ci.Checkers + ci.Transfers + 1,
|
|
|
|
// ConnectTimeout, if non-zero, is the maximum amount of time a dial will wait for
|
|
// a connect to complete.
|
|
ConnectTimeout: ci.ConnectTimeout,
|
|
|
|
// ResponseHeaderTimeout, if non-zero, specifies the amount of
|
|
// time to wait for a server's response headers after fully
|
|
// writing the request (including its body, if any). This
|
|
// time does not include the time to read the response body.
|
|
ResponseHeaderTimeout: ci.Timeout,
|
|
|
|
// RequestTimeout, if non-zero, specifies the amount of time for the entire
|
|
// request to complete (including all of the above timeouts + entire response body).
|
|
// This should never be less than the sum total of the above two timeouts.
|
|
//RequestTimeout: NOT SET,
|
|
|
|
// ReadWriteTimeout, if non-zero, will set a deadline for every Read and
|
|
// Write operation on the request connection.
|
|
ReadWriteTimeout: ci.Timeout,
|
|
|
|
// InsecureSkipVerify controls whether a client verifies the
|
|
// server's certificate chain and host name.
|
|
// If InsecureSkipVerify is true, TLS accepts any certificate
|
|
// presented by the server and any host name in that certificate.
|
|
// In this mode, TLS is susceptible to man-in-the-middle attacks.
|
|
// This should be used only for testing.
|
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: ci.InsecureSkipVerify},
|
|
|
|
// DisableCompression, if true, prevents the Transport from
|
|
// requesting compression with an "Accept-Encoding: gzip"
|
|
// request header when the Request contains no existing
|
|
// Accept-Encoding value. If the Transport requests gzip on
|
|
// its own and gets a gzipped response, it's transparently
|
|
// decoded in the Response.Body. However, if the user
|
|
// explicitly requested gzip it is not automatically
|
|
// uncompressed.
|
|
DisableCompression: *noGzip,
|
|
}
|
|
if ci.DumpHeaders || ci.DumpBodies {
|
|
return NewLoggedTransport(t, ci.DumpBodies)
|
|
}
|
|
return t
|
|
}
|
|
|
|
// Client returns an http.Client with the correct timeouts
|
|
func (ci *ConfigInfo) Client() *http.Client {
|
|
return &http.Client{
|
|
Transport: ci.Transport(),
|
|
}
|
|
}
|
|
|
|
// Find the config directory
|
|
func configHome() string {
|
|
// Find users home directory
|
|
usr, err := user.Current()
|
|
if err == nil {
|
|
return usr.HomeDir
|
|
}
|
|
// Fall back to reading $HOME - work around user.Current() not
|
|
// working for cross compiled binaries on OSX.
|
|
// https://github.com/golang/go/issues/6376
|
|
home := os.Getenv("HOME")
|
|
if home != "" {
|
|
return home
|
|
}
|
|
log.Printf("Couldn't find home directory or read HOME environment variable.")
|
|
log.Printf("Defaulting to storing config in current directory.")
|
|
log.Printf("Use -config flag to workaround.")
|
|
log.Printf("Error was: %v", err)
|
|
return ""
|
|
}
|
|
|
|
// LoadConfig loads the config file
|
|
func LoadConfig() {
|
|
// Read some flags if set
|
|
//
|
|
// FIXME read these from the config file too
|
|
Config.Verbose = *verbose
|
|
Config.Quiet = *quiet
|
|
Config.ModifyWindow = *modifyWindow
|
|
Config.Checkers = *checkers
|
|
Config.Transfers = *transfers
|
|
Config.DryRun = *dryRun
|
|
Config.Timeout = *timeout
|
|
Config.ConnectTimeout = *connectTimeout
|
|
Config.CheckSum = *checkSum
|
|
Config.SizeOnly = *sizeOnly
|
|
Config.IgnoreTimes = *ignoreTimes
|
|
Config.IgnoreExisting = *ignoreExisting
|
|
Config.DumpHeaders = *dumpHeaders
|
|
Config.DumpBodies = *dumpBodies
|
|
Config.InsecureSkipVerify = *skipVerify
|
|
Config.LowLevelRetries = *lowLevelRetries
|
|
Config.UpdateOlder = *updateOlder
|
|
Config.NoGzip = *noGzip
|
|
Config.MaxDepth = *maxDepth
|
|
|
|
ConfigPath = *configFile
|
|
|
|
Config.DeleteBefore = *deleteBefore
|
|
Config.DeleteDuring = *deleteDuring
|
|
Config.DeleteAfter = *deleteAfter
|
|
|
|
switch strings.ToLower(*dedupeMode) {
|
|
case "interactive":
|
|
Config.DedupeMode = DeduplicateInteractive
|
|
case "skip":
|
|
Config.DedupeMode = DeduplicateSkip
|
|
case "first":
|
|
Config.DedupeMode = DeduplicateFirst
|
|
case "newest":
|
|
Config.DedupeMode = DeduplicateNewest
|
|
case "oldest":
|
|
Config.DedupeMode = DeduplicateOldest
|
|
case "rename":
|
|
Config.DedupeMode = DeduplicateRename
|
|
default:
|
|
log.Fatalf(`Unknown mode for --dedupe-mode %q.`, *dedupeMode)
|
|
}
|
|
|
|
switch {
|
|
case *deleteBefore && (*deleteDuring || *deleteAfter),
|
|
*deleteDuring && *deleteAfter:
|
|
log.Fatalf(`Only one of --delete-before, --delete-during or --delete-after can be used.`)
|
|
|
|
// If none are specified, use "during".
|
|
case !*deleteBefore && !*deleteDuring && !*deleteAfter:
|
|
Config.DeleteDuring = true
|
|
}
|
|
|
|
// Load configuration file.
|
|
var err error
|
|
ConfigFile, err = loadConfigFile()
|
|
if err != nil {
|
|
log.Fatalf("Failed to config file \"%s\": %v", ConfigPath, err)
|
|
}
|
|
|
|
// Load filters
|
|
Config.Filter, err = NewFilter()
|
|
if err != nil {
|
|
log.Fatalf("Failed to load filters: %v", err)
|
|
}
|
|
|
|
// Start the token bucket limiter
|
|
startTokenBucket()
|
|
}
|
|
|
|
// loadConfigFile will load a config file, and
|
|
// automatically decrypt it.
|
|
func loadConfigFile() (*goconfig.ConfigFile, error) {
|
|
b, err := ioutil.ReadFile(ConfigPath)
|
|
if err != nil {
|
|
log.Printf("Failed to load config file \"%v\" - using defaults: %v", ConfigPath, err)
|
|
return goconfig.LoadFromReader(&bytes.Buffer{})
|
|
}
|
|
|
|
// Find first non-empty line
|
|
r := bufio.NewReader(bytes.NewBuffer(b))
|
|
for {
|
|
line, _, err := r.ReadLine()
|
|
if err != nil {
|
|
if err == io.EOF {
|
|
return goconfig.LoadFromReader(bytes.NewBuffer(b))
|
|
}
|
|
return nil, err
|
|
}
|
|
l := strings.TrimSpace(string(line))
|
|
if len(l) == 0 || strings.HasPrefix(l, ";") || strings.HasPrefix(l, "#") {
|
|
continue
|
|
}
|
|
// First non-empty or non-comment must be ENCRYPT_V0
|
|
if l == "RCLONE_ENCRYPT_V0:" {
|
|
break
|
|
}
|
|
if strings.HasPrefix(l, "RCLONE_ENCRYPT_V") {
|
|
return nil, fmt.Errorf("Unsupported configuration encryption. Update rclone for support.")
|
|
}
|
|
return goconfig.LoadFromReader(bytes.NewBuffer(b))
|
|
}
|
|
|
|
// Encrypted content is base64 encoded.
|
|
dec := base64.NewDecoder(base64.StdEncoding, r)
|
|
box, err := ioutil.ReadAll(dec)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Failed to load base64 encoded data: %v", err)
|
|
}
|
|
if len(box) < 24+secretbox.Overhead {
|
|
return nil, fmt.Errorf("Configuration data too short")
|
|
}
|
|
envpw := os.Getenv("RCLONE_CONFIG_PASS")
|
|
|
|
var out []byte
|
|
for {
|
|
if len(configKey) == 0 && envpw != "" {
|
|
err := setPassword(envpw)
|
|
if err != nil {
|
|
fmt.Println("Using RCLONE_CONFIG_PASS returned:", err)
|
|
envpw = ""
|
|
} else {
|
|
Debug(nil, "Using RCLONE_CONFIG_PASS password.")
|
|
}
|
|
}
|
|
if len(configKey) == 0 {
|
|
if !*AskPassword {
|
|
return nil, fmt.Errorf("Unable to decrypt configuration and not allowed to ask for password. Set RCLONE_CONFIG_PASS to your configuration password.")
|
|
}
|
|
getPassword("Enter configuration password:")
|
|
}
|
|
|
|
// Nonce is first 24 bytes of the ciphertext
|
|
var nonce [24]byte
|
|
copy(nonce[:], box[:24])
|
|
var key [32]byte
|
|
copy(key[:], configKey[:32])
|
|
|
|
// Attempt to decrypt
|
|
var ok bool
|
|
out, ok = secretbox.Open(nil, box[24:], &nonce, &key)
|
|
if ok {
|
|
break
|
|
}
|
|
|
|
// Retry
|
|
log.Println("Couldn't decrypt configuration, most likely wrong password.")
|
|
configKey = nil
|
|
envpw = ""
|
|
}
|
|
return goconfig.LoadFromReader(bytes.NewBuffer(out))
|
|
}
|
|
|
|
// getPassword will query the user for a password the
|
|
// first time it is required.
|
|
func getPassword(q string) {
|
|
if len(configKey) != 0 {
|
|
return
|
|
}
|
|
for {
|
|
fmt.Println(q)
|
|
fmt.Print("password:")
|
|
err := setPassword(ReadPassword())
|
|
if err == nil {
|
|
return
|
|
}
|
|
fmt.Println("Error:", err)
|
|
}
|
|
}
|
|
|
|
// setPassword will set the configKey to the hash of
|
|
// the password. If the length of the password is
|
|
// zero after trimming+normalization, an error is returned.
|
|
func setPassword(password string) error {
|
|
if !utf8.ValidString(password) {
|
|
return fmt.Errorf("Password contains invalid utf8 characters")
|
|
}
|
|
// Remove leading+trailing whitespace
|
|
password = strings.TrimSpace(password)
|
|
|
|
// Normalize to reduce weird variations.
|
|
password = norm.NFKC.String(password)
|
|
if len(password) == 0 {
|
|
return fmt.Errorf("No characters in password")
|
|
}
|
|
// Create SHA256 has of the password
|
|
sha := sha256.New()
|
|
_, err := sha.Write([]byte("[" + password + "][rclone-config]"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
configKey = sha.Sum(nil)
|
|
return nil
|
|
}
|
|
|
|
// SaveConfig saves configuration file.
|
|
// if configKey has been set, the file will be encrypted.
|
|
func SaveConfig() {
|
|
if len(configKey) == 0 {
|
|
err := goconfig.SaveConfigFile(ConfigFile, ConfigPath)
|
|
if err != nil {
|
|
log.Fatalf("Failed to save config file: %v", err)
|
|
}
|
|
err = os.Chmod(ConfigPath, 0600)
|
|
if err != nil {
|
|
log.Printf("Failed to set permissions on config file: %v", err)
|
|
}
|
|
return
|
|
}
|
|
var buf bytes.Buffer
|
|
err := goconfig.SaveConfigData(ConfigFile, &buf)
|
|
if err != nil {
|
|
log.Fatalf("Failed to save config file: %v", err)
|
|
}
|
|
|
|
f, err := os.Create(ConfigPath)
|
|
if err != nil {
|
|
log.Fatalf("Failed to save config file: %v", err)
|
|
}
|
|
|
|
fmt.Fprintln(f, "# Encrypted rclone configuration File")
|
|
fmt.Fprintln(f, "")
|
|
fmt.Fprintln(f, "RCLONE_ENCRYPT_V0:")
|
|
|
|
// Generate new nonce and write it to the start of the ciphertext
|
|
var nonce [24]byte
|
|
n, _ := rand.Read(nonce[:])
|
|
if n != 24 {
|
|
log.Fatalf("nonce short read: %d", n)
|
|
}
|
|
enc := base64.NewEncoder(base64.StdEncoding, f)
|
|
_, err = enc.Write(nonce[:])
|
|
if err != nil {
|
|
log.Fatalf("Failed to write config file: %v", err)
|
|
}
|
|
|
|
var key [32]byte
|
|
copy(key[:], configKey[:32])
|
|
|
|
b := secretbox.Seal(nil, buf.Bytes(), &nonce, &key)
|
|
_, err = enc.Write(b)
|
|
if err != nil {
|
|
log.Fatalf("Failed to write config file: %v", err)
|
|
}
|
|
_ = enc.Close()
|
|
err = f.Close()
|
|
if err != nil {
|
|
log.Fatalf("Failed to close config file: %v", err)
|
|
}
|
|
|
|
err = os.Chmod(ConfigPath, 0600)
|
|
if err != nil {
|
|
log.Printf("Failed to set permissions on config file: %v", err)
|
|
}
|
|
}
|
|
|
|
// ShowRemotes shows an overview of the config file
|
|
func ShowRemotes() {
|
|
remotes := ConfigFile.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, ConfigFile.MustValue(remote, "type"))
|
|
}
|
|
}
|
|
|
|
// ChooseRemote chooses a remote name
|
|
func ChooseRemote() string {
|
|
remotes := ConfigFile.GetSectionList()
|
|
sort.Strings(remotes)
|
|
return Choose("remote", remotes, nil, false)
|
|
}
|
|
|
|
// ReadLine reads some input
|
|
func ReadLine() 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)
|
|
}
|
|
|
|
// Command - choose one
|
|
func Command(commands []string) byte {
|
|
opts := []string{}
|
|
for _, text := range commands {
|
|
fmt.Printf("%c) %s\n", text[0], text[1:])
|
|
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) != 1 {
|
|
continue
|
|
}
|
|
i := strings.Index(optString, string(result[0]))
|
|
if i >= 0 {
|
|
return result[0]
|
|
}
|
|
}
|
|
}
|
|
|
|
// Confirm asks the user for Yes or No and returns true or false
|
|
func Confirm() bool {
|
|
return Command([]string{"yYes", "nNo"}) == '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 {
|
|
fmt.Printf("Choose a number from below")
|
|
if newOk {
|
|
fmt.Printf(", or type in your own value")
|
|
}
|
|
fmt.Println()
|
|
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
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
for {
|
|
fmt.Printf("%s> ", what)
|
|
result := ReadLine()
|
|
i, err := strconv.Atoi(result)
|
|
if err != nil {
|
|
if newOk {
|
|
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)
|
|
for _, key := range ConfigFile.GetKeyList(name) {
|
|
fmt.Printf("%s = %s\n", key, ConfigFile.MustValue(name, key))
|
|
}
|
|
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 := Command([]string{"yYes this is OK", "eEdit this remote", "dDelete this remote"}); i {
|
|
case 'y':
|
|
return true
|
|
case 'e':
|
|
return false
|
|
case 'd':
|
|
ConfigFile.DeleteSection(name)
|
|
return true
|
|
default:
|
|
log.Printf("Bad choice %d", i)
|
|
}
|
|
return false
|
|
}
|
|
|
|
// RemoteConfig runs the config helper for the remote if needed
|
|
func RemoteConfig(name string) {
|
|
fmt.Printf("Remote config\n")
|
|
fsName := ConfigFile.MustValue(name, "type")
|
|
if fsName == "" {
|
|
log.Fatalf("Couldn't find type of fs for %q", name)
|
|
}
|
|
f, err := Find(fsName)
|
|
if err != nil {
|
|
log.Fatalf("Didn't find filing system: %v", err)
|
|
}
|
|
if f.Config != nil {
|
|
f.Config(name)
|
|
}
|
|
}
|
|
|
|
// ChooseOption asks the user to choose an option
|
|
func ChooseOption(o *Option) string {
|
|
fmt.Println(o.Help)
|
|
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)
|
|
}
|
|
return Choose(o.Name, values, help, true)
|
|
}
|
|
fmt.Printf("%s> ", o.Name)
|
|
return ReadLine()
|
|
}
|
|
|
|
// fsOption returns an Option describing the possible remotes
|
|
func fsOption() *Option {
|
|
o := &Option{
|
|
Name: "Storage",
|
|
Help: "Type of storage to configure.",
|
|
}
|
|
for _, item := range fsRegistry {
|
|
example := OptionExample{
|
|
Value: item.Name,
|
|
Help: item.Description,
|
|
}
|
|
o.Examples = append(o.Examples, example)
|
|
}
|
|
o.Examples.Sort()
|
|
return o
|
|
}
|
|
|
|
// NewRemote make a new remote from its name
|
|
func NewRemote(name string) {
|
|
newType := ChooseOption(fsOption())
|
|
ConfigFile.SetValue(name, "type", newType)
|
|
fs, err := Find(newType)
|
|
if err != nil {
|
|
log.Fatalf("Failed to find fs: %v", err)
|
|
}
|
|
for _, option := range fs.Options {
|
|
ConfigFile.SetValue(name, option.Name, ChooseOption(&option))
|
|
}
|
|
RemoteConfig(name)
|
|
if OkRemote(name) {
|
|
SaveConfig()
|
|
return
|
|
}
|
|
EditRemote(name)
|
|
}
|
|
|
|
// EditRemote gets the user to edit a remote
|
|
func EditRemote(name string) {
|
|
ShowRemote(name)
|
|
fmt.Printf("Edit remote\n")
|
|
for {
|
|
for _, key := range ConfigFile.GetKeyList(name) {
|
|
value := ConfigFile.MustValue(name, key)
|
|
fmt.Printf("Press enter to accept current value, or type in a new one\n")
|
|
fmt.Printf("%s = %s>", key, value)
|
|
newValue := ReadLine()
|
|
if newValue != "" {
|
|
ConfigFile.SetValue(name, key, newValue)
|
|
}
|
|
}
|
|
RemoteConfig(name)
|
|
if OkRemote(name) {
|
|
break
|
|
}
|
|
}
|
|
SaveConfig()
|
|
}
|
|
|
|
// DeleteRemote gets the user to delete a remote
|
|
func DeleteRemote(name string) {
|
|
ConfigFile.DeleteSection(name)
|
|
SaveConfig()
|
|
}
|
|
|
|
// EditConfig edits the config file interactively
|
|
func EditConfig() {
|
|
for {
|
|
haveRemotes := len(ConfigFile.GetSectionList()) != 0
|
|
what := []string{"eEdit existing remote", "nNew remote", "dDelete 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")
|
|
what = append(what[1:2], what[3:]...)
|
|
}
|
|
switch i := Command(what); i {
|
|
case 'e':
|
|
name := ChooseRemote()
|
|
EditRemote(name)
|
|
case 'n':
|
|
nameLoop:
|
|
for {
|
|
fmt.Printf("name> ")
|
|
name := ReadLine()
|
|
parts := matcher.FindStringSubmatch(name + ":")
|
|
switch {
|
|
case name == "":
|
|
fmt.Printf("Can't use empty name\n")
|
|
case isDriveLetter(name):
|
|
fmt.Printf("Can't use %q as it can be confused a drive letter\n", name)
|
|
case len(parts) != 3 || parts[2] != "":
|
|
fmt.Printf("Can't use %q as it has invalid characters in it %v\n", name, parts)
|
|
default:
|
|
NewRemote(name)
|
|
break nameLoop
|
|
}
|
|
}
|
|
case 'd':
|
|
name := ChooseRemote()
|
|
DeleteRemote(name)
|
|
case 's':
|
|
SetPassword()
|
|
case 'q':
|
|
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':
|
|
changePassword()
|
|
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':
|
|
changePassword()
|
|
SaveConfig()
|
|
fmt.Println("Password set")
|
|
continue
|
|
case 'q':
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// changePassword will query the user twice
|
|
// for a password. If the same password is entered
|
|
// twice the key is updated.
|
|
func changePassword() {
|
|
for {
|
|
configKey = nil
|
|
getPassword("Enter NEW configuration password:")
|
|
a := configKey
|
|
// re-enter password
|
|
configKey = nil
|
|
getPassword("Confirm NEW password:")
|
|
b := configKey
|
|
if bytes.Equal(a, b) {
|
|
return
|
|
}
|
|
fmt.Println("Passwords does not match!")
|
|
}
|
|
}
|
|
|
|
// 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(args []string) {
|
|
switch len(args) {
|
|
case 1, 3:
|
|
default:
|
|
log.Fatalf("Invalid number of arguments: %d", len(args))
|
|
}
|
|
newType := args[0]
|
|
fs, err := Find(newType)
|
|
if err != nil {
|
|
log.Fatalf("Failed to find fs: %v", err)
|
|
}
|
|
|
|
if fs.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 want fully automatic configuration.
|
|
ConfigFile.SetValue(name, ConfigAutomatic, "yes")
|
|
if len(args) == 3 {
|
|
ConfigFile.SetValue(name, ConfigClientID, args[1])
|
|
ConfigFile.SetValue(name, ConfigClientSecret, args[2])
|
|
}
|
|
fs.Config(name)
|
|
}
|