mirror of
https://github.com/rclone/rclone.git
synced 2026-01-21 20:03:22 +00:00
Compare commits
86 Commits
v1.55-stab
...
fix-log-fa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
661fa5786d | ||
|
|
2fe4fe2766 | ||
|
|
40ac32f2a6 | ||
|
|
f4068d406b | ||
|
|
7511b6f4f1 | ||
|
|
e618ea83dd | ||
|
|
34dc257c55 | ||
|
|
4cacf5d30c | ||
|
|
0537791d14 | ||
|
|
4b1d28550a | ||
|
|
d27c35ee4a | ||
|
|
ffec0d4f03 | ||
|
|
89daa9efd1 | ||
|
|
ee502a757f | ||
|
|
386acaa110 | ||
|
|
efdee3a5fe | ||
|
|
5d85e6bc9c | ||
|
|
4a9469a3dc | ||
|
|
f8884a7200 | ||
|
|
2a40f00077 | ||
|
|
9799fdbae2 | ||
|
|
492504a601 | ||
|
|
0c03a7fead | ||
|
|
7afb4487ef | ||
|
|
b9d0ed4f5c | ||
|
|
baa4c039a0 | ||
|
|
31a8211afa | ||
|
|
3544e09e95 | ||
|
|
b456be4303 | ||
|
|
3e96752079 | ||
|
|
4a5cbf2a19 | ||
|
|
dcd4edc9f5 | ||
|
|
7f5e347d94 | ||
|
|
040677ab5b | ||
|
|
6366d3dfc5 | ||
|
|
60d376c323 | ||
|
|
7b1ca716bf | ||
|
|
d8711cf7f9 | ||
|
|
cd69f9e6e8 | ||
|
|
a737ff21af | ||
|
|
ad9aa693a3 | ||
|
|
964c3e0732 | ||
|
|
a46a3c0811 | ||
|
|
60dcafe04d | ||
|
|
813bf029d4 | ||
|
|
f2d3264054 | ||
|
|
23a0d4a1e6 | ||
|
|
b96ebfc40b | ||
|
|
3fe2aaf96c | ||
|
|
c163e6b250 | ||
|
|
c1492cfa28 | ||
|
|
38a8071a58 | ||
|
|
8c68a76a4a | ||
|
|
e7b736f8ca | ||
|
|
cb30a8c80e | ||
|
|
629a3eeca2 | ||
|
|
f52ae75a51 | ||
|
|
9d5c5bf7ab | ||
|
|
53573b4a09 | ||
|
|
3622e064f5 | ||
|
|
6d28ea7ab5 | ||
|
|
b9fd02039b | ||
|
|
1a41c930f3 | ||
|
|
ddb7eb6e0a | ||
|
|
c114695a66 | ||
|
|
fcba51557f | ||
|
|
9393225a1d | ||
|
|
3d3ff61f74 | ||
|
|
d98f192425 | ||
|
|
54771e4402 | ||
|
|
dc286529bc | ||
|
|
7dc7c021db | ||
|
|
fe1aa13069 | ||
|
|
5fa8e7d957 | ||
|
|
9db7c51eaa | ||
|
|
3859fe2f52 | ||
|
|
0caf417779 | ||
|
|
9eab258ffb | ||
|
|
7df57cd625 | ||
|
|
1fd9b483c8 | ||
|
|
93353c431b | ||
|
|
886dfd23e2 | ||
|
|
116a8021bb | ||
|
|
9e2fbe0f1a | ||
|
|
6d65d116df | ||
|
|
edaeb51ea9 |
33
.github/ISSUE_TEMPLATE/Bug.md
vendored
33
.github/ISSUE_TEMPLATE/Bug.md
vendored
@@ -5,19 +5,31 @@ about: Report a problem with rclone
|
||||
|
||||
<!--
|
||||
|
||||
Welcome :-) We understand you are having a problem with rclone; we want to help you with that!
|
||||
We understand you are having a problem with rclone; we want to help you with that!
|
||||
|
||||
If you've just got a question or aren't sure if you've found a bug then please use the rclone forum:
|
||||
**STOP and READ**
|
||||
**YOUR POST WILL BE REMOVED IF IT IS LOW QUALITY**:
|
||||
Please show the effort you've put in to solving the problem and please be specific.
|
||||
People are volunteering their time to help! Low effort posts are not likely to get good answers!
|
||||
|
||||
If you think you might have found a bug, try to replicate it with the latest beta (or stable).
|
||||
The update instructions are available at https://rclone.org/commands/rclone_selfupdate/
|
||||
|
||||
If you can still replicate it or just got a question then please use the rclone forum:
|
||||
|
||||
https://forum.rclone.org/
|
||||
|
||||
instead of filing an issue for a quick response.
|
||||
for a quick response instead of filing an issue on this repo.
|
||||
|
||||
If you think you might have found a bug, please can you try to replicate it with the latest beta?
|
||||
If nothing else helps, then please fill in the info below which helps us help you.
|
||||
|
||||
https://beta.rclone.org/
|
||||
|
||||
If you can still replicate it with the latest beta, then please fill in the info below which makes our lives much easier. A log with -vv will make our day :-)
|
||||
**DO NOT REDACT** any information except passwords/keys/personal info.
|
||||
|
||||
You should use 3 backticks to begin and end your paste to make it readable.
|
||||
|
||||
Make sure to include a log obtained with '-vv'.
|
||||
|
||||
You can also use '-vv --log-file bug.log' and a service such as https://pastebin.com or https://gist.github.com/
|
||||
|
||||
Thank you
|
||||
|
||||
@@ -25,6 +37,11 @@ The Rclone Developers
|
||||
|
||||
-->
|
||||
|
||||
|
||||
#### The associated forum post URL from `https://forum.rclone.org`
|
||||
|
||||
|
||||
|
||||
#### What is the problem you are having with rclone?
|
||||
|
||||
|
||||
@@ -37,7 +54,7 @@ The Rclone Developers
|
||||
|
||||
|
||||
|
||||
#### Which cloud storage system are you using? (e.g. Google Drive)
|
||||
#### Which cloud storage system are you using? (e.g. Google Drive)
|
||||
|
||||
|
||||
|
||||
|
||||
16
.github/ISSUE_TEMPLATE/Feature.md
vendored
16
.github/ISSUE_TEMPLATE/Feature.md
vendored
@@ -7,12 +7,16 @@ about: Suggest a new feature or enhancement for rclone
|
||||
|
||||
Welcome :-)
|
||||
|
||||
So you've got an idea to improve rclone? We love that! You'll be glad to hear we've incorporated hundreds of ideas from contributors already.
|
||||
So you've got an idea to improve rclone? We love that!
|
||||
You'll be glad to hear we've incorporated hundreds of ideas from contributors already.
|
||||
|
||||
Here is a checklist of things to do:
|
||||
Probably the latest beta (or stable) release has your feature, so try to update your rclone.
|
||||
The update instructions are available at https://rclone.org/commands/rclone_selfupdate/
|
||||
|
||||
1. Please search the old issues first for your idea and +1 or comment on an existing issue if possible.
|
||||
2. Discuss on the forum first: https://forum.rclone.org/
|
||||
If it still isn't there, here is a checklist of things to do:
|
||||
|
||||
1. Search the old issues for your idea and +1 or comment on an existing issue if possible.
|
||||
2. Discuss on the forum: https://forum.rclone.org/
|
||||
3. Make a feature request issue (this is the right place!).
|
||||
4. Be prepared to get involved making the feature :-)
|
||||
|
||||
@@ -23,6 +27,10 @@ The Rclone Developers
|
||||
-->
|
||||
|
||||
|
||||
#### The associated forum post URL from `https://forum.rclone.org`
|
||||
|
||||
|
||||
|
||||
#### What is your current rclone version (output from `rclone version`)?
|
||||
|
||||
|
||||
|
||||
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -221,6 +221,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# Upgrade together with NDK version
|
||||
- name: Set up Go 1.14
|
||||
|
||||
@@ -33,10 +33,11 @@ page](https://github.com/rclone/rclone).
|
||||
|
||||
Now in your terminal
|
||||
|
||||
go get -u github.com/rclone/rclone
|
||||
cd $GOPATH/src/github.com/rclone/rclone
|
||||
git clone https://github.com/rclone/rclone.git
|
||||
cd rclone
|
||||
git remote rename origin upstream
|
||||
git remote add origin git@github.com:YOURUSER/rclone.git
|
||||
go build
|
||||
|
||||
Make a branch to add your new feature
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ var (
|
||||
)
|
||||
|
||||
func prepare(t *testing.T, root string) {
|
||||
configfile.LoadConfig(context.Background())
|
||||
require.NoError(t, configfile.LoadConfig(context.Background()))
|
||||
|
||||
// Configure the remote
|
||||
config.FileSet(remoteName, "type", "alias")
|
||||
|
||||
@@ -41,6 +41,7 @@ import (
|
||||
_ "github.com/rclone/rclone/backend/swift"
|
||||
_ "github.com/rclone/rclone/backend/tardigrade"
|
||||
_ "github.com/rclone/rclone/backend/union"
|
||||
_ "github.com/rclone/rclone/backend/uptobox"
|
||||
_ "github.com/rclone/rclone/backend/webdav"
|
||||
_ "github.com/rclone/rclone/backend/yandex"
|
||||
_ "github.com/rclone/rclone/backend/zoho"
|
||||
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
@@ -70,11 +69,12 @@ func init() {
|
||||
Prefix: "acd",
|
||||
Description: "Amazon Drive",
|
||||
NewFs: NewFs,
|
||||
Config: func(ctx context.Context, name string, m configmap.Mapper) {
|
||||
Config: func(ctx context.Context, name string, m configmap.Mapper) error {
|
||||
err := oauthutil.Config(ctx, "amazon cloud drive", name, m, acdConfig, nil)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to configure token: %v", err)
|
||||
return errors.Wrap(err, "failed to configure token")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Options: append(oauthutil.SharedOptions, []fs.Option{{
|
||||
Name: "checkpoint",
|
||||
|
||||
@@ -2,12 +2,11 @@ package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rclone/rclone/fs/fserrors"
|
||||
"github.com/rclone/rclone/lib/version"
|
||||
)
|
||||
|
||||
// Error describes a B2 error response
|
||||
@@ -63,16 +62,17 @@ func (t *Timestamp) UnmarshalJSON(data []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
const versionFormat = "-v2006-01-02-150405.000"
|
||||
// HasVersion returns true if it looks like the passed filename has a timestamp on it.
|
||||
//
|
||||
// Note that the passed filename's timestamp may still be invalid even if this
|
||||
// function returns true.
|
||||
func HasVersion(remote string) bool {
|
||||
return version.Match(remote)
|
||||
}
|
||||
|
||||
// AddVersion adds the timestamp as a version string into the filename passed in.
|
||||
func (t Timestamp) AddVersion(remote string) string {
|
||||
ext := path.Ext(remote)
|
||||
base := remote[:len(remote)-len(ext)]
|
||||
s := time.Time(t).Format(versionFormat)
|
||||
// Replace the '.' with a '-'
|
||||
s = strings.Replace(s, ".", "-", -1)
|
||||
return base + s + ext
|
||||
return version.Add(remote, time.Time(t))
|
||||
}
|
||||
|
||||
// RemoveVersion removes the timestamp from a filename as a version string.
|
||||
@@ -80,24 +80,9 @@ func (t Timestamp) AddVersion(remote string) string {
|
||||
// It returns the new file name and a timestamp, or the old filename
|
||||
// and a zero timestamp.
|
||||
func RemoveVersion(remote string) (t Timestamp, newRemote string) {
|
||||
newRemote = remote
|
||||
ext := path.Ext(remote)
|
||||
base := remote[:len(remote)-len(ext)]
|
||||
if len(base) < len(versionFormat) {
|
||||
return
|
||||
}
|
||||
versionStart := len(base) - len(versionFormat)
|
||||
// Check it ends in -xxx
|
||||
if base[len(base)-4] != '-' {
|
||||
return
|
||||
}
|
||||
// Replace with .xxx for parsing
|
||||
base = base[:len(base)-4] + "." + base[len(base)-3:]
|
||||
newT, err := time.Parse(versionFormat, base[versionStart:])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return Timestamp(newT), base[:versionStart] + ext
|
||||
time, newRemote := version.Remove(remote)
|
||||
t = Timestamp(time)
|
||||
return
|
||||
}
|
||||
|
||||
// IsZero returns true if the timestamp is uninitialized
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
var (
|
||||
emptyT api.Timestamp
|
||||
t0 = api.Timestamp(fstest.Time("1970-01-01T01:01:01.123456789Z"))
|
||||
t0r = api.Timestamp(fstest.Time("1970-01-01T01:01:01.123000000Z"))
|
||||
t1 = api.Timestamp(fstest.Time("2001-02-03T04:05:06.123000000Z"))
|
||||
)
|
||||
|
||||
@@ -36,40 +35,6 @@ func TestTimestampUnmarshalJSON(t *testing.T) {
|
||||
assert.Equal(t, (time.Time)(t1), (time.Time)(tActual))
|
||||
}
|
||||
|
||||
func TestTimestampAddVersion(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
t api.Timestamp
|
||||
in string
|
||||
expected string
|
||||
}{
|
||||
{t0, "potato.txt", "potato-v1970-01-01-010101-123.txt"},
|
||||
{t1, "potato", "potato-v2001-02-03-040506-123"},
|
||||
{t1, "", "-v2001-02-03-040506-123"},
|
||||
} {
|
||||
actual := test.t.AddVersion(test.in)
|
||||
assert.Equal(t, test.expected, actual, test.in)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTimestampRemoveVersion(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
in string
|
||||
expectedT api.Timestamp
|
||||
expectedRemote string
|
||||
}{
|
||||
{"potato.txt", emptyT, "potato.txt"},
|
||||
{"potato-v1970-01-01-010101-123.txt", t0r, "potato.txt"},
|
||||
{"potato-v2001-02-03-040506-123", t1, "potato"},
|
||||
{"-v2001-02-03-040506-123", t1, ""},
|
||||
{"potato-v2A01-02-03-040506-123", emptyT, "potato-v2A01-02-03-040506-123"},
|
||||
{"potato-v2001-02-03-040506=123", emptyT, "potato-v2001-02-03-040506=123"},
|
||||
} {
|
||||
actualT, actualRemote := api.RemoveVersion(test.in)
|
||||
assert.Equal(t, test.expectedT, actualT, test.in)
|
||||
assert.Equal(t, test.expectedRemote, actualRemote, test.in)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTimestampIsZero(t *testing.T) {
|
||||
assert.True(t, emptyT.IsZero())
|
||||
assert.False(t, t0.IsZero())
|
||||
|
||||
@@ -1353,7 +1353,7 @@ func (f *Fs) getDownloadAuthorization(ctx context.Context, bucket, remote string
|
||||
}
|
||||
var request = api.GetDownloadAuthorizationRequest{
|
||||
BucketID: bucketID,
|
||||
FileNamePrefix: f.opt.Enc.FromStandardPath(path.Join(f.root, remote)),
|
||||
FileNamePrefix: f.opt.Enc.FromStandardPath(path.Join(f.rootDirectory, remote)),
|
||||
ValidDurationInSeconds: validDurationInSeconds,
|
||||
}
|
||||
var response api.GetDownloadAuthorizationResponse
|
||||
|
||||
@@ -36,13 +36,13 @@ func (t *Time) UnmarshalJSON(data []byte) error {
|
||||
|
||||
// Error is returned from box when things go wrong
|
||||
type Error struct {
|
||||
Type string `json:"type"`
|
||||
Status int `json:"status"`
|
||||
Code string `json:"code"`
|
||||
ContextInfo json.RawMessage
|
||||
HelpURL string `json:"help_url"`
|
||||
Message string `json:"message"`
|
||||
RequestID string `json:"request_id"`
|
||||
Type string `json:"type"`
|
||||
Status int `json:"status"`
|
||||
Code string `json:"code"`
|
||||
ContextInfo json.RawMessage `json:"context_info"`
|
||||
HelpURL string `json:"help_url"`
|
||||
Message string `json:"message"`
|
||||
RequestID string `json:"request_id"`
|
||||
}
|
||||
|
||||
// Error returns a string for the error and satisfies the error interface
|
||||
@@ -132,6 +132,38 @@ type UploadFile struct {
|
||||
ContentModifiedAt Time `json:"content_modified_at"`
|
||||
}
|
||||
|
||||
// PreUploadCheck is the request for upload preflight check
|
||||
type PreUploadCheck struct {
|
||||
Name string `json:"name"`
|
||||
Parent Parent `json:"parent"`
|
||||
Size *int64 `json:"size,omitempty"`
|
||||
}
|
||||
|
||||
// PreUploadCheckResponse is the response from upload preflight check
|
||||
// if successful
|
||||
type PreUploadCheckResponse struct {
|
||||
UploadToken string `json:"upload_token"`
|
||||
UploadURL string `json:"upload_url"`
|
||||
}
|
||||
|
||||
// PreUploadCheckConflict is returned in the ContextInfo error field
|
||||
// from PreUploadCheck when the error code is "item_name_in_use"
|
||||
type PreUploadCheckConflict struct {
|
||||
Conflicts struct {
|
||||
Type string `json:"type"`
|
||||
ID string `json:"id"`
|
||||
FileVersion struct {
|
||||
Type string `json:"type"`
|
||||
ID string `json:"id"`
|
||||
Sha1 string `json:"sha1"`
|
||||
} `json:"file_version"`
|
||||
SequenceID string `json:"sequence_id"`
|
||||
Etag string `json:"etag"`
|
||||
Sha1 string `json:"sha1"`
|
||||
Name string `json:"name"`
|
||||
} `json:"conflicts"`
|
||||
}
|
||||
|
||||
// UpdateFileModTime is used in Update File Info
|
||||
type UpdateFileModTime struct {
|
||||
ContentModifiedAt Time `json:"content_modified_at"`
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
@@ -84,7 +83,7 @@ func init() {
|
||||
Name: "box",
|
||||
Description: "Box",
|
||||
NewFs: NewFs,
|
||||
Config: func(ctx context.Context, name string, m configmap.Mapper) {
|
||||
Config: func(ctx context.Context, name string, m configmap.Mapper) error {
|
||||
jsonFile, ok := m.Get("box_config_file")
|
||||
boxSubType, boxSubTypeOk := m.Get("box_sub_type")
|
||||
boxAccessToken, boxAccessTokenOk := m.Get("access_token")
|
||||
@@ -93,15 +92,16 @@ func init() {
|
||||
if ok && boxSubTypeOk && jsonFile != "" && boxSubType != "" {
|
||||
err = refreshJWTToken(ctx, jsonFile, boxSubType, name, m)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to configure token with jwt authentication: %v", err)
|
||||
return errors.Wrap(err, "failed to configure token with jwt authentication")
|
||||
}
|
||||
// Else, if not using an access token, use oauth2
|
||||
} else if boxAccessToken == "" || !boxAccessTokenOk {
|
||||
err = oauthutil.Config(ctx, "box", name, m, oauthConfig, nil)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to configure token with oauth authentication: %v", err)
|
||||
return errors.Wrap(err, "failed to configure token with oauth authentication")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Options: append(oauthutil.SharedOptions, []fs.Option{{
|
||||
Name: "root_folder_id",
|
||||
@@ -157,15 +157,15 @@ func refreshJWTToken(ctx context.Context, jsonFile string, boxSubType string, na
|
||||
jsonFile = env.ShellExpand(jsonFile)
|
||||
boxConfig, err := getBoxConfig(jsonFile)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to configure token: %v", err)
|
||||
return errors.Wrap(err, "get box config")
|
||||
}
|
||||
privateKey, err := getDecryptedPrivateKey(boxConfig)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to configure token: %v", err)
|
||||
return errors.Wrap(err, "get decrypted private key")
|
||||
}
|
||||
claims, err := getClaims(boxConfig, boxSubType)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to configure token: %v", err)
|
||||
return errors.Wrap(err, "get claims")
|
||||
}
|
||||
signingHeaders := getSigningHeaders(boxConfig)
|
||||
queryParams := getQueryParams(boxConfig)
|
||||
@@ -686,22 +686,80 @@ func (f *Fs) createObject(ctx context.Context, remote string, modTime time.Time,
|
||||
return o, leaf, directoryID, nil
|
||||
}
|
||||
|
||||
// preUploadCheck checks to see if a file can be uploaded
|
||||
//
|
||||
// It returns "", nil if the file is good to go
|
||||
// It returns "ID", nil if the file must be updated
|
||||
func (f *Fs) preUploadCheck(ctx context.Context, leaf, directoryID string, size int64) (ID string, err error) {
|
||||
check := api.PreUploadCheck{
|
||||
Name: f.opt.Enc.FromStandardName(leaf),
|
||||
Parent: api.Parent{
|
||||
ID: directoryID,
|
||||
},
|
||||
}
|
||||
if size >= 0 {
|
||||
check.Size = &size
|
||||
}
|
||||
opts := rest.Opts{
|
||||
Method: "OPTIONS",
|
||||
Path: "/files/content/",
|
||||
}
|
||||
var result api.PreUploadCheckResponse
|
||||
var resp *http.Response
|
||||
err = f.pacer.Call(func() (bool, error) {
|
||||
resp, err = f.srv.CallJSON(ctx, &opts, &check, &result)
|
||||
return shouldRetry(ctx, resp, err)
|
||||
})
|
||||
if err != nil {
|
||||
if apiErr, ok := err.(*api.Error); ok && apiErr.Code == "item_name_in_use" {
|
||||
var conflict api.PreUploadCheckConflict
|
||||
err = json.Unmarshal(apiErr.ContextInfo, &conflict)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "pre-upload check: JSON decode failed")
|
||||
}
|
||||
if conflict.Conflicts.Type != api.ItemTypeFile {
|
||||
return "", errors.Wrap(err, "pre-upload check: can't overwrite non file with file")
|
||||
}
|
||||
return conflict.Conflicts.ID, nil
|
||||
}
|
||||
return "", errors.Wrap(err, "pre-upload check")
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// Put the object
|
||||
//
|
||||
// Copy the reader in to the new object which is returned
|
||||
//
|
||||
// The new object may have been created if an error is returned
|
||||
func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
|
||||
existingObj, err := f.newObjectWithInfo(ctx, src.Remote(), nil)
|
||||
switch err {
|
||||
case nil:
|
||||
return existingObj, existingObj.Update(ctx, in, src, options...)
|
||||
case fs.ErrorObjectNotFound:
|
||||
// Not found so create it
|
||||
return f.PutUnchecked(ctx, in, src)
|
||||
default:
|
||||
// If directory doesn't exist, file doesn't exist so can upload
|
||||
remote := src.Remote()
|
||||
leaf, directoryID, err := f.dirCache.FindPath(ctx, remote, false)
|
||||
if err != nil {
|
||||
if err == fs.ErrorDirNotFound {
|
||||
return f.PutUnchecked(ctx, in, src, options...)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Preflight check the upload, which returns the ID if the
|
||||
// object already exists
|
||||
ID, err := f.preUploadCheck(ctx, leaf, directoryID, src.Size())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ID == "" {
|
||||
return f.PutUnchecked(ctx, in, src, options...)
|
||||
}
|
||||
|
||||
// If object exists then create a skeleton one with just id
|
||||
o := &Object{
|
||||
fs: f,
|
||||
remote: remote,
|
||||
id: ID,
|
||||
}
|
||||
return o, o.Update(ctx, in, src, options...)
|
||||
}
|
||||
|
||||
// PutStream uploads to the remote path with the modTime given of indeterminate size
|
||||
|
||||
2
backend/cache/cache_internal_test.go
vendored
2
backend/cache/cache_internal_test.go
vendored
@@ -836,7 +836,7 @@ func newRun() *run {
|
||||
if uploadDir == "" {
|
||||
r.tmpUploadDir, err = ioutil.TempDir("", "rclonecache-tmp")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create temp dir: %v", err)
|
||||
panic(fmt.Sprintf("Failed to create temp dir: %v", err))
|
||||
}
|
||||
} else {
|
||||
r.tmpUploadDir = uploadDir
|
||||
|
||||
@@ -53,7 +53,7 @@ const (
|
||||
Gzip = 2
|
||||
)
|
||||
|
||||
var nameRegexp = regexp.MustCompile("^(.+?)\\.([A-Za-z0-9+_]{11})$")
|
||||
var nameRegexp = regexp.MustCompile("^(.+?)\\.([A-Za-z0-9-_]{11})$")
|
||||
|
||||
// Register with Fs
|
||||
func init() {
|
||||
|
||||
@@ -12,12 +12,14 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rclone/rclone/backend/crypt/pkcs7"
|
||||
"github.com/rclone/rclone/fs"
|
||||
"github.com/rclone/rclone/fs/accounting"
|
||||
"github.com/rclone/rclone/lib/version"
|
||||
"github.com/rfjakob/eme"
|
||||
"golang.org/x/crypto/nacl/secretbox"
|
||||
"golang.org/x/crypto/scrypt"
|
||||
@@ -442,11 +444,32 @@ func (c *Cipher) encryptFileName(in string) string {
|
||||
if !c.dirNameEncrypt && i != (len(segments)-1) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Strip version string so that only the non-versioned part
|
||||
// of the file name gets encrypted/obfuscated
|
||||
hasVersion := false
|
||||
var t time.Time
|
||||
if i == (len(segments)-1) && version.Match(segments[i]) {
|
||||
var s string
|
||||
t, s = version.Remove(segments[i])
|
||||
// version.Remove can fail, in which case it returns segments[i]
|
||||
if s != segments[i] {
|
||||
segments[i] = s
|
||||
hasVersion = true
|
||||
}
|
||||
}
|
||||
|
||||
if c.mode == NameEncryptionStandard {
|
||||
segments[i] = c.encryptSegment(segments[i])
|
||||
} else {
|
||||
segments[i] = c.obfuscateSegment(segments[i])
|
||||
}
|
||||
|
||||
// Add back a version to the encrypted/obfuscated
|
||||
// file name, if we stripped it off earlier
|
||||
if hasVersion {
|
||||
segments[i] = version.Add(segments[i], t)
|
||||
}
|
||||
}
|
||||
return strings.Join(segments, "/")
|
||||
}
|
||||
@@ -477,6 +500,21 @@ func (c *Cipher) decryptFileName(in string) (string, error) {
|
||||
if !c.dirNameEncrypt && i != (len(segments)-1) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Strip version string so that only the non-versioned part
|
||||
// of the file name gets decrypted/deobfuscated
|
||||
hasVersion := false
|
||||
var t time.Time
|
||||
if i == (len(segments)-1) && version.Match(segments[i]) {
|
||||
var s string
|
||||
t, s = version.Remove(segments[i])
|
||||
// version.Remove can fail, in which case it returns segments[i]
|
||||
if s != segments[i] {
|
||||
segments[i] = s
|
||||
hasVersion = true
|
||||
}
|
||||
}
|
||||
|
||||
if c.mode == NameEncryptionStandard {
|
||||
segments[i], err = c.decryptSegment(segments[i])
|
||||
} else {
|
||||
@@ -486,6 +524,12 @@ func (c *Cipher) decryptFileName(in string) (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Add back a version to the decrypted/deobfuscated
|
||||
// file name, if we stripped it off earlier
|
||||
if hasVersion {
|
||||
segments[i] = version.Add(segments[i], t)
|
||||
}
|
||||
}
|
||||
return strings.Join(segments, "/"), nil
|
||||
}
|
||||
@@ -494,10 +538,18 @@ func (c *Cipher) decryptFileName(in string) (string, error) {
|
||||
func (c *Cipher) DecryptFileName(in string) (string, error) {
|
||||
if c.mode == NameEncryptionOff {
|
||||
remainingLength := len(in) - len(encryptedSuffix)
|
||||
if remainingLength > 0 && strings.HasSuffix(in, encryptedSuffix) {
|
||||
return in[:remainingLength], nil
|
||||
if remainingLength == 0 || !strings.HasSuffix(in, encryptedSuffix) {
|
||||
return "", ErrorNotAnEncryptedFile
|
||||
}
|
||||
return "", ErrorNotAnEncryptedFile
|
||||
decrypted := in[:remainingLength]
|
||||
if version.Match(decrypted) {
|
||||
_, unversioned := version.Remove(decrypted)
|
||||
if unversioned == "" {
|
||||
return "", ErrorNotAnEncryptedFile
|
||||
}
|
||||
}
|
||||
// Leave the version string on, if it was there
|
||||
return decrypted, nil
|
||||
}
|
||||
return c.decryptFileName(in)
|
||||
}
|
||||
|
||||
@@ -160,22 +160,29 @@ func TestEncryptFileName(t *testing.T) {
|
||||
assert.Equal(t, "p0e52nreeaj0a5ea7s64m4j72s", c.EncryptFileName("1"))
|
||||
assert.Equal(t, "p0e52nreeaj0a5ea7s64m4j72s/l42g6771hnv3an9cgc8cr2n1ng", c.EncryptFileName("1/12"))
|
||||
assert.Equal(t, "p0e52nreeaj0a5ea7s64m4j72s/l42g6771hnv3an9cgc8cr2n1ng/qgm4avr35m5loi1th53ato71v0", c.EncryptFileName("1/12/123"))
|
||||
assert.Equal(t, "p0e52nreeaj0a5ea7s64m4j72s-v2001-02-03-040506-123", c.EncryptFileName("1-v2001-02-03-040506-123"))
|
||||
assert.Equal(t, "p0e52nreeaj0a5ea7s64m4j72s/l42g6771hnv3an9cgc8cr2n1ng-v2001-02-03-040506-123", c.EncryptFileName("1/12-v2001-02-03-040506-123"))
|
||||
// Standard mode with directory name encryption off
|
||||
c, _ = newCipher(NameEncryptionStandard, "", "", false)
|
||||
assert.Equal(t, "p0e52nreeaj0a5ea7s64m4j72s", c.EncryptFileName("1"))
|
||||
assert.Equal(t, "1/l42g6771hnv3an9cgc8cr2n1ng", c.EncryptFileName("1/12"))
|
||||
assert.Equal(t, "1/12/qgm4avr35m5loi1th53ato71v0", c.EncryptFileName("1/12/123"))
|
||||
assert.Equal(t, "p0e52nreeaj0a5ea7s64m4j72s-v2001-02-03-040506-123", c.EncryptFileName("1-v2001-02-03-040506-123"))
|
||||
assert.Equal(t, "1/l42g6771hnv3an9cgc8cr2n1ng-v2001-02-03-040506-123", c.EncryptFileName("1/12-v2001-02-03-040506-123"))
|
||||
// Now off mode
|
||||
c, _ = newCipher(NameEncryptionOff, "", "", true)
|
||||
assert.Equal(t, "1/12/123.bin", c.EncryptFileName("1/12/123"))
|
||||
// Obfuscation mode
|
||||
c, _ = newCipher(NameEncryptionObfuscated, "", "", true)
|
||||
assert.Equal(t, "49.6/99.23/150.890/53.!!lipps", c.EncryptFileName("1/12/123/!hello"))
|
||||
assert.Equal(t, "49.6/99.23/150.890/53-v2001-02-03-040506-123.!!lipps", c.EncryptFileName("1/12/123/!hello-v2001-02-03-040506-123"))
|
||||
assert.Equal(t, "49.6/99.23/150.890/162.uryyB-v2001-02-03-040506-123.GKG", c.EncryptFileName("1/12/123/hello-v2001-02-03-040506-123.txt"))
|
||||
assert.Equal(t, "161.\u00e4", c.EncryptFileName("\u00a1"))
|
||||
assert.Equal(t, "160.\u03c2", c.EncryptFileName("\u03a0"))
|
||||
// Obfuscation mode with directory name encryption off
|
||||
c, _ = newCipher(NameEncryptionObfuscated, "", "", false)
|
||||
assert.Equal(t, "1/12/123/53.!!lipps", c.EncryptFileName("1/12/123/!hello"))
|
||||
assert.Equal(t, "1/12/123/53-v2001-02-03-040506-123.!!lipps", c.EncryptFileName("1/12/123/!hello-v2001-02-03-040506-123"))
|
||||
assert.Equal(t, "161.\u00e4", c.EncryptFileName("\u00a1"))
|
||||
assert.Equal(t, "160.\u03c2", c.EncryptFileName("\u03a0"))
|
||||
}
|
||||
@@ -194,14 +201,19 @@ func TestDecryptFileName(t *testing.T) {
|
||||
{NameEncryptionStandard, true, "p0e52nreeaj0a5ea7s64m4j72s/l42g6771hnv3an9cgc8cr2n1ng/qgm4avr35m5loi1th53ato71v0", "1/12/123", nil},
|
||||
{NameEncryptionStandard, true, "p0e52nreeaj0a5ea7s64m4j72s/l42g6771hnv3an9cgc8cr2n1/qgm4avr35m5loi1th53ato71v0", "", ErrorNotAMultipleOfBlocksize},
|
||||
{NameEncryptionStandard, false, "1/12/qgm4avr35m5loi1th53ato71v0", "1/12/123", nil},
|
||||
{NameEncryptionStandard, true, "p0e52nreeaj0a5ea7s64m4j72s-v2001-02-03-040506-123", "1-v2001-02-03-040506-123", nil},
|
||||
{NameEncryptionOff, true, "1/12/123.bin", "1/12/123", nil},
|
||||
{NameEncryptionOff, true, "1/12/123.bix", "", ErrorNotAnEncryptedFile},
|
||||
{NameEncryptionOff, true, ".bin", "", ErrorNotAnEncryptedFile},
|
||||
{NameEncryptionOff, true, "1/12/123-v2001-02-03-040506-123.bin", "1/12/123-v2001-02-03-040506-123", nil},
|
||||
{NameEncryptionOff, true, "1/12/123-v1970-01-01-010101-123-v2001-02-03-040506-123.bin", "1/12/123-v1970-01-01-010101-123-v2001-02-03-040506-123", nil},
|
||||
{NameEncryptionOff, true, "1/12/123-v1970-01-01-010101-123-v2001-02-03-040506-123.txt.bin", "1/12/123-v1970-01-01-010101-123-v2001-02-03-040506-123.txt", nil},
|
||||
{NameEncryptionObfuscated, true, "!.hello", "hello", nil},
|
||||
{NameEncryptionObfuscated, true, "hello", "", ErrorNotAnEncryptedFile},
|
||||
{NameEncryptionObfuscated, true, "161.\u00e4", "\u00a1", nil},
|
||||
{NameEncryptionObfuscated, true, "160.\u03c2", "\u03a0", nil},
|
||||
{NameEncryptionObfuscated, false, "1/12/123/53.!!lipps", "1/12/123/!hello", nil},
|
||||
{NameEncryptionObfuscated, false, "1/12/123/53-v2001-02-03-040506-123.!!lipps", "1/12/123/!hello-v2001-02-03-040506-123", nil},
|
||||
} {
|
||||
c, _ := newCipher(test.mode, "", "", test.dirNameEncrypt)
|
||||
actual, actualErr := c.DecryptFileName(test.in)
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"mime"
|
||||
"net/http"
|
||||
"path"
|
||||
@@ -183,13 +182,12 @@ func init() {
|
||||
Description: "Google Drive",
|
||||
NewFs: NewFs,
|
||||
CommandHelp: commandHelp,
|
||||
Config: func(ctx context.Context, name string, m configmap.Mapper) {
|
||||
Config: func(ctx context.Context, name string, m configmap.Mapper) error {
|
||||
// Parse config into Options struct
|
||||
opt := new(Options)
|
||||
err := configstruct.Set(m, opt)
|
||||
if err != nil {
|
||||
fs.Errorf(nil, "Couldn't parse config into struct: %v", err)
|
||||
return
|
||||
return errors.Wrap(err, "couldn't parse config into struct")
|
||||
}
|
||||
|
||||
// Fill in the scopes
|
||||
@@ -199,16 +197,17 @@ func init() {
|
||||
m.Set("root_folder_id", "appDataFolder")
|
||||
}
|
||||
|
||||
if opt.ServiceAccountFile == "" {
|
||||
if opt.ServiceAccountFile == "" && opt.ServiceAccountCredentials == "" {
|
||||
err = oauthutil.Config(ctx, "drive", name, m, driveConfig, nil)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to configure token: %v", err)
|
||||
return errors.Wrap(err, "failed to configure token")
|
||||
}
|
||||
}
|
||||
err = configTeamDrive(ctx, opt, m, name)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to configure Shared Drive: %v", err)
|
||||
return errors.Wrap(err, "failed to configure Shared Drive")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Options: append(driveOAuthOptions(), []fs.Option{{
|
||||
Name: "scope",
|
||||
@@ -522,7 +521,7 @@ If this flag is set then rclone will ignore shortcut files completely.
|
||||
} {
|
||||
for mimeType, extension := range m {
|
||||
if err := mime.AddExtensionType(extension, mimeType); err != nil {
|
||||
log.Fatalf("Failed to register MIME type %q: %v", mimeType, err)
|
||||
fs.Errorf("Failed to register MIME type %q: %v", mimeType, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2959,12 +2958,12 @@ func (f *Fs) makeShortcut(ctx context.Context, srcPath string, dstFs *Fs, dstPat
|
||||
}
|
||||
|
||||
// List all team drives
|
||||
func (f *Fs) listTeamDrives(ctx context.Context) (drives []*drive.TeamDrive, err error) {
|
||||
drives = []*drive.TeamDrive{}
|
||||
listTeamDrives := f.svc.Teamdrives.List().PageSize(100)
|
||||
func (f *Fs) listTeamDrives(ctx context.Context) (drives []*drive.Drive, err error) {
|
||||
drives = []*drive.Drive{}
|
||||
listTeamDrives := f.svc.Drives.List().PageSize(100)
|
||||
var defaultFs Fs // default Fs with default Options
|
||||
for {
|
||||
var teamDrives *drive.TeamDriveList
|
||||
var teamDrives *drive.DriveList
|
||||
err = f.pacer.Call(func() (bool, error) {
|
||||
teamDrives, err = listTeamDrives.Context(ctx).Do()
|
||||
return defaultFs.shouldRetry(ctx, err)
|
||||
@@ -2972,7 +2971,7 @@ func (f *Fs) listTeamDrives(ctx context.Context) (drives []*drive.TeamDrive, err
|
||||
if err != nil {
|
||||
return drives, errors.Wrap(err, "listing Team Drives failed")
|
||||
}
|
||||
drives = append(drives, teamDrives.TeamDrives...)
|
||||
drives = append(drives, teamDrives.Drives...)
|
||||
if teamDrives.NextPageToken == "" {
|
||||
break
|
||||
}
|
||||
@@ -3069,7 +3068,7 @@ func (f *Fs) copyID(ctx context.Context, id, dest string) (err error) {
|
||||
return err
|
||||
}
|
||||
if destLeaf == "" {
|
||||
destLeaf = info.Name
|
||||
destLeaf = path.Base(o.Remote())
|
||||
}
|
||||
if destDir == "" {
|
||||
destDir = "."
|
||||
|
||||
@@ -25,7 +25,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
@@ -99,8 +98,10 @@ var (
|
||||
"files.content.write",
|
||||
"files.content.read",
|
||||
"sharing.write",
|
||||
"account_info.read", // needed for About
|
||||
// "file_requests.write",
|
||||
// "members.read", // needed for impersonate - but causes app to need to be approved by Dropbox Team Admin during the flow
|
||||
// "team_data.member"
|
||||
},
|
||||
// Endpoint: oauth2.Endpoint{
|
||||
// AuthURL: "https://www.dropbox.com/1/oauth2/authorize",
|
||||
@@ -130,8 +131,8 @@ func getOauthConfig(m configmap.Mapper) *oauth2.Config {
|
||||
}
|
||||
// Make a copy of the config
|
||||
config := *dropboxConfig
|
||||
// Make a copy of the scopes with "members.read" appended
|
||||
config.Scopes = append(config.Scopes, "members.read")
|
||||
// Make a copy of the scopes with extra scopes requires appended
|
||||
config.Scopes = append(config.Scopes, "members.read", "team_data.member")
|
||||
return &config
|
||||
}
|
||||
|
||||
@@ -142,7 +143,7 @@ func init() {
|
||||
Name: "dropbox",
|
||||
Description: "Dropbox",
|
||||
NewFs: NewFs,
|
||||
Config: func(ctx context.Context, name string, m configmap.Mapper) {
|
||||
Config: func(ctx context.Context, name string, m configmap.Mapper) error {
|
||||
opt := oauthutil.Options{
|
||||
NoOffline: true,
|
||||
OAuth2Opts: []oauth2.AuthCodeOption{
|
||||
@@ -151,8 +152,9 @@ func init() {
|
||||
}
|
||||
err := oauthutil.Config(ctx, "dropbox", name, m, getOauthConfig(m), &opt)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to configure token: %v", err)
|
||||
return errors.Wrap(err, "failed to configure token")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Options: append(oauthutil.SharedOptions, []fs.Option{{
|
||||
Name: "chunk_size",
|
||||
@@ -1084,13 +1086,30 @@ func (f *Fs) PublicLink(ctx context.Context, remote string, expire fs.Duration,
|
||||
fs.Debugf(f, "attempting to share '%s' (absolute path: %s)", remote, absPath)
|
||||
createArg := sharing.CreateSharedLinkWithSettingsArg{
|
||||
Path: absPath,
|
||||
// FIXME this gives settings_error/not_authorized/.. errors
|
||||
// and the expires setting isn't in the documentation so remove
|
||||
// for now.
|
||||
// Settings: &sharing.SharedLinkSettings{
|
||||
// Expires: time.Now().Add(time.Duration(expire)).UTC().Round(time.Second),
|
||||
// },
|
||||
Settings: &sharing.SharedLinkSettings{
|
||||
RequestedVisibility: &sharing.RequestedVisibility{
|
||||
Tagged: dropbox.Tagged{Tag: sharing.RequestedVisibilityPublic},
|
||||
},
|
||||
Audience: &sharing.LinkAudience{
|
||||
Tagged: dropbox.Tagged{Tag: sharing.LinkAudiencePublic},
|
||||
},
|
||||
Access: &sharing.RequestedLinkAccessLevel{
|
||||
Tagged: dropbox.Tagged{Tag: sharing.RequestedLinkAccessLevelViewer},
|
||||
},
|
||||
},
|
||||
}
|
||||
if expire < fs.DurationOff {
|
||||
expiryTime := time.Now().Add(time.Duration(expire)).UTC().Round(time.Second)
|
||||
createArg.Settings.Expires = expiryTime
|
||||
}
|
||||
// FIXME note we can't set Settings for non enterprise dropbox
|
||||
// because of https://github.com/dropbox/dropbox-sdk-go-unofficial/issues/75
|
||||
// however this only goes wrong when we set Expires, so as a
|
||||
// work-around remove Settings unless expire is set.
|
||||
if expire == fs.DurationOff {
|
||||
createArg.Settings = nil
|
||||
}
|
||||
|
||||
var linkRes sharing.IsSharedLinkMetadata
|
||||
err = f.pacer.Call(func() (bool, error) {
|
||||
linkRes, err = f.sharing.CreateSharedLinkWithSettings(&createArg)
|
||||
@@ -1334,13 +1353,13 @@ func (f *Fs) changeNotifyRunner(ctx context.Context, notifyFunc func(string, fs.
|
||||
switch info := entry.(type) {
|
||||
case *files.FolderMetadata:
|
||||
entryType = fs.EntryDirectory
|
||||
entryPath = strings.TrimLeft(info.PathDisplay, f.slashRootSlash)
|
||||
entryPath = strings.TrimPrefix(info.PathDisplay, f.slashRootSlash)
|
||||
case *files.FileMetadata:
|
||||
entryType = fs.EntryObject
|
||||
entryPath = strings.TrimLeft(info.PathDisplay, f.slashRootSlash)
|
||||
entryPath = strings.TrimPrefix(info.PathDisplay, f.slashRootSlash)
|
||||
case *files.DeletedMetadata:
|
||||
entryType = fs.EntryObject
|
||||
entryPath = strings.TrimLeft(info.PathDisplay, f.slashRootSlash)
|
||||
entryPath = strings.TrimPrefix(info.PathDisplay, f.slashRootSlash)
|
||||
default:
|
||||
fs.Errorf(entry, "dropbox ChangeNotify: ignoring unknown EntryType %T", entry)
|
||||
continue
|
||||
|
||||
@@ -35,9 +35,7 @@ func init() {
|
||||
fs.Register(&fs.RegInfo{
|
||||
Name: "fichier",
|
||||
Description: "1Fichier",
|
||||
Config: func(ctx context.Context, name string, config configmap.Mapper) {
|
||||
},
|
||||
NewFs: NewFs,
|
||||
NewFs: NewFs,
|
||||
Options: []fs.Option{{
|
||||
Help: "Your API Key, get it from https://1fichier.com/console/params.pl",
|
||||
Name: "api_key",
|
||||
@@ -348,8 +346,10 @@ func (f *Fs) putUnchecked(ctx context.Context, in io.Reader, remote string, size
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(fileUploadResponse.Links) != 1 {
|
||||
return nil, errors.New("unexpected amount of files")
|
||||
if len(fileUploadResponse.Links) == 0 {
|
||||
return nil, errors.New("upload response not found")
|
||||
} else if len(fileUploadResponse.Links) > 1 {
|
||||
fs.Debugf(remote, "Multiple upload responses found, using the first")
|
||||
}
|
||||
|
||||
link := fileUploadResponse.Links[0]
|
||||
|
||||
@@ -241,23 +241,6 @@ func (dl *debugLog) Write(p []byte) (n int, err error) {
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
type dialCtx struct {
|
||||
f *Fs
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
// dial a new connection with fshttp dialer
|
||||
func (d *dialCtx) dial(network, address string) (net.Conn, error) {
|
||||
conn, err := fshttp.NewDialer(d.ctx).Dial(network, address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if d.f.tlsConf != nil {
|
||||
conn = tls.Client(conn, d.f.tlsConf)
|
||||
}
|
||||
return conn, err
|
||||
}
|
||||
|
||||
// shouldRetry returns a boolean as to whether this err deserve to be
|
||||
// retried. It returns the err as a convenience
|
||||
func shouldRetry(ctx context.Context, err error) (bool, error) {
|
||||
@@ -277,9 +260,22 @@ func shouldRetry(ctx context.Context, err error) (bool, error) {
|
||||
// Open a new connection to the FTP server.
|
||||
func (f *Fs) ftpConnection(ctx context.Context) (c *ftp.ServerConn, err error) {
|
||||
fs.Debugf(f, "Connecting to FTP server")
|
||||
dCtx := dialCtx{f, ctx}
|
||||
ftpConfig := []ftp.DialOption{ftp.DialWithDialFunc(dCtx.dial)}
|
||||
if f.opt.ExplicitTLS {
|
||||
|
||||
// Make ftp library dial with fshttp dialer optionally using TLS
|
||||
dial := func(network, address string) (conn net.Conn, err error) {
|
||||
conn, err = fshttp.NewDialer(ctx).Dial(network, address)
|
||||
if f.tlsConf != nil && err == nil {
|
||||
conn = tls.Client(conn, f.tlsConf)
|
||||
}
|
||||
return
|
||||
}
|
||||
ftpConfig := []ftp.DialOption{ftp.DialWithDialFunc(dial)}
|
||||
|
||||
if f.opt.TLS {
|
||||
// Our dialer takes care of TLS but ftp library also needs tlsConf
|
||||
// as a trigger for sending PSBZ and PROT options to server.
|
||||
ftpConfig = append(ftpConfig, ftp.DialWithTLS(f.tlsConf))
|
||||
} else if f.opt.ExplicitTLS {
|
||||
ftpConfig = append(ftpConfig, ftp.DialWithExplicitTLS(f.tlsConf))
|
||||
// Initial connection needs to be cleartext for explicit TLS
|
||||
conn, err := fshttp.NewDialer(ctx).Dial("tcp", f.dialAddr)
|
||||
|
||||
@@ -19,7 +19,6 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
@@ -76,17 +75,18 @@ func init() {
|
||||
Prefix: "gcs",
|
||||
Description: "Google Cloud Storage (this is not Google Drive)",
|
||||
NewFs: NewFs,
|
||||
Config: func(ctx context.Context, name string, m configmap.Mapper) {
|
||||
Config: func(ctx context.Context, name string, m configmap.Mapper) error {
|
||||
saFile, _ := m.Get("service_account_file")
|
||||
saCreds, _ := m.Get("service_account_credentials")
|
||||
anonymous, _ := m.Get("anonymous")
|
||||
if saFile != "" || saCreds != "" || anonymous == "true" {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
err := oauthutil.Config(ctx, "google cloud storage", name, m, storageConfig, nil)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to configure token: %v", err)
|
||||
return errors.Wrap(err, "failed to configure token")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Options: append(oauthutil.SharedOptions, []fs.Option{{
|
||||
Name: "project_number",
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
golog "log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
@@ -78,13 +77,12 @@ func init() {
|
||||
Prefix: "gphotos",
|
||||
Description: "Google Photos",
|
||||
NewFs: NewFs,
|
||||
Config: func(ctx context.Context, name string, m configmap.Mapper) {
|
||||
Config: func(ctx context.Context, name string, m configmap.Mapper) error {
|
||||
// Parse config into Options struct
|
||||
opt := new(Options)
|
||||
err := configstruct.Set(m, opt)
|
||||
if err != nil {
|
||||
fs.Errorf(nil, "Couldn't parse config into struct: %v", err)
|
||||
return
|
||||
return errors.Wrap(err, "couldn't parse config into struct")
|
||||
}
|
||||
|
||||
// Fill in the scopes
|
||||
@@ -97,7 +95,7 @@ func init() {
|
||||
// Do the oauth
|
||||
err = oauthutil.Config(ctx, "google photos", name, m, oauthConfig, nil)
|
||||
if err != nil {
|
||||
golog.Fatalf("Failed to configure token: %v", err)
|
||||
return errors.Wrap(err, "failed to configure token")
|
||||
}
|
||||
|
||||
// Warn the user
|
||||
@@ -108,6 +106,7 @@ func init() {
|
||||
|
||||
`)
|
||||
|
||||
return nil
|
||||
},
|
||||
Options: append(oauthutil.SharedOptions, []fs.Option{{
|
||||
Name: "read_only",
|
||||
|
||||
@@ -47,7 +47,7 @@ func prepareServer(t *testing.T) (configmap.Simple, func()) {
|
||||
ts := httptest.NewServer(handler)
|
||||
|
||||
// Configure the remote
|
||||
configfile.LoadConfig(context.Background())
|
||||
require.NoError(t, configfile.LoadConfig(context.Background()))
|
||||
// fs.Config.LogLevel = fs.LogLevelDebug
|
||||
// fs.Config.DumpHeaders = true
|
||||
// fs.Config.DumpBodies = true
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -56,11 +55,12 @@ func init() {
|
||||
Name: "hubic",
|
||||
Description: "Hubic",
|
||||
NewFs: NewFs,
|
||||
Config: func(ctx context.Context, name string, m configmap.Mapper) {
|
||||
Config: func(ctx context.Context, name string, m configmap.Mapper) error {
|
||||
err := oauthutil.Config(ctx, "hubic", name, m, oauthConfig, nil)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to configure token: %v", err)
|
||||
return errors.Wrap(err, "failed to configure token")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Options: append(oauthutil.SharedOptions, swift.SharedOptions...),
|
||||
})
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -87,12 +86,12 @@ func init() {
|
||||
Name: "jottacloud",
|
||||
Description: "Jottacloud",
|
||||
NewFs: NewFs,
|
||||
Config: func(ctx context.Context, name string, m configmap.Mapper) {
|
||||
Config: func(ctx context.Context, name string, m configmap.Mapper) error {
|
||||
refresh := false
|
||||
if version, ok := m.Get("configVersion"); ok {
|
||||
ver, err := strconv.Atoi(version)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse config version - corrupted config")
|
||||
return errors.Wrap(err, "failed to parse config version - corrupted config")
|
||||
}
|
||||
refresh = (ver != configVersion) && (ver != v1configVersion)
|
||||
}
|
||||
@@ -104,7 +103,7 @@ func init() {
|
||||
if ok && tokenString != "" {
|
||||
fmt.Printf("Already have a token - refresh?\n")
|
||||
if !config.Confirm(false) {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,11 +115,13 @@ func init() {
|
||||
|
||||
switch config.ChooseNumber("Your choice", 1, 3) {
|
||||
case 1:
|
||||
v2config(ctx, name, m)
|
||||
return v2config(ctx, name, m)
|
||||
case 2:
|
||||
v1config(ctx, name, m)
|
||||
return v1config(ctx, name, m)
|
||||
case 3:
|
||||
teliaCloudConfig(ctx, name, m)
|
||||
return teliaCloudConfig(ctx, name, m)
|
||||
default:
|
||||
return errors.New("unknown config choice")
|
||||
}
|
||||
},
|
||||
Options: []fs.Option{{
|
||||
@@ -242,7 +243,7 @@ func shouldRetry(ctx context.Context, resp *http.Response, err error) (bool, err
|
||||
return fserrors.ShouldRetry(err) || fserrors.ShouldRetryHTTP(resp, retryErrorCodes), err
|
||||
}
|
||||
|
||||
func teliaCloudConfig(ctx context.Context, name string, m configmap.Mapper) {
|
||||
func teliaCloudConfig(ctx context.Context, name string, m configmap.Mapper) error {
|
||||
teliaCloudOauthConfig := &oauth2.Config{
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: teliaCloudAuthURL,
|
||||
@@ -255,15 +256,14 @@ func teliaCloudConfig(ctx context.Context, name string, m configmap.Mapper) {
|
||||
|
||||
err := oauthutil.Config(ctx, "jottacloud", name, m, teliaCloudOauthConfig, nil)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to configure token: %v", err)
|
||||
return
|
||||
return errors.Wrap(err, "failed to configure token")
|
||||
}
|
||||
|
||||
fmt.Printf("\nDo you want to use a non standard device/mountpoint e.g. for accessing files uploaded using the official Jottacloud client?\n\n")
|
||||
if config.Confirm(false) {
|
||||
oAuthClient, _, err := oauthutil.NewClient(ctx, name, m, teliaCloudOauthConfig)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load oAuthClient: %s", err)
|
||||
return errors.Wrap(err, "failed to load oAuthClient")
|
||||
}
|
||||
|
||||
srv := rest.NewClient(oAuthClient).SetRoot(rootURL)
|
||||
@@ -271,7 +271,7 @@ func teliaCloudConfig(ctx context.Context, name string, m configmap.Mapper) {
|
||||
|
||||
device, mountpoint, err := setupMountpoint(ctx, srv, apiSrv)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to setup mountpoint: %s", err)
|
||||
return errors.Wrap(err, "failed to setup mountpoint")
|
||||
}
|
||||
m.Set(configDevice, device)
|
||||
m.Set(configMountpoint, mountpoint)
|
||||
@@ -280,17 +280,18 @@ func teliaCloudConfig(ctx context.Context, name string, m configmap.Mapper) {
|
||||
m.Set("configVersion", strconv.Itoa(configVersion))
|
||||
m.Set(configClientID, teliaCloudClientID)
|
||||
m.Set(configTokenURL, teliaCloudTokenURL)
|
||||
return nil
|
||||
}
|
||||
|
||||
// v1config configure a jottacloud backend using legacy authentication
|
||||
func v1config(ctx context.Context, name string, m configmap.Mapper) {
|
||||
func v1config(ctx context.Context, name string, m configmap.Mapper) error {
|
||||
srv := rest.NewClient(fshttp.NewClient(ctx))
|
||||
|
||||
fmt.Printf("\nDo you want to create a machine specific API key?\n\nRclone has it's own Jottacloud API KEY which works fine as long as one only uses rclone on a single machine. When you want to use rclone with this account on more than one machine it's recommended to create a machine specific API key. These keys can NOT be shared between machines.\n\n")
|
||||
if config.Confirm(false) {
|
||||
deviceRegistration, err := registerDevice(ctx, srv)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to register device: %v", err)
|
||||
return errors.Wrap(err, "failed to register device")
|
||||
}
|
||||
|
||||
m.Set(configClientID, deviceRegistration.ClientID)
|
||||
@@ -318,18 +319,18 @@ func v1config(ctx context.Context, name string, m configmap.Mapper) {
|
||||
|
||||
token, err := doAuthV1(ctx, srv, username, password)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to get oauth token: %s", err)
|
||||
return errors.Wrap(err, "failed to get oauth token")
|
||||
}
|
||||
err = oauthutil.PutToken(name, m, &token, true)
|
||||
if err != nil {
|
||||
log.Fatalf("Error while saving token: %s", err)
|
||||
return errors.Wrap(err, "error while saving token")
|
||||
}
|
||||
|
||||
fmt.Printf("\nDo you want to use a non standard device/mountpoint e.g. for accessing files uploaded using the official Jottacloud client?\n\n")
|
||||
if config.Confirm(false) {
|
||||
oAuthClient, _, err := oauthutil.NewClient(ctx, name, m, oauthConfig)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load oAuthClient: %s", err)
|
||||
return errors.Wrap(err, "failed to load oAuthClient")
|
||||
}
|
||||
|
||||
srv = rest.NewClient(oAuthClient).SetRoot(rootURL)
|
||||
@@ -337,13 +338,14 @@ func v1config(ctx context.Context, name string, m configmap.Mapper) {
|
||||
|
||||
device, mountpoint, err := setupMountpoint(ctx, srv, apiSrv)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to setup mountpoint: %s", err)
|
||||
return errors.Wrap(err, "failed to setup mountpoint")
|
||||
}
|
||||
m.Set(configDevice, device)
|
||||
m.Set(configMountpoint, mountpoint)
|
||||
}
|
||||
|
||||
m.Set("configVersion", strconv.Itoa(v1configVersion))
|
||||
return nil
|
||||
}
|
||||
|
||||
// registerDevice register a new device for use with the jottacloud API
|
||||
@@ -418,7 +420,7 @@ func doAuthV1(ctx context.Context, srv *rest.Client, username, password string)
|
||||
}
|
||||
|
||||
// v2config configure a jottacloud backend using the modern JottaCli token based authentication
|
||||
func v2config(ctx context.Context, name string, m configmap.Mapper) {
|
||||
func v2config(ctx context.Context, name string, m configmap.Mapper) error {
|
||||
srv := rest.NewClient(fshttp.NewClient(ctx))
|
||||
|
||||
fmt.Printf("Generate a personal login token here: https://www.jottacloud.com/web/secure\n")
|
||||
@@ -430,31 +432,32 @@ func v2config(ctx context.Context, name string, m configmap.Mapper) {
|
||||
|
||||
token, err := doAuthV2(ctx, srv, loginToken, m)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to get oauth token: %s", err)
|
||||
return errors.Wrap(err, "failed to get oauth token")
|
||||
}
|
||||
err = oauthutil.PutToken(name, m, &token, true)
|
||||
if err != nil {
|
||||
log.Fatalf("Error while saving token: %s", err)
|
||||
return errors.Wrap(err, "error while saving token")
|
||||
}
|
||||
|
||||
fmt.Printf("\nDo you want to use a non standard device/mountpoint e.g. for accessing files uploaded using the official Jottacloud client?\n\n")
|
||||
if config.Confirm(false) {
|
||||
oAuthClient, _, err := oauthutil.NewClient(ctx, name, m, oauthConfig)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load oAuthClient: %s", err)
|
||||
return errors.Wrap(err, "failed to load oAuthClient")
|
||||
}
|
||||
|
||||
srv = rest.NewClient(oAuthClient).SetRoot(rootURL)
|
||||
apiSrv := rest.NewClient(oAuthClient).SetRoot(apiURL)
|
||||
device, mountpoint, err := setupMountpoint(ctx, srv, apiSrv)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to setup mountpoint: %s", err)
|
||||
return errors.Wrap(err, "failed to setup mountpoint")
|
||||
}
|
||||
m.Set(configDevice, device)
|
||||
m.Set(configMountpoint, mountpoint)
|
||||
}
|
||||
|
||||
m.Set("configVersion", strconv.Itoa(configVersion))
|
||||
return nil
|
||||
}
|
||||
|
||||
// doAuthV2 runs the actual token request for V2 authentication
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
@@ -48,7 +48,7 @@ func (w *BinWriter) Reader() io.Reader {
|
||||
// WritePu16 writes a short as unsigned varint
|
||||
func (w *BinWriter) WritePu16(val int) {
|
||||
if val < 0 || val > 65535 {
|
||||
log.Fatalf("Invalid UInt16 %v", val)
|
||||
panic(fmt.Sprintf("Invalid UInt16 %v", val))
|
||||
}
|
||||
w.WritePu64(int64(val))
|
||||
}
|
||||
@@ -56,7 +56,7 @@ func (w *BinWriter) WritePu16(val int) {
|
||||
// WritePu32 writes a signed long as unsigned varint
|
||||
func (w *BinWriter) WritePu32(val int64) {
|
||||
if val < 0 || val > 4294967295 {
|
||||
log.Fatalf("Invalid UInt32 %v", val)
|
||||
panic(fmt.Sprintf("Invalid UInt32 %v", val))
|
||||
}
|
||||
w.WritePu64(val)
|
||||
}
|
||||
@@ -64,7 +64,7 @@ func (w *BinWriter) WritePu32(val int64) {
|
||||
// WritePu64 writes an unsigned (actually, signed) long as unsigned varint
|
||||
func (w *BinWriter) WritePu64(val int64) {
|
||||
if val < 0 {
|
||||
log.Fatalf("Invalid UInt64 %v", val)
|
||||
panic(fmt.Sprintf("Invalid UInt64 %v", val))
|
||||
}
|
||||
w.b.Write(w.a[:binary.PutUvarint(w.a, uint64(val))])
|
||||
}
|
||||
@@ -123,7 +123,7 @@ func (r *BinReader) check(err error) bool {
|
||||
r.err = err
|
||||
}
|
||||
if err != io.EOF {
|
||||
log.Fatalf("Error parsing response: %v", err)
|
||||
panic(fmt.Sprintf("Error parsing response: %v", err))
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
@@ -99,7 +98,7 @@ func init() {
|
||||
Name: "onedrive",
|
||||
Description: "Microsoft OneDrive",
|
||||
NewFs: NewFs,
|
||||
Config: func(ctx context.Context, name string, m configmap.Mapper) {
|
||||
Config: func(ctx context.Context, name string, m configmap.Mapper) error {
|
||||
region, _ := m.Get("region")
|
||||
graphURL := graphAPIEndpoint[region] + "/v1.0"
|
||||
oauthConfig.Endpoint = oauth2.Endpoint{
|
||||
@@ -109,13 +108,12 @@ func init() {
|
||||
ci := fs.GetConfig(ctx)
|
||||
err := oauthutil.Config(ctx, "onedrive", name, m, oauthConfig, nil)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to configure token: %v", err)
|
||||
return
|
||||
return errors.Wrap(err, "failed to configure token")
|
||||
}
|
||||
|
||||
// Stop if we are running non-interactive config
|
||||
if ci.AutoConfirm {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
type driveResource struct {
|
||||
@@ -138,7 +136,7 @@ func init() {
|
||||
|
||||
oAuthClient, _, err := oauthutil.NewClient(ctx, name, m, oauthConfig)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to configure OneDrive: %v", err)
|
||||
return errors.Wrap(err, "failed to configure OneDrive")
|
||||
}
|
||||
srv := rest.NewClient(oAuthClient)
|
||||
|
||||
@@ -203,18 +201,17 @@ func init() {
|
||||
sites := siteResponse{}
|
||||
_, err := srv.CallJSON(ctx, &opts, nil, &sites)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to query available sites: %v", err)
|
||||
return errors.Wrap(err, "failed to query available sites")
|
||||
}
|
||||
|
||||
if len(sites.Sites) == 0 {
|
||||
log.Fatalf("Search for '%s' returned no results", searchTerm)
|
||||
} else {
|
||||
fmt.Printf("Found %d sites, please select the one you want to use:\n", len(sites.Sites))
|
||||
for index, site := range sites.Sites {
|
||||
fmt.Printf("%d: %s (%s) id=%s\n", index, site.SiteName, site.SiteURL, site.SiteID)
|
||||
}
|
||||
siteID = sites.Sites[config.ChooseNumber("Chose drive to use:", 0, len(sites.Sites)-1)].SiteID
|
||||
return errors.Errorf("search for %q returned no results", searchTerm)
|
||||
}
|
||||
fmt.Printf("Found %d sites, please select the one you want to use:\n", len(sites.Sites))
|
||||
for index, site := range sites.Sites {
|
||||
fmt.Printf("%d: %s (%s) id=%s\n", index, site.SiteName, site.SiteURL, site.SiteID)
|
||||
}
|
||||
siteID = sites.Sites[config.ChooseNumber("Chose drive to use:", 0, len(sites.Sites)-1)].SiteID
|
||||
}
|
||||
|
||||
// if we use server-relative URL for finding the drive
|
||||
@@ -227,7 +224,7 @@ func init() {
|
||||
site := siteResource{}
|
||||
_, err := srv.CallJSON(ctx, &opts, nil, &site)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to query available site by relative path: %v", err)
|
||||
return errors.Wrap(err, "failed to query available site by relative path")
|
||||
}
|
||||
siteID = site.SiteID
|
||||
}
|
||||
@@ -247,7 +244,7 @@ func init() {
|
||||
drives := drivesResponse{}
|
||||
_, err := srv.CallJSON(ctx, &opts, nil, &drives)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to query available drives: %v", err)
|
||||
return errors.Wrap(err, "failed to query available drives")
|
||||
}
|
||||
|
||||
// Also call /me/drive as sometimes /me/drives doesn't return it #4068
|
||||
@@ -256,7 +253,7 @@ func init() {
|
||||
meDrive := driveResource{}
|
||||
_, err := srv.CallJSON(ctx, &opts, nil, &meDrive)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to query available drives: %v", err)
|
||||
return errors.Wrap(err, "failed to query available drives")
|
||||
}
|
||||
found := false
|
||||
for _, drive := range drives.Drives {
|
||||
@@ -273,14 +270,13 @@ func init() {
|
||||
}
|
||||
|
||||
if len(drives.Drives) == 0 {
|
||||
log.Fatalf("No drives found")
|
||||
} else {
|
||||
fmt.Printf("Found %d drives, please select the one you want to use:\n", len(drives.Drives))
|
||||
for index, drive := range drives.Drives {
|
||||
fmt.Printf("%d: %s (%s) id=%s\n", index, drive.DriveName, drive.DriveType, drive.DriveID)
|
||||
}
|
||||
finalDriveID = drives.Drives[config.ChooseNumber("Chose drive to use:", 0, len(drives.Drives)-1)].DriveID
|
||||
return errors.New("no drives found")
|
||||
}
|
||||
fmt.Printf("Found %d drives, please select the one you want to use:\n", len(drives.Drives))
|
||||
for index, drive := range drives.Drives {
|
||||
fmt.Printf("%d: %s (%s) id=%s\n", index, drive.DriveName, drive.DriveType, drive.DriveID)
|
||||
}
|
||||
finalDriveID = drives.Drives[config.ChooseNumber("Chose drive to use:", 0, len(drives.Drives)-1)].DriveID
|
||||
}
|
||||
|
||||
// Test the driveID and get drive type
|
||||
@@ -291,17 +287,18 @@ func init() {
|
||||
var rootItem api.Item
|
||||
_, err = srv.CallJSON(ctx, &opts, nil, &rootItem)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to query root for drive %s: %v", finalDriveID, err)
|
||||
return errors.Wrapf(err, "failed to query root for drive %s", finalDriveID)
|
||||
}
|
||||
|
||||
fmt.Printf("Found drive '%s' of type '%s', URL: %s\nIs that okay?\n", rootItem.Name, rootItem.ParentReference.DriveType, rootItem.WebURL)
|
||||
// This does not work, YET :)
|
||||
if !config.ConfirmWithConfig(ctx, m, "config_drive_ok", true) {
|
||||
log.Fatalf("Cancelled by user")
|
||||
return errors.New("cancelled by user")
|
||||
}
|
||||
|
||||
m.Set(configDriveID, finalDriveID)
|
||||
m.Set(configDriveType, rootItem.ParentReference.DriveType)
|
||||
return nil
|
||||
},
|
||||
Options: append(oauthutil.SharedOptions, []fs.Option{{
|
||||
Name: "region",
|
||||
@@ -361,6 +358,11 @@ This will only work if you are copying between two OneDrive *Personal* drives AN
|
||||
the files to copy are already shared between them. In other cases, rclone will
|
||||
fall back to normal copy (which will be slightly slower).`,
|
||||
Advanced: true,
|
||||
}, {
|
||||
Name: "list_chunk",
|
||||
Help: "Size of listing chunk.",
|
||||
Default: 1000,
|
||||
Advanced: true,
|
||||
}, {
|
||||
Name: "no_versions",
|
||||
Default: false,
|
||||
@@ -468,6 +470,7 @@ type Options struct {
|
||||
DriveType string `config:"drive_type"`
|
||||
ExposeOneNoteFiles bool `config:"expose_onenote_files"`
|
||||
ServerSideAcrossConfigs bool `config:"server_side_across_configs"`
|
||||
ListChunk int64 `config:"list_chunk"`
|
||||
NoVersions bool `config:"no_versions"`
|
||||
LinkScope string `config:"link_scope"`
|
||||
LinkType string `config:"link_type"`
|
||||
@@ -560,6 +563,9 @@ func shouldRetry(ctx context.Context, resp *http.Response, err error) (bool, err
|
||||
if len(resp.Header["Www-Authenticate"]) == 1 && strings.Index(resp.Header["Www-Authenticate"][0], "expired_token") >= 0 {
|
||||
retry = true
|
||||
fs.Debugf(nil, "Should retry: %v", err)
|
||||
} else if err != nil && strings.Contains(err.Error(), "Unable to initialize RPS") {
|
||||
retry = true
|
||||
fs.Debugf(nil, "HTTP 401: Unable to initialize RPS. Trying again.")
|
||||
}
|
||||
case 429: // Too Many Requests.
|
||||
// see https://docs.microsoft.com/en-us/sharepoint/dev/general-development/how-to-avoid-getting-throttled-or-blocked-in-sharepoint-online
|
||||
@@ -896,7 +902,7 @@ type listAllFn func(*api.Item) bool
|
||||
func (f *Fs) listAll(ctx context.Context, dirID string, directoriesOnly bool, filesOnly bool, fn listAllFn) (found bool, err error) {
|
||||
// Top parameter asks for bigger pages of data
|
||||
// https://dev.onedrive.com/odata/optional-query-parameters.htm
|
||||
opts := f.newOptsCall(dirID, "GET", "/children?$top=1000")
|
||||
opts := f.newOptsCall(dirID, "GET", fmt.Sprintf("/children?$top=%d", f.opt.ListChunk))
|
||||
OUTER:
|
||||
for {
|
||||
var result api.ListChildrenResponse
|
||||
@@ -1423,7 +1429,7 @@ func (f *Fs) PublicLink(ctx context.Context, remote string, expire fs.Duration,
|
||||
Password: f.opt.LinkPassword,
|
||||
}
|
||||
|
||||
if expire < fs.Duration(time.Hour*24*365*100) {
|
||||
if expire < fs.DurationOff {
|
||||
expiry := time.Now().Add(time.Duration(expire))
|
||||
share.Expiry = &expiry
|
||||
}
|
||||
@@ -1851,7 +1857,7 @@ func (o *Object) uploadMultipart(ctx context.Context, in io.Reader, size int64,
|
||||
fs.Debugf(o, "Cancelling multipart upload: %v", err)
|
||||
cancelErr := o.cancelUploadSession(ctx, uploadURL)
|
||||
if cancelErr != nil {
|
||||
fs.Logf(o, "Failed to cancel multipart upload: %v", cancelErr)
|
||||
fs.Logf(o, "Failed to cancel multipart upload: %v (upload failed due to: %v)", cancelErr, err)
|
||||
}
|
||||
})()
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
@@ -72,7 +71,7 @@ func init() {
|
||||
Name: "pcloud",
|
||||
Description: "Pcloud",
|
||||
NewFs: NewFs,
|
||||
Config: func(ctx context.Context, name string, m configmap.Mapper) {
|
||||
Config: func(ctx context.Context, name string, m configmap.Mapper) error {
|
||||
optc := new(Options)
|
||||
err := configstruct.Set(m, optc)
|
||||
if err != nil {
|
||||
@@ -100,8 +99,9 @@ func init() {
|
||||
}
|
||||
err = oauthutil.Config(ctx, "pcloud", name, m, oauthConfig, &opt)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to configure token: %v", err)
|
||||
return errors.Wrap(err, "failed to configure token")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Options: append(oauthutil.SharedOptions, []fs.Option{{
|
||||
Name: config.ConfigEncoding,
|
||||
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -78,11 +77,12 @@ func init() {
|
||||
Name: "premiumizeme",
|
||||
Description: "premiumize.me",
|
||||
NewFs: NewFs,
|
||||
Config: func(ctx context.Context, name string, m configmap.Mapper) {
|
||||
Config: func(ctx context.Context, name string, m configmap.Mapper) error {
|
||||
err := oauthutil.Config(ctx, "premiumizeme", name, m, oauthConfig, nil)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to configure token: %v", err)
|
||||
return errors.Wrap(err, "failed to configure token")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Options: []fs.Option{{
|
||||
Name: "api_key",
|
||||
|
||||
@@ -2,10 +2,10 @@ package putio
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rclone/rclone/fs"
|
||||
"github.com/rclone/rclone/fs/config"
|
||||
"github.com/rclone/rclone/fs/config/configmap"
|
||||
@@ -60,14 +60,15 @@ func init() {
|
||||
Name: "putio",
|
||||
Description: "Put.io",
|
||||
NewFs: NewFs,
|
||||
Config: func(ctx context.Context, name string, m configmap.Mapper) {
|
||||
Config: func(ctx context.Context, name string, m configmap.Mapper) error {
|
||||
opt := oauthutil.Options{
|
||||
NoOffline: true,
|
||||
}
|
||||
err := oauthutil.Config(ctx, "putio", name, m, putioConfig, &opt)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to configure token: %v", err)
|
||||
return errors.Wrap(err, "failed to configure token")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Options: []fs.Option{{
|
||||
Name: config.ConfigEncoding,
|
||||
|
||||
@@ -26,7 +26,6 @@ import (
|
||||
"github.com/aws/aws-sdk-go/aws/corehandlers"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
|
||||
"github.com/aws/aws-sdk-go/aws/defaults"
|
||||
"github.com/aws/aws-sdk-go/aws/ec2metadata"
|
||||
"github.com/aws/aws-sdk-go/aws/endpoints"
|
||||
@@ -1511,11 +1510,6 @@ func s3Connection(ctx context.Context, opt *Options, client *http.Client) (*s3.S
|
||||
}),
|
||||
ExpiryWindow: 3 * time.Minute,
|
||||
},
|
||||
|
||||
// Pick up IAM role if we are in EKS
|
||||
&stscreds.WebIdentityRoleProvider{
|
||||
ExpiryWindow: 3 * time.Minute,
|
||||
},
|
||||
}
|
||||
cred := credentials.NewChainCredentials(providers)
|
||||
|
||||
|
||||
@@ -296,36 +296,32 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
||||
}
|
||||
|
||||
// Config callback for 2FA
|
||||
func Config(ctx context.Context, name string, m configmap.Mapper) {
|
||||
func Config(ctx context.Context, name string, m configmap.Mapper) error {
|
||||
ci := fs.GetConfig(ctx)
|
||||
serverURL, ok := m.Get(configURL)
|
||||
if !ok || serverURL == "" {
|
||||
// If there's no server URL, it means we're trying an operation at the backend level, like a "rclone authorize seafile"
|
||||
fmt.Print("\nOperation not supported on this remote.\nIf you need a 2FA code on your account, use the command:\n\nrclone config reconnect <remote name>:\n\n")
|
||||
return
|
||||
return errors.New("operation not supported on this remote. If you need a 2FA code on your account, use the command: nrclone config reconnect <remote name>: ")
|
||||
}
|
||||
|
||||
// Stop if we are running non-interactive config
|
||||
if ci.AutoConfirm {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
u, err := url.Parse(serverURL)
|
||||
if err != nil {
|
||||
fs.Errorf(nil, "Invalid server URL %s", serverURL)
|
||||
return
|
||||
return errors.Errorf("invalid server URL %s", serverURL)
|
||||
}
|
||||
|
||||
is2faEnabled, _ := m.Get(config2FA)
|
||||
if is2faEnabled != "true" {
|
||||
fmt.Println("Two-factor authentication is not enabled on this account.")
|
||||
return
|
||||
return errors.New("two-factor authentication is not enabled on this account")
|
||||
}
|
||||
|
||||
username, _ := m.Get(configUser)
|
||||
if username == "" {
|
||||
fs.Errorf(nil, "A username is required")
|
||||
return
|
||||
return errors.New("a username is required")
|
||||
}
|
||||
|
||||
password, _ := m.Get(configPassword)
|
||||
@@ -376,6 +372,7 @@ func Config(ctx context.Context, name string, m configmap.Mapper) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// sets the AuthorizationToken up
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
@@ -223,6 +224,17 @@ have a server which returns
|
||||
Then you may need to enable this flag.
|
||||
|
||||
If concurrent reads are disabled, the use_fstat option is ignored.
|
||||
`,
|
||||
Advanced: true,
|
||||
}, {
|
||||
Name: "disable_concurrent_writes",
|
||||
Default: false,
|
||||
Help: `If set don't use concurrent writes
|
||||
|
||||
Normally rclone uses concurrent writes to upload files. This improves
|
||||
the performance greatly, especially for distant servers.
|
||||
|
||||
This option disables concurrent writes should that be necessary.
|
||||
`,
|
||||
Advanced: true,
|
||||
}, {
|
||||
@@ -243,29 +255,30 @@ Set to 0 to keep connections indefinitely.
|
||||
|
||||
// Options defines the configuration for this backend
|
||||
type Options struct {
|
||||
Host string `config:"host"`
|
||||
User string `config:"user"`
|
||||
Port string `config:"port"`
|
||||
Pass string `config:"pass"`
|
||||
KeyPem string `config:"key_pem"`
|
||||
KeyFile string `config:"key_file"`
|
||||
KeyFilePass string `config:"key_file_pass"`
|
||||
PubKeyFile string `config:"pubkey_file"`
|
||||
KnownHostsFile string `config:"known_hosts_file"`
|
||||
KeyUseAgent bool `config:"key_use_agent"`
|
||||
UseInsecureCipher bool `config:"use_insecure_cipher"`
|
||||
DisableHashCheck bool `config:"disable_hashcheck"`
|
||||
AskPassword bool `config:"ask_password"`
|
||||
PathOverride string `config:"path_override"`
|
||||
SetModTime bool `config:"set_modtime"`
|
||||
Md5sumCommand string `config:"md5sum_command"`
|
||||
Sha1sumCommand string `config:"sha1sum_command"`
|
||||
SkipLinks bool `config:"skip_links"`
|
||||
Subsystem string `config:"subsystem"`
|
||||
ServerCommand string `config:"server_command"`
|
||||
UseFstat bool `config:"use_fstat"`
|
||||
DisableConcurrentReads bool `config:"disable_concurrent_reads"`
|
||||
IdleTimeout fs.Duration `config:"idle_timeout"`
|
||||
Host string `config:"host"`
|
||||
User string `config:"user"`
|
||||
Port string `config:"port"`
|
||||
Pass string `config:"pass"`
|
||||
KeyPem string `config:"key_pem"`
|
||||
KeyFile string `config:"key_file"`
|
||||
KeyFilePass string `config:"key_file_pass"`
|
||||
PubKeyFile string `config:"pubkey_file"`
|
||||
KnownHostsFile string `config:"known_hosts_file"`
|
||||
KeyUseAgent bool `config:"key_use_agent"`
|
||||
UseInsecureCipher bool `config:"use_insecure_cipher"`
|
||||
DisableHashCheck bool `config:"disable_hashcheck"`
|
||||
AskPassword bool `config:"ask_password"`
|
||||
PathOverride string `config:"path_override"`
|
||||
SetModTime bool `config:"set_modtime"`
|
||||
Md5sumCommand string `config:"md5sum_command"`
|
||||
Sha1sumCommand string `config:"sha1sum_command"`
|
||||
SkipLinks bool `config:"skip_links"`
|
||||
Subsystem string `config:"subsystem"`
|
||||
ServerCommand string `config:"server_command"`
|
||||
UseFstat bool `config:"use_fstat"`
|
||||
DisableConcurrentReads bool `config:"disable_concurrent_reads"`
|
||||
DisableConcurrentWrites bool `config:"disable_concurrent_writes"`
|
||||
IdleTimeout fs.Duration `config:"idle_timeout"`
|
||||
}
|
||||
|
||||
// Fs stores the interface to the remote SFTP files
|
||||
@@ -286,6 +299,7 @@ type Fs struct {
|
||||
drain *time.Timer // used to drain the pool when we stop using the connections
|
||||
pacer *fs.Pacer // pacer for operations
|
||||
savedpswd string
|
||||
transfers int32 // count in use references
|
||||
}
|
||||
|
||||
// Object is a remote SFTP file that has been stat'd (so it exists, but is not necessarily open for reading)
|
||||
@@ -348,6 +362,23 @@ func (c *conn) closed() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Show that we are doing an upload or download
|
||||
//
|
||||
// Call removeTransfer() when done
|
||||
func (f *Fs) addTransfer() {
|
||||
atomic.AddInt32(&f.transfers, 1)
|
||||
}
|
||||
|
||||
// Show the upload or download done
|
||||
func (f *Fs) removeTransfer() {
|
||||
atomic.AddInt32(&f.transfers, -1)
|
||||
}
|
||||
|
||||
// getTransfers shows whether there are any transfers in progress
|
||||
func (f *Fs) getTransfers() int32 {
|
||||
return atomic.LoadInt32(&f.transfers)
|
||||
}
|
||||
|
||||
// Open a new connection to the SFTP server.
|
||||
func (f *Fs) sftpConnection(ctx context.Context) (c *conn, err error) {
|
||||
// Rate limit rate of new connections
|
||||
@@ -396,7 +427,11 @@ func (f *Fs) newSftpClient(conn *ssh.Client, opts ...sftp.ClientOption) (*sftp.C
|
||||
opts = append(opts,
|
||||
sftp.UseFstat(f.opt.UseFstat),
|
||||
sftp.UseConcurrentReads(!f.opt.DisableConcurrentReads),
|
||||
sftp.UseConcurrentWrites(!f.opt.DisableConcurrentWrites),
|
||||
)
|
||||
if f.opt.DisableConcurrentReads { // FIXME
|
||||
fs.Errorf(f, "Ignoring disable_concurrent_reads after library reversion - see #5197")
|
||||
}
|
||||
|
||||
return sftp.NewClientPipe(pr, pw, opts...)
|
||||
}
|
||||
@@ -474,6 +509,13 @@ func (f *Fs) putSftpConnection(pc **conn, err error) {
|
||||
func (f *Fs) drainPool(ctx context.Context) (err error) {
|
||||
f.poolMu.Lock()
|
||||
defer f.poolMu.Unlock()
|
||||
if transfers := f.getTransfers(); transfers != 0 {
|
||||
fs.Debugf(f, "Not closing %d unused connections as %d transfers in progress", len(f.pool), transfers)
|
||||
if f.opt.IdleTimeout > 0 {
|
||||
f.drain.Reset(time.Duration(f.opt.IdleTimeout)) // nudge on the pool emptying timer
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if f.opt.IdleTimeout > 0 {
|
||||
f.drain.Stop()
|
||||
}
|
||||
@@ -1380,18 +1422,22 @@ func (o *Object) Storable() bool {
|
||||
|
||||
// objectReader represents a file open for reading on the SFTP server
|
||||
type objectReader struct {
|
||||
f *Fs
|
||||
sftpFile *sftp.File
|
||||
pipeReader *io.PipeReader
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
func newObjectReader(sftpFile *sftp.File) *objectReader {
|
||||
func (f *Fs) newObjectReader(sftpFile *sftp.File) *objectReader {
|
||||
pipeReader, pipeWriter := io.Pipe()
|
||||
file := &objectReader{
|
||||
f: f,
|
||||
sftpFile: sftpFile,
|
||||
pipeReader: pipeReader,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
// Show connection in use
|
||||
f.addTransfer()
|
||||
|
||||
go func() {
|
||||
// Use sftpFile.WriteTo to pump data so that it gets a
|
||||
@@ -1421,6 +1467,8 @@ func (file *objectReader) Close() (err error) {
|
||||
_ = file.pipeReader.Close()
|
||||
// Wait for the background process to finish
|
||||
<-file.done
|
||||
// Show connection no longer in use
|
||||
file.f.removeTransfer()
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1454,12 +1502,27 @@ func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.Read
|
||||
return nil, errors.Wrap(err, "Open Seek failed")
|
||||
}
|
||||
}
|
||||
in = readers.NewLimitedReadCloser(newObjectReader(sftpFile), limit)
|
||||
in = readers.NewLimitedReadCloser(o.fs.newObjectReader(sftpFile), limit)
|
||||
return in, nil
|
||||
}
|
||||
|
||||
type sizeReader struct {
|
||||
io.Reader
|
||||
size int64
|
||||
}
|
||||
|
||||
// Size returns the expected size of the stream
|
||||
//
|
||||
// It is used in sftpFile.ReadFrom as a hint to work out the
|
||||
// concurrency needed
|
||||
func (sr *sizeReader) Size() int64 {
|
||||
return sr.size
|
||||
}
|
||||
|
||||
// Update a remote sftp file using the data <in> and ModTime from <src>
|
||||
func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) error {
|
||||
o.fs.addTransfer() // Show transfer in progress
|
||||
defer o.fs.removeTransfer()
|
||||
// Clear the hash cache since we are about to update the object
|
||||
o.md5sum = nil
|
||||
o.sha1sum = nil
|
||||
@@ -1487,7 +1550,7 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
|
||||
fs.Debugf(src, "Removed after failed upload: %v", err)
|
||||
}
|
||||
}
|
||||
_, err = file.ReadFrom(in)
|
||||
_, err = file.ReadFrom(&sizeReader{Reader: in, size: src.Size()})
|
||||
if err != nil {
|
||||
remove()
|
||||
return errors.Wrap(err, "Update ReadFrom failed")
|
||||
|
||||
@@ -77,7 +77,6 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
@@ -136,7 +135,7 @@ func init() {
|
||||
Name: "sharefile",
|
||||
Description: "Citrix Sharefile",
|
||||
NewFs: NewFs,
|
||||
Config: func(ctx context.Context, name string, m configmap.Mapper) {
|
||||
Config: func(ctx context.Context, name string, m configmap.Mapper) error {
|
||||
oauthConfig := newOauthConfig("")
|
||||
checkAuth := func(oauthConfig *oauth2.Config, auth *oauthutil.AuthResult) error {
|
||||
if auth == nil || auth.Form == nil {
|
||||
@@ -157,8 +156,9 @@ func init() {
|
||||
}
|
||||
err := oauthutil.Config(ctx, "sharefile", name, m, oauthConfig, &opt)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to configure token: %v", err)
|
||||
return errors.Wrap(err, "failed to configure token")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Options: []fs.Option{{
|
||||
Name: "upload_cutoff",
|
||||
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
@@ -76,17 +75,17 @@ func init() {
|
||||
Name: "sugarsync",
|
||||
Description: "Sugarsync",
|
||||
NewFs: NewFs,
|
||||
Config: func(ctx context.Context, name string, m configmap.Mapper) {
|
||||
Config: func(ctx context.Context, name string, m configmap.Mapper) error {
|
||||
opt := new(Options)
|
||||
err := configstruct.Set(m, opt)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to read options: %v", err)
|
||||
return errors.Wrap(err, "failed to read options")
|
||||
}
|
||||
|
||||
if opt.RefreshToken != "" {
|
||||
fmt.Printf("Already have a token - refresh?\n")
|
||||
if !config.ConfirmWithConfig(ctx, m, "config_refresh_token", true) {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
}
|
||||
fmt.Printf("Username (email address)> ")
|
||||
@@ -114,10 +113,11 @@ func init() {
|
||||
// return shouldRetry(ctx, resp, err)
|
||||
//})
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to get token: %v", err)
|
||||
return errors.Wrap(err, "failed to get token")
|
||||
}
|
||||
opt.RefreshToken = resp.Header.Get("Location")
|
||||
m.Set("refresh_token", opt.RefreshToken)
|
||||
return nil
|
||||
},
|
||||
Options: []fs.Option{{
|
||||
Name: "app_id",
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -42,7 +41,7 @@ func init() {
|
||||
Name: "tardigrade",
|
||||
Description: "Tardigrade Decentralized Cloud Storage",
|
||||
NewFs: NewFs,
|
||||
Config: func(ctx context.Context, name string, configMapper configmap.Mapper) {
|
||||
Config: func(ctx context.Context, name string, configMapper configmap.Mapper) error {
|
||||
provider, _ := configMapper.Get(fs.ConfigProvider)
|
||||
|
||||
config.FileDeleteKey(name, fs.ConfigProvider)
|
||||
@@ -54,7 +53,7 @@ func init() {
|
||||
|
||||
// satelliteString contains always default and passphrase can be empty
|
||||
if apiKey == "" {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
satellite, found := satMap[satelliteString]
|
||||
@@ -64,12 +63,12 @@ func init() {
|
||||
|
||||
access, err := uplink.RequestAccessWithPassphrase(context.TODO(), satellite, apiKey, passphrase)
|
||||
if err != nil {
|
||||
log.Fatalf("Couldn't create access grant: %v", err)
|
||||
return errors.Wrap(err, "couldn't create access grant")
|
||||
}
|
||||
|
||||
serializedAccess, err := access.Serialize()
|
||||
if err != nil {
|
||||
log.Fatalf("Couldn't serialize access grant: %v", err)
|
||||
return errors.Wrap(err, "couldn't serialize access grant")
|
||||
}
|
||||
configMapper.Set("satellite_address", satellite)
|
||||
configMapper.Set("access_grant", serializedAccess)
|
||||
@@ -78,8 +77,9 @@ func init() {
|
||||
config.FileDeleteKey(name, "api_key")
|
||||
config.FileDeleteKey(name, "passphrase")
|
||||
} else {
|
||||
log.Fatalf("Invalid provider type: %s", provider)
|
||||
return errors.Errorf("invalid provider type: %s", provider)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Options: []fs.Option{
|
||||
{
|
||||
|
||||
170
backend/uptobox/api/types.go
Normal file
170
backend/uptobox/api/types.go
Normal file
@@ -0,0 +1,170 @@
|
||||
package api
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Error contains the error code and message returned by the API
|
||||
type Error struct {
|
||||
Success bool `json:"success,omitempty"`
|
||||
StatusCode int `json:"statusCode,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Data string `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// Error returns a string for the error and satisfies the error interface
|
||||
func (e Error) Error() string {
|
||||
out := fmt.Sprintf("api error %d", e.StatusCode)
|
||||
if e.Message != "" {
|
||||
out += ": " + e.Message
|
||||
}
|
||||
if e.Data != "" {
|
||||
out += ": " + e.Data
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// FolderEntry represents a Uptobox subfolder when listing folder contents
|
||||
type FolderEntry struct {
|
||||
FolderID uint64 `json:"fld_id"`
|
||||
Description string `json:"fld_descr"`
|
||||
Password string `json:"fld_password"`
|
||||
FullPath string `json:"fullPath"`
|
||||
Path string `json:"fld_name"`
|
||||
Name string `json:"name"`
|
||||
Hash string `json:"hash"`
|
||||
}
|
||||
|
||||
// FolderInfo represents the current folder when listing folder contents
|
||||
type FolderInfo struct {
|
||||
FolderID uint64 `json:"fld_id"`
|
||||
Hash string `json:"hash"`
|
||||
FileCount uint64 `json:"fileCount"`
|
||||
TotalFileSize int64 `json:"totalFileSize"`
|
||||
}
|
||||
|
||||
// FileInfo represents a file when listing folder contents
|
||||
type FileInfo struct {
|
||||
Name string `json:"file_name"`
|
||||
Description string `json:"file_descr"`
|
||||
Created string `json:"file_created"`
|
||||
Size int64 `json:"file_size"`
|
||||
Downloads uint64 `json:"file_downloads"`
|
||||
Code string `json:"file_code"`
|
||||
Password string `json:"file_password"`
|
||||
Public int `json:"file_public"`
|
||||
LastDownload string `json:"file_last_download"`
|
||||
ID uint64 `json:"id"`
|
||||
}
|
||||
|
||||
// ReadMetadataResponse is the response when listing folder contents
|
||||
type ReadMetadataResponse struct {
|
||||
StatusCode int `json:"statusCode"`
|
||||
Message string `json:"message"`
|
||||
Data struct {
|
||||
CurrentFolder FolderInfo `json:"currentFolder"`
|
||||
Folders []FolderEntry `json:"folders"`
|
||||
Files []FileInfo `json:"files"`
|
||||
PageCount int `json:"pageCount"`
|
||||
TotalFileCount int `json:"totalFileCount"`
|
||||
TotalFileSize int64 `json:"totalFileSize"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// UploadInfo is the response when initiating an upload
|
||||
type UploadInfo struct {
|
||||
StatusCode int `json:"statusCode"`
|
||||
Message string `json:"message"`
|
||||
Data struct {
|
||||
UploadLink string `json:"uploadLink"`
|
||||
MaxUpload string `json:"maxUpload"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// UploadResponse is the respnse to a successful upload
|
||||
type UploadResponse struct {
|
||||
Files []struct {
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
URL string `json:"url"`
|
||||
DeleteURL string `json:"deleteUrl"`
|
||||
} `json:"files"`
|
||||
}
|
||||
|
||||
// UpdateResponse is a generic response to various action on files (rename/copy/move)
|
||||
type UpdateResponse struct {
|
||||
Message string `json:"message"`
|
||||
StatusCode int `json:"statusCode"`
|
||||
}
|
||||
|
||||
// Download is the response when requesting a download link
|
||||
type Download struct {
|
||||
StatusCode int `json:"statusCode"`
|
||||
Message string `json:"message"`
|
||||
Data struct {
|
||||
DownloadLink string `json:"dlLink"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// MetadataRequestOptions represents all the options when listing folder contents
|
||||
type MetadataRequestOptions struct {
|
||||
Limit uint64
|
||||
Offset uint64
|
||||
SearchField string
|
||||
Search string
|
||||
}
|
||||
|
||||
// CreateFolderRequest is used for creating a folder
|
||||
type CreateFolderRequest struct {
|
||||
Token string `json:"token"`
|
||||
Path string `json:"path"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// DeleteFolderRequest is used for deleting a folder
|
||||
type DeleteFolderRequest struct {
|
||||
Token string `json:"token"`
|
||||
FolderID uint64 `json:"fld_id"`
|
||||
}
|
||||
|
||||
// CopyMoveFileRequest is used for moving/copying a file
|
||||
type CopyMoveFileRequest struct {
|
||||
Token string `json:"token"`
|
||||
FileCodes string `json:"file_codes"`
|
||||
DestinationFolderID uint64 `json:"destination_fld_id"`
|
||||
Action string `json:"action"`
|
||||
}
|
||||
|
||||
// MoveFolderRequest is used for moving a folder
|
||||
type MoveFolderRequest struct {
|
||||
Token string `json:"token"`
|
||||
FolderID uint64 `json:"fld_id"`
|
||||
DestinationFolderID uint64 `json:"destination_fld_id"`
|
||||
Action string `json:"action"`
|
||||
}
|
||||
|
||||
// RenameFolderRequest is used for renaming a folder
|
||||
type RenameFolderRequest struct {
|
||||
Token string `json:"token"`
|
||||
FolderID uint64 `json:"fld_id"`
|
||||
NewName string `json:"new_name"`
|
||||
}
|
||||
|
||||
// UpdateFileInformation is used for renaming a file
|
||||
type UpdateFileInformation struct {
|
||||
Token string `json:"token"`
|
||||
FileCode string `json:"file_code"`
|
||||
NewName string `json:"new_name,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
Public string `json:"public,omitempty"`
|
||||
}
|
||||
|
||||
// RemoveFileRequest is used for deleting a file
|
||||
type RemoveFileRequest struct {
|
||||
Token string `json:"token"`
|
||||
FileCodes string `json:"file_codes"`
|
||||
}
|
||||
|
||||
// Token represents the authentication token
|
||||
type Token struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
1055
backend/uptobox/uptobox.go
Normal file
1055
backend/uptobox/uptobox.go
Normal file
File diff suppressed because it is too large
Load Diff
21
backend/uptobox/uptobox_test.go
Normal file
21
backend/uptobox/uptobox_test.go
Normal file
@@ -0,0 +1,21 @@
|
||||
// Test Uptobox filesystem interface
|
||||
package uptobox_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/rclone/rclone/backend/uptobox"
|
||||
"github.com/rclone/rclone/fstest"
|
||||
"github.com/rclone/rclone/fstest/fstests"
|
||||
)
|
||||
|
||||
// TestIntegration runs integration tests against the remote
|
||||
func TestIntegration(t *testing.T) {
|
||||
if *fstest.RemoteName == "" {
|
||||
*fstest.RemoteName = "TestUptobox:"
|
||||
}
|
||||
fstests.Run(t, &fstests.Opt{
|
||||
RemoteName: *fstest.RemoteName,
|
||||
NilObject: (*uptobox.Object)(nil),
|
||||
})
|
||||
}
|
||||
@@ -125,7 +125,7 @@ func (ca *CookieAuth) getSPCookie(conf *SharepointSuccessResponse) (*CookieRespo
|
||||
return nil, errors.Wrap(err, "Error while constructing endpoint URL")
|
||||
}
|
||||
|
||||
u, err := url.Parse("https://" + spRoot.Host + "/_forms/default.aspx?wa=wsignin1.0")
|
||||
u, err := url.Parse(spRoot.Scheme + "://" + spRoot.Host + "/_forms/default.aspx?wa=wsignin1.0")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Error while constructing login URL")
|
||||
}
|
||||
|
||||
@@ -60,12 +60,12 @@ func init() {
|
||||
Name: "yandex",
|
||||
Description: "Yandex Disk",
|
||||
NewFs: NewFs,
|
||||
Config: func(ctx context.Context, name string, m configmap.Mapper) {
|
||||
Config: func(ctx context.Context, name string, m configmap.Mapper) error {
|
||||
err := oauthutil.Config(ctx, "yandex", name, m, oauthConfig, nil)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to configure token: %v", err)
|
||||
return
|
||||
return errors.Wrap(err, "failed to configure token")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Options: append(oauthutil.SharedOptions, []fs.Option{{
|
||||
Name: config.ConfigEncoding,
|
||||
@@ -251,22 +251,22 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
||||
|
||||
token, err := oauthutil.GetToken(name, m)
|
||||
if err != nil {
|
||||
log.Fatalf("Couldn't read OAuth token (this should never happen).")
|
||||
return nil, errors.Wrap(err, "couldn't read OAuth token")
|
||||
}
|
||||
if token.RefreshToken == "" {
|
||||
log.Fatalf("Unable to get RefreshToken. If you are upgrading from older versions of rclone, please run `rclone config` and re-configure this backend.")
|
||||
return nil, errors.New("unable to get RefreshToken. If you are upgrading from older versions of rclone, please run `rclone config` and re-configure this backend")
|
||||
}
|
||||
if token.TokenType != "OAuth" {
|
||||
token.TokenType = "OAuth"
|
||||
err = oauthutil.PutToken(name, m, token, false)
|
||||
if err != nil {
|
||||
log.Fatalf("Couldn't save OAuth token (this should never happen).")
|
||||
return nil, errors.Wrap(err, "couldn't save OAuth token")
|
||||
}
|
||||
log.Printf("Automatically upgraded OAuth config.")
|
||||
}
|
||||
oAuthClient, _, err := oauthutil.NewClient(ctx, name, m, oauthConfig)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to configure Yandex: %v", err)
|
||||
return nil, errors.Wrap(err, "failed to configure Yandex")
|
||||
}
|
||||
|
||||
ci := fs.GetConfig(ctx)
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
@@ -73,32 +72,41 @@ func init() {
|
||||
Name: "zoho",
|
||||
Description: "Zoho",
|
||||
NewFs: NewFs,
|
||||
Config: func(ctx context.Context, name string, m configmap.Mapper) {
|
||||
Config: func(ctx context.Context, name string, m configmap.Mapper) error {
|
||||
// Need to setup region before configuring oauth
|
||||
setupRegion(m)
|
||||
err := setupRegion(m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opt := oauthutil.Options{
|
||||
// No refresh token unless ApprovalForce is set
|
||||
OAuth2Opts: []oauth2.AuthCodeOption{oauth2.ApprovalForce},
|
||||
}
|
||||
if err := oauthutil.Config(ctx, "zoho", name, m, oauthConfig, &opt); err != nil {
|
||||
log.Fatalf("Failed to configure token: %v", err)
|
||||
return errors.Wrap(err, "failed to configure token")
|
||||
}
|
||||
// We need to rewrite the token type to "Zoho-oauthtoken" because Zoho wants
|
||||
// it's own custom type
|
||||
token, err := oauthutil.GetToken(name, m)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to read token: %v", err)
|
||||
return errors.Wrap(err, "failed to read token")
|
||||
}
|
||||
if token.TokenType != "Zoho-oauthtoken" {
|
||||
token.TokenType = "Zoho-oauthtoken"
|
||||
err = oauthutil.PutToken(name, m, token, false)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to configure token: %v", err)
|
||||
return errors.Wrap(err, "failed to configure token")
|
||||
}
|
||||
}
|
||||
if err = setupRoot(ctx, name, m); err != nil {
|
||||
log.Fatalf("Failed to configure root directory: %v", err)
|
||||
|
||||
if fs.GetConfig(ctx).AutoConfirm {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err = setupRoot(ctx, name, m); err != nil {
|
||||
return errors.Wrap(err, "failed to configure root directory")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Options: append(oauthutil.SharedOptions, []fs.Option{{
|
||||
Name: "region",
|
||||
@@ -159,15 +167,16 @@ type Object struct {
|
||||
|
||||
// ------------------------------------------------------------
|
||||
|
||||
func setupRegion(m configmap.Mapper) {
|
||||
func setupRegion(m configmap.Mapper) error {
|
||||
region, ok := m.Get("region")
|
||||
if !ok {
|
||||
log.Fatalf("No region set\n")
|
||||
if !ok || region == "" {
|
||||
return errors.New("no region set")
|
||||
}
|
||||
rootURL = fmt.Sprintf("https://workdrive.zoho.%s/api/v1", region)
|
||||
accountsURL = fmt.Sprintf("https://accounts.zoho.%s", region)
|
||||
oauthConfig.Endpoint.AuthURL = fmt.Sprintf("https://accounts.zoho.%s/oauth/v2/auth", region)
|
||||
oauthConfig.Endpoint.TokenURL = fmt.Sprintf("https://accounts.zoho.%s/oauth/v2/token", region)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
@@ -203,7 +212,7 @@ func listWorkspaces(ctx context.Context, teamID string, srv *rest.Client) ([]api
|
||||
func setupRoot(ctx context.Context, name string, m configmap.Mapper) error {
|
||||
oAuthClient, _, err := oauthutil.NewClient(ctx, name, m, oauthConfig)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load oAuthClient: %s", err)
|
||||
return errors.Wrap(err, "failed to load oAuthClient")
|
||||
}
|
||||
authSrv := rest.NewClient(oAuthClient).SetRoot(accountsURL)
|
||||
opts := rest.Opts{
|
||||
@@ -372,7 +381,10 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
||||
if err := configstruct.Set(m, opt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
setupRegion(m)
|
||||
err := setupRegion(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
root = parsePath(root)
|
||||
oAuthClient, _, err := oauthutil.NewClient(ctx, name, m, oauthConfig)
|
||||
|
||||
@@ -62,6 +62,7 @@ docs = [
|
||||
"sftp.md",
|
||||
"sugarsync.md",
|
||||
"tardigrade.md",
|
||||
"uptobox.md",
|
||||
"union.md",
|
||||
"webdav.md",
|
||||
"yandex.md",
|
||||
|
||||
@@ -44,10 +44,10 @@ var commandDefinition = &cobra.Command{
|
||||
Use: "about remote:",
|
||||
Short: `Get quota information from the remote.`,
|
||||
Long: `
|
||||
` + "`rclone about`" + `prints quota information about a remote to standard
|
||||
` + "`rclone about`" + ` prints quota information about a remote to standard
|
||||
output. The output is typically used, free, quota and trash contents.
|
||||
|
||||
E.g. Typical output from` + "`rclone about remote:`" + `is:
|
||||
E.g. Typical output from ` + "`rclone about remote:`" + ` is:
|
||||
|
||||
Total: 17G
|
||||
Used: 7.444G
|
||||
@@ -75,7 +75,7 @@ Applying a ` + "`--full`" + ` flag to the command prints the bytes in full, e.g.
|
||||
Trashed: 104857602
|
||||
Other: 8849156022
|
||||
|
||||
A ` + "`--json`" + `flag generates conveniently computer readable output, e.g.
|
||||
A ` + "`--json`" + ` flag generates conveniently computer readable output, e.g.
|
||||
|
||||
{
|
||||
"total": 18253611008,
|
||||
|
||||
@@ -54,6 +54,7 @@ import (
|
||||
_ "github.com/rclone/rclone/cmd/size"
|
||||
_ "github.com/rclone/rclone/cmd/sync"
|
||||
_ "github.com/rclone/rclone/cmd/test"
|
||||
_ "github.com/rclone/rclone/cmd/test/changenotify"
|
||||
_ "github.com/rclone/rclone/cmd/test/histogram"
|
||||
_ "github.com/rclone/rclone/cmd/test/info"
|
||||
_ "github.com/rclone/rclone/cmd/test/makefiles"
|
||||
|
||||
20
cmd/cmd.go
20
cmd/cmd.go
@@ -75,8 +75,19 @@ const (
|
||||
|
||||
// ShowVersion prints the version to stdout
|
||||
func ShowVersion() {
|
||||
osVersion, osKernel := buildinfo.GetOSVersion()
|
||||
if osVersion == "" {
|
||||
osVersion = "unknown"
|
||||
}
|
||||
if osKernel == "" {
|
||||
osKernel = "unknown"
|
||||
}
|
||||
|
||||
linking, tagString := buildinfo.GetLinkingAndTags()
|
||||
|
||||
fmt.Printf("rclone %s\n", fs.Version)
|
||||
fmt.Printf("- os/version: %s\n", osVersion)
|
||||
fmt.Printf("- os/kernel: %s\n", osKernel)
|
||||
fmt.Printf("- os/type: %s\n", runtime.GOOS)
|
||||
fmt.Printf("- os/arch: %s\n", runtime.GOARCH)
|
||||
fmt.Printf("- go/version: %s\n", runtime.Version())
|
||||
@@ -389,7 +400,10 @@ func initConfig() {
|
||||
configflags.SetFlags(ci)
|
||||
|
||||
// Load the config
|
||||
configfile.LoadConfig(ctx)
|
||||
err := configfile.LoadConfig(ctx)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load config: %v", err)
|
||||
}
|
||||
|
||||
// Start accounting
|
||||
accounting.Start(ctx)
|
||||
@@ -400,7 +414,7 @@ func initConfig() {
|
||||
}
|
||||
|
||||
// Load filters
|
||||
err := filterflags.Reload(ctx)
|
||||
err = filterflags.Reload(ctx)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load filters: %v", err)
|
||||
}
|
||||
@@ -553,7 +567,7 @@ func Main() {
|
||||
setupRootCommand(Root)
|
||||
AddBackendFlags()
|
||||
if err := Root.Execute(); err != nil {
|
||||
if strings.HasPrefix(err.Error(), "unknown command") {
|
||||
if strings.HasPrefix(err.Error(), "unknown command") && selfupdateEnabled {
|
||||
Root.PrintErrf("You could use '%s selfupdate' to get latest features.\n\n", Root.CommandPath())
|
||||
}
|
||||
log.Fatalf("Fatal error: %v", err)
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"github.com/rclone/rclone/cmd/mountlib"
|
||||
"github.com/rclone/rclone/fs"
|
||||
"github.com/rclone/rclone/lib/atexit"
|
||||
"github.com/rclone/rclone/lib/buildinfo"
|
||||
"github.com/rclone/rclone/vfs"
|
||||
)
|
||||
|
||||
@@ -35,6 +36,7 @@ func init() {
|
||||
cmd.Aliases = append(cmd.Aliases, "cmount")
|
||||
}
|
||||
mountlib.AddRc("cmount", mount)
|
||||
buildinfo.Tags = append(buildinfo.Tags, "cmount")
|
||||
}
|
||||
|
||||
// Find the option string in the current options
|
||||
|
||||
@@ -22,6 +22,7 @@ func init() {
|
||||
cmd.Root.AddCommand(configCommand)
|
||||
configCommand.AddCommand(configEditCommand)
|
||||
configCommand.AddCommand(configFileCommand)
|
||||
configCommand.AddCommand(configTouchCommand)
|
||||
configCommand.AddCommand(configShowCommand)
|
||||
configCommand.AddCommand(configDumpCommand)
|
||||
configCommand.AddCommand(configProvidersCommand)
|
||||
@@ -41,9 +42,9 @@ var configCommand = &cobra.Command{
|
||||
remotes and manage existing ones. You may also set or remove a
|
||||
password to protect your configuration.
|
||||
`,
|
||||
Run: func(command *cobra.Command, args []string) {
|
||||
RunE: func(command *cobra.Command, args []string) error {
|
||||
cmd.CheckArgs(0, 0, command, args)
|
||||
config.EditConfig(context.Background())
|
||||
return config.EditConfig(context.Background())
|
||||
},
|
||||
}
|
||||
|
||||
@@ -63,6 +64,15 @@ var configFileCommand = &cobra.Command{
|
||||
},
|
||||
}
|
||||
|
||||
var configTouchCommand = &cobra.Command{
|
||||
Use: "touch",
|
||||
Short: `Ensure configuration file exists.`,
|
||||
Run: func(command *cobra.Command, args []string) {
|
||||
cmd.CheckArgs(0, 0, command, args)
|
||||
config.SaveConfig()
|
||||
},
|
||||
}
|
||||
|
||||
var configShowCommand = &cobra.Command{
|
||||
Use: "show [<remote>]",
|
||||
Short: `Print (decrypted) config file, or the config for a single remote.`,
|
||||
@@ -262,8 +272,7 @@ This normally means going through the interactive oauth flow again.
|
||||
if fsInfo.Config == nil {
|
||||
return errors.Errorf("%s: doesn't support Reconnect", configName)
|
||||
}
|
||||
fsInfo.Config(ctx, configName, config)
|
||||
return nil
|
||||
return fsInfo.Config(ctx, configName, config)
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ var commandDefinition = &cobra.Command{
|
||||
Download a URL's content and copy it to the destination without saving
|
||||
it in temporary storage.
|
||||
|
||||
Setting ` + "`--auto-filename`" + `will cause the file name to be retrieved from
|
||||
Setting ` + "`--auto-filename`" + ` will cause the file name to be retrieved from
|
||||
the from URL (after any redirections) and used in the destination
|
||||
path. With ` + "`--print-filename`" + ` in addition, the resuling file name will
|
||||
be printed.
|
||||
|
||||
@@ -3,7 +3,6 @@ package link
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/rclone/rclone/cmd"
|
||||
"github.com/rclone/rclone/fs"
|
||||
@@ -13,7 +12,7 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
expire = fs.Duration(time.Hour * 24 * 365 * 100)
|
||||
expire = fs.DurationOff
|
||||
unlink = false
|
||||
)
|
||||
|
||||
|
||||
@@ -334,7 +334,7 @@ metadata about files like in UNIX. One case that may arise is that other program
|
||||
(incorrectly) interprets this as the file being accessible by everyone. For example
|
||||
an SSH client may warn about "unprotected private key file".
|
||||
|
||||
WinFsp 2021 (version 1.9, still in beta) introduces a new FUSE option "FileSecurity",
|
||||
WinFsp 2021 (version 1.9) introduces a new FUSE option "FileSecurity",
|
||||
that allows the complete specification of file security descriptors using
|
||||
[SDDL](https://docs.microsoft.com/en-us/windows/win32/secauthz/security-descriptor-string-format).
|
||||
With this you can work around issues such as the mentioned "unprotected private key file"
|
||||
@@ -342,19 +342,38 @@ by specifying |-o FileSecurity="D:P(A;;FA;;;OW)"|, for file all access (FA) to t
|
||||
|
||||
#### Windows caveats
|
||||
|
||||
Note that drives created as Administrator are not visible by other
|
||||
accounts (including the account that was elevated as
|
||||
Administrator). So if you start a Windows drive from an Administrative
|
||||
Command Prompt and then try to access the same drive from Explorer
|
||||
(which does not run as Administrator), you will not be able to see the
|
||||
new drive.
|
||||
Drives created as Administrator are not visible to other accounts,
|
||||
not even an account that was elevated to Administrator with the
|
||||
User Account Control (UAC) feature. A result of this is that if you mount
|
||||
to a drive letter from a Command Prompt run as Administrator, and then try
|
||||
to access the same drive from Windows Explorer (which does not run as
|
||||
Administrator), you will not be able to see the mounted drive.
|
||||
|
||||
The easiest way around this is to start the drive from a normal
|
||||
command prompt. It is also possible to start a drive from the SYSTEM
|
||||
account (using [the WinFsp.Launcher
|
||||
infrastructure](https://github.com/billziss-gh/winfsp/wiki/WinFsp-Service-Architecture))
|
||||
which creates drives accessible for everyone on the system or
|
||||
alternatively using [the nssm service manager](https://nssm.cc/usage).
|
||||
If you don't need to access the drive from applications running with
|
||||
administrative privileges, the easiest way around this is to always
|
||||
create the mount from a non-elevated command prompt.
|
||||
|
||||
To make mapped drives available to the user account that created them
|
||||
regardless if elevated or not, there is a special Windows setting called
|
||||
[linked connections](https://docs.microsoft.com/en-us/troubleshoot/windows-client/networking/mapped-drives-not-available-from-elevated-command#detail-to-configure-the-enablelinkedconnections-registry-entry)
|
||||
that can be enabled.
|
||||
|
||||
It is also possible to make a drive mount available to everyone on the system,
|
||||
by running the process creating it as the built-in SYSTEM account.
|
||||
There are several ways to do this: One is to use the command-line
|
||||
utility [PsExec](https://docs.microsoft.com/en-us/sysinternals/downloads/psexec),
|
||||
from Microsoft's Sysinternals suite, which has option |-s| to start
|
||||
processes as the SYSTEM account. Another alternative is to run the mount
|
||||
command from a Windows Scheduled Task, or a Windows Service, configured
|
||||
to run as the SYSTEM account. A third alternative is to use the
|
||||
[WinFsp.Launcher infrastructure](https://github.com/billziss-gh/winfsp/wiki/WinFsp-Service-Architecture)).
|
||||
Note that when running rclone as another user, it will not use
|
||||
the configuration file from your profile unless you tell it to
|
||||
with the [|--config|](https://rclone.org/docs/#config-config-file) option.
|
||||
Read more in the [install documentation](https://rclone.org/install/).
|
||||
|
||||
Note that mapping to a directory path, instead of a drive letter,
|
||||
does not suffer from the same limitations.
|
||||
|
||||
### Limitations
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
|
||||
func TestRc(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
configfile.LoadConfig(ctx)
|
||||
require.NoError(t, configfile.LoadConfig(ctx))
|
||||
mount := rc.Calls.Get("mount/mount")
|
||||
assert.NotNil(t, mount)
|
||||
unmount := rc.Calls.Get("mount/unmount")
|
||||
|
||||
@@ -485,11 +485,15 @@ func (u *UI) removeEntry(pos int) {
|
||||
|
||||
// delete the entry at the current position
|
||||
func (u *UI) delete() {
|
||||
if u.d == nil || len(u.entries) == 0 {
|
||||
return
|
||||
}
|
||||
ctx := context.Background()
|
||||
dirPos := u.sortPerm[u.dirPosMap[u.path].entry]
|
||||
entry := u.entries[dirPos]
|
||||
cursorPos := u.dirPosMap[u.path]
|
||||
dirPos := u.sortPerm[cursorPos.entry]
|
||||
dirEntry := u.entries[dirPos]
|
||||
u.boxMenu = []string{"cancel", "confirm"}
|
||||
if obj, isFile := entry.(fs.Object); isFile {
|
||||
if obj, isFile := dirEntry.(fs.Object); isFile {
|
||||
u.boxMenuHandler = func(f fs.Fs, p string, o int) (string, error) {
|
||||
if o != 1 {
|
||||
return "Aborted!", nil
|
||||
@@ -499,27 +503,33 @@ func (u *UI) delete() {
|
||||
return "", err
|
||||
}
|
||||
u.removeEntry(dirPos)
|
||||
if cursorPos.entry >= len(u.entries) {
|
||||
u.move(-1) // move back onto a valid entry
|
||||
}
|
||||
return "Successfully deleted file!", nil
|
||||
}
|
||||
u.popupBox([]string{
|
||||
"Delete this file?",
|
||||
u.fsName + entry.String()})
|
||||
u.fsName + dirEntry.String()})
|
||||
} else {
|
||||
u.boxMenuHandler = func(f fs.Fs, p string, o int) (string, error) {
|
||||
if o != 1 {
|
||||
return "Aborted!", nil
|
||||
}
|
||||
err := operations.Purge(ctx, f, entry.String())
|
||||
err := operations.Purge(ctx, f, dirEntry.String())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
u.removeEntry(dirPos)
|
||||
if cursorPos.entry >= len(u.entries) {
|
||||
u.move(-1) // move back onto a valid entry
|
||||
}
|
||||
return "Successfully purged folder!", nil
|
||||
}
|
||||
u.popupBox([]string{
|
||||
"Purge this directory?",
|
||||
"ALL files in it will be deleted",
|
||||
u.fsName + entry.String()})
|
||||
u.fsName + dirEntry.String()})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,12 +7,19 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/rclone/rclone/cmd"
|
||||
"github.com/rclone/rclone/fs/config/flags"
|
||||
"github.com/rclone/rclone/fs/operations"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
size = int64(-1)
|
||||
)
|
||||
|
||||
func init() {
|
||||
cmd.Root.AddCommand(commandDefinition)
|
||||
cmdFlags := commandDefinition.Flags()
|
||||
flags.Int64VarP(cmdFlags, &size, "size", "", size, "File size hint to preallocate")
|
||||
}
|
||||
|
||||
var commandDefinition = &cobra.Command{
|
||||
@@ -37,6 +44,13 @@ must fit into RAM. The cutoff needs to be small enough to adhere
|
||||
the limits of your remote, please see there. Generally speaking,
|
||||
setting this cutoff too high will decrease your performance.
|
||||
|
||||
Use the |--size| flag to preallocate the file in advance at the remote end
|
||||
and actually stream it, even if remote backend doesn't support streaming.
|
||||
|
||||
|--size| should be the exact size of the input stream in bytes. If the
|
||||
size of the stream is different in length to the |--size| passed in
|
||||
then the transfer will likely fail.
|
||||
|
||||
Note that the upload can also not be retried because the data is
|
||||
not kept around until the upload succeeds. If you need to transfer
|
||||
a lot of data, you're better off caching locally and then
|
||||
@@ -51,7 +65,7 @@ a lot of data, you're better off caching locally and then
|
||||
|
||||
fdst, dstFileName := cmd.NewFsDstFile(args)
|
||||
cmd.Run(false, false, command, func() error {
|
||||
_, err := operations.Rcat(context.Background(), fdst, dstFileName, os.Stdin, time.Now())
|
||||
_, err := operations.RcatSize(context.Background(), fdst, dstFileName, os.Stdin, size, time.Now())
|
||||
return err
|
||||
})
|
||||
},
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// +build !noselfupdate
|
||||
|
||||
package selfupdate
|
||||
|
||||
// Note: "|" will be replaced by backticks in the help string below
|
||||
@@ -27,7 +29,7 @@ If the old version contains only dots and digits (for example |v1.54.0|)
|
||||
then it's a stable release so you won't need the |--beta| flag. Beta releases
|
||||
have an additional information similar to |v1.54.0-beta.5111.06f1c0c61|.
|
||||
(if you are a developer and use a locally built rclone, the version number
|
||||
will end with |-DEV|, you will have to rebuild it as it obvisously can't
|
||||
will end with |-DEV|, you will have to rebuild it as it obviously can't
|
||||
be distributed).
|
||||
|
||||
If you previously installed rclone via a package manager, the package may
|
||||
|
||||
11
cmd/selfupdate/noselfupdate.go
Normal file
11
cmd/selfupdate/noselfupdate.go
Normal file
@@ -0,0 +1,11 @@
|
||||
// +build noselfupdate
|
||||
|
||||
package selfupdate
|
||||
|
||||
import (
|
||||
"github.com/rclone/rclone/lib/buildinfo"
|
||||
)
|
||||
|
||||
func init() {
|
||||
buildinfo.Tags = append(buildinfo.Tags, "noselfupdate")
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
// +build !noselfupdate
|
||||
|
||||
package selfupdate
|
||||
|
||||
import (
|
||||
@@ -143,14 +145,9 @@ func InstallUpdate(ctx context.Context, opt *Options) error {
|
||||
return errors.New("--stable and --beta are mutually exclusive")
|
||||
}
|
||||
|
||||
gotCmount := false
|
||||
for _, tag := range buildinfo.Tags {
|
||||
if tag == "cmount" {
|
||||
gotCmount = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if gotCmount && !cmount.ProvidedBy(runtime.GOOS) {
|
||||
// The `cmount` tag is added by cmd/cmount/mount.go only if build is static.
|
||||
_, tags := buildinfo.GetLinkingAndTags()
|
||||
if strings.Contains(" "+tags+" ", " cmount ") && !cmount.ProvidedBy(runtime.GOOS) {
|
||||
return errors.New("updating would discard the mount FUSE capability, aborting")
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// +build !noselfupdate
|
||||
|
||||
package selfupdate
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// +build !noselfupdate
|
||||
|
||||
package selfupdate
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// +build !windows,!plan9,!js
|
||||
// +build !noselfupdate
|
||||
|
||||
package selfupdate
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// +build plan9 js
|
||||
// +build !noselfupdate
|
||||
|
||||
package selfupdate
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// +build windows
|
||||
// +build !noselfupdate
|
||||
|
||||
package selfupdate
|
||||
|
||||
|
||||
5
cmd/selfupdate_disabled.go
Normal file
5
cmd/selfupdate_disabled.go
Normal file
@@ -0,0 +1,5 @@
|
||||
// +build noselfupdate
|
||||
|
||||
package cmd
|
||||
|
||||
const selfupdateEnabled = false
|
||||
7
cmd/selfupdate_enabled.go
Normal file
7
cmd/selfupdate_enabled.go
Normal file
@@ -0,0 +1,7 @@
|
||||
// +build !noselfupdate
|
||||
|
||||
package cmd
|
||||
|
||||
// This constant must be in the `cmd` package rather than `cmd/selfupdate`
|
||||
// to prevent build failure due to dependency loop.
|
||||
const selfupdateEnabled = true
|
||||
@@ -41,7 +41,7 @@ func startServer(t *testing.T, f fs.Fs) {
|
||||
}
|
||||
|
||||
func TestInit(t *testing.T) {
|
||||
configfile.LoadConfig(context.Background())
|
||||
require.NoError(t, configfile.LoadConfig(context.Background()))
|
||||
|
||||
f, err := fs.NewFs(context.Background(), "testdata/files")
|
||||
l, _ := f.List(context.Background(), "")
|
||||
|
||||
@@ -61,7 +61,7 @@ var (
|
||||
func TestInit(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
// Configure the remote
|
||||
configfile.LoadConfig(context.Background())
|
||||
require.NoError(t, configfile.LoadConfig(context.Background()))
|
||||
// fs.Config.LogLevel = fs.LogLevelDebug
|
||||
// fs.Config.DumpHeaders = true
|
||||
// fs.Config.DumpBodies = true
|
||||
|
||||
@@ -66,7 +66,7 @@ func createOverwriteDeleteSeq(t testing.TB, path string) []TestRequest {
|
||||
// TestResticHandler runs tests on the restic handler code, especially in append-only mode.
|
||||
func TestResticHandler(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
configfile.LoadConfig(ctx)
|
||||
require.NoError(t, configfile.LoadConfig(ctx))
|
||||
buf := make([]byte, 32)
|
||||
_, err := io.ReadFull(rand.Reader, buf)
|
||||
require.NoError(t, err)
|
||||
|
||||
54
cmd/test/changenotify/changenotify.go
Normal file
54
cmd/test/changenotify/changenotify.go
Normal file
@@ -0,0 +1,54 @@
|
||||
// Package changenotify tests rclone's changenotify support
|
||||
package changenotify
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/rclone/rclone/cmd"
|
||||
"github.com/rclone/rclone/cmd/test"
|
||||
"github.com/rclone/rclone/fs"
|
||||
"github.com/rclone/rclone/fs/config/flags"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
pollInterval = 10 * time.Second
|
||||
)
|
||||
|
||||
func init() {
|
||||
test.Command.AddCommand(commandDefinition)
|
||||
cmdFlags := commandDefinition.Flags()
|
||||
flags.DurationVarP(cmdFlags, &pollInterval, "poll-interval", "", pollInterval, "Time to wait between polling for changes.")
|
||||
}
|
||||
|
||||
var commandDefinition = &cobra.Command{
|
||||
Use: "changenotify remote:",
|
||||
Short: `Log any change notify requests for the remote passed in.`,
|
||||
RunE: func(command *cobra.Command, args []string) error {
|
||||
cmd.CheckArgs(1, 1, command, args)
|
||||
f := cmd.NewFsSrc(args)
|
||||
ctx := context.Background()
|
||||
|
||||
// Start polling function
|
||||
features := f.Features()
|
||||
if do := features.ChangeNotify; do != nil {
|
||||
pollChan := make(chan time.Duration)
|
||||
do(ctx, changeNotify, pollChan)
|
||||
pollChan <- pollInterval
|
||||
fs.Logf(nil, "Waiting for changes, polling every %v", pollInterval)
|
||||
} else {
|
||||
return errors.New("poll-interval is not supported by this remote")
|
||||
}
|
||||
select {}
|
||||
},
|
||||
}
|
||||
|
||||
// changeNotify invalidates the directory cache for the relativePath
|
||||
// passed in.
|
||||
//
|
||||
// if entryType is a directory it invalidates the parent of the directory too.
|
||||
func changeNotify(relativePath string, entryType fs.EntryType) {
|
||||
fs.Logf(nil, "%q: %v", relativePath, entryType)
|
||||
}
|
||||
@@ -3,12 +3,12 @@
|
||||
package makefiles
|
||||
|
||||
import (
|
||||
cryptrand "crypto/rand"
|
||||
"io"
|
||||
"log"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/rclone/rclone/cmd"
|
||||
"github.com/rclone/rclone/cmd/test"
|
||||
@@ -27,8 +27,10 @@ var (
|
||||
maxFileSize = fs.SizeSuffix(100)
|
||||
minFileNameLength = 4
|
||||
maxFileNameLength = 12
|
||||
seed = int64(1)
|
||||
|
||||
// Globals
|
||||
randSource *rand.Rand
|
||||
directoriesToCreate int
|
||||
totalDirectories int
|
||||
fileNames = map[string]struct{}{} // keep a note of which file name we've used already
|
||||
@@ -44,6 +46,7 @@ func init() {
|
||||
flags.FVarP(cmdFlags, &maxFileSize, "max-file-size", "", "Maximum size of files to create")
|
||||
flags.IntVarP(cmdFlags, &minFileNameLength, "min-name-length", "", minFileNameLength, "Minimum size of file names")
|
||||
flags.IntVarP(cmdFlags, &maxFileNameLength, "max-name-length", "", maxFileNameLength, "Maximum size of file names")
|
||||
flags.Int64VarP(cmdFlags, &seed, "seed", "", seed, "Seed for the random number generator (0 for random)")
|
||||
}
|
||||
|
||||
var commandDefinition = &cobra.Command{
|
||||
@@ -51,28 +54,36 @@ var commandDefinition = &cobra.Command{
|
||||
Short: `Make a random file hierarchy in <dir>`,
|
||||
Run: func(command *cobra.Command, args []string) {
|
||||
cmd.CheckArgs(1, 1, command, args)
|
||||
if seed == 0 {
|
||||
seed = time.Now().UnixNano()
|
||||
fs.Logf(nil, "Using random seed = %d", seed)
|
||||
}
|
||||
randSource = rand.New(rand.NewSource(seed))
|
||||
outputDirectory := args[0]
|
||||
directoriesToCreate = numberOfFiles / averageFilesPerDirectory
|
||||
averageSize := (minFileSize + maxFileSize) / 2
|
||||
log.Printf("Creating %d files of average size %v in %d directories in %q.", numberOfFiles, averageSize, directoriesToCreate, outputDirectory)
|
||||
start := time.Now()
|
||||
fs.Logf(nil, "Creating %d files of average size %v in %d directories in %q.", numberOfFiles, averageSize, directoriesToCreate, outputDirectory)
|
||||
root := &dir{name: outputDirectory, depth: 1}
|
||||
for totalDirectories < directoriesToCreate {
|
||||
root.createDirectories()
|
||||
}
|
||||
dirs := root.list("", []string{})
|
||||
totalBytes := int64(0)
|
||||
for i := 0; i < numberOfFiles; i++ {
|
||||
dir := dirs[rand.Intn(len(dirs))]
|
||||
writeFile(dir, fileName())
|
||||
dir := dirs[randSource.Intn(len(dirs))]
|
||||
totalBytes += writeFile(dir, fileName())
|
||||
}
|
||||
log.Printf("Done.")
|
||||
dt := time.Since(start)
|
||||
fs.Logf(nil, "Written %viB in %v at %viB/s.", fs.SizeSuffix(totalBytes), dt.Round(time.Millisecond), fs.SizeSuffix((totalBytes*int64(time.Second))/int64(dt)))
|
||||
},
|
||||
}
|
||||
|
||||
// fileName creates a unique random file or directory name
|
||||
func fileName() (name string) {
|
||||
for {
|
||||
length := rand.Intn(maxFileNameLength-minFileNameLength) + minFileNameLength
|
||||
name = random.String(length)
|
||||
length := randSource.Intn(maxFileNameLength-minFileNameLength) + minFileNameLength
|
||||
name = random.StringFn(length, randSource.Intn)
|
||||
if _, found := fileNames[name]; !found {
|
||||
break
|
||||
}
|
||||
@@ -99,7 +110,7 @@ func (d *dir) createDirectories() {
|
||||
}
|
||||
d.children = append(d.children, newDir)
|
||||
totalDirectories++
|
||||
switch rand.Intn(4) {
|
||||
switch randSource.Intn(4) {
|
||||
case 0:
|
||||
if d.depth < maxDepth {
|
||||
newDir.createDirectories()
|
||||
@@ -122,7 +133,7 @@ func (d *dir) list(path string, output []string) []string {
|
||||
}
|
||||
|
||||
// writeFile writes a random file at dir/name
|
||||
func writeFile(dir, name string) {
|
||||
func writeFile(dir, name string) int64 {
|
||||
err := os.MkdirAll(dir, 0777)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to make directory %q: %v", dir, err)
|
||||
@@ -132,8 +143,8 @@ func writeFile(dir, name string) {
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to open file %q: %v", path, err)
|
||||
}
|
||||
size := rand.Int63n(int64(maxFileSize-minFileSize)) + int64(minFileSize)
|
||||
_, err = io.CopyN(fd, cryptrand.Reader, size)
|
||||
size := randSource.Int63n(int64(maxFileSize-minFileSize)) + int64(minFileSize)
|
||||
_, err = io.CopyN(fd, randSource, size)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to write %v bytes to file %q: %v", size, path, err)
|
||||
}
|
||||
@@ -141,4 +152,6 @@ func writeFile(dir, name string) {
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to close file %q: %v", path, err)
|
||||
}
|
||||
fs.Infof(path, "Written file size %v", fs.SizeSuffix(size))
|
||||
return size
|
||||
}
|
||||
|
||||
@@ -29,13 +29,16 @@ var commandDefinition = &cobra.Command{
|
||||
Use: "version",
|
||||
Short: `Show the version number.`,
|
||||
Long: `
|
||||
Show the rclone version number, the go version, the build target OS and
|
||||
architecture, build tags and the type of executable (static or dynamic).
|
||||
Show the rclone version number, the go version, the build target
|
||||
OS and architecture, the runtime OS and kernel version and bitness,
|
||||
build tags and the type of executable (static or dynamic).
|
||||
|
||||
For example:
|
||||
|
||||
$ rclone version
|
||||
rclone v1.54
|
||||
rclone v1.55.0
|
||||
- os/version: ubuntu 18.04 (64 bit)
|
||||
- os/kernel: 4.15.0-136-generic (x86_64)
|
||||
- os/type: linux
|
||||
- os/arch: amd64
|
||||
- go/version: go1.16
|
||||
|
||||
@@ -26,12 +26,12 @@ func TestVersionWorksWithoutAccessibleConfigFile(t *testing.T) {
|
||||
}
|
||||
// re-wire
|
||||
oldOsStdout := os.Stdout
|
||||
oldConfigPath := config.ConfigPath
|
||||
config.ConfigPath = path
|
||||
oldConfigPath := config.GetConfigPath()
|
||||
assert.NoError(t, config.SetConfigPath(path))
|
||||
os.Stdout = nil
|
||||
defer func() {
|
||||
os.Stdout = oldOsStdout
|
||||
config.ConfigPath = oldConfigPath
|
||||
assert.NoError(t, config.SetConfigPath(oldConfigPath))
|
||||
}()
|
||||
|
||||
cmd.Root.SetArgs([]string{"version"})
|
||||
|
||||
@@ -152,6 +152,7 @@ WebDAV or S3, that work out of the box.)
|
||||
{{< provider name="SugarSync" home="https://sugarsync.com/" config="/sugarsync/" >}}
|
||||
{{< provider name="Tardigrade" home="https://tardigrade.io/" config="/tardigrade/" >}}
|
||||
{{< provider name="Tencent Cloud Object Storage (COS)" home="https://intl.cloud.tencent.com/product/cos" config="/s3/#tencent-cos" >}}
|
||||
{{< provider name="Uptobox" home="https://uptobox.com" config="/uptobox/" >}}
|
||||
{{< provider name="Wasabi" home="https://wasabi.com/" config="/s3/#wasabi" >}}
|
||||
{{< provider name="WebDAV" home="https://en.wikipedia.org/wiki/WebDAV" config="/webdav/" >}}
|
||||
{{< provider name="Yandex Disk" home="https://disk.yandex.com/" config="/yandex/" >}}
|
||||
|
||||
@@ -372,7 +372,7 @@ put them back in again.` >}}
|
||||
* Fred <fred@creativeprojects.tech>
|
||||
* Sébastien Gross <renard@users.noreply.github.com>
|
||||
* Maxime Suret <11944422+msuret@users.noreply.github.com>
|
||||
* Caleb Case <caleb@storj.io>
|
||||
* Caleb Case <caleb@storj.io> <calebcase@gmail.com>
|
||||
* Ben Zenker <imbenzenker@gmail.com>
|
||||
* Martin Michlmayr <tbm@cyrius.com>
|
||||
* Brandon McNama <bmcnama@pagerduty.com>
|
||||
@@ -478,3 +478,12 @@ put them back in again.` >}}
|
||||
* Manish Kumar <krmanish260@gmail.com>
|
||||
* x0b <x0bdev@gmail.com>
|
||||
* CERN through the CS3MESH4EOSC Project
|
||||
* Nick Gaya <nicholasgaya+github@gmail.com>
|
||||
* Ashok Gelal <401055+ashokgelal@users.noreply.github.com>
|
||||
* Dominik Mydlil <dominik.mydlil@outlook.com>
|
||||
* Nazar Mishturak <nazarmx@gmail.com>
|
||||
* Ansh Mittal <iamAnshMittal@gmail.com>
|
||||
* noabody <noabody@yahoo.com>
|
||||
* OleFrost <82263101+olefrost@users.noreply.github.com>
|
||||
* Kenny Parsons <kennyparsons93@gmail.com>
|
||||
* Jeffrey Tolar <tolar.jeffrey@gmail.com>
|
||||
|
||||
@@ -172,11 +172,6 @@ the file instead of hiding it.
|
||||
Old versions of files, where available, are visible using the
|
||||
`--b2-versions` flag.
|
||||
|
||||
**NB** Note that `--b2-versions` does not work with crypt at the
|
||||
moment [#1627](https://github.com/rclone/rclone/issues/1627). Using
|
||||
[--backup-dir](/docs/#backup-dir-dir) with rclone is the recommended
|
||||
way of working around this.
|
||||
|
||||
If you wish to remove all the old versions then you can use the
|
||||
`rclone cleanup remote:bucket` command which will delete all the old
|
||||
versions of files, leaving the current ones intact. You can also
|
||||
|
||||
@@ -5,6 +5,44 @@ description: "Rclone Changelog"
|
||||
|
||||
# Changelog
|
||||
|
||||
## v1.55.1 - 2021-04-26
|
||||
|
||||
[See commits](https://github.com/rclone/rclone/compare/v1.55.0...v1.55.1)
|
||||
|
||||
* Bug Fixes
|
||||
* selfupdate
|
||||
* Dont detect FUSE if build is static (Ivan Andreev)
|
||||
* Add build tag noselfupdate (Ivan Andreev)
|
||||
* sync: Fix incorrect error reported by graceful cutoff (Nick Craig-Wood)
|
||||
* install.sh: fix macOS arm64 download (Nick Craig-Wood)
|
||||
* build: Fix version numbers in android branch builds (Nick Craig-Wood)
|
||||
* docs
|
||||
* Contributing.md: update setup instructions for go1.16 (Nick Gaya)
|
||||
* WinFsp 2021 is out of beta (albertony)
|
||||
* Minor cleanup of space around code section (albertony)
|
||||
* Fixed some typos (albertony)
|
||||
* VFS
|
||||
* Fix a code path which allows dirty data to be removed causing data loss (Nick Craig-Wood)
|
||||
* Compress
|
||||
* Fix compressed name regexp (buengese)
|
||||
* Drive
|
||||
* Fix backend copyid of google doc to directory (Nick Craig-Wood)
|
||||
* Don't open browser when service account... (Ansh Mittal)
|
||||
* Dropbox
|
||||
* Add missing team_data.member scope for use with --impersonate (Nick Craig-Wood)
|
||||
* Fix About after scopes changes - rclone config reconnect needed (Nick Craig-Wood)
|
||||
* Fix Unable to decrypt returned paths from changeNotify (Nick Craig-Wood)
|
||||
* FTP
|
||||
* Fix implicit TLS (Ivan Andreev)
|
||||
* Onedrive
|
||||
* Work around for random "Unable to initialize RPS" errors (OleFrost)
|
||||
* SFTP
|
||||
* Revert sftp library to v1.12.0 from v1.13.0 to fix performance regression (Nick Craig-Wood)
|
||||
* Fix Update ReadFrom failed: failed to send packet: EOF errors (Nick Craig-Wood)
|
||||
* Zoho
|
||||
* Fix error when region isn't set (buengese)
|
||||
* Do not ask for mountpoint twice when using headless setup (buengese)
|
||||
|
||||
## v1.55.0 - 2021-03-31
|
||||
|
||||
[See commits](https://github.com/rclone/rclone/compare/v1.54.0...v1.55.0)
|
||||
|
||||
@@ -57,6 +57,7 @@ See the following for detailed instructions for
|
||||
* [SugarSync](/sugarsync/)
|
||||
* [Tardigrade](/tardigrade/)
|
||||
* [Union](/union/)
|
||||
* [Uptobox](/uptobox/)
|
||||
* [WebDAV](/webdav/)
|
||||
* [Yandex Disk](/yandex/)
|
||||
* [Zoho WorkDrive](/zoho/)
|
||||
@@ -639,25 +640,54 @@ See `--copy-dest` and `--backup-dir`.
|
||||
|
||||
### --config=CONFIG_FILE ###
|
||||
|
||||
Specify the location of the rclone configuration file.
|
||||
Specify the location of the rclone configuration file, to override
|
||||
the default. E.g. `rclone config --config="rclone.conf"`.
|
||||
|
||||
Normally the config file is in your home directory as a file called
|
||||
`.config/rclone/rclone.conf` (or `.rclone.conf` if created with an
|
||||
older version). If `$XDG_CONFIG_HOME` is set it will be at
|
||||
`$XDG_CONFIG_HOME/rclone/rclone.conf`.
|
||||
The exact default is a bit complex to describe, due to changes
|
||||
introduced through different versions of rclone while preserving
|
||||
backwards compatibility, but in most cases it is as simple as:
|
||||
|
||||
If there is a file `rclone.conf` in the same directory as the rclone
|
||||
executable it will be preferred. This file must be created manually
|
||||
for Rclone to use it, it will never be created automatically.
|
||||
- `%APPDATA%/rclone/rclone.conf` on Windows
|
||||
- `~/.config/rclone/rclone.conf` on other
|
||||
|
||||
The complete logic is as follows: Rclone will look for an existing
|
||||
configuration file in any of the following locations, in priority order:
|
||||
|
||||
1. `rclone.conf` (in program directory, where rclone executable is)
|
||||
2. `%APPDATA%/rclone/rclone.conf` (only on Windows)
|
||||
3. `$XDG_CONFIG_HOME/rclone/rclone.conf` (on all systems, including Windows)
|
||||
4. `~/.config/rclone/rclone.conf` (see below for explanation of ~ symbol)
|
||||
5. `~/.rclone.conf`
|
||||
|
||||
If no existing configuration file is found, then a new one will be created
|
||||
in the following location:
|
||||
|
||||
- On Windows: Location 2 listed above, except in the unlikely event
|
||||
that `APPDATA` is not defined, then location 4 is used instead.
|
||||
- On Unix: Location 3 if `XDG_CONFIG_HOME` is defined, else location 4.
|
||||
- Fallback to location 5 (on all OS), when the rclone directory cannot be
|
||||
created, but if also a home directory was not found then path
|
||||
`.rclone.conf` relative to current working directory will be used as
|
||||
a final resort.
|
||||
|
||||
The `~` symbol in paths above represent the home directory of the current user
|
||||
on any OS, and the value is defined as following:
|
||||
|
||||
- On Windows: `%HOME%` if defined, else `%USERPROFILE%`, or else `%HOMEDRIVE%\%HOMEPATH%`.
|
||||
- On Unix: `$HOME` if defined, else by looking up current user in OS-specific user database
|
||||
(e.g. passwd file), or else use the result from shell command `cd && pwd`.
|
||||
|
||||
If you run `rclone config file` you will see where the default
|
||||
location is for you.
|
||||
|
||||
Use this flag to override the config location, e.g. `rclone
|
||||
--config=".myconfig" .config`.
|
||||
The fact that an existing file `rclone.conf` in the same directory
|
||||
as the rclone executable is always preferred, means that it is easy
|
||||
to run in "portable" mode by downloading rclone executable to a
|
||||
writable directory and then create an empty file `rclone.conf` in the
|
||||
same directory.
|
||||
|
||||
If the location is set to empty string `""` or the special value
|
||||
`/notfound`, or the os null device represented by value `NUL` on
|
||||
If the location is set to empty string `""` or path to a file
|
||||
with name `notfound`, or the os null device represented by value `NUL` on
|
||||
Windows and `/dev/null` on Unix systems, then rclone will keep the
|
||||
config file in memory only.
|
||||
|
||||
@@ -1890,11 +1920,12 @@ Nevertheless, rclone will read any configuration file found
|
||||
according to the rules described [above](https://rclone.org/docs/#config-config-file).
|
||||
If an encrypted configuration file is found, this means you will be prompted for
|
||||
password (unless using `--password-command`). To avoid this, you can bypass
|
||||
the loading of the configuration file by overriding the location with an empty
|
||||
string `""` or the special value `/notfound`, or the os null device represented
|
||||
by value `NUL` on Windows and `/dev/null` on Unix systems (before rclone
|
||||
version 1.55 only this null device alternative was supported).
|
||||
E.g. `rclone --config="" genautocomplete bash`.
|
||||
the loading of the default configuration file by overriding the location,
|
||||
e.g. with one of the documented special values for memory-only configuration:
|
||||
|
||||
```
|
||||
rclone genautocomplete bash --config=""
|
||||
```
|
||||
|
||||
Developer options
|
||||
-----------------
|
||||
@@ -2119,7 +2150,7 @@ mys3:
|
||||
Note that if you want to create a remote using environment variables
|
||||
you must create the `..._TYPE` variable as above.
|
||||
|
||||
Note also that now rclone has [connectionstrings](#connection-strings),
|
||||
Note also that now rclone has [connection strings](#connection-strings),
|
||||
it is probably easier to use those instead which makes the above example
|
||||
|
||||
rclone lsd :s3,access_key_id=XXX,secret_access_key=XXX:
|
||||
|
||||
@@ -285,6 +285,12 @@ dropbox:dir` will return the error `Failed to purge: There are too
|
||||
many files involved in this operation`. As a work-around do an
|
||||
`rclone delete dropbox:dir` followed by an `rclone rmdir dropbox:dir`.
|
||||
|
||||
When using `rclone link` you'll need to set `--expire` if using a
|
||||
non-personal account otherwise the visibility may not be correct.
|
||||
(Note that `--expire` isn't supported on personal accounts). See the
|
||||
[forum discussion](https://forum.rclone.org/t/rclone-link-dropbox-permissions/23211) and the
|
||||
[dropbox SDK issue](https://github.com/dropbox/dropbox-sdk-go-unofficial/issues/75).
|
||||
|
||||
### Get your own Dropbox App ID ###
|
||||
|
||||
When you use rclone with Dropbox in its default configuration you are using rclone's App ID. This is shared between all the rclone users.
|
||||
|
||||
@@ -415,6 +415,7 @@ and may be set in the config file.
|
||||
--onedrive-link-password string Set the password for links created by the link command.
|
||||
--onedrive-link-scope string Set the scope of the links created by the link command. (default "anonymous")
|
||||
--onedrive-link-type string Set the type of the links created by the link command. (default "view")
|
||||
--onedrive-list-chunk int Size of listing chunk. (default 1000)
|
||||
--onedrive-no-versions Remove all versions on modifying operations
|
||||
--onedrive-region string Choose national cloud region for OneDrive. (default "global")
|
||||
--onedrive-server-side-across-configs Allow server-side operations (e.g. copy) to work across different onedrive configs.
|
||||
|
||||
@@ -12,6 +12,7 @@ Rclone is a Go program and comes as a single binary file.
|
||||
* [Download](/downloads/) the relevant binary.
|
||||
* Extract the `rclone` or `rclone.exe` binary from the archive
|
||||
* Run `rclone config` to setup. See [rclone config docs](/docs/) for more details.
|
||||
* Optionally configure [automatic execution](#autostart).
|
||||
|
||||
See below for some expanded Linux / macOS instructions.
|
||||
|
||||
@@ -226,3 +227,147 @@ Instructions
|
||||
roles:
|
||||
- rclone
|
||||
```
|
||||
|
||||
# Autostart #
|
||||
|
||||
After installing and configuring rclone, as described above, you are ready to use rclone
|
||||
as an interactive command line utility. If your goal is to perform *periodic* operations,
|
||||
such as a regular [sync](https://rclone.org/commands/rclone_sync/), you will probably want
|
||||
to configure your rclone command in your operating system's scheduler. If you need to
|
||||
expose *service*-like features, such as [remote control](https://rclone.org/rc/),
|
||||
[GUI](https://rclone.org/gui/), [serve](https://rclone.org/commands/rclone_serve/)
|
||||
or [mount](https://rclone.org/commands/rclone_move/), you will often want an rclone
|
||||
command always running in the background, and configuring it to run in a service infrastructure
|
||||
may be a better option. Below are some alternatives on how to achieve this on
|
||||
different operating systems.
|
||||
|
||||
NOTE: Before setting up autorun it is highly recommended that you have tested your command
|
||||
manually from a Command Prompt first.
|
||||
|
||||
## Autostart on Windows ##
|
||||
|
||||
The most relevant alternatives for autostart on Windows are:
|
||||
- Run at user log on using the Startup folder
|
||||
- Run at user log on, at system startup or at schedule using Task Scheduler
|
||||
- Run at system startup using Windows service
|
||||
|
||||
### Running in background
|
||||
|
||||
Rclone is a console application, so if not starting from an existing Command Prompt,
|
||||
e.g. when starting rclone.exe from a shortcut, it will open a Command Prompt window.
|
||||
When configuring rclone to run from task scheduler and windows service you are able
|
||||
to set it to run hidden in background. From rclone version 1.54 you can also make it
|
||||
run hidden from anywhere by adding option `--no-console` (it may still flash briefly
|
||||
when the program starts). Since rclone normally writes information and any error
|
||||
messages to the console, you must redirect this to a file to be able to see it.
|
||||
Rclone has a built-in option `--log-file` for that.
|
||||
|
||||
Example command to run a sync in background:
|
||||
```
|
||||
c:\rclone\rclone.exe sync c:\files remote:/files --no-console --log-file c:\rclone\logs\sync_files.txt
|
||||
```
|
||||
|
||||
### User account
|
||||
|
||||
As mentioned in the [mount](https://rclone.org/commands/rclone_move/) documentation,
|
||||
mounted drives created as Administrator are not visible to other accounts, not even the
|
||||
account that was elevated as Administrator. By running the mount command as the
|
||||
built-in `SYSTEM` user account, it will create drives accessible for everyone on
|
||||
the system. Both scheduled task and Windows service can be used to achieve this.
|
||||
|
||||
NOTE: Remember that when rclone runs as the `SYSTEM` user, the user profile
|
||||
that it sees will not be yours. This means that if you normally run rclone with
|
||||
configuration file in the default location, to be able to use the same configuration
|
||||
when running as the system user you must explicitely tell rclone where to find
|
||||
it with the [`--config`](https://rclone.org/docs/#config-config-file) option,
|
||||
or else it will look in the system users profile path (`C:\Windows\System32\config\systemprofile`).
|
||||
To test your command manually from a Command Prompt, you can run it with
|
||||
the [PsExec](https://docs.microsoft.com/en-us/sysinternals/downloads/psexec)
|
||||
utility from Microsoft's Sysinternals suite, which takes option `-s` to
|
||||
execute commands as the `SYSTEM` user.
|
||||
|
||||
### Start from Startup folder ###
|
||||
|
||||
To quickly execute an rclone command you can simply create a standard
|
||||
Windows Explorer shortcut for the complete rclone command you want to run. If you
|
||||
store this shortcut in the special "Startup" start-menu folder, Windows will
|
||||
automatically run it at login. To open this folder in Windows Explorer,
|
||||
enter path `%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup`,
|
||||
or `C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp` if you want
|
||||
the command to start for *every* user that logs in.
|
||||
|
||||
This is the easiest approach to autostarting of rclone, but it offers no
|
||||
functionality to set it to run as different user, or to set conditions or
|
||||
actions on certain events. Setting up a scheduled task as described below
|
||||
will often give you better results.
|
||||
|
||||
### Start from Task Scheduler ###
|
||||
|
||||
Task Scheduler is an administrative tool built into Windows, and it can be used to
|
||||
configure rclone to be started automatically in a highly configurable way, e.g.
|
||||
periodically on a schedule, on user log on, or at system startup. It can run
|
||||
be configured to run as the current user, or for a mount command that needs to
|
||||
be available to all users it can run as the `SYSTEM` user.
|
||||
For technical information, see
|
||||
https://docs.microsoft.com/windows/win32/taskschd/task-scheduler-start-page.
|
||||
|
||||
### Run as service ###
|
||||
|
||||
For running rclone at system startup, you can create a Windows service that executes
|
||||
your rclone command, as an alternative to scheduled task configured to run at startup.
|
||||
|
||||
#### Mount command built-in service integration ####
|
||||
|
||||
For mount commands, Rclone has a built-in Windows service integration via the third party
|
||||
WinFsp library it uses. Registering as a regular Windows service easy, as you just have to
|
||||
execute the built-in PowerShell command `New-Service` (requires administrative privileges).
|
||||
|
||||
Example of a PowerShell command that creates a Windows service for mounting
|
||||
some `remote:/files` as drive letter `X:`, for *all* users (service will be running as the
|
||||
local system account):
|
||||
|
||||
```
|
||||
New-Service -Name Rclone -BinaryPathName 'c:\rclone\rclone.exe mount remote:/files X: --config c:\rclone\config\rclone.conf --log-file c:\rclone\logs\mount.txt'
|
||||
```
|
||||
|
||||
The [WinFsp service infrastructure](https://github.com/billziss-gh/winfsp/wiki/WinFsp-Service-Architecture)
|
||||
supports incorporating services for file system implementations, such as rclone,
|
||||
into its own launcher service, as kind of "child services". This has the additional
|
||||
advantage that it also implements a network provider that integrates into
|
||||
Windows standard methods for managing network drives. This is currently not
|
||||
officially supported by Rclone, but with WinFsp version 2019.3 B2 / v1.5B2 or later
|
||||
it should be possible through path rewriting as described [here](https://github.com/rclone/rclone/issues/3340).
|
||||
|
||||
#### Third party service integration ####
|
||||
|
||||
To Windows service running any rclone command, the excellent third party utility
|
||||
[NSSM](http://nssm.cc), the "Non-Sucking Service Manager", can be used.
|
||||
It includes some advanced features such as adjusting process periority, defining
|
||||
process environment variables, redirect to file anything written to stdout, and
|
||||
customized response to different exit codes, with a GUI to configure everything from
|
||||
(although it can also be used from command line ).
|
||||
|
||||
There are also several other alternatives. To mention one more,
|
||||
[WinSW](https://github.com/winsw/winsw), "Windows Service Wrapper", is worth checking out.
|
||||
It requires .NET Framework, but it is preinstalled on newer versions of Windows, and it
|
||||
also provides alternative standalone distributions which includes necessary runtime (.NET 5).
|
||||
WinSW is a command-line only utility, where you have to manually create an XML file with
|
||||
service configuration. This may be a drawback for some, but it can also be an advantage
|
||||
as it is easy to back up and re-use the configuration
|
||||
settings, without having go through manual steps in a GUI. One thing to note is that
|
||||
by default it does not restart the service on error, one have to explicit enable this
|
||||
in the configuration file (via the "onfailure" parameter).
|
||||
|
||||
## Autostart on Linux
|
||||
|
||||
### Start as a service
|
||||
|
||||
To always run rclone in background, relevant for mount commands etc,
|
||||
you can use systemd to set up rclone as a system or user service. Running as a
|
||||
system service ensures that it is run at startup even if the user it is running as
|
||||
has no active session. Running rclone as a user service ensures that it only
|
||||
starts after the configured user has logged into the system.
|
||||
|
||||
### Run periodically from cron
|
||||
|
||||
To run a periodic command, such as a copy/sync, you can set up a cron job.
|
||||
|
||||
@@ -53,9 +53,9 @@ export XDG_CONFIG_HOME=config
|
||||
#check installed version of rclone to determine if update is necessary
|
||||
version=$(rclone --version 2>>errors | head -n 1)
|
||||
if [ -z "$install_beta" ]; then
|
||||
current_version=$(curl -f https://downloads.rclone.org/version.txt)
|
||||
current_version=$(curl -fsS https://downloads.rclone.org/version.txt)
|
||||
else
|
||||
current_version=$(curl -f https://beta.rclone.org/version.txt)
|
||||
current_version=$(curl -fsS https://beta.rclone.org/version.txt)
|
||||
fi
|
||||
|
||||
if [ "$version" = "$current_version" ]; then
|
||||
@@ -101,12 +101,12 @@ case "$OS_type" in
|
||||
i?86|x86)
|
||||
OS_type='386'
|
||||
;;
|
||||
aarch64|arm64)
|
||||
OS_type='arm64'
|
||||
;;
|
||||
arm*)
|
||||
OS_type='arm'
|
||||
;;
|
||||
aarch64)
|
||||
OS_type='arm64'
|
||||
;;
|
||||
*)
|
||||
echo 'OS type not supported'
|
||||
exit 2
|
||||
@@ -123,7 +123,7 @@ else
|
||||
rclone_zip="rclone-beta-latest-${OS}-${OS_type}.zip"
|
||||
fi
|
||||
|
||||
curl -Of "$download_link"
|
||||
curl -OfsS "$download_link"
|
||||
unzip_dir="tmp_unzip_dir_for_rclone"
|
||||
# there should be an entry in this switch for each element of unzip_tools_list
|
||||
case "$unzip_tool" in
|
||||
|
||||
@@ -172,7 +172,7 @@ like symlinks under Windows).
|
||||
|
||||
If you supply `--copy-links` or `-L` then rclone will follow the
|
||||
symlink and copy the pointed to file or directory. Note that this
|
||||
flag is incompatible with `-links` / `-l`.
|
||||
flag is incompatible with `--links` / `-l`.
|
||||
|
||||
This flag applies to all commands.
|
||||
|
||||
|
||||
@@ -325,6 +325,15 @@ fall back to normal copy (which will be slightly slower).
|
||||
- Type: bool
|
||||
- Default: false
|
||||
|
||||
#### --onedrive-list-chunk
|
||||
|
||||
Size of listing chunk.
|
||||
|
||||
- Config: list_chunk
|
||||
- Env Var: RCLONE_ONEDRIVE_LIST_CHUNK
|
||||
- Type: int
|
||||
- Default: 1000
|
||||
|
||||
#### --onedrive-no-versions
|
||||
|
||||
Remove all versions on modifying operations
|
||||
|
||||
@@ -48,6 +48,7 @@ Here is an overview of the major features of each cloud storage system.
|
||||
| SFTP | MD5, SHA1 ² | Yes | Depends | No | - |
|
||||
| SugarSync | - | No | No | No | - |
|
||||
| Tardigrade | - | Yes | No | No | - |
|
||||
| Uptobox | - | No | No | Yes | - |
|
||||
| WebDAV | MD5, SHA1 ³ | Yes ⁴ | Depends | No | - |
|
||||
| Yandex Disk | MD5 | Yes | No | No | R |
|
||||
| Zoho WorkDrive | - | No | No | No | - |
|
||||
@@ -361,6 +362,7 @@ upon backend specific capabilities.
|
||||
| SFTP | No | No | Yes | Yes | No | No | Yes | No | Yes | Yes |
|
||||
| SugarSync | Yes | Yes | Yes | Yes | No | No | Yes | Yes | No | Yes |
|
||||
| Tardigrade | Yes † | No | No | No | No | Yes | Yes | No | No | No |
|
||||
| Uptobox | No | Yes | Yes | Yes | No | No | No | No | No | No |
|
||||
| WebDAV | Yes | Yes | Yes | Yes | No | No | Yes ‡ | No | Yes | Yes |
|
||||
| Yandex Disk | Yes | Yes | Yes | Yes | Yes | No | Yes | Yes | Yes | Yes |
|
||||
| Zoho WorkDrive | Yes | Yes | Yes | Yes | No | No | No | No | Yes | Yes |
|
||||
|
||||
@@ -21,7 +21,10 @@ SSH installations.
|
||||
|
||||
Paths are specified as `remote:path`. If the path does not begin with
|
||||
a `/` it is relative to the home directory of the user. An empty path
|
||||
`remote:` refers to the user's home directory.
|
||||
`remote:` refers to the user's home directory. For example, `rclone lsd remote:`
|
||||
would list the home directory of the user cofigured in the rclone remote config
|
||||
(`i.e /home/sftpuser`). However, `rclone lsd remote:/` would list the root
|
||||
directory for remote machine (i.e. `/`)
|
||||
|
||||
"Note that some SFTP servers will need the leading / - Synology is a
|
||||
good example of this. rsync.net, on the other hand, requires users to
|
||||
@@ -84,6 +87,10 @@ See all directories in the home directory
|
||||
|
||||
rclone lsd remote:
|
||||
|
||||
See all directories in the root directory
|
||||
|
||||
rclone lsd remote:/
|
||||
|
||||
Make a new directory
|
||||
|
||||
rclone mkdir remote:path/to/directory
|
||||
@@ -97,6 +104,11 @@ excess files in the directory.
|
||||
|
||||
rclone sync -i /home/local/directory remote:directory
|
||||
|
||||
Mount the remote path `/srv/www-data/` to the local path
|
||||
`/mnt/www-data`
|
||||
|
||||
rclone mount remote:/srv/www-data/ /mnt/www-data
|
||||
|
||||
### SSH Authentication ###
|
||||
|
||||
The SFTP remote supports three authentication methods:
|
||||
|
||||
141
docs/content/uptobox.md
Normal file
141
docs/content/uptobox.md
Normal file
@@ -0,0 +1,141 @@
|
||||
---
|
||||
title: "Uptobox"
|
||||
description: "Rclone docs for Uptobox"
|
||||
---
|
||||
|
||||
{{< icon "fa fa-archive" >}} Uptobox
|
||||
-----------------------------------------
|
||||
|
||||
This is a Backend for Uptobox file storage service. Uptobox is closer to a one-click hoster than a traditional
|
||||
cloud storage provider and therefore not suitable for long term storage.
|
||||
|
||||
Paths are specified as `remote:path`
|
||||
|
||||
Paths may be as deep as required, e.g. `remote:directory/subdirectory`.
|
||||
|
||||
## Setup
|
||||
|
||||
To configure an Uptobox backend you'll need your personal api token. You'll find it in you
|
||||
[account settings](https://uptobox.com/my_account)
|
||||
|
||||
|
||||
### Example
|
||||
|
||||
Here is an example of how to make a remote called `remote` with the default setup. First run:
|
||||
|
||||
rclone config
|
||||
|
||||
This will guide you through an interactive setup process:
|
||||
|
||||
```
|
||||
Current remotes:
|
||||
|
||||
Name Type
|
||||
==== ====
|
||||
TestUptobox uptobox
|
||||
|
||||
e) Edit existing remote
|
||||
n) New remote
|
||||
d) Delete remote
|
||||
r) Rename remote
|
||||
c) Copy remote
|
||||
s) Set configuration password
|
||||
q) Quit config
|
||||
e/n/d/r/c/s/q> n
|
||||
name> uptobox
|
||||
Type of storage to configure.
|
||||
Enter a string value. Press Enter for the default ("").
|
||||
Choose a number from below, or type in your own value
|
||||
[...]
|
||||
37 / Uptobox
|
||||
\ "uptobox"
|
||||
[...]
|
||||
Storage> uptobox
|
||||
** See help for uptobox backend at: https://rclone.org/uptobox/ **
|
||||
|
||||
Your API Key, get it from https://uptobox.com/my_account
|
||||
Enter a string value. Press Enter for the default ("").
|
||||
api_key> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
Edit advanced config? (y/n)
|
||||
y) Yes
|
||||
n) No (default)
|
||||
y/n> n
|
||||
Remote config
|
||||
--------------------
|
||||
[uptobox]
|
||||
type = uptobox
|
||||
api_key = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
--------------------
|
||||
y) Yes this is OK (default)
|
||||
e) Edit this remote
|
||||
d) Delete this remote
|
||||
y/e/d>
|
||||
```
|
||||
Once configured you can then use `rclone` like this,
|
||||
|
||||
List directories in top level of your Uptobox
|
||||
|
||||
rclone lsd remote:
|
||||
|
||||
List all the files in your Uptobox
|
||||
|
||||
rclone ls remote:
|
||||
|
||||
To copy a local directory to an Uptobox directory called backup
|
||||
|
||||
rclone copy /home/source remote:backup
|
||||
|
||||
### Modified time and hashes
|
||||
|
||||
Uptobox supports neither modified times nor checksums.
|
||||
|
||||
#### Restricted filename characters
|
||||
|
||||
In addition to the [default restricted characters set](/overview/#restricted-characters)
|
||||
the following characters are also replaced:
|
||||
|
||||
| Character | Value | Replacement |
|
||||
| --------- |:-----:|:-----------:|
|
||||
| " | 0x22 | " |
|
||||
| ` | 0x41 | ` |
|
||||
|
||||
Invalid UTF-8 bytes will also be [replaced](/overview/#invalid-utf8),
|
||||
as they can't be used in XML strings.
|
||||
|
||||
{{< rem autogenerated options start" - DO NOT EDIT - instead edit fs.RegInfo in backend/uptobox/uptobox.go then run make backenddocs" >}}
|
||||
### Standard Options
|
||||
|
||||
Here are the standard options specific to uptobox (Uptobox).
|
||||
|
||||
#### --uptobox-api-key
|
||||
|
||||
Your API Key, get it from https://uptobox.com/my_account
|
||||
|
||||
- Config: api_key
|
||||
- Env Var: RCLONE_UPTOBOX_API_KEY
|
||||
- Type: string
|
||||
- Default: ""
|
||||
|
||||
### Advanced Options
|
||||
|
||||
Here are the advanced options specific to uptobox (Uptobox).
|
||||
|
||||
#### --uptobox-encoding
|
||||
|
||||
This sets the encoding for the backend.
|
||||
|
||||
See: the [encoding section in the overview](/overview/#encoding) for more info.
|
||||
|
||||
- Config: encoding
|
||||
- Env Var: RCLONE_UPTOBOX_ENCODING
|
||||
- Type: MultiEncoder
|
||||
- Default: Slash,LtGt,DoubleQuote,SingleQuote,BackQuote,Dollar,BackSlash,Del,Ctl,LeftSpace,RightSpace,InvalidUtf8,Dot
|
||||
|
||||
{{< rem autogenerated options stop >}}
|
||||
|
||||
### Limitations
|
||||
|
||||
Uptobox will delete inactive files that have not been accessed in 60 days.
|
||||
|
||||
`rclone about` is not supported by this backend an overview of used space can however
|
||||
been seen in the uptobox web interface.
|
||||
@@ -98,6 +98,7 @@
|
||||
<a class="dropdown-item" href="/sftp/"><i class="fa fa-server"></i> SFTP</a>
|
||||
<a class="dropdown-item" href="/sugarsync/"><i class="fas fa-dove"></i> SugarSync</a>
|
||||
<a class="dropdown-item" href="/tardigrade/"><i class="fas fa-dove"></i> Tardigrade</a>
|
||||
<a class="dropdown-item" href="/uptobox/"><i class="fa fa-archive"></i> Uptobox</a>
|
||||
<a class="dropdown-item" href="/union/"><i class="fa fa-link"></i> Union (merge backends)</a>
|
||||
<a class="dropdown-item" href="/webdav/"><i class="fa fa-server"></i> WebDAV</a>
|
||||
<a class="dropdown-item" href="/yandex/"><i class="fa fa-space-shuttle"></i> Yandex Disk</a>
|
||||
|
||||
@@ -1 +1 @@
|
||||
v1.55.0
|
||||
v1.56.0
|
||||
@@ -2,9 +2,11 @@ package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rclone/rclone/fs"
|
||||
"github.com/rclone/rclone/fs/config/configmap"
|
||||
)
|
||||
|
||||
// Authorize is for remote authorization of headless machines.
|
||||
@@ -16,33 +18,64 @@ import (
|
||||
func Authorize(ctx context.Context, args []string, noAutoBrowser bool) error {
|
||||
ctx = suppressConfirm(ctx)
|
||||
switch len(args) {
|
||||
case 1, 3:
|
||||
case 1, 2, 3:
|
||||
default:
|
||||
return errors.Errorf("invalid number of arguments: %d", len(args))
|
||||
}
|
||||
newType := args[0]
|
||||
f := fs.MustFind(newType)
|
||||
if f.Config == nil {
|
||||
return errors.Errorf("can't authorize fs %q", newType)
|
||||
Type := args[0] // FIXME could read this from input
|
||||
ri, err := fs.Find(Type)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ri.Config == nil {
|
||||
return errors.Errorf("can't authorize fs %q", Type)
|
||||
}
|
||||
// Name used for temporary fs
|
||||
name := "**temp-fs**"
|
||||
|
||||
// Make sure we delete it
|
||||
defer DeleteRemote(name)
|
||||
// Config map for remote
|
||||
inM := configmap.Simple{}
|
||||
|
||||
// Indicate that we are running rclone authorize
|
||||
Data.SetValue(name, ConfigAuthorize, "true")
|
||||
inM[ConfigAuthorize] = "true"
|
||||
if noAutoBrowser {
|
||||
Data.SetValue(name, ConfigAuthNoBrowser, "true")
|
||||
inM[ConfigAuthNoBrowser] = "true"
|
||||
}
|
||||
|
||||
if len(args) == 3 {
|
||||
Data.SetValue(name, ConfigClientID, args[1])
|
||||
Data.SetValue(name, ConfigClientSecret, args[2])
|
||||
// Add extra parameters if supplied
|
||||
if len(args) == 2 {
|
||||
err := inM.Decode(args[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if len(args) == 3 {
|
||||
inM[ConfigClientID] = args[1]
|
||||
inM[ConfigClientSecret] = args[2]
|
||||
}
|
||||
|
||||
m := fs.ConfigMap(f, name, nil)
|
||||
f.Config(ctx, name, m)
|
||||
// Name used for temporary remote
|
||||
name := "**temp-fs**"
|
||||
|
||||
m := fs.ConfigMap(ri, name, inM)
|
||||
outM := configmap.Simple{}
|
||||
m.ClearSetters()
|
||||
m.AddSetter(outM)
|
||||
m.AddGetter(outM, configmap.PriorityNormal)
|
||||
|
||||
err = ri.Config(ctx, name, m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Print the code for the user to paste
|
||||
out := outM["token"]
|
||||
|
||||
// If received a config blob, then return one
|
||||
if len(args) == 2 {
|
||||
out, err = outM.Encode()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
fmt.Printf("Paste the following into your remote machine --->\n%s\n<---End paste\n", out)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
mathrand "math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -22,12 +21,14 @@ import (
|
||||
"github.com/rclone/rclone/fs/config/obscure"
|
||||
"github.com/rclone/rclone/fs/fspath"
|
||||
"github.com/rclone/rclone/fs/rc"
|
||||
"github.com/rclone/rclone/lib/file"
|
||||
"github.com/rclone/rclone/lib/random"
|
||||
)
|
||||
|
||||
const (
|
||||
configFileName = "rclone.conf"
|
||||
hiddenConfigFileName = "." + configFileName
|
||||
noConfigFile = "notfound"
|
||||
|
||||
// ConfigToken is the key used to store the token under
|
||||
ConfigToken = "token"
|
||||
@@ -107,72 +108,140 @@ var (
|
||||
// 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
|
||||
)
|
||||
|
||||
var configPath string
|
||||
|
||||
func init() {
|
||||
// Set the function pointers up in fs
|
||||
fs.ConfigFileGet = FileGetFlag
|
||||
fs.ConfigFileSet = SetValueAndSave
|
||||
configPath = makeConfigPath()
|
||||
}
|
||||
|
||||
// Join directory with filename, and check if exists
|
||||
func findFile(dir string, name string) string {
|
||||
path := filepath.Join(dir, name)
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
return ""
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
// Find current user's home directory
|
||||
func findHomeDir() (string, error) {
|
||||
path, err := homedir.Dir()
|
||||
if err != nil {
|
||||
fs.Debugf(nil, "Home directory lookup failed and cannot be used as configuration location: %v", err)
|
||||
} else if path == "" {
|
||||
// On Unix homedir return success but empty string for user with empty home configured in passwd file
|
||||
fs.Debugf(nil, "Home directory not defined and cannot be used as configuration location")
|
||||
}
|
||||
return path, err
|
||||
}
|
||||
|
||||
// Find rclone executable directory and look for existing rclone.conf there
|
||||
// (<rclone_exe_dir>/rclone.conf)
|
||||
func findLocalConfig() (configDir string, configFile string) {
|
||||
if exePath, err := os.Executable(); err == nil {
|
||||
configDir = filepath.Dir(exePath)
|
||||
configFile = findFile(configDir, configFileName)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Get path to Windows AppData config subdirectory for rclone and look for existing rclone.conf there
|
||||
// ($AppData/rclone/rclone.conf)
|
||||
func findAppDataConfig() (configDir string, configFile string) {
|
||||
if appDataDir := os.Getenv("APPDATA"); appDataDir != "" {
|
||||
configDir = filepath.Join(appDataDir, "rclone")
|
||||
configFile = findFile(configDir, configFileName)
|
||||
} else {
|
||||
fs.Debugf(nil, "Environment variable APPDATA is not defined and cannot be used as configuration location")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Get path to XDG config subdirectory for rclone and look for existing rclone.conf there
|
||||
// (see XDG Base Directory specification: https://specifications.freedesktop.org/basedir-spec/latest/).
|
||||
// ($XDG_CONFIG_HOME\rclone\rclone.conf)
|
||||
func findXDGConfig() (configDir string, configFile string) {
|
||||
if xdgConfigDir := os.Getenv("XDG_CONFIG_HOME"); xdgConfigDir != "" {
|
||||
configDir = filepath.Join(xdgConfigDir, "rclone")
|
||||
configFile = findFile(configDir, configFileName)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Get path to .config subdirectory for rclone and look for existing rclone.conf there
|
||||
// (~/.config/rclone/rclone.conf)
|
||||
func findDotConfigConfig(home string) (configDir string, configFile string) {
|
||||
if home != "" {
|
||||
configDir = filepath.Join(home, ".config", "rclone")
|
||||
configFile = findFile(configDir, configFileName)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Look for existing .rclone.conf (legacy hidden filename) in root of user's home directory
|
||||
// (~/.rclone.conf)
|
||||
func findOldHomeConfig(home string) (configDir string, configFile string) {
|
||||
if home != "" {
|
||||
configDir = home
|
||||
configFile = findFile(home, hiddenConfigFileName)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
// Look for existing rclone.conf in prioritized list of known locations
|
||||
// Also get configuration directory to use for new config file when no existing is found.
|
||||
var (
|
||||
configFile string
|
||||
configDir string
|
||||
primaryConfigDir string
|
||||
fallbackConfigDir string
|
||||
)
|
||||
// <rclone_exe_dir>/rclone.conf
|
||||
if _, configFile = findLocalConfig(); configFile != "" {
|
||||
return configFile
|
||||
}
|
||||
// Windows: $AppData/rclone/rclone.conf
|
||||
// This is also the default location for new config when no existing is found
|
||||
if runtime.GOOS == "windows" {
|
||||
if primaryConfigDir, configFile = findAppDataConfig(); configFile != "" {
|
||||
return configFile
|
||||
}
|
||||
}
|
||||
|
||||
// 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")
|
||||
// $XDG_CONFIG_HOME/rclone/rclone.conf
|
||||
// Also looking for this on Windows, for backwards compatibility reasons.
|
||||
if configDir, configFile = findXDGConfig(); configFile != "" {
|
||||
return configFile
|
||||
}
|
||||
if runtime.GOOS != "windows" {
|
||||
// On Unix this is also the default location for new config when no existing is found
|
||||
primaryConfigDir = configDir
|
||||
}
|
||||
// ~/.config/rclone/rclone.conf
|
||||
// This is also the fallback location for new config
|
||||
// (when $AppData on Windows and $XDG_CONFIG_HOME on Unix is not defined)
|
||||
homeDir, homeDirErr := findHomeDir()
|
||||
if fallbackConfigDir, configFile = findDotConfigConfig(homeDir); configFile != "" {
|
||||
return configFile
|
||||
}
|
||||
// ~/.rclone.conf
|
||||
if _, configFile = findOldHomeConfig(homeDir); configFile != "" {
|
||||
return configFile
|
||||
}
|
||||
|
||||
// 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.
|
||||
// No existing config file found, prepare proper default for a new one.
|
||||
// But first check if if user supplied a --config variable or environment
|
||||
// variable, since then we skip actually trying to create the default
|
||||
// and report any errors related to it (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 {
|
||||
@@ -182,49 +251,100 @@ func makeConfigPath() string {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 we found a configuration directory to be used for new config during search
|
||||
// above, then create it to be ready for rclone.conf file to be written into it
|
||||
// later, and also as a test of permissions to use fallback if not even able to
|
||||
// create the directory.
|
||||
if primaryConfigDir != "" {
|
||||
configDir = primaryConfigDir
|
||||
} else if fallbackConfigDir != "" {
|
||||
configDir = fallbackConfigDir
|
||||
} else {
|
||||
configDir = ""
|
||||
}
|
||||
if configDir != "" {
|
||||
configFile = filepath.Join(configDir, configFileName)
|
||||
if configSupplied {
|
||||
return cfgpath
|
||||
// User supplied custom config option, just return the default path
|
||||
// as is without creating any directories, since it will not be used
|
||||
// anyway and we don't want to unnecessarily create empty directory.
|
||||
return configFile
|
||||
}
|
||||
err := os.MkdirAll(cfgdir, os.ModePerm)
|
||||
if err == nil {
|
||||
return cfgpath
|
||||
var mkdirErr error
|
||||
if mkdirErr = os.MkdirAll(configDir, os.ModePerm); mkdirErr == nil {
|
||||
return configFile
|
||||
}
|
||||
// Problem: Try a fallback location. If we did find a home directory then
|
||||
// just assume file .rclone.conf (legacy hidden filename) can be written in
|
||||
// its root (~/.rclone.conf).
|
||||
if homeDir != "" {
|
||||
fs.Debugf(nil, "Configuration directory could not be created and will not be used: %v", mkdirErr)
|
||||
return filepath.Join(homeDir, hiddenConfigFileName)
|
||||
}
|
||||
if !configSupplied {
|
||||
fs.Errorf(nil, "Couldn't find home directory nor create configuration directory: %v", mkdirErr)
|
||||
}
|
||||
} else if !configSupplied {
|
||||
if homeDirErr != nil {
|
||||
fs.Errorf(nil, "Couldn't find configuration directory nor home directory: %v", homeDirErr)
|
||||
} else {
|
||||
fs.Errorf(nil, "Couldn't find configuration directory nor home directory")
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
// No known location that can be used: Did possibly find a configDir
|
||||
// (XDG_CONFIG_HOME or APPDATA) which couldn't be created, but in any case
|
||||
// did not find a home directory!
|
||||
// Report it as an error, and return as last resort the path relative to current
|
||||
// working directory, of .rclone.conf (legacy hidden filename).
|
||||
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))
|
||||
// GetConfigPath returns the current config file path
|
||||
func GetConfigPath() string {
|
||||
return 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)
|
||||
// SetConfigPath sets new config file path
|
||||
//
|
||||
// Checks for empty string, os null device, or special path, all of which indicates in-memory config.
|
||||
func SetConfigPath(path string) (err error) {
|
||||
var cfgPath string
|
||||
if path == "" || path == os.DevNull {
|
||||
cfgPath = ""
|
||||
} else if filepath.Base(path) == noConfigFile {
|
||||
cfgPath = ""
|
||||
} else if err = file.IsReserved(path); err != nil {
|
||||
return err
|
||||
} else if cfgPath, err = filepath.Abs(path); err != nil {
|
||||
return err
|
||||
}
|
||||
configPath = cfgPath
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadConfig loads the config file
|
||||
func LoadConfig(ctx context.Context) error {
|
||||
// Set RCLONE_CONFIG_DIR for backend config and subprocesses
|
||||
// If empty configPath (in-memory only) the value will be "."
|
||||
_ = os.Setenv("RCLONE_CONFIG_DIR", filepath.Dir(configPath))
|
||||
// Load configuration from file (or initialize sensible default if no file or error)
|
||||
if err := Data.Load(); err == ErrorConfigFileNotFound {
|
||||
if configPath == "" {
|
||||
fs.Debugf(nil, "Config is memory-only - using defaults")
|
||||
} else {
|
||||
fs.Logf(nil, "Config file %q not found - using defaults", configPath)
|
||||
}
|
||||
} else if err != nil {
|
||||
fs.Errorf(nil, "Failed to load config file %q: %v", configPath, err)
|
||||
return errors.Wrap(err, "failed to load config file")
|
||||
} else {
|
||||
fs.Debugf(nil, "Using config file from %q", configPath)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ErrorConfigFileNotFound is returned when the config file is not found
|
||||
@@ -233,6 +353,10 @@ 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() {
|
||||
if configPath == "" {
|
||||
fs.Debugf(nil, "Skipping save for memory-only config")
|
||||
return
|
||||
}
|
||||
ctx := context.Background()
|
||||
ci := fs.GetConfig(ctx)
|
||||
var err error
|
||||
@@ -244,7 +368,6 @@ func SaveConfig() {
|
||||
time.Sleep(time.Duration(waitingTimeMs) * time.Millisecond)
|
||||
}
|
||||
fs.Errorf(nil, "Failed to save config after %d tries: %v", ci.LowLevelRetries, err)
|
||||
return
|
||||
}
|
||||
|
||||
// SetValueAndSave sets the key to the value and saves just that
|
||||
@@ -315,7 +438,10 @@ func UpdateRemote(ctx context.Context, name string, keyValues rc.Params, doObscu
|
||||
}
|
||||
Data.SetValue(name, k, vStr)
|
||||
}
|
||||
RemoteConfig(ctx, name)
|
||||
err = RemoteConfig(ctx, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
SaveConfig()
|
||||
cache.ClearConfig(name) // remove any remotes based on this config from the cache
|
||||
return nil
|
||||
|
||||
@@ -9,16 +9,17 @@ import (
|
||||
"github.com/rclone/rclone/fs/config"
|
||||
"github.com/rclone/rclone/fs/config/configfile"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestConfigLoad(t *testing.T) {
|
||||
oldConfigPath := config.ConfigPath
|
||||
config.ConfigPath = "./testdata/plain.conf"
|
||||
oldConfigPath := config.GetConfigPath()
|
||||
assert.NoError(t, config.SetConfigPath("./testdata/plain.conf"))
|
||||
defer func() {
|
||||
config.ConfigPath = oldConfigPath
|
||||
assert.NoError(t, config.SetConfigPath(oldConfigPath))
|
||||
}()
|
||||
config.ClearConfigPassword()
|
||||
configfile.LoadConfig(context.Background())
|
||||
require.NoError(t, configfile.LoadConfig(context.Background()))
|
||||
sections := config.Data.GetSectionList()
|
||||
var expect = []string{"RCLONE_ENCRYPT_V0", "nounc", "unc"}
|
||||
assert.Equal(t, expect, sections)
|
||||
|
||||
@@ -15,13 +15,10 @@ import (
|
||||
"github.com/rclone/rclone/fs/config"
|
||||
)
|
||||
|
||||
// Special value indicating in memory config file. Empty string works also.
|
||||
const noConfigFile = "/notfound"
|
||||
|
||||
// LoadConfig installs the config file handler and calls config.LoadConfig
|
||||
func LoadConfig(ctx context.Context) {
|
||||
func LoadConfig(ctx context.Context) error {
|
||||
config.Data = &Storage{}
|
||||
config.LoadConfig(ctx)
|
||||
return config.LoadConfig(ctx)
|
||||
}
|
||||
|
||||
// Storage implements config.Storage for saving and loading config
|
||||
@@ -32,29 +29,22 @@ type Storage struct {
|
||||
fi os.FileInfo // stat of the file when last loaded
|
||||
}
|
||||
|
||||
// Return whether we have a real config file or not
|
||||
func (s *Storage) noConfig() bool {
|
||||
return config.ConfigPath == "" || config.ConfigPath == noConfigFile
|
||||
}
|
||||
|
||||
// Check to see if we need to reload the config
|
||||
func (s *Storage) check() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.noConfig() {
|
||||
return
|
||||
}
|
||||
|
||||
// Check to see if config file has changed since it was last loaded
|
||||
fi, err := os.Stat(config.ConfigPath)
|
||||
if err == nil {
|
||||
// check to see if config file has changed and if it has, reload it
|
||||
if s.fi == nil || !fi.ModTime().Equal(s.fi.ModTime()) || fi.Size() != s.fi.Size() {
|
||||
fs.Debugf(nil, "Config file has changed externaly - reloading")
|
||||
err := s._load()
|
||||
if err != nil {
|
||||
fs.Errorf(nil, "Failed to read config file - using previous config: %v", err)
|
||||
if configPath := config.GetConfigPath(); configPath != "" {
|
||||
// Check to see if config file has changed since it was last loaded
|
||||
fi, err := os.Stat(configPath)
|
||||
if err == nil {
|
||||
// check to see if config file has changed and if it has, reload it
|
||||
if s.fi == nil || !fi.ModTime().Equal(s.fi.ModTime()) || fi.Size() != s.fi.Size() {
|
||||
fs.Debugf(nil, "Config file has changed externaly - reloading")
|
||||
err := s._load()
|
||||
if err != nil {
|
||||
fs.Errorf(nil, "Failed to read config file - using previous config: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,11 +61,12 @@ func (s *Storage) _load() (err error) {
|
||||
}
|
||||
}()
|
||||
|
||||
if s.noConfig() {
|
||||
configPath := config.GetConfigPath()
|
||||
if configPath == "" {
|
||||
return config.ErrorConfigFileNotFound
|
||||
}
|
||||
|
||||
fd, err := os.Open(config.ConfigPath)
|
||||
fd, err := os.Open(configPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return config.ErrorConfigFileNotFound
|
||||
@@ -85,7 +76,7 @@ func (s *Storage) _load() (err error) {
|
||||
defer fs.CheckClose(fd, &err)
|
||||
|
||||
// Update s.fi with the current file info
|
||||
s.fi, _ = os.Stat(config.ConfigPath)
|
||||
s.fi, _ = os.Stat(configPath)
|
||||
|
||||
cryptReader, err := config.Decrypt(fd)
|
||||
if err != nil {
|
||||
@@ -113,11 +104,12 @@ func (s *Storage) Save() error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.noConfig() {
|
||||
return nil
|
||||
configPath := config.GetConfigPath()
|
||||
if configPath == "" {
|
||||
return errors.Errorf("Failed to save config file: Path is empty")
|
||||
}
|
||||
|
||||
dir, name := filepath.Split(config.ConfigPath)
|
||||
dir, name := filepath.Split(configPath)
|
||||
err := os.MkdirAll(dir, os.ModePerm)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create config directory")
|
||||
@@ -149,7 +141,7 @@ func (s *Storage) Save() error {
|
||||
}
|
||||
|
||||
var fileMode os.FileMode = 0600
|
||||
info, err := os.Stat(config.ConfigPath)
|
||||
info, err := os.Stat(configPath)
|
||||
if err != nil {
|
||||
fs.Debugf(nil, "Using default permissions for config file: %v", fileMode)
|
||||
} else if info.Mode() != fileMode {
|
||||
@@ -157,25 +149,25 @@ func (s *Storage) Save() error {
|
||||
fileMode = info.Mode()
|
||||
}
|
||||
|
||||
attemptCopyGroup(config.ConfigPath, f.Name())
|
||||
attemptCopyGroup(configPath, f.Name())
|
||||
|
||||
err = os.Chmod(f.Name(), fileMode)
|
||||
if err != nil {
|
||||
fs.Errorf(nil, "Failed to set permissions on config file: %v", err)
|
||||
}
|
||||
|
||||
if err = os.Rename(config.ConfigPath, config.ConfigPath+".old"); err != nil && !os.IsNotExist(err) {
|
||||
if err = os.Rename(configPath, configPath+".old"); err != nil && !os.IsNotExist(err) {
|
||||
return errors.Errorf("Failed to move previous config to backup location: %v", err)
|
||||
}
|
||||
if err = os.Rename(f.Name(), config.ConfigPath); err != nil {
|
||||
if err = os.Rename(f.Name(), configPath); err != nil {
|
||||
return errors.Errorf("Failed to move newly written config from %s to final location: %v", f.Name(), err)
|
||||
}
|
||||
if err := os.Remove(config.ConfigPath + ".old"); err != nil && !os.IsNotExist(err) {
|
||||
if err := os.Remove(configPath + ".old"); err != nil && !os.IsNotExist(err) {
|
||||
fs.Errorf(nil, "Failed to remove backup config file: %v", err)
|
||||
}
|
||||
|
||||
// Update s.fi with the newly written file
|
||||
s.fi, _ = os.Stat(config.ConfigPath)
|
||||
s.fi, _ = os.Stat(configPath)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -39,10 +39,10 @@ func setConfigFile(t *testing.T, data string) func() {
|
||||
|
||||
require.NoError(t, out.Close())
|
||||
|
||||
old := config.ConfigPath
|
||||
config.ConfigPath = filePath
|
||||
old := config.GetConfigPath()
|
||||
assert.NoError(t, config.SetConfigPath(filePath))
|
||||
return func() {
|
||||
config.ConfigPath = old
|
||||
assert.NoError(t, config.SetConfigPath(old))
|
||||
_ = os.Remove(filePath)
|
||||
}
|
||||
}
|
||||
@@ -160,7 +160,7 @@ type = number3
|
||||
`, toUnix(buf))
|
||||
t.Run("Save", func(t *testing.T) {
|
||||
require.NoError(t, data.Save())
|
||||
buf, err := ioutil.ReadFile(config.ConfigPath)
|
||||
buf, err := ioutil.ReadFile(config.GetConfigPath())
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, `[one]
|
||||
fruit = potato
|
||||
@@ -188,7 +188,7 @@ func TestConfigFileReload(t *testing.T) {
|
||||
assert.Equal(t, "", value)
|
||||
|
||||
// Now write a new value on the end
|
||||
out, err := os.OpenFile(config.ConfigPath, os.O_APPEND|os.O_WRONLY, 0777)
|
||||
out, err := os.OpenFile(config.GetConfigPath(), os.O_APPEND|os.O_WRONLY, 0777)
|
||||
require.NoError(t, err)
|
||||
fmt.Fprintln(out, "appended = what magic")
|
||||
require.NoError(t, out.Close())
|
||||
@@ -203,7 +203,7 @@ func TestConfigFileDoesNotExist(t *testing.T) {
|
||||
defer setConfigFile(t, configData)()
|
||||
data := &Storage{}
|
||||
|
||||
require.NoError(t, os.Remove(config.ConfigPath))
|
||||
require.NoError(t, os.Remove(config.GetConfigPath()))
|
||||
|
||||
err := data.Load()
|
||||
require.Equal(t, config.ErrorConfigFileNotFound, err)
|
||||
@@ -215,7 +215,7 @@ func TestConfigFileDoesNotExist(t *testing.T) {
|
||||
}
|
||||
|
||||
func testConfigFileNoConfig(t *testing.T, configPath string) {
|
||||
config.ConfigPath = configPath
|
||||
assert.NoError(t, config.SetConfigPath(configPath))
|
||||
data := &Storage{}
|
||||
|
||||
err := data.Load()
|
||||
@@ -227,13 +227,13 @@ func testConfigFileNoConfig(t *testing.T, configPath string) {
|
||||
assert.Equal(t, "42", value)
|
||||
|
||||
err = data.Save()
|
||||
require.NoError(t, err)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestConfigFileNoConfig(t *testing.T) {
|
||||
old := config.ConfigPath
|
||||
old := config.GetConfigPath()
|
||||
defer func() {
|
||||
config.ConfigPath = old
|
||||
assert.NoError(t, config.SetConfigPath(old))
|
||||
}()
|
||||
|
||||
t.Run("Empty", func(t *testing.T) {
|
||||
|
||||
@@ -6,7 +6,6 @@ package configflags
|
||||
import (
|
||||
"log"
|
||||
"net"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -23,6 +22,7 @@ var (
|
||||
// these will get interpreted into fs.Config via SetFlags() below
|
||||
verbose int
|
||||
quiet bool
|
||||
configPath string
|
||||
dumpHeaders bool
|
||||
dumpBodies bool
|
||||
deleteBefore bool
|
||||
@@ -45,7 +45,7 @@ func AddFlags(ci *fs.ConfigInfo, flagSet *pflag.FlagSet) {
|
||||
flags.DurationVarP(flagSet, &ci.ModifyWindow, "modify-window", "", ci.ModifyWindow, "Max time diff to be considered the same")
|
||||
flags.IntVarP(flagSet, &ci.Checkers, "checkers", "", ci.Checkers, "Number of checkers to run in parallel.")
|
||||
flags.IntVarP(flagSet, &ci.Transfers, "transfers", "", ci.Transfers, "Number of file transfers to run in parallel.")
|
||||
flags.StringVarP(flagSet, &config.ConfigPath, "config", "", config.ConfigPath, "Config file.")
|
||||
flags.StringVarP(flagSet, &configPath, "config", "", config.GetConfigPath(), "Config file.")
|
||||
flags.StringVarP(flagSet, &config.CacheDir, "cache-dir", "", config.CacheDir, "Directory rclone will use for caching.")
|
||||
flags.BoolVarP(flagSet, &ci.CheckSum, "checksum", "c", ci.CheckSum, "Skip based on checksum (if available) & size, not mod-time & size")
|
||||
flags.BoolVarP(flagSet, &ci.SizeOnly, "size-only", "", ci.SizeOnly, "Skip based on size only, not mod-time or checksum")
|
||||
@@ -267,10 +267,9 @@ func SetFlags(ci *fs.ConfigInfo) {
|
||||
}
|
||||
}
|
||||
|
||||
// Make the config file absolute
|
||||
configPath, err := filepath.Abs(config.ConfigPath)
|
||||
if err == nil {
|
||||
config.ConfigPath = configPath
|
||||
// Set path to configuration file
|
||||
if err := config.SetConfigPath(configPath); err != nil {
|
||||
log.Fatalf("--config: Failed to set %q as config path: %v", configPath, err)
|
||||
}
|
||||
|
||||
// Set whether multi-thread-streams was set
|
||||
|
||||
@@ -2,8 +2,24 @@
|
||||
package configmap
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Priority of getters
|
||||
type Priority int8
|
||||
|
||||
// Priority levels for AddGetter
|
||||
const (
|
||||
PriorityNormal Priority = iota
|
||||
PriorityConfig // use for reading from the config
|
||||
PriorityDefault // use for default values
|
||||
PriorityMax
|
||||
)
|
||||
|
||||
// Getter provides an interface to get config items
|
||||
@@ -29,9 +45,13 @@ type Mapper interface {
|
||||
// Map provides a wrapper around multiple Setter and
|
||||
// Getter interfaces.
|
||||
type Map struct {
|
||||
setters []Setter
|
||||
getters []Getter
|
||||
override []Getter
|
||||
setters []Setter
|
||||
getters []getprio
|
||||
}
|
||||
|
||||
type getprio struct {
|
||||
getter Getter
|
||||
priority Priority
|
||||
}
|
||||
|
||||
// New returns an empty Map
|
||||
@@ -39,18 +59,12 @@ func New() *Map {
|
||||
return &Map{}
|
||||
}
|
||||
|
||||
// AddGetter appends a getter onto the end of the getters
|
||||
func (c *Map) AddGetter(getter Getter) *Map {
|
||||
c.getters = append(c.getters, getter)
|
||||
return c
|
||||
}
|
||||
|
||||
// AddOverrideGetter appends a getter onto the end of the getters
|
||||
//
|
||||
// It also appends it onto the override getters for GetOverride
|
||||
func (c *Map) AddOverrideGetter(getter Getter) *Map {
|
||||
c.getters = append(c.getters, getter)
|
||||
c.override = append(c.override, getter)
|
||||
// AddGetter appends a getter onto the end of the getters in priority order
|
||||
func (c *Map) AddGetter(getter Getter, priority Priority) *Map {
|
||||
c.getters = append(c.getters, getprio{getter, priority})
|
||||
sort.SliceStable(c.getters, func(i, j int) bool {
|
||||
return c.getters[i].priority < c.getters[j].priority
|
||||
})
|
||||
return c
|
||||
}
|
||||
|
||||
@@ -60,12 +74,34 @@ func (c *Map) AddSetter(setter Setter) *Map {
|
||||
return c
|
||||
}
|
||||
|
||||
// get gets an item with the key passed in and return the value from
|
||||
// the first getter. If the item is found then it returns true,
|
||||
// otherwise false.
|
||||
func (c *Map) get(key string, getters []Getter) (value string, ok bool) {
|
||||
for _, do := range getters {
|
||||
value, ok = do.Get(key)
|
||||
// ClearSetters removes all the setters set so far
|
||||
func (c *Map) ClearSetters() *Map {
|
||||
c.setters = nil
|
||||
return c
|
||||
}
|
||||
|
||||
// ClearGetters removes all the getters with the priority given
|
||||
func (c *Map) ClearGetters(priority Priority) *Map {
|
||||
getters := c.getters[:0]
|
||||
for _, item := range c.getters {
|
||||
if item.priority != priority {
|
||||
getters = append(getters, item)
|
||||
}
|
||||
}
|
||||
c.getters = getters
|
||||
return c
|
||||
}
|
||||
|
||||
// GetPriority gets an item with the key passed in and return the
|
||||
// value from the first getter to return a result with priority <=
|
||||
// maxPriority. If the item is found then it returns true, otherwise
|
||||
// false.
|
||||
func (c *Map) GetPriority(key string, maxPriority Priority) (value string, ok bool) {
|
||||
for _, item := range c.getters {
|
||||
if item.priority > maxPriority {
|
||||
break
|
||||
}
|
||||
value, ok = item.getter.Get(key)
|
||||
if ok {
|
||||
return value, ok
|
||||
}
|
||||
@@ -77,14 +113,7 @@ func (c *Map) get(key string, getters []Getter) (value string, ok bool) {
|
||||
// the first getter. If the item is found then it returns true,
|
||||
// otherwise false.
|
||||
func (c *Map) Get(key string) (value string, ok bool) {
|
||||
return c.get(key, c.getters)
|
||||
}
|
||||
|
||||
// GetOverride gets an item with the key passed in and return the
|
||||
// value from the first override getter. If the item is found then it
|
||||
// returns true, otherwise false.
|
||||
func (c *Map) GetOverride(key string) (value string, ok bool) {
|
||||
return c.get(key, c.override)
|
||||
return c.GetPriority(key, PriorityMax)
|
||||
}
|
||||
|
||||
// Set sets an item into all the stored setters.
|
||||
@@ -135,3 +164,38 @@ func (c Simple) String() string {
|
||||
}
|
||||
return out.String()
|
||||
}
|
||||
|
||||
// Encode from c into a string suitable for putting on the command line
|
||||
func (c Simple) Encode() (string, error) {
|
||||
if len(c) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
buf, err := json.Marshal(c)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "encode simple map")
|
||||
}
|
||||
return base64.RawStdEncoding.EncodeToString(buf), nil
|
||||
}
|
||||
|
||||
// Decode an Encode~d string in into c
|
||||
func (c Simple) Decode(in string) error {
|
||||
// Remove all whitespace from the input string
|
||||
in = strings.Map(func(r rune) rune {
|
||||
if unicode.IsSpace(r) {
|
||||
return -1
|
||||
}
|
||||
return r
|
||||
}, in)
|
||||
if len(in) == 0 {
|
||||
return nil
|
||||
}
|
||||
decodedM, err := base64.RawStdEncoding.DecodeString(in)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "decode simple map")
|
||||
}
|
||||
err = json.Unmarshal(decodedM, &c)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "parse simple map")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package configmap
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -27,7 +29,7 @@ func TestConfigMapGet(t *testing.T) {
|
||||
"config1": "one",
|
||||
}
|
||||
|
||||
m.AddGetter(m1)
|
||||
m.AddGetter(m1, PriorityNormal)
|
||||
|
||||
value, found = m.Get("config1")
|
||||
assert.Equal(t, "one", value)
|
||||
@@ -42,7 +44,7 @@ func TestConfigMapGet(t *testing.T) {
|
||||
"config2": "two2",
|
||||
}
|
||||
|
||||
m.AddGetter(m2)
|
||||
m.AddGetter(m2, PriorityNormal)
|
||||
|
||||
value, found = m.Get("config1")
|
||||
assert.Equal(t, "one", value)
|
||||
@@ -88,56 +90,160 @@ func TestConfigMapSet(t *testing.T) {
|
||||
"config1": "beetroot",
|
||||
"config2": "potato",
|
||||
}, m2)
|
||||
|
||||
m.ClearSetters()
|
||||
|
||||
// Check that nothing gets set
|
||||
m.Set("config1", "BEETROOT")
|
||||
|
||||
assert.Equal(t, Simple{
|
||||
"config1": "beetroot",
|
||||
"config2": "potato",
|
||||
}, m1)
|
||||
assert.Equal(t, Simple{
|
||||
"config1": "beetroot",
|
||||
"config2": "potato",
|
||||
}, m2)
|
||||
|
||||
}
|
||||
|
||||
func TestConfigMapGetOverride(t *testing.T) {
|
||||
func TestConfigMapGetPriority(t *testing.T) {
|
||||
m := New()
|
||||
|
||||
value, found := m.GetOverride("config1")
|
||||
value, found := m.GetPriority("config1", PriorityMax)
|
||||
assert.Equal(t, "", value)
|
||||
assert.Equal(t, false, found)
|
||||
|
||||
value, found = m.GetOverride("config2")
|
||||
value, found = m.GetPriority("config2", PriorityMax)
|
||||
assert.Equal(t, "", value)
|
||||
assert.Equal(t, false, found)
|
||||
|
||||
m1 := Simple{
|
||||
"config1": "one",
|
||||
"config3": "three",
|
||||
}
|
||||
|
||||
m.AddOverrideGetter(m1)
|
||||
m.AddGetter(m1, PriorityConfig)
|
||||
|
||||
value, found = m.GetOverride("config1")
|
||||
value, found = m.GetPriority("config1", PriorityNormal)
|
||||
assert.Equal(t, "", value)
|
||||
assert.Equal(t, false, found)
|
||||
|
||||
value, found = m.GetPriority("config2", PriorityNormal)
|
||||
assert.Equal(t, "", value)
|
||||
assert.Equal(t, false, found)
|
||||
|
||||
value, found = m.GetPriority("config3", PriorityNormal)
|
||||
assert.Equal(t, "", value)
|
||||
assert.Equal(t, false, found)
|
||||
|
||||
value, found = m.GetPriority("config1", PriorityConfig)
|
||||
assert.Equal(t, "one", value)
|
||||
assert.Equal(t, true, found)
|
||||
|
||||
value, found = m.GetOverride("config2")
|
||||
value, found = m.GetPriority("config2", PriorityConfig)
|
||||
assert.Equal(t, "", value)
|
||||
assert.Equal(t, false, found)
|
||||
|
||||
value, found = m.GetPriority("config3", PriorityConfig)
|
||||
assert.Equal(t, "three", value)
|
||||
assert.Equal(t, true, found)
|
||||
|
||||
value, found = m.GetPriority("config1", PriorityMax)
|
||||
assert.Equal(t, "one", value)
|
||||
assert.Equal(t, true, found)
|
||||
|
||||
value, found = m.GetPriority("config2", PriorityMax)
|
||||
assert.Equal(t, "", value)
|
||||
assert.Equal(t, false, found)
|
||||
|
||||
value, found = m.GetPriority("config3", PriorityMax)
|
||||
assert.Equal(t, "three", value)
|
||||
assert.Equal(t, true, found)
|
||||
|
||||
m2 := Simple{
|
||||
"config1": "one2",
|
||||
"config2": "two2",
|
||||
}
|
||||
|
||||
m.AddGetter(m2)
|
||||
m.AddGetter(m2, PriorityNormal)
|
||||
|
||||
value, found = m.GetOverride("config1")
|
||||
assert.Equal(t, "one", value)
|
||||
value, found = m.GetPriority("config1", PriorityNormal)
|
||||
assert.Equal(t, "one2", value)
|
||||
assert.Equal(t, true, found)
|
||||
|
||||
value, found = m.GetOverride("config2")
|
||||
assert.Equal(t, "", value)
|
||||
assert.Equal(t, false, found)
|
||||
|
||||
value, found = m.Get("config1")
|
||||
assert.Equal(t, "one", value)
|
||||
assert.Equal(t, true, found)
|
||||
|
||||
value, found = m.Get("config2")
|
||||
value, found = m.GetPriority("config2", PriorityNormal)
|
||||
assert.Equal(t, "two2", value)
|
||||
assert.Equal(t, true, found)
|
||||
|
||||
value, found = m.GetPriority("config3", PriorityNormal)
|
||||
assert.Equal(t, "", value)
|
||||
assert.Equal(t, false, found)
|
||||
|
||||
value, found = m.GetPriority("config1", PriorityConfig)
|
||||
assert.Equal(t, "one2", value)
|
||||
assert.Equal(t, true, found)
|
||||
|
||||
value, found = m.GetPriority("config2", PriorityConfig)
|
||||
assert.Equal(t, "two2", value)
|
||||
assert.Equal(t, true, found)
|
||||
|
||||
value, found = m.GetPriority("config3", PriorityConfig)
|
||||
assert.Equal(t, "three", value)
|
||||
assert.Equal(t, true, found)
|
||||
|
||||
value, found = m.GetPriority("config1", PriorityMax)
|
||||
assert.Equal(t, "one2", value)
|
||||
assert.Equal(t, true, found)
|
||||
|
||||
value, found = m.GetPriority("config2", PriorityMax)
|
||||
assert.Equal(t, "two2", value)
|
||||
assert.Equal(t, true, found)
|
||||
|
||||
value, found = m.GetPriority("config3", PriorityMax)
|
||||
assert.Equal(t, "three", value)
|
||||
assert.Equal(t, true, found)
|
||||
}
|
||||
|
||||
func TestConfigMapClearGetters(t *testing.T) {
|
||||
m := New()
|
||||
m1 := Simple{}
|
||||
m2 := Simple{}
|
||||
m3 := Simple{}
|
||||
m.AddGetter(m1, PriorityNormal)
|
||||
m.AddGetter(m2, PriorityDefault)
|
||||
m.AddGetter(m3, PriorityConfig)
|
||||
assert.Equal(t, []getprio{
|
||||
{m1, PriorityNormal},
|
||||
{m3, PriorityConfig},
|
||||
{m2, PriorityDefault},
|
||||
}, m.getters)
|
||||
m.ClearGetters(PriorityConfig)
|
||||
assert.Equal(t, []getprio{
|
||||
{m1, PriorityNormal},
|
||||
{m2, PriorityDefault},
|
||||
}, m.getters)
|
||||
m.ClearGetters(PriorityNormal)
|
||||
assert.Equal(t, []getprio{
|
||||
{m2, PriorityDefault},
|
||||
}, m.getters)
|
||||
m.ClearGetters(PriorityDefault)
|
||||
assert.Equal(t, []getprio{}, m.getters)
|
||||
m.ClearGetters(PriorityDefault)
|
||||
assert.Equal(t, []getprio{}, m.getters)
|
||||
}
|
||||
|
||||
func TestConfigMapClearSetters(t *testing.T) {
|
||||
m := New()
|
||||
m1 := Simple{}
|
||||
m2 := Simple{}
|
||||
m3 := Simple{}
|
||||
m.AddSetter(m1)
|
||||
m.AddSetter(m2)
|
||||
m.AddSetter(m3)
|
||||
assert.Equal(t, []Setter{m1, m2, m3}, m.setters)
|
||||
m.ClearSetters()
|
||||
assert.Equal(t, []Setter(nil), m.setters)
|
||||
}
|
||||
|
||||
func TestSimpleString(t *testing.T) {
|
||||
@@ -163,3 +269,91 @@ func TestSimpleString(t *testing.T) {
|
||||
"apple": "",
|
||||
}.String())
|
||||
}
|
||||
|
||||
func TestSimpleEncode(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
in Simple
|
||||
want string
|
||||
}{
|
||||
{
|
||||
in: Simple{},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
in: Simple{
|
||||
"one": "potato",
|
||||
},
|
||||
want: "eyJvbmUiOiJwb3RhdG8ifQ",
|
||||
},
|
||||
{
|
||||
in: Simple{
|
||||
"one": "potato",
|
||||
"two": "",
|
||||
},
|
||||
want: "eyJvbmUiOiJwb3RhdG8iLCJ0d28iOiIifQ",
|
||||
},
|
||||
} {
|
||||
got, err := test.in.Encode()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, test.want, got)
|
||||
gotM := Simple{}
|
||||
err = gotM.Decode(got)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, test.in, gotM)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSimpleDecode(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
in string
|
||||
want Simple
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
in: "",
|
||||
want: Simple{},
|
||||
},
|
||||
{
|
||||
in: "eyJvbmUiOiJwb3RhdG8ifQ",
|
||||
want: Simple{
|
||||
"one": "potato",
|
||||
},
|
||||
},
|
||||
{
|
||||
in: " e yJvbm UiOiJwb\r\n 3Rhd\tG8ifQ\n\n ",
|
||||
want: Simple{
|
||||
"one": "potato",
|
||||
},
|
||||
},
|
||||
{
|
||||
in: "eyJvbmUiOiJwb3RhdG8iLCJ0d28iOiIifQ",
|
||||
want: Simple{
|
||||
"one": "potato",
|
||||
"two": "",
|
||||
},
|
||||
},
|
||||
{
|
||||
in: "!!!!!",
|
||||
want: Simple{},
|
||||
wantErr: "decode simple map",
|
||||
},
|
||||
{
|
||||
in: base64.RawStdEncoding.EncodeToString([]byte(`null`)),
|
||||
want: Simple{},
|
||||
},
|
||||
{
|
||||
in: base64.RawStdEncoding.EncodeToString([]byte(`rubbish`)),
|
||||
want: Simple{},
|
||||
wantErr: "parse simple map",
|
||||
},
|
||||
} {
|
||||
got := Simple{}
|
||||
err := got.Decode(test.in)
|
||||
assert.Equal(t, test.want, got, test.in)
|
||||
if test.wantErr == "" {
|
||||
require.NoError(t, err, test.in)
|
||||
} else {
|
||||
assert.Contains(t, err.Error(), test.wantErr, test.in)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,10 +16,10 @@ import (
|
||||
|
||||
func TestConfigLoadEncrypted(t *testing.T) {
|
||||
var err error
|
||||
oldConfigPath := config.ConfigPath
|
||||
config.ConfigPath = "./testdata/encrypted.conf"
|
||||
oldConfigPath := config.GetConfigPath()
|
||||
assert.NoError(t, config.SetConfigPath("./testdata/encrypted.conf"))
|
||||
defer func() {
|
||||
config.ConfigPath = oldConfigPath
|
||||
assert.NoError(t, config.SetConfigPath(oldConfigPath))
|
||||
config.ClearConfigPassword()
|
||||
}()
|
||||
|
||||
@@ -40,13 +40,13 @@ func TestConfigLoadEncrypted(t *testing.T) {
|
||||
func TestConfigLoadEncryptedWithValidPassCommand(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ci := fs.GetConfig(ctx)
|
||||
oldConfigPath := config.ConfigPath
|
||||
oldConfigPath := config.GetConfigPath()
|
||||
oldConfig := *ci
|
||||
config.ConfigPath = "./testdata/encrypted.conf"
|
||||
assert.NoError(t, config.SetConfigPath("./testdata/encrypted.conf"))
|
||||
// using ci.PasswordCommand, correct password
|
||||
ci.PasswordCommand = fs.SpaceSepList{"echo", "asdf"}
|
||||
defer func() {
|
||||
config.ConfigPath = oldConfigPath
|
||||
assert.NoError(t, config.SetConfigPath(oldConfigPath))
|
||||
config.ClearConfigPassword()
|
||||
*ci = oldConfig
|
||||
ci.PasswordCommand = nil
|
||||
@@ -69,13 +69,13 @@ func TestConfigLoadEncryptedWithValidPassCommand(t *testing.T) {
|
||||
func TestConfigLoadEncryptedWithInvalidPassCommand(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ci := fs.GetConfig(ctx)
|
||||
oldConfigPath := config.ConfigPath
|
||||
oldConfigPath := config.GetConfigPath()
|
||||
oldConfig := *ci
|
||||
config.ConfigPath = "./testdata/encrypted.conf"
|
||||
assert.NoError(t, config.SetConfigPath("./testdata/encrypted.conf"))
|
||||
// using ci.PasswordCommand, incorrect password
|
||||
ci.PasswordCommand = fs.SpaceSepList{"echo", "asdf-blurfl"}
|
||||
defer func() {
|
||||
config.ConfigPath = oldConfigPath
|
||||
assert.NoError(t, config.SetConfigPath(oldConfigPath))
|
||||
config.ClearConfigPassword()
|
||||
*ci = oldConfig
|
||||
ci.PasswordCommand = nil
|
||||
@@ -92,24 +92,24 @@ 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 }()
|
||||
oldConfigPath := config.GetConfigPath()
|
||||
assert.NoError(t, config.SetConfigPath("./testdata/enc-short.conf"))
|
||||
defer func() { assert.NoError(t, config.SetConfigPath(oldConfigPath)) }()
|
||||
err = config.Data.Load()
|
||||
require.Error(t, err)
|
||||
|
||||
// This file contains invalid base64 characters.
|
||||
config.ConfigPath = "./testdata/enc-invalid.conf"
|
||||
assert.NoError(t, config.SetConfigPath("./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"
|
||||
assert.NoError(t, config.SetConfigPath("./testdata/enc-too-new.conf"))
|
||||
err = config.Data.Load()
|
||||
require.Error(t, err)
|
||||
|
||||
// This file does not exist.
|
||||
config.ConfigPath = "./testdata/filenotfound.conf"
|
||||
assert.NoError(t, config.SetConfigPath("./testdata/filenotfound.conf"))
|
||||
err = config.Data.Load()
|
||||
assert.Equal(t, config.ErrorConfigFileNotFound, err)
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ const testName = "configTestNameForRc"
|
||||
|
||||
func TestRc(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
configfile.LoadConfig(ctx)
|
||||
require.NoError(t, configfile.LoadConfig(ctx))
|
||||
// Create the test remote
|
||||
call := rc.Calls.Get("config/create")
|
||||
assert.NotNil(t, call)
|
||||
|
||||
@@ -270,13 +270,14 @@ func OkRemote(name string) bool {
|
||||
}
|
||||
|
||||
// RemoteConfig runs the config helper for the remote if needed
|
||||
func RemoteConfig(ctx context.Context, name string) {
|
||||
func RemoteConfig(ctx context.Context, name string) error {
|
||||
fmt.Printf("Remote config\n")
|
||||
f := mustFindByName(name)
|
||||
if f.Config != nil {
|
||||
m := fs.ConfigMap(f, name, nil)
|
||||
f.Config(ctx, name, m)
|
||||
return f.Config(ctx, name, m)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// matchProvider returns true if provider matches the providerConfig string.
|
||||
@@ -456,7 +457,7 @@ func editOptions(ri *fs.RegInfo, name string, isNew bool) {
|
||||
}
|
||||
|
||||
// NewRemote make a new remote from its name
|
||||
func NewRemote(ctx context.Context, name string) {
|
||||
func NewRemote(ctx context.Context, name string) error {
|
||||
var (
|
||||
newType string
|
||||
ri *fs.RegInfo
|
||||
@@ -476,16 +477,19 @@ func NewRemote(ctx context.Context, name string) {
|
||||
Data.SetValue(name, "type", newType)
|
||||
|
||||
editOptions(ri, name, true)
|
||||
RemoteConfig(ctx, name)
|
||||
err = RemoteConfig(ctx, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if OkRemote(name) {
|
||||
SaveConfig()
|
||||
return
|
||||
return nil
|
||||
}
|
||||
EditRemote(ctx, ri, name)
|
||||
return EditRemote(ctx, ri, name)
|
||||
}
|
||||
|
||||
// EditRemote gets the user to edit a remote
|
||||
func EditRemote(ctx context.Context, ri *fs.RegInfo, name string) {
|
||||
func EditRemote(ctx context.Context, ri *fs.RegInfo, name string) error {
|
||||
ShowRemote(name)
|
||||
fmt.Printf("Edit remote\n")
|
||||
for {
|
||||
@@ -495,7 +499,7 @@ func EditRemote(ctx context.Context, ri *fs.RegInfo, name string) {
|
||||
}
|
||||
}
|
||||
SaveConfig()
|
||||
RemoteConfig(ctx, name)
|
||||
return RemoteConfig(ctx, name)
|
||||
}
|
||||
|
||||
// DeleteRemote gets the user to delete a remote
|
||||
@@ -535,12 +539,16 @@ func CopyRemote(name string) {
|
||||
|
||||
// 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:")
|
||||
if configPath := GetConfigPath(); configPath == "" {
|
||||
fmt.Println("Configuration is in memory only")
|
||||
} else {
|
||||
fmt.Println("Configuration file is stored at:")
|
||||
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)
|
||||
}
|
||||
fmt.Printf("%s\n", ConfigPath)
|
||||
}
|
||||
|
||||
// ShowConfig prints the (unencrypted) config options
|
||||
@@ -556,7 +564,7 @@ func ShowConfig() {
|
||||
}
|
||||
|
||||
// EditConfig edits the config file interactively
|
||||
func EditConfig(ctx context.Context) {
|
||||
func EditConfig(ctx context.Context) (err error) {
|
||||
for {
|
||||
haveRemotes := len(Data.GetSectionList()) != 0
|
||||
what := []string{"eEdit existing remote", "nNew remote", "dDelete remote", "rRename remote", "cCopy remote", "sSet configuration password", "qQuit config"}
|
||||
@@ -573,9 +581,15 @@ func EditConfig(ctx context.Context) {
|
||||
case 'e':
|
||||
name := ChooseRemote()
|
||||
fs := mustFindByName(name)
|
||||
EditRemote(ctx, fs, name)
|
||||
err = EditRemote(ctx, fs, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case 'n':
|
||||
NewRemote(ctx, NewRemoteName())
|
||||
err = NewRemote(ctx, NewRemoteName())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case 'd':
|
||||
name := ChooseRemote()
|
||||
DeleteRemote(name)
|
||||
@@ -586,8 +600,7 @@ func EditConfig(ctx context.Context) {
|
||||
case 's':
|
||||
SetPassword()
|
||||
case 'q':
|
||||
return
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,16 +34,16 @@ func testConfigFile(t *testing.T, configFileName string) func() {
|
||||
|
||||
// temporarily adapt configuration
|
||||
oldOsStdout := os.Stdout
|
||||
oldConfigPath := config.ConfigPath
|
||||
oldConfigPath := config.GetConfigPath()
|
||||
oldConfig := *ci
|
||||
oldConfigFile := config.Data
|
||||
oldReadLine := config.ReadLine
|
||||
oldPassword := config.Password
|
||||
os.Stdout = nil
|
||||
config.ConfigPath = path
|
||||
assert.NoError(t, config.SetConfigPath(path))
|
||||
ci = &fs.ConfigInfo{}
|
||||
|
||||
configfile.LoadConfig(ctx)
|
||||
require.NoError(t, configfile.LoadConfig(ctx))
|
||||
assert.Equal(t, []string{}, config.Data.GetSectionList())
|
||||
|
||||
// Fake a remote
|
||||
@@ -69,7 +69,7 @@ func testConfigFile(t *testing.T, configFileName string) func() {
|
||||
assert.NoError(t, err)
|
||||
|
||||
os.Stdout = oldOsStdout
|
||||
config.ConfigPath = oldConfigPath
|
||||
assert.NoError(t, config.SetConfigPath(oldConfigPath))
|
||||
config.ReadLine = oldReadLine
|
||||
config.Password = oldPassword
|
||||
*ci = oldConfig
|
||||
@@ -103,7 +103,7 @@ func TestCRUD(t *testing.T) {
|
||||
"secret", // repeat
|
||||
"y", // looks good, save
|
||||
})
|
||||
config.NewRemote(ctx, "test")
|
||||
require.NoError(t, config.NewRemote(ctx, "test"))
|
||||
|
||||
assert.Equal(t, []string{"test"}, config.Data.GetSectionList())
|
||||
assert.Equal(t, "config_test_remote", config.FileGet("test", "type"))
|
||||
@@ -146,7 +146,7 @@ func TestChooseOption(t *testing.T) {
|
||||
assert.Equal(t, 1024, bits)
|
||||
return "not very random password", nil
|
||||
}
|
||||
config.NewRemote(ctx, "test")
|
||||
require.NoError(t, 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")))
|
||||
@@ -158,7 +158,7 @@ func TestChooseOption(t *testing.T) {
|
||||
"n", // not required
|
||||
"y", // looks good, save
|
||||
})
|
||||
config.NewRemote(ctx, "test")
|
||||
require.NoError(t, config.NewRemote(ctx, "test"))
|
||||
|
||||
assert.Equal(t, "true", config.FileGet("test", "bool"))
|
||||
assert.Equal(t, "", config.FileGet("test", "pass"))
|
||||
@@ -175,7 +175,7 @@ func TestNewRemoteName(t *testing.T) {
|
||||
"n", // not required
|
||||
"y", // looks good, save
|
||||
})
|
||||
config.NewRemote(ctx, "test")
|
||||
require.NoError(t, config.NewRemote(ctx, "test"))
|
||||
|
||||
config.ReadLine = makeReadLine([]string{
|
||||
"test", // already exists
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user