mirror of
https://github.com/rclone/rclone.git
synced 2026-02-10 21:50:00 +00:00
Compare commits
64 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b1194c57e | ||
|
|
e6dd121f52 | ||
|
|
e600217666 | ||
|
|
bc17ca7ed9 | ||
|
|
1916410316 | ||
|
|
dddfbec92a | ||
|
|
75a88de55c | ||
|
|
2466f4d152 | ||
|
|
39283c8a35 | ||
|
|
46c2f55545 | ||
|
|
fc2afcbcbd | ||
|
|
fa0a9653d2 | ||
|
|
181267e20e | ||
|
|
75e8ea383c | ||
|
|
8c8b58a7de | ||
|
|
b961e07c57 | ||
|
|
0b80d1481a | ||
|
|
89550e7121 | ||
|
|
370c218c63 | ||
|
|
b972dcb0ae | ||
|
|
0bfa9811f7 | ||
|
|
aa9b2c31f4 | ||
|
|
cff75db6a4 | ||
|
|
75252e4a89 | ||
|
|
2089405e1b | ||
|
|
a379eec9d9 | ||
|
|
45d5339fcb | ||
|
|
bb5637d46a | ||
|
|
1f05d5bf4a | ||
|
|
ff87da9c3b | ||
|
|
3d81b75f44 | ||
|
|
baba6d67e6 | ||
|
|
04c0564fe2 | ||
|
|
91cfdb81f5 | ||
|
|
deae7bf33c | ||
|
|
04a0da1f92 | ||
|
|
9486df0226 | ||
|
|
948a5d25c2 | ||
|
|
f7c31cd210 | ||
|
|
696e7b2833 | ||
|
|
e76cf1217f | ||
|
|
543e37f662 | ||
|
|
c514cb752d | ||
|
|
c0ca93ae6f | ||
|
|
38a89d49ae | ||
|
|
6531126eb2 | ||
|
|
25d0e59ef8 | ||
|
|
b0db08fd2b | ||
|
|
07addf74fd | ||
|
|
52c7c738ca | ||
|
|
5c32b32011 | ||
|
|
fe61cff079 | ||
|
|
fbab1e55bb | ||
|
|
1bfd07567e | ||
|
|
f97c4c8d9d | ||
|
|
a3c55462a8 | ||
|
|
bbb9a504a8 | ||
|
|
dedc7d885c | ||
|
|
c5ac96e9e7 | ||
|
|
9959c5f17f | ||
|
|
e8d0a363fc | ||
|
|
935b7c1c0f | ||
|
|
15ce0ae57c | ||
|
|
67703a73de |
@@ -4,7 +4,6 @@ dist: trusty
|
|||||||
os:
|
os:
|
||||||
- linux
|
- linux
|
||||||
go:
|
go:
|
||||||
- 1.7.x
|
|
||||||
- 1.8.x
|
- 1.8.x
|
||||||
- 1.9.x
|
- 1.9.x
|
||||||
- 1.10.x
|
- 1.10.x
|
||||||
|
|||||||
@@ -123,6 +123,13 @@ but they can be run against any of the remotes.
|
|||||||
cd fs/operations
|
cd fs/operations
|
||||||
go test -v -remote TestDrive:
|
go test -v -remote TestDrive:
|
||||||
|
|
||||||
|
If you want to use the integration test framework to run these tests
|
||||||
|
all together with an HTML report and test retries then from the
|
||||||
|
project root:
|
||||||
|
|
||||||
|
go install github.com/ncw/rclone/fstest/test_all
|
||||||
|
test_all -backend drive
|
||||||
|
|
||||||
If you want to run all the integration tests against all the remotes,
|
If you want to run all the integration tests against all the remotes,
|
||||||
then change into the project root and run
|
then change into the project root and run
|
||||||
|
|
||||||
@@ -343,7 +350,7 @@ Unit tests
|
|||||||
|
|
||||||
Integration tests
|
Integration tests
|
||||||
|
|
||||||
* Add your fs to `fstest/test_all/test_all.go`
|
* Add your backend to `fstest/test_all/config.yaml`
|
||||||
* Make sure integration tests pass with
|
* Make sure integration tests pass with
|
||||||
* `cd fs/operations`
|
* `cd fs/operations`
|
||||||
* `go test -v -remote TestRemote:`
|
* `go test -v -remote TestRemote:`
|
||||||
|
|||||||
7
Makefile
7
Makefile
@@ -50,10 +50,9 @@ version:
|
|||||||
|
|
||||||
# Full suite of integration tests
|
# Full suite of integration tests
|
||||||
test: rclone
|
test: rclone
|
||||||
go install github.com/ncw/rclone/fstest/test_all
|
go install --ldflags "-s -X github.com/ncw/rclone/fs.Version=$(TAG)" $(BUILDTAGS) github.com/ncw/rclone/fstest/test_all
|
||||||
-go test -v -count 1 -timeout 20m $(BUILDTAGS) $(GO_FILES) 2>&1 | tee test.log
|
-test_all 2>&1 | tee test_all.log
|
||||||
-test_all github.com/ncw/rclone/fs/operations github.com/ncw/rclone/fs/sync 2>&1 | tee fs/test_all.log
|
@echo "Written logs in test_all.log"
|
||||||
@echo "Written logs in test.log and fs/test_all.log"
|
|
||||||
|
|
||||||
# Quick test
|
# Quick test
|
||||||
quicktest:
|
quicktest:
|
||||||
|
|||||||
13
README.md
13
README.md
@@ -55,6 +55,8 @@ Rclone *("rsync for cloud storage")* is a command line program to sync files and
|
|||||||
* WebDAV [:page_facing_up:](https://rclone.org/webdav/)
|
* WebDAV [:page_facing_up:](https://rclone.org/webdav/)
|
||||||
* Yandex Disk [:page_facing_up:](https://rclone.org/yandex/)
|
* Yandex Disk [:page_facing_up:](https://rclone.org/yandex/)
|
||||||
* The local filesystem [:page_facing_up:](https://rclone.org/local/)
|
* The local filesystem [:page_facing_up:](https://rclone.org/local/)
|
||||||
|
|
||||||
|
Please see [the full list of all storage providers and their features](https://rclone.org/overview/)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@@ -71,10 +73,15 @@ Rclone *("rsync for cloud storage")* is a command line program to sync files and
|
|||||||
|
|
||||||
## Installation & documentation
|
## Installation & documentation
|
||||||
|
|
||||||
Please see the rclone website for installation, usage, documentation,
|
Please see the [rclone website](https://rclone.org/) for:
|
||||||
changelog and configuration walkthroughs.
|
|
||||||
|
|
||||||
* https://rclone.org/
|
* [Installation](https://rclone.org/install/)
|
||||||
|
* [Documentation & configuration](https://rclone.org/docs/)
|
||||||
|
* [Changelog](https://rclone.org/changelog/)
|
||||||
|
* [FAQ](https://rclone.org/faq/)
|
||||||
|
* [Storage providers](https://rclone.org/overview/)
|
||||||
|
* [Forum](https://forum.rclone.org/)
|
||||||
|
* ...and more
|
||||||
|
|
||||||
## Downloads
|
## Downloads
|
||||||
|
|
||||||
|
|||||||
15
RELEASE.md
15
RELEASE.md
@@ -32,6 +32,21 @@ Early in the next release cycle update the vendored dependencies
|
|||||||
* git add new files
|
* git add new files
|
||||||
* git commit -a -v
|
* git commit -a -v
|
||||||
|
|
||||||
|
If `make update` fails with errors like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
# github.com/cpuguy83/go-md2man/md2man
|
||||||
|
../../../../pkg/mod/github.com/cpuguy83/go-md2man@v1.0.8/md2man/md2man.go:11:16: undefined: blackfriday.EXTENSION_NO_INTRA_EMPHASIS
|
||||||
|
../../../../pkg/mod/github.com/cpuguy83/go-md2man@v1.0.8/md2man/md2man.go:12:16: undefined: blackfriday.EXTENSION_TABLES
|
||||||
|
```
|
||||||
|
|
||||||
|
Can be fixed with
|
||||||
|
|
||||||
|
* GO111MODULE=on go get -u github.com/russross/blackfriday@v1.5.2
|
||||||
|
* GO111MODULE=on go mod tidy
|
||||||
|
* GO111MODULE=on go mod vendor
|
||||||
|
|
||||||
|
|
||||||
Making a point release. If rclone needs a point release due to some
|
Making a point release. If rclone needs a point release due to some
|
||||||
horrendous bug, then
|
horrendous bug, then
|
||||||
* git branch v1.XX v1.XX-fixes
|
* git branch v1.XX v1.XX-fixes
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Azure/azure-storage-blob-go/2018-03-28/azblob"
|
"github.com/Azure/azure-storage-blob-go/azblob"
|
||||||
"github.com/ncw/rclone/fs"
|
"github.com/ncw/rclone/fs"
|
||||||
"github.com/ncw/rclone/fs/accounting"
|
"github.com/ncw/rclone/fs/accounting"
|
||||||
"github.com/ncw/rclone/fs/config/configmap"
|
"github.com/ncw/rclone/fs/config/configmap"
|
||||||
@@ -1368,7 +1368,7 @@ func (o *Object) SetTier(tier string) error {
|
|||||||
blob := o.getBlobReference()
|
blob := o.getBlobReference()
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
err := o.fs.pacer.Call(func() (bool, error) {
|
err := o.fs.pacer.Call(func() (bool, error) {
|
||||||
_, err := blob.SetTier(ctx, desiredAccessTier)
|
_, err := blob.SetTier(ctx, desiredAccessTier, azblob.LeaseAccessConditions{})
|
||||||
return o.fs.shouldRetry(err)
|
return o.fs.shouldRetry(err)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
2
backend/cache/cache.go
vendored
2
backend/cache/cache.go
vendored
@@ -471,7 +471,7 @@ func NewFs(name, rootPath string, m configmap.Mapper) (fs.Fs, error) {
|
|||||||
fs.Infof(name, "Chunk Clean Interval: %v", f.opt.ChunkCleanInterval)
|
fs.Infof(name, "Chunk Clean Interval: %v", f.opt.ChunkCleanInterval)
|
||||||
fs.Infof(name, "Workers: %v", f.opt.TotalWorkers)
|
fs.Infof(name, "Workers: %v", f.opt.TotalWorkers)
|
||||||
fs.Infof(name, "File Age: %v", f.opt.InfoAge)
|
fs.Infof(name, "File Age: %v", f.opt.InfoAge)
|
||||||
if !f.opt.StoreWrites {
|
if f.opt.StoreWrites {
|
||||||
fs.Infof(name, "Cache Writes: enabled")
|
fs.Infof(name, "Cache Writes: enabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
26
backend/cache/cache_internal_test.go
vendored
26
backend/cache/cache_internal_test.go
vendored
@@ -5,14 +5,12 @@ package cache_test
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
|
||||||
goflag "flag"
|
goflag "flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -32,11 +30,11 @@ import (
|
|||||||
"github.com/ncw/rclone/fs/config/configmap"
|
"github.com/ncw/rclone/fs/config/configmap"
|
||||||
"github.com/ncw/rclone/fs/object"
|
"github.com/ncw/rclone/fs/object"
|
||||||
"github.com/ncw/rclone/fs/rc"
|
"github.com/ncw/rclone/fs/rc"
|
||||||
"github.com/ncw/rclone/fs/rc/rcflags"
|
|
||||||
"github.com/ncw/rclone/fstest"
|
"github.com/ncw/rclone/fstest"
|
||||||
"github.com/ncw/rclone/vfs"
|
"github.com/ncw/rclone/vfs"
|
||||||
"github.com/ncw/rclone/vfs/vfsflags"
|
"github.com/ncw/rclone/vfs/vfsflags"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -692,8 +690,8 @@ func TestInternalChangeSeenAfterDirCacheFlush(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestInternalChangeSeenAfterRc(t *testing.T) {
|
func TestInternalChangeSeenAfterRc(t *testing.T) {
|
||||||
rcflags.Opt.Enabled = true
|
cacheExpire := rc.Calls.Get("cache/expire")
|
||||||
rc.Start(&rcflags.Opt)
|
assert.NotNil(t, cacheExpire)
|
||||||
|
|
||||||
id := fmt.Sprintf("ticsarc%v", time.Now().Unix())
|
id := fmt.Sprintf("ticsarc%v", time.Now().Unix())
|
||||||
rootFs, boltDb := runInstance.newCacheFs(t, remoteName, id, false, true, nil, nil)
|
rootFs, boltDb := runInstance.newCacheFs(t, remoteName, id, false, true, nil, nil)
|
||||||
@@ -726,13 +724,8 @@ func TestInternalChangeSeenAfterRc(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotEqual(t, o.ModTime().String(), co.ModTime().String())
|
require.NotEqual(t, o.ModTime().String(), co.ModTime().String())
|
||||||
|
|
||||||
m := make(map[string]string)
|
// Call the rc function
|
||||||
res, err := http.Post(fmt.Sprintf("http://localhost:5572/cache/expire?remote=%s", "data.bin"), "application/json; charset=utf-8", strings.NewReader(""))
|
m, err := cacheExpire.Fn(rc.Params{"remote": "data.bin"})
|
||||||
require.NoError(t, err)
|
|
||||||
defer func() {
|
|
||||||
_ = res.Body.Close()
|
|
||||||
}()
|
|
||||||
_ = json.NewDecoder(res.Body).Decode(&m)
|
|
||||||
require.Contains(t, m, "status")
|
require.Contains(t, m, "status")
|
||||||
require.Contains(t, m, "message")
|
require.Contains(t, m, "message")
|
||||||
require.Equal(t, "ok", m["status"])
|
require.Equal(t, "ok", m["status"])
|
||||||
@@ -752,13 +745,8 @@ func TestInternalChangeSeenAfterRc(t *testing.T) {
|
|||||||
li1, err = runInstance.list(t, rootFs, "")
|
li1, err = runInstance.list(t, rootFs, "")
|
||||||
require.Len(t, li1, 1)
|
require.Len(t, li1, 1)
|
||||||
|
|
||||||
m = make(map[string]string)
|
// Call the rc function
|
||||||
res2, err := http.Post("http://localhost:5572/cache/expire?remote=/", "application/json; charset=utf-8", strings.NewReader(""))
|
m, err = cacheExpire.Fn(rc.Params{"remote": "/"})
|
||||||
require.NoError(t, err)
|
|
||||||
defer func() {
|
|
||||||
_ = res2.Body.Close()
|
|
||||||
}()
|
|
||||||
_ = json.NewDecoder(res2.Body).Decode(&m)
|
|
||||||
require.Contains(t, m, "status")
|
require.Contains(t, m, "status")
|
||||||
require.Contains(t, m, "message")
|
require.Contains(t, m, "message")
|
||||||
require.Equal(t, "ok", m["status"])
|
require.Equal(t, "ok", m["status"])
|
||||||
|
|||||||
@@ -7,13 +7,30 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/ncw/rclone/backend/crypt"
|
"github.com/ncw/rclone/backend/crypt"
|
||||||
|
_ "github.com/ncw/rclone/backend/drive" // for integration tests
|
||||||
_ "github.com/ncw/rclone/backend/local"
|
_ "github.com/ncw/rclone/backend/local"
|
||||||
|
_ "github.com/ncw/rclone/backend/swift" // for integration tests
|
||||||
"github.com/ncw/rclone/fs/config/obscure"
|
"github.com/ncw/rclone/fs/config/obscure"
|
||||||
|
"github.com/ncw/rclone/fstest"
|
||||||
"github.com/ncw/rclone/fstest/fstests"
|
"github.com/ncw/rclone/fstest/fstests"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TestIntegration runs integration tests against the remote
|
||||||
|
func TestIntegration(t *testing.T) {
|
||||||
|
if *fstest.RemoteName == "" {
|
||||||
|
t.Skip("Skipping as -remote not set")
|
||||||
|
}
|
||||||
|
fstests.Run(t, &fstests.Opt{
|
||||||
|
RemoteName: *fstest.RemoteName,
|
||||||
|
NilObject: (*crypt.Object)(nil),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// TestStandard runs integration tests against the remote
|
// TestStandard runs integration tests against the remote
|
||||||
func TestStandard(t *testing.T) {
|
func TestStandard(t *testing.T) {
|
||||||
|
if *fstest.RemoteName != "" {
|
||||||
|
t.Skip("Skipping as -remote set")
|
||||||
|
}
|
||||||
tempdir := filepath.Join(os.TempDir(), "rclone-crypt-test-standard")
|
tempdir := filepath.Join(os.TempDir(), "rclone-crypt-test-standard")
|
||||||
name := "TestCrypt"
|
name := "TestCrypt"
|
||||||
fstests.Run(t, &fstests.Opt{
|
fstests.Run(t, &fstests.Opt{
|
||||||
@@ -30,6 +47,9 @@ func TestStandard(t *testing.T) {
|
|||||||
|
|
||||||
// TestOff runs integration tests against the remote
|
// TestOff runs integration tests against the remote
|
||||||
func TestOff(t *testing.T) {
|
func TestOff(t *testing.T) {
|
||||||
|
if *fstest.RemoteName != "" {
|
||||||
|
t.Skip("Skipping as -remote set")
|
||||||
|
}
|
||||||
tempdir := filepath.Join(os.TempDir(), "rclone-crypt-test-off")
|
tempdir := filepath.Join(os.TempDir(), "rclone-crypt-test-off")
|
||||||
name := "TestCrypt2"
|
name := "TestCrypt2"
|
||||||
fstests.Run(t, &fstests.Opt{
|
fstests.Run(t, &fstests.Opt{
|
||||||
@@ -46,6 +66,9 @@ func TestOff(t *testing.T) {
|
|||||||
|
|
||||||
// TestObfuscate runs integration tests against the remote
|
// TestObfuscate runs integration tests against the remote
|
||||||
func TestObfuscate(t *testing.T) {
|
func TestObfuscate(t *testing.T) {
|
||||||
|
if *fstest.RemoteName != "" {
|
||||||
|
t.Skip("Skipping as -remote set")
|
||||||
|
}
|
||||||
tempdir := filepath.Join(os.TempDir(), "rclone-crypt-test-obfuscate")
|
tempdir := filepath.Join(os.TempDir(), "rclone-crypt-test-obfuscate")
|
||||||
name := "TestCrypt3"
|
name := "TestCrypt3"
|
||||||
fstests.Run(t, &fstests.Opt{
|
fstests.Run(t, &fstests.Opt{
|
||||||
|
|||||||
@@ -464,12 +464,12 @@ func (f *Fs) listFileDir(remoteStartPath string, startFolder *api.JottaFolder, f
|
|||||||
if folder.Deleted {
|
if folder.Deleted {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
folderPath := path.Join(folder.Path, folder.Name)
|
folderPath := restoreReservedChars(path.Join(folder.Path, folder.Name))
|
||||||
remoteDirLength := len(folderPath) - pathPrefixLength
|
folderPathLength := len(folderPath)
|
||||||
var remoteDir string
|
var remoteDir string
|
||||||
if remoteDirLength > 0 {
|
if folderPathLength > pathPrefixLength {
|
||||||
remoteDir = restoreReservedChars(folderPath[pathPrefixLength+1:])
|
remoteDir = folderPath[pathPrefixLength+1:]
|
||||||
if remoteDirLength > startPathLength {
|
if folderPathLength > startPathLength {
|
||||||
d := fs.NewDir(remoteDir, time.Time(folder.ModifiedAt))
|
d := fs.NewDir(remoteDir, time.Time(folder.ModifiedAt))
|
||||||
err := fn(d)
|
err := fn(d)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -20,6 +21,7 @@ import (
|
|||||||
"github.com/ncw/rclone/fs/hash"
|
"github.com/ncw/rclone/fs/hash"
|
||||||
"github.com/ncw/rclone/lib/dircache"
|
"github.com/ncw/rclone/lib/dircache"
|
||||||
"github.com/ncw/rclone/lib/pacer"
|
"github.com/ncw/rclone/lib/pacer"
|
||||||
|
"github.com/ncw/rclone/lib/readers"
|
||||||
"github.com/ncw/rclone/lib/rest"
|
"github.com/ncw/rclone/lib/rest"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
@@ -930,8 +932,9 @@ func (o *Object) Update(in io.Reader, src fs.ObjectInfo, options ...fs.OpenOptio
|
|||||||
// resp.Body.Close()
|
// resp.Body.Close()
|
||||||
// fs.Debugf(nil, "PostOpen: %#v", openResponse)
|
// fs.Debugf(nil, "PostOpen: %#v", openResponse)
|
||||||
|
|
||||||
// 1 MB chunks size
|
// 10 MB chunks size
|
||||||
chunkSize := int64(1024 * 1024 * 10)
|
chunkSize := int64(1024 * 1024 * 10)
|
||||||
|
buf := make([]byte, int(chunkSize))
|
||||||
chunkOffset := int64(0)
|
chunkOffset := int64(0)
|
||||||
remainingBytes := size
|
remainingBytes := size
|
||||||
chunkCounter := 0
|
chunkCounter := 0
|
||||||
@@ -944,14 +947,19 @@ func (o *Object) Update(in io.Reader, src fs.ObjectInfo, options ...fs.OpenOptio
|
|||||||
remainingBytes -= currentChunkSize
|
remainingBytes -= currentChunkSize
|
||||||
fs.Debugf(o, "Uploading chunk %d, size=%d, remain=%d", chunkCounter, currentChunkSize, remainingBytes)
|
fs.Debugf(o, "Uploading chunk %d, size=%d, remain=%d", chunkCounter, currentChunkSize, remainingBytes)
|
||||||
|
|
||||||
|
chunk := readers.NewRepeatableLimitReaderBuffer(in, buf, currentChunkSize)
|
||||||
err = o.fs.pacer.Call(func() (bool, error) {
|
err = o.fs.pacer.Call(func() (bool, error) {
|
||||||
|
// seek to the start in case this is a retry
|
||||||
|
if _, err = chunk.Seek(0, io.SeekStart); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
var formBody bytes.Buffer
|
var formBody bytes.Buffer
|
||||||
w := multipart.NewWriter(&formBody)
|
w := multipart.NewWriter(&formBody)
|
||||||
fw, err := w.CreateFormFile("file_data", o.remote)
|
fw, err := w.CreateFormFile("file_data", o.remote)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
if _, err = io.CopyN(fw, in, currentChunkSize); err != nil {
|
if _, err = io.Copy(fw, chunk); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
// Add session_id
|
// Add session_id
|
||||||
@@ -1082,7 +1090,7 @@ func (o *Object) readMetaData() (err error) {
|
|||||||
err = o.fs.pacer.Call(func() (bool, error) {
|
err = o.fs.pacer.Call(func() (bool, error) {
|
||||||
opts := rest.Opts{
|
opts := rest.Opts{
|
||||||
Method: "GET",
|
Method: "GET",
|
||||||
Path: "/folder/itembyname.json/" + o.fs.session.SessionID + "/" + directoryID + "?name=" + rest.URLPathEscape(replaceReservedChars(leaf)),
|
Path: "/folder/itembyname.json/" + o.fs.session.SessionID + "/" + directoryID + "?name=" + url.QueryEscape(replaceReservedChars(leaf)),
|
||||||
}
|
}
|
||||||
resp, err = o.fs.srv.CallJSON(&opts, nil, &folderList)
|
resp, err = o.fs.srv.CallJSON(&opts, nil, &folderList)
|
||||||
return o.fs.shouldRetry(resp, err)
|
return o.fs.shouldRetry(resp, err)
|
||||||
|
|||||||
@@ -448,7 +448,12 @@ func init() {
|
|||||||
Provider: "!AWS,IBMCOS",
|
Provider: "!AWS,IBMCOS",
|
||||||
}, {
|
}, {
|
||||||
Name: "acl",
|
Name: "acl",
|
||||||
Help: "Canned ACL used when creating buckets and/or storing objects in S3.\nFor more info visit https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#canned-acl",
|
Help: `Canned ACL used when creating buckets and storing or copying objects.
|
||||||
|
|
||||||
|
For more info visit https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#canned-acl
|
||||||
|
|
||||||
|
Note that this ACL is applied when server side copying objects as S3
|
||||||
|
doesn't copy the ACL from the source but rather writes a fresh one.`,
|
||||||
Examples: []fs.OptionExample{{
|
Examples: []fs.OptionExample{{
|
||||||
Value: "private",
|
Value: "private",
|
||||||
Help: "Owner gets FULL_CONTROL. No one else has access rights (default).",
|
Help: "Owner gets FULL_CONTROL. No one else has access rights (default).",
|
||||||
@@ -1286,6 +1291,7 @@ func (f *Fs) Copy(src fs.Object, remote string) (fs.Object, error) {
|
|||||||
source := pathEscape(srcFs.bucket + "/" + srcFs.root + srcObj.remote)
|
source := pathEscape(srcFs.bucket + "/" + srcFs.root + srcObj.remote)
|
||||||
req := s3.CopyObjectInput{
|
req := s3.CopyObjectInput{
|
||||||
Bucket: &f.bucket,
|
Bucket: &f.bucket,
|
||||||
|
ACL: &f.opt.ACL,
|
||||||
Key: &key,
|
Key: &key,
|
||||||
CopySource: &source,
|
CopySource: &source,
|
||||||
MetadataDirective: aws.String(s3.MetadataDirectiveCopy),
|
MetadataDirective: aws.String(s3.MetadataDirectiveCopy),
|
||||||
|
|||||||
@@ -769,6 +769,10 @@ func (o *Object) Hash(r hash.Type) (string, error) {
|
|||||||
return "", hash.ErrUnsupported
|
return "", hash.ErrUnsupported
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if o.fs.opt.DisableHashCheck {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
c, err := o.fs.getSftpConnection()
|
c, err := o.fs.getSftpConnection()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.Wrap(err, "Hash get SFTP connection")
|
return "", errors.Wrap(err, "Hash get SFTP connection")
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ var timeFormats = []string{
|
|||||||
time.RFC1123Z, // Fri, 05 Jan 2018 14:14:38 +0000 (as used by mydrive.ch)
|
time.RFC1123Z, // Fri, 05 Jan 2018 14:14:38 +0000 (as used by mydrive.ch)
|
||||||
time.UnixDate, // Wed May 17 15:31:58 UTC 2017 (as used in an internal server)
|
time.UnixDate, // Wed May 17 15:31:58 UTC 2017 (as used in an internal server)
|
||||||
noZerosRFC1123, // Fri, 7 Sep 2018 08:49:58 GMT (as used by server in #2574)
|
noZerosRFC1123, // Fri, 7 Sep 2018 08:49:58 GMT (as used by server in #2574)
|
||||||
|
time.RFC3339, // Wed, 31 Oct 2018 13:57:11 CET (as used by komfortcloud.de)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalXML turns XML into a Time
|
// UnmarshalXML turns XML into a Time
|
||||||
|
|||||||
@@ -968,6 +968,7 @@ func (o *Object) Update(in io.Reader, src fs.ObjectInfo, options ...fs.OpenOptio
|
|||||||
Body: in,
|
Body: in,
|
||||||
NoResponse: true,
|
NoResponse: true,
|
||||||
ContentLength: &size, // FIXME this isn't necessary with owncloud - See https://github.com/nextcloud/nextcloud-snap/issues/365
|
ContentLength: &size, // FIXME this isn't necessary with owncloud - See https://github.com/nextcloud/nextcloud-snap/issues/365
|
||||||
|
ContentType: fs.MimeType(src),
|
||||||
}
|
}
|
||||||
if o.fs.useOCMtime {
|
if o.fs.useOCMtime {
|
||||||
opts.ExtraHeaders = map[string]string{
|
opts.ExtraHeaders = map[string]string{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python2
|
||||||
"""
|
"""
|
||||||
Make backend documentation
|
Make backend documentation
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python2
|
||||||
"""
|
"""
|
||||||
Make single page versions of the documentation for release and
|
Make single page versions of the documentation for release and
|
||||||
conversion into man pages etc.
|
conversion into man pages etc.
|
||||||
|
|||||||
@@ -4,18 +4,20 @@
|
|||||||
set -e
|
set -e
|
||||||
|
|
||||||
go install
|
go install
|
||||||
mkdir -p /tmp/rclone_cache_test
|
mkdir -p /tmp/rclone/cache_test
|
||||||
|
mkdir -p /tmp/rclone/rc_mount
|
||||||
export RCLONE_CONFIG_RCDOCS_TYPE=cache
|
export RCLONE_CONFIG_RCDOCS_TYPE=cache
|
||||||
export RCLONE_CONFIG_RCDOCS_REMOTE=/tmp/rclone/cache_test
|
export RCLONE_CONFIG_RCDOCS_REMOTE=/tmp/rclone/cache_test
|
||||||
rclone -q --rc mount rcdocs: /mnt/tmp/ &
|
rclone -q --rc mount rcdocs: /tmp/rclone/rc_mount &
|
||||||
sleep 0.5
|
sleep 0.5
|
||||||
rclone rc > /tmp/z.md
|
rclone rc > /tmp/rclone/z.md
|
||||||
fusermount -z -u /mnt/tmp/
|
fusermount -u -z /tmp/rclone/rc_mount > /dev/null 2>&1 || umount /tmp/rclone/rc_mount
|
||||||
|
|
||||||
awk '
|
awk '
|
||||||
BEGIN {p=1}
|
BEGIN {p=1}
|
||||||
/^<!--- autogenerated start/ {print;system("cat /tmp/z.md");p=0}
|
/^<!--- autogenerated start/ {print;system("cat /tmp/rclone/z.md");p=0}
|
||||||
/^<!--- autogenerated stop/ {p=1}
|
/^<!--- autogenerated stop/ {p=1}
|
||||||
p' docs/content/rc.md > /tmp/rc.md
|
p' docs/content/rc.md > /tmp/rclone/rc.md
|
||||||
|
|
||||||
mv /tmp/rc.md docs/content/rc.md
|
mv /tmp/rclone/rc.md docs/content/rc.md
|
||||||
|
rm -rf /tmp/rclone
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import (
|
|||||||
_ "github.com/ncw/rclone/cmd/purge"
|
_ "github.com/ncw/rclone/cmd/purge"
|
||||||
_ "github.com/ncw/rclone/cmd/rc"
|
_ "github.com/ncw/rclone/cmd/rc"
|
||||||
_ "github.com/ncw/rclone/cmd/rcat"
|
_ "github.com/ncw/rclone/cmd/rcat"
|
||||||
|
_ "github.com/ncw/rclone/cmd/rcd"
|
||||||
_ "github.com/ncw/rclone/cmd/reveal"
|
_ "github.com/ncw/rclone/cmd/reveal"
|
||||||
_ "github.com/ncw/rclone/cmd/rmdir"
|
_ "github.com/ncw/rclone/cmd/rmdir"
|
||||||
_ "github.com/ncw/rclone/cmd/rmdirs"
|
_ "github.com/ncw/rclone/cmd/rmdirs"
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ import (
|
|||||||
"github.com/ncw/rclone/fs/fserrors"
|
"github.com/ncw/rclone/fs/fserrors"
|
||||||
"github.com/ncw/rclone/fs/fspath"
|
"github.com/ncw/rclone/fs/fspath"
|
||||||
fslog "github.com/ncw/rclone/fs/log"
|
fslog "github.com/ncw/rclone/fs/log"
|
||||||
"github.com/ncw/rclone/fs/rc"
|
|
||||||
"github.com/ncw/rclone/fs/rc/rcflags"
|
"github.com/ncw/rclone/fs/rc/rcflags"
|
||||||
|
"github.com/ncw/rclone/fs/rc/rcserver"
|
||||||
"github.com/ncw/rclone/lib/atexit"
|
"github.com/ncw/rclone/lib/atexit"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -352,8 +352,11 @@ func initConfig() {
|
|||||||
// Write the args for debug purposes
|
// Write the args for debug purposes
|
||||||
fs.Debugf("rclone", "Version %q starting with parameters %q", fs.Version, os.Args)
|
fs.Debugf("rclone", "Version %q starting with parameters %q", fs.Version, os.Args)
|
||||||
|
|
||||||
// Start the remote control if configured
|
// Start the remote control server if configured
|
||||||
rc.Start(&rcflags.Opt)
|
_, err = rcserver.Start(&rcflags.Opt)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to start remote control: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Setup CPU profiling if desired
|
// Setup CPU profiling if desired
|
||||||
if *cpuProfile != "" {
|
if *cpuProfile != "" {
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
"github.com/ncw/rclone/cmd"
|
"github.com/ncw/rclone/cmd"
|
||||||
"github.com/ncw/rclone/fs/config"
|
"github.com/ncw/rclone/fs/config"
|
||||||
|
"github.com/ncw/rclone/fs/rc"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -93,7 +96,16 @@ you would do:
|
|||||||
`,
|
`,
|
||||||
RunE: func(command *cobra.Command, args []string) error {
|
RunE: func(command *cobra.Command, args []string) error {
|
||||||
cmd.CheckArgs(2, 256, command, args)
|
cmd.CheckArgs(2, 256, command, args)
|
||||||
return config.CreateRemote(args[0], args[1], args[2:])
|
in, err := argsToMap(args[2:])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = config.CreateRemote(args[0], args[1], in)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
config.ShowRemote(args[0])
|
||||||
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,7 +122,16 @@ For example to update the env_auth field of a remote of name myremote you would
|
|||||||
`,
|
`,
|
||||||
RunE: func(command *cobra.Command, args []string) error {
|
RunE: func(command *cobra.Command, args []string) error {
|
||||||
cmd.CheckArgs(3, 256, command, args)
|
cmd.CheckArgs(3, 256, command, args)
|
||||||
return config.UpdateRemote(args[0], args[1:])
|
in, err := argsToMap(args[1:])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = config.UpdateRemote(args[0], in)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
config.ShowRemote(args[0])
|
||||||
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,6 +157,29 @@ For example to set password of a remote of name myremote you would do:
|
|||||||
`,
|
`,
|
||||||
RunE: func(command *cobra.Command, args []string) error {
|
RunE: func(command *cobra.Command, args []string) error {
|
||||||
cmd.CheckArgs(3, 256, command, args)
|
cmd.CheckArgs(3, 256, command, args)
|
||||||
return config.PasswordRemote(args[0], args[1:])
|
in, err := argsToMap(args[1:])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = config.PasswordRemote(args[0], in)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
config.ShowRemote(args[0])
|
||||||
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This takes a list of arguments in key value key value form and
|
||||||
|
// converts it into a map
|
||||||
|
func argsToMap(args []string) (out rc.Params, err error) {
|
||||||
|
if len(args)%2 != 0 {
|
||||||
|
return nil, errors.New("found key without value")
|
||||||
|
}
|
||||||
|
out = rc.Params{}
|
||||||
|
// Set the config
|
||||||
|
for i := 0; i < len(args); i += 2 {
|
||||||
|
out[args[i]] = args[i+1]
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ If you are familiar with ` + "`rsync`" + `, rclone always works as if you had
|
|||||||
written a trailing / - meaning "copy the contents of this directory".
|
written a trailing / - meaning "copy the contents of this directory".
|
||||||
This applies to all commands and whether you are talking about the
|
This applies to all commands and whether you are talking about the
|
||||||
source or destination.
|
source or destination.
|
||||||
|
|
||||||
|
**Note**: Use the ` + "`-P`" + `/` + "`--progress`" + ` flag to view real-time transfer statistics
|
||||||
`,
|
`,
|
||||||
Run: func(command *cobra.Command, args []string) {
|
Run: func(command *cobra.Command, args []string) {
|
||||||
cmd.CheckArgs(2, 2, command, args)
|
cmd.CheckArgs(2, 2, command, args)
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ This will:
|
|||||||
This doesn't transfer unchanged files, testing by size and
|
This doesn't transfer unchanged files, testing by size and
|
||||||
modification time or MD5SUM. It doesn't delete files from the
|
modification time or MD5SUM. It doesn't delete files from the
|
||||||
destination.
|
destination.
|
||||||
|
|
||||||
|
**Note**: Use the ` + "`-P`" + `/` + "`--progress`" + ` flag to view real-time transfer statistics
|
||||||
`,
|
`,
|
||||||
Run: func(command *cobra.Command, args []string) {
|
Run: func(command *cobra.Command, args []string) {
|
||||||
cmd.CheckArgs(2, 2, command, args)
|
cmd.CheckArgs(2, 2, command, args)
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
package copyurl
|
package copyurl
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ncw/rclone/cmd"
|
"github.com/ncw/rclone/cmd"
|
||||||
"github.com/ncw/rclone/fs/operations"
|
"github.com/ncw/rclone/fs/operations"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -25,14 +22,7 @@ without saving it in tmp storage.
|
|||||||
fsdst, dstFileName := cmd.NewFsDstFile(args[1:])
|
fsdst, dstFileName := cmd.NewFsDstFile(args[1:])
|
||||||
|
|
||||||
cmd.Run(true, true, command, func() error {
|
cmd.Run(true, true, command, func() error {
|
||||||
resp, err := http.Get(args[0])
|
_, err := operations.CopyURL(fsdst, dstFileName, args[0])
|
||||||
if err != nil {
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = operations.RcatSize(fsdst, dstFileName, resp.Body, resp.ContentLength, time.Now())
|
|
||||||
|
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,9 +14,13 @@ var commandDefintion = &cobra.Command{
|
|||||||
Use: "delete remote:path",
|
Use: "delete remote:path",
|
||||||
Short: `Remove the contents of path.`,
|
Short: `Remove the contents of path.`,
|
||||||
Long: `
|
Long: `
|
||||||
Remove the contents of path. Unlike ` + "`" + `purge` + "`" + ` it obeys include/exclude
|
Remove the files in path. Unlike ` + "`" + `purge` + "`" + ` it obeys include/exclude
|
||||||
filters so can be used to selectively delete files.
|
filters so can be used to selectively delete files.
|
||||||
|
|
||||||
|
` + "`" + `rclone delete` + "`" + ` only deletes objects but leaves the directory structure
|
||||||
|
alone. If you want to delete a directory and all of its contents use
|
||||||
|
` + "`" + `rclone purge` + "`" + `
|
||||||
|
|
||||||
Eg delete all files bigger than 100MBytes
|
Eg delete all files bigger than 100MBytes
|
||||||
|
|
||||||
Check what would be deleted first (use either)
|
Check what would be deleted first (use either)
|
||||||
|
|||||||
@@ -3,62 +3,26 @@ package lsjson
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ncw/rclone/backend/crypt"
|
|
||||||
"github.com/ncw/rclone/cmd"
|
"github.com/ncw/rclone/cmd"
|
||||||
"github.com/ncw/rclone/cmd/ls/lshelp"
|
"github.com/ncw/rclone/cmd/ls/lshelp"
|
||||||
"github.com/ncw/rclone/fs"
|
|
||||||
"github.com/ncw/rclone/fs/operations"
|
"github.com/ncw/rclone/fs/operations"
|
||||||
"github.com/ncw/rclone/fs/walk"
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
recurse bool
|
opt operations.ListJSONOpt
|
||||||
showHash bool
|
|
||||||
showEncrypted bool
|
|
||||||
showOrigIDs bool
|
|
||||||
noModTime bool
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
cmd.Root.AddCommand(commandDefintion)
|
cmd.Root.AddCommand(commandDefintion)
|
||||||
commandDefintion.Flags().BoolVarP(&recurse, "recursive", "R", false, "Recurse into the listing.")
|
commandDefintion.Flags().BoolVarP(&opt.Recurse, "recursive", "R", false, "Recurse into the listing.")
|
||||||
commandDefintion.Flags().BoolVarP(&showHash, "hash", "", false, "Include hashes in the output (may take longer).")
|
commandDefintion.Flags().BoolVarP(&opt.ShowHash, "hash", "", false, "Include hashes in the output (may take longer).")
|
||||||
commandDefintion.Flags().BoolVarP(&noModTime, "no-modtime", "", false, "Don't read the modification time (can speed things up).")
|
commandDefintion.Flags().BoolVarP(&opt.NoModTime, "no-modtime", "", false, "Don't read the modification time (can speed things up).")
|
||||||
commandDefintion.Flags().BoolVarP(&showEncrypted, "encrypted", "M", false, "Show the encrypted names.")
|
commandDefintion.Flags().BoolVarP(&opt.ShowEncrypted, "encrypted", "M", false, "Show the encrypted names.")
|
||||||
commandDefintion.Flags().BoolVarP(&showOrigIDs, "original", "", false, "Show the ID of the underlying Object.")
|
commandDefintion.Flags().BoolVarP(&opt.ShowOrigIDs, "original", "", false, "Show the ID of the underlying Object.")
|
||||||
}
|
|
||||||
|
|
||||||
// lsJSON in the struct which gets marshalled for each line
|
|
||||||
type lsJSON struct {
|
|
||||||
Path string
|
|
||||||
Name string
|
|
||||||
Encrypted string `json:",omitempty"`
|
|
||||||
Size int64
|
|
||||||
MimeType string `json:",omitempty"`
|
|
||||||
ModTime Timestamp //`json:",omitempty"`
|
|
||||||
IsDir bool
|
|
||||||
Hashes map[string]string `json:",omitempty"`
|
|
||||||
ID string `json:",omitempty"`
|
|
||||||
OrigID string `json:",omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Timestamp a time in RFC3339 format with Nanosecond precision secongs
|
|
||||||
type Timestamp time.Time
|
|
||||||
|
|
||||||
// MarshalJSON turns a Timestamp into JSON
|
|
||||||
func (t Timestamp) MarshalJSON() (out []byte, err error) {
|
|
||||||
tt := time.Time(t)
|
|
||||||
if tt.IsZero() {
|
|
||||||
return []byte(`""`), nil
|
|
||||||
}
|
|
||||||
return []byte(`"` + tt.Format(time.RFC3339Nano) + `"`), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var commandDefintion = &cobra.Command{
|
var commandDefintion = &cobra.Command{
|
||||||
@@ -104,107 +68,27 @@ can be processed line by line as each item is written one to a line.
|
|||||||
Run: func(command *cobra.Command, args []string) {
|
Run: func(command *cobra.Command, args []string) {
|
||||||
cmd.CheckArgs(1, 1, command, args)
|
cmd.CheckArgs(1, 1, command, args)
|
||||||
fsrc := cmd.NewFsSrc(args)
|
fsrc := cmd.NewFsSrc(args)
|
||||||
var cipher crypt.Cipher
|
|
||||||
if showEncrypted {
|
|
||||||
fsInfo, _, _, config, err := fs.ConfigFs(args[0])
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf(err.Error())
|
|
||||||
}
|
|
||||||
if fsInfo.Name != "crypt" {
|
|
||||||
log.Fatalf("The remote needs to be of type \"crypt\"")
|
|
||||||
}
|
|
||||||
cipher, err = crypt.NewCipher(config)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf(err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cmd.Run(false, false, command, func() error {
|
cmd.Run(false, false, command, func() error {
|
||||||
fmt.Println("[")
|
fmt.Println("[")
|
||||||
first := true
|
first := true
|
||||||
err := walk.Walk(fsrc, "", false, operations.ConfigMaxDepth(recurse), func(dirPath string, entries fs.DirEntries, err error) error {
|
err := operations.ListJSON(fsrc, "", &opt, func(item *operations.ListJSONItem) error {
|
||||||
|
out, err := json.Marshal(item)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fs.CountError(err)
|
return errors.Wrap(err, "failed to marshal list object")
|
||||||
fs.Errorf(dirPath, "error listing: %v", err)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
for _, entry := range entries {
|
if first {
|
||||||
item := lsJSON{
|
first = false
|
||||||
Path: entry.Remote(),
|
} else {
|
||||||
Name: path.Base(entry.Remote()),
|
fmt.Print(",\n")
|
||||||
Size: entry.Size(),
|
}
|
||||||
MimeType: fs.MimeTypeDirEntry(entry),
|
_, err = os.Stdout.Write(out)
|
||||||
}
|
if err != nil {
|
||||||
if !noModTime {
|
return errors.Wrap(err, "failed to write to output")
|
||||||
item.ModTime = Timestamp(entry.ModTime())
|
|
||||||
}
|
|
||||||
if cipher != nil {
|
|
||||||
switch entry.(type) {
|
|
||||||
case fs.Directory:
|
|
||||||
item.Encrypted = cipher.EncryptDirName(path.Base(entry.Remote()))
|
|
||||||
case fs.Object:
|
|
||||||
item.Encrypted = cipher.EncryptFileName(path.Base(entry.Remote()))
|
|
||||||
default:
|
|
||||||
fs.Errorf(nil, "Unknown type %T in listing", entry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if do, ok := entry.(fs.IDer); ok {
|
|
||||||
item.ID = do.ID()
|
|
||||||
}
|
|
||||||
if showOrigIDs {
|
|
||||||
cur := entry
|
|
||||||
for {
|
|
||||||
u, ok := cur.(fs.ObjectUnWrapper)
|
|
||||||
if !ok {
|
|
||||||
break // not a wrapped object, use current id
|
|
||||||
}
|
|
||||||
next := u.UnWrap()
|
|
||||||
if next == nil {
|
|
||||||
break // no base object found, use current id
|
|
||||||
}
|
|
||||||
cur = next
|
|
||||||
}
|
|
||||||
if do, ok := cur.(fs.IDer); ok {
|
|
||||||
item.OrigID = do.ID()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
switch x := entry.(type) {
|
|
||||||
case fs.Directory:
|
|
||||||
item.IsDir = true
|
|
||||||
case fs.Object:
|
|
||||||
item.IsDir = false
|
|
||||||
if showHash {
|
|
||||||
item.Hashes = make(map[string]string)
|
|
||||||
for _, hashType := range x.Fs().Hashes().Array() {
|
|
||||||
hash, err := x.Hash(hashType)
|
|
||||||
if err != nil {
|
|
||||||
fs.Errorf(x, "Failed to read hash: %v", err)
|
|
||||||
} else if hash != "" {
|
|
||||||
item.Hashes[hashType.String()] = hash
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
fs.Errorf(nil, "Unknown type %T in listing", entry)
|
|
||||||
}
|
|
||||||
out, err := json.Marshal(item)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "failed to marshal list object")
|
|
||||||
}
|
|
||||||
if first {
|
|
||||||
first = false
|
|
||||||
} else {
|
|
||||||
fmt.Print(",\n")
|
|
||||||
}
|
|
||||||
_, err = os.Stdout.Write(out)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "failed to write to output")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "error listing JSON")
|
return err
|
||||||
}
|
}
|
||||||
if !first {
|
if !first {
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ If you want to delete empty source directories after move, use the --delete-empt
|
|||||||
|
|
||||||
**Important**: Since this can cause data loss, test first with the
|
**Important**: Since this can cause data loss, test first with the
|
||||||
--dry-run flag.
|
--dry-run flag.
|
||||||
|
|
||||||
|
**Note**: Use the ` + "`-P`" + `/` + "`--progress`" + ` flag to view real-time transfer statistics.
|
||||||
`,
|
`,
|
||||||
Run: func(command *cobra.Command, args []string) {
|
Run: func(command *cobra.Command, args []string) {
|
||||||
cmd.CheckArgs(2, 2, command, args)
|
cmd.CheckArgs(2, 2, command, args)
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ transfer.
|
|||||||
|
|
||||||
**Important**: Since this can cause data loss, test first with the
|
**Important**: Since this can cause data loss, test first with the
|
||||||
--dry-run flag.
|
--dry-run flag.
|
||||||
|
|
||||||
|
**Note**: Use the ` + "`-P`" + `/` + "`--progress`" + ` flag to view real-time transfer statistics.
|
||||||
`,
|
`,
|
||||||
Run: func(command *cobra.Command, args []string) {
|
Run: func(command *cobra.Command, args []string) {
|
||||||
cmd.CheckArgs(2, 2, command, args)
|
cmd.CheckArgs(2, 2, command, args)
|
||||||
|
|||||||
231
cmd/ncdu/ncdu.go
231
cmd/ncdu/ncdu.go
@@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/ncw/rclone/cmd"
|
"github.com/ncw/rclone/cmd"
|
||||||
"github.com/ncw/rclone/cmd/ncdu/scan"
|
"github.com/ncw/rclone/cmd/ncdu/scan"
|
||||||
"github.com/ncw/rclone/fs"
|
"github.com/ncw/rclone/fs"
|
||||||
|
"github.com/ncw/rclone/fs/operations"
|
||||||
termbox "github.com/nsf/termbox-go"
|
termbox "github.com/nsf/termbox-go"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -42,8 +43,11 @@ Here are the keys - press '?' to toggle the help on and off
|
|||||||
` + strings.Join(helpText[1:], "\n ") + `
|
` + strings.Join(helpText[1:], "\n ") + `
|
||||||
|
|
||||||
This an homage to the [ncdu tool](https://dev.yorhel.nl/ncdu) but for
|
This an homage to the [ncdu tool](https://dev.yorhel.nl/ncdu) but for
|
||||||
rclone remotes. It is missing lots of features at the moment, most
|
rclone remotes. It is missing lots of features at the moment
|
||||||
importantly deleting files, but is useful as it stands.
|
but is useful as it stands.
|
||||||
|
|
||||||
|
Note that it might take some time to delete big files/folders. The
|
||||||
|
UI won't respond in the meantime since the deletion is done synchronously.
|
||||||
`,
|
`,
|
||||||
Run: func(command *cobra.Command, args []string) {
|
Run: func(command *cobra.Command, args []string) {
|
||||||
cmd.CheckArgs(1, 1, command, args)
|
cmd.CheckArgs(1, 1, command, args)
|
||||||
@@ -63,6 +67,7 @@ var helpText = []string{
|
|||||||
" c toggle counts",
|
" c toggle counts",
|
||||||
" g toggle graph",
|
" g toggle graph",
|
||||||
" n,s,C sort by name,size,count",
|
" n,s,C sort by name,size,count",
|
||||||
|
" d delete file/directory",
|
||||||
" ^L refresh screen",
|
" ^L refresh screen",
|
||||||
" ? to toggle help on and off",
|
" ? to toggle help on and off",
|
||||||
" q/ESC/c-C to quit",
|
" q/ESC/c-C to quit",
|
||||||
@@ -70,24 +75,27 @@ var helpText = []string{
|
|||||||
|
|
||||||
// UI contains the state of the user interface
|
// UI contains the state of the user interface
|
||||||
type UI struct {
|
type UI struct {
|
||||||
f fs.Fs // fs being displayed
|
f fs.Fs // fs being displayed
|
||||||
fsName string // human name of Fs
|
fsName string // human name of Fs
|
||||||
root *scan.Dir // root directory
|
root *scan.Dir // root directory
|
||||||
d *scan.Dir // current directory being displayed
|
d *scan.Dir // current directory being displayed
|
||||||
path string // path of current directory
|
path string // path of current directory
|
||||||
showBox bool // whether to show a box
|
showBox bool // whether to show a box
|
||||||
boxText []string // text to show in box
|
boxText []string // text to show in box
|
||||||
entries fs.DirEntries // entries of current directory
|
boxMenu []string // box menu options
|
||||||
sortPerm []int // order to display entries in after sorting
|
boxMenuButton int
|
||||||
invSortPerm []int // inverse order
|
boxMenuHandler func(fs fs.Fs, path string, option int) (string, error)
|
||||||
dirListHeight int // height of listing
|
entries fs.DirEntries // entries of current directory
|
||||||
listing bool // whether listing is in progress
|
sortPerm []int // order to display entries in after sorting
|
||||||
showGraph bool // toggle showing graph
|
invSortPerm []int // inverse order
|
||||||
showCounts bool // toggle showing counts
|
dirListHeight int // height of listing
|
||||||
sortByName int8 // +1 for normal, 0 for off, -1 for reverse
|
listing bool // whether listing is in progress
|
||||||
sortBySize int8
|
showGraph bool // toggle showing graph
|
||||||
sortByCount int8
|
showCounts bool // toggle showing counts
|
||||||
dirPosMap map[string]dirPos // store for directory positions
|
sortByName int8 // +1 for normal, 0 for off, -1 for reverse
|
||||||
|
sortBySize int8
|
||||||
|
sortByCount int8
|
||||||
|
dirPosMap map[string]dirPos // store for directory positions
|
||||||
}
|
}
|
||||||
|
|
||||||
// Where we have got to in the directory listing
|
// Where we have got to in the directory listing
|
||||||
@@ -130,6 +138,54 @@ func Linef(x, y, xmax int, fg, bg termbox.Attribute, spacer rune, format string,
|
|||||||
Line(x, y, xmax, fg, bg, spacer, s)
|
Line(x, y, xmax, fg, bg, spacer, s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LineOptions Print line of selectable options
|
||||||
|
func LineOptions(x, y, xmax int, fg, bg termbox.Attribute, options []string, selected int) {
|
||||||
|
defaultBg := bg
|
||||||
|
defaultFg := fg
|
||||||
|
|
||||||
|
// Print left+right whitespace to center the options
|
||||||
|
xoffset := ((xmax - x) - lineOptionLength(options)) / 2
|
||||||
|
for j := x; j < x+xoffset; j++ {
|
||||||
|
termbox.SetCell(j, y, ' ', fg, bg)
|
||||||
|
}
|
||||||
|
for j := xmax - xoffset; j < xmax; j++ {
|
||||||
|
termbox.SetCell(j, y, ' ', fg, bg)
|
||||||
|
}
|
||||||
|
x += xoffset
|
||||||
|
|
||||||
|
for i, o := range options {
|
||||||
|
termbox.SetCell(x, y, ' ', fg, bg)
|
||||||
|
|
||||||
|
if i == selected {
|
||||||
|
bg = termbox.ColorBlack
|
||||||
|
fg = termbox.ColorWhite
|
||||||
|
}
|
||||||
|
termbox.SetCell(x+1, y, '<', fg, bg)
|
||||||
|
x += 2
|
||||||
|
|
||||||
|
// print option text
|
||||||
|
for _, c := range o {
|
||||||
|
termbox.SetCell(x, y, c, fg, bg)
|
||||||
|
x++
|
||||||
|
}
|
||||||
|
|
||||||
|
termbox.SetCell(x, y, '>', fg, bg)
|
||||||
|
bg = defaultBg
|
||||||
|
fg = defaultFg
|
||||||
|
|
||||||
|
termbox.SetCell(x+1, y, ' ', fg, bg)
|
||||||
|
x += 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func lineOptionLength(o []string) int {
|
||||||
|
count := 0
|
||||||
|
for _, i := range o {
|
||||||
|
count += len(i)
|
||||||
|
}
|
||||||
|
return count + 4*len(o) // spacer and arrows <entry>
|
||||||
|
}
|
||||||
|
|
||||||
// Box the u.boxText onto the screen
|
// Box the u.boxText onto the screen
|
||||||
func (u *UI) Box() {
|
func (u *UI) Box() {
|
||||||
w, h := termbox.Size()
|
w, h := termbox.Size()
|
||||||
@@ -147,6 +203,15 @@ func (u *UI) Box() {
|
|||||||
x := (w - boxWidth) / 2
|
x := (w - boxWidth) / 2
|
||||||
y := (h - boxHeight) / 2
|
y := (h - boxHeight) / 2
|
||||||
xmax := x + boxWidth
|
xmax := x + boxWidth
|
||||||
|
if len(u.boxMenu) != 0 {
|
||||||
|
count := lineOptionLength(u.boxMenu)
|
||||||
|
if x+boxWidth > x+count {
|
||||||
|
xmax = x + boxWidth
|
||||||
|
} else {
|
||||||
|
xmax = x + count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ymax := y + len(u.boxText)
|
||||||
|
|
||||||
// draw text
|
// draw text
|
||||||
fg, bg := termbox.ColorRed, termbox.ColorWhite
|
fg, bg := termbox.ColorRed, termbox.ColorWhite
|
||||||
@@ -155,7 +220,43 @@ func (u *UI) Box() {
|
|||||||
fg = termbox.ColorBlack
|
fg = termbox.ColorBlack
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME draw a box around
|
if len(u.boxMenu) != 0 {
|
||||||
|
ymax++
|
||||||
|
LineOptions(x, ymax-1, xmax, fg, bg, u.boxMenu, u.boxMenuButton)
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw top border
|
||||||
|
for i := y; i < ymax; i++ {
|
||||||
|
termbox.SetCell(x-1, i, '│', fg, bg)
|
||||||
|
termbox.SetCell(xmax, i, '│', fg, bg)
|
||||||
|
}
|
||||||
|
for j := x; j < xmax; j++ {
|
||||||
|
termbox.SetCell(j, y-1, '─', fg, bg)
|
||||||
|
termbox.SetCell(j, ymax, '─', fg, bg)
|
||||||
|
}
|
||||||
|
|
||||||
|
termbox.SetCell(x-1, y-1, '┌', fg, bg)
|
||||||
|
termbox.SetCell(xmax, y-1, '┐', fg, bg)
|
||||||
|
termbox.SetCell(x-1, ymax, '└', fg, bg)
|
||||||
|
termbox.SetCell(xmax, ymax, '┘', fg, bg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UI) moveBox(to int) {
|
||||||
|
if len(u.boxMenu) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if to > 0 { // move right
|
||||||
|
u.boxMenuButton++
|
||||||
|
} else { // move left
|
||||||
|
u.boxMenuButton--
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.boxMenuButton >= len(u.boxMenu) {
|
||||||
|
u.boxMenuButton = len(u.boxMenu) - 1
|
||||||
|
} else if u.boxMenuButton < 0 {
|
||||||
|
u.boxMenuButton = 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// find the biggest entry in the current listing
|
// find the biggest entry in the current listing
|
||||||
@@ -314,6 +415,57 @@ func (u *UI) move(d int) {
|
|||||||
u.dirPosMap[u.path] = dirPos
|
u.dirPosMap[u.path] = dirPos
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *UI) removeEntry(pos int) {
|
||||||
|
u.d.Remove(pos)
|
||||||
|
u.setCurrentDir(u.d)
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete the entry at the current position
|
||||||
|
func (u *UI) delete() {
|
||||||
|
dirPos := u.sortPerm[u.dirPosMap[u.path].entry]
|
||||||
|
entry := u.entries[dirPos]
|
||||||
|
|
||||||
|
file := false
|
||||||
|
d, _ := u.d.GetDir(dirPos)
|
||||||
|
if d == nil {
|
||||||
|
file = true
|
||||||
|
}
|
||||||
|
|
||||||
|
u.boxMenu = []string{"cancel", "confirm"}
|
||||||
|
if file {
|
||||||
|
u.boxMenuHandler = func(f fs.Fs, p string, o int) (string, error) {
|
||||||
|
if o != 1 {
|
||||||
|
return "Aborted!", nil
|
||||||
|
}
|
||||||
|
err := f.Rmdir(entry.String())
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
u.removeEntry(dirPos)
|
||||||
|
return "Successfully deleted file!", nil
|
||||||
|
}
|
||||||
|
u.popupBox([]string{
|
||||||
|
"Delete this file?",
|
||||||
|
u.fsName + entry.String()})
|
||||||
|
} else {
|
||||||
|
u.boxMenuHandler = func(f fs.Fs, p string, o int) (string, error) {
|
||||||
|
if o != 1 {
|
||||||
|
return "Aborted!", nil
|
||||||
|
}
|
||||||
|
err := operations.Purge(f, entry.String())
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
u.removeEntry(dirPos)
|
||||||
|
return "Successfully purged folder!", nil
|
||||||
|
}
|
||||||
|
u.popupBox([]string{
|
||||||
|
"Purge this directory?",
|
||||||
|
"ALL files in it will be deleted",
|
||||||
|
u.fsName + entry.String()})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Sort by the configured sort method
|
// Sort by the configured sort method
|
||||||
type ncduSort struct {
|
type ncduSort struct {
|
||||||
sortPerm []int
|
sortPerm []int
|
||||||
@@ -405,6 +557,25 @@ func (u *UI) enter() {
|
|||||||
u.setCurrentDir(d)
|
u.setCurrentDir(d)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handles a box option that was selected
|
||||||
|
func (u *UI) handleBoxOption() {
|
||||||
|
msg, err := u.boxMenuHandler(u.f, u.path, u.boxMenuButton)
|
||||||
|
// reset
|
||||||
|
u.boxMenuButton = 0
|
||||||
|
u.boxMenu = []string{}
|
||||||
|
u.boxMenuHandler = nil
|
||||||
|
if err != nil {
|
||||||
|
u.popupBox([]string{
|
||||||
|
"error:",
|
||||||
|
err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
u.popupBox([]string{"Finished:", msg})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// up goes up to the parent directory
|
// up goes up to the parent directory
|
||||||
func (u *UI) up() {
|
func (u *UI) up() {
|
||||||
if u.d == nil {
|
if u.d == nil {
|
||||||
@@ -524,8 +695,22 @@ outer:
|
|||||||
case termbox.KeyPgup, '=', '+':
|
case termbox.KeyPgup, '=', '+':
|
||||||
u.move(-u.dirListHeight)
|
u.move(-u.dirListHeight)
|
||||||
case termbox.KeyArrowLeft, 'h':
|
case termbox.KeyArrowLeft, 'h':
|
||||||
|
if u.showBox {
|
||||||
|
u.moveBox(-1)
|
||||||
|
break
|
||||||
|
}
|
||||||
u.up()
|
u.up()
|
||||||
case termbox.KeyArrowRight, 'l', termbox.KeyEnter:
|
case termbox.KeyEnter:
|
||||||
|
if len(u.boxMenu) > 0 {
|
||||||
|
u.handleBoxOption()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
u.enter()
|
||||||
|
case termbox.KeyArrowRight, 'l':
|
||||||
|
if u.showBox {
|
||||||
|
u.moveBox(1)
|
||||||
|
break
|
||||||
|
}
|
||||||
u.enter()
|
u.enter()
|
||||||
case 'c':
|
case 'c':
|
||||||
u.showCounts = !u.showCounts
|
u.showCounts = !u.showCounts
|
||||||
@@ -537,6 +722,8 @@ outer:
|
|||||||
u.toggleSort(&u.sortBySize)
|
u.toggleSort(&u.sortBySize)
|
||||||
case 'C':
|
case 'C':
|
||||||
u.toggleSort(&u.sortByCount)
|
u.toggleSort(&u.sortByCount)
|
||||||
|
case 'd':
|
||||||
|
u.delete()
|
||||||
case '?':
|
case '?':
|
||||||
u.togglePopupBox(helpText)
|
u.togglePopupBox(helpText)
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,45 @@ func (d *Dir) Entries() fs.DirEntries {
|
|||||||
return append(fs.DirEntries(nil), d.entries...)
|
return append(fs.DirEntries(nil), d.entries...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove removes the i-th entry from the
|
||||||
|
// in-memory representation of the remote directory
|
||||||
|
func (d *Dir) Remove(i int) {
|
||||||
|
d.mu.Lock()
|
||||||
|
defer d.mu.Unlock()
|
||||||
|
d.remove(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// removes the i-th entry from the
|
||||||
|
// in-memory representation of the remote directory
|
||||||
|
//
|
||||||
|
// Call with d.mu held
|
||||||
|
func (d *Dir) remove(i int) {
|
||||||
|
size := d.entries[i].Size()
|
||||||
|
count := int64(1)
|
||||||
|
|
||||||
|
subDir, ok := d.getDir(i)
|
||||||
|
if ok {
|
||||||
|
size = subDir.size
|
||||||
|
count = subDir.count
|
||||||
|
delete(d.dirs, path.Base(subDir.path))
|
||||||
|
}
|
||||||
|
|
||||||
|
d.size -= size
|
||||||
|
d.count -= count
|
||||||
|
d.entries = append(d.entries[:i], d.entries[i+1:]...)
|
||||||
|
|
||||||
|
dir := d
|
||||||
|
// populate changed size and count to parent(s)
|
||||||
|
for parent := d.parent; parent != nil; parent = parent.parent {
|
||||||
|
parent.mu.Lock()
|
||||||
|
parent.dirs[path.Base(dir.path)] = dir
|
||||||
|
parent.size -= size
|
||||||
|
parent.count -= count
|
||||||
|
dir = parent
|
||||||
|
parent.mu.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// gets the directory of the i-th entry
|
// gets the directory of the i-th entry
|
||||||
//
|
//
|
||||||
// returns nil if it is a file
|
// returns nil if it is a file
|
||||||
|
|||||||
107
cmd/rc/rc.go
107
cmd/rc/rc.go
@@ -19,31 +19,50 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
noOutput = false
|
noOutput = false
|
||||||
url = "http://localhost:5572/"
|
url = "http://localhost:5572/"
|
||||||
|
jsonInput = ""
|
||||||
|
authUser = ""
|
||||||
|
authPass = ""
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
cmd.Root.AddCommand(commandDefintion)
|
cmd.Root.AddCommand(commandDefintion)
|
||||||
commandDefintion.Flags().BoolVarP(&noOutput, "no-output", "", noOutput, "If set don't output the JSON result.")
|
commandDefintion.Flags().BoolVarP(&noOutput, "no-output", "", noOutput, "If set don't output the JSON result.")
|
||||||
commandDefintion.Flags().StringVarP(&url, "url", "", url, "URL to connect to rclone remote control.")
|
commandDefintion.Flags().StringVarP(&url, "url", "", url, "URL to connect to rclone remote control.")
|
||||||
|
commandDefintion.Flags().StringVarP(&jsonInput, "json", "", jsonInput, "Input JSON - use instead of key=value args.")
|
||||||
|
commandDefintion.Flags().StringVarP(&authUser, "user", "", "", "Username to use to rclone remote control.")
|
||||||
|
commandDefintion.Flags().StringVarP(&authPass, "pass", "", "", "Password to use to connect to rclone remote control.")
|
||||||
}
|
}
|
||||||
|
|
||||||
var commandDefintion = &cobra.Command{
|
var commandDefintion = &cobra.Command{
|
||||||
Use: "rc commands parameter",
|
Use: "rc commands parameter",
|
||||||
Short: `Run a command against a running rclone.`,
|
Short: `Run a command against a running rclone.`,
|
||||||
Long: `
|
Long: `
|
||||||
This runs a command against a running rclone. By default it will use
|
|
||||||
that specified in the --rc-addr command.
|
This runs a command against a running rclone. Use the --url flag to
|
||||||
|
specify an non default URL to connect on. This can be either a
|
||||||
|
":port" which is taken to mean "http://localhost:port" or a
|
||||||
|
"host:port" which is taken to mean "http://host:port"
|
||||||
|
|
||||||
|
A username and password can be passed in with --user and --pass.
|
||||||
|
|
||||||
|
Note that --rc-addr, --rc-user, --rc-pass will be read also for --url,
|
||||||
|
--user, --pass.
|
||||||
|
|
||||||
Arguments should be passed in as parameter=value.
|
Arguments should be passed in as parameter=value.
|
||||||
|
|
||||||
The result will be returned as a JSON object by default.
|
The result will be returned as a JSON object by default.
|
||||||
|
|
||||||
|
The --json parameter can be used to pass in a JSON blob as an input
|
||||||
|
instead of key=value arguments. This is the only way of passing in
|
||||||
|
more complicated values.
|
||||||
|
|
||||||
Use "rclone rc" to see a list of all possible commands.`,
|
Use "rclone rc" to see a list of all possible commands.`,
|
||||||
Run: func(command *cobra.Command, args []string) {
|
Run: func(command *cobra.Command, args []string) {
|
||||||
cmd.CheckArgs(0, 1E9, command, args)
|
cmd.CheckArgs(0, 1E9, command, args)
|
||||||
cmd.Run(false, false, command, func() error {
|
cmd.Run(false, false, command, func() error {
|
||||||
|
parseFlags()
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return list()
|
return list()
|
||||||
}
|
}
|
||||||
@@ -52,30 +71,56 @@ Use "rclone rc" to see a list of all possible commands.`,
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse the flags
|
||||||
|
func parseFlags() {
|
||||||
|
// set alternates from alternate flags
|
||||||
|
setAlternateFlag("rc-addr", &url)
|
||||||
|
setAlternateFlag("rc-user", &authUser)
|
||||||
|
setAlternateFlag("rc-pass", &authPass)
|
||||||
|
// If url is just :port then fix it up
|
||||||
|
if strings.HasPrefix(url, ":") {
|
||||||
|
url = "localhost" + url
|
||||||
|
}
|
||||||
|
// if url is just host:port add http://
|
||||||
|
if !strings.HasPrefix(url, "http:") && !strings.HasPrefix(url, "https:") {
|
||||||
|
url = "http://" + url
|
||||||
|
}
|
||||||
|
// if url doesn't end with / add it
|
||||||
|
if !strings.HasSuffix(url, "/") {
|
||||||
|
url += "/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the user set flagName set the output to its value
|
||||||
|
func setAlternateFlag(flagName string, output *string) {
|
||||||
|
if rcFlag := pflag.Lookup(flagName); rcFlag != nil && rcFlag.Changed {
|
||||||
|
*output = rcFlag.Value.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// do a call from (path, in) to (out, err).
|
// do a call from (path, in) to (out, err).
|
||||||
//
|
//
|
||||||
// if err is set, out may be a valid error return or it may be nil
|
// if err is set, out may be a valid error return or it may be nil
|
||||||
func doCall(path string, in rc.Params) (out rc.Params, err error) {
|
func doCall(path string, in rc.Params) (out rc.Params, err error) {
|
||||||
// Do HTTP request
|
// Do HTTP request
|
||||||
client := fshttp.NewClient(fs.Config)
|
client := fshttp.NewClient(fs.Config)
|
||||||
url := url
|
|
||||||
// set the user use --rc-addr as well as --url
|
|
||||||
if rcAddrFlag := pflag.Lookup("rc-addr"); rcAddrFlag != nil && rcAddrFlag.Changed {
|
|
||||||
url = rcAddrFlag.Value.String()
|
|
||||||
if strings.HasPrefix(url, ":") {
|
|
||||||
url = "localhost" + url
|
|
||||||
}
|
|
||||||
url = "http://" + url + "/"
|
|
||||||
}
|
|
||||||
if !strings.HasSuffix(url, "/") {
|
|
||||||
url += "/"
|
|
||||||
}
|
|
||||||
url += path
|
url += path
|
||||||
data, err := json.Marshal(in)
|
data, err := json.Marshal(in)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to encode JSON")
|
return nil, errors.Wrap(err, "failed to encode JSON")
|
||||||
}
|
}
|
||||||
resp, err := client.Post(url, "application/json", bytes.NewBuffer(data))
|
|
||||||
|
req, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to make request")
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
if authUser != "" || authPass != "" {
|
||||||
|
req.SetBasicAuth(authUser, authPass)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "connection failed")
|
return nil, errors.Wrap(err, "connection failed")
|
||||||
}
|
}
|
||||||
@@ -115,13 +160,24 @@ func run(args []string) (err error) {
|
|||||||
|
|
||||||
// parse input
|
// parse input
|
||||||
in := make(rc.Params)
|
in := make(rc.Params)
|
||||||
for _, param := range args[1:] {
|
params := args[1:]
|
||||||
equals := strings.IndexRune(param, '=')
|
if jsonInput == "" {
|
||||||
if equals < 0 {
|
for _, param := range params {
|
||||||
return errors.Errorf("No '=' found in parameter %q", param)
|
equals := strings.IndexRune(param, '=')
|
||||||
|
if equals < 0 {
|
||||||
|
return errors.Errorf("no '=' found in parameter %q", param)
|
||||||
|
}
|
||||||
|
key, value := param[:equals], param[equals+1:]
|
||||||
|
in[key] = value
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if len(params) > 0 {
|
||||||
|
return errors.New("can't use --json and parameters together")
|
||||||
|
}
|
||||||
|
err = json.Unmarshal([]byte(jsonInput), &in)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "bad --json input")
|
||||||
}
|
}
|
||||||
key, value := param[:equals], param[equals+1:]
|
|
||||||
in[key] = value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do the call
|
// Do the call
|
||||||
@@ -155,6 +211,11 @@ func list() error {
|
|||||||
}
|
}
|
||||||
fmt.Printf("### %s: %s\n\n", info["Path"], info["Title"])
|
fmt.Printf("### %s: %s\n\n", info["Path"], info["Title"])
|
||||||
fmt.Printf("%s\n\n", info["Help"])
|
fmt.Printf("%s\n\n", info["Help"])
|
||||||
|
if authRequired := info["AuthRequired"]; authRequired != nil {
|
||||||
|
if authRequired.(bool) {
|
||||||
|
fmt.Printf("Authentication is required for this call.\n\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
49
cmd/rcd/rcd.go
Normal file
49
cmd/rcd/rcd.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package rcd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/ncw/rclone/cmd"
|
||||||
|
"github.com/ncw/rclone/fs/rc/rcflags"
|
||||||
|
"github.com/ncw/rclone/fs/rc/rcserver"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
cmd.Root.AddCommand(commandDefintion)
|
||||||
|
}
|
||||||
|
|
||||||
|
var commandDefintion = &cobra.Command{
|
||||||
|
Use: "rcd <path to files to serve>*",
|
||||||
|
Short: `Run rclone listening to remote control commands only.`,
|
||||||
|
Long: `
|
||||||
|
This runs rclone so that it only listents to remote control commands.
|
||||||
|
|
||||||
|
This is useful if you are controlling rclone via the rc API.
|
||||||
|
|
||||||
|
If you pass in a path to a directory, rclone will serve that directory
|
||||||
|
for GET requests on the URL passed in. It will also open the URL in
|
||||||
|
the browser when rclone is run.
|
||||||
|
|
||||||
|
See the [rc documentation](/rc/) for more info on the rc flags.
|
||||||
|
`,
|
||||||
|
Run: func(command *cobra.Command, args []string) {
|
||||||
|
cmd.CheckArgs(0, 1, command, args)
|
||||||
|
if rcflags.Opt.Enabled {
|
||||||
|
log.Fatalf("Don't supply --rc flag when using rcd")
|
||||||
|
}
|
||||||
|
// Start the rc
|
||||||
|
rcflags.Opt.Enabled = true
|
||||||
|
if len(args) > 0 {
|
||||||
|
rcflags.Opt.Files = args[0]
|
||||||
|
}
|
||||||
|
s, err := rcserver.Start(&rcflags.Opt)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to start remote control: %v", err)
|
||||||
|
}
|
||||||
|
if s == nil {
|
||||||
|
log.Fatal("rc server not configured")
|
||||||
|
}
|
||||||
|
s.Wait()
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package ftpflags
|
|||||||
import (
|
import (
|
||||||
"github.com/ncw/rclone/cmd/serve/ftp/ftpopt"
|
"github.com/ncw/rclone/cmd/serve/ftp/ftpopt"
|
||||||
"github.com/ncw/rclone/fs/config/flags"
|
"github.com/ncw/rclone/fs/config/flags"
|
||||||
|
"github.com/ncw/rclone/fs/rc"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -13,6 +14,7 @@ var (
|
|||||||
|
|
||||||
// AddFlagsPrefix adds flags for the ftpopt
|
// AddFlagsPrefix adds flags for the ftpopt
|
||||||
func AddFlagsPrefix(flagSet *pflag.FlagSet, prefix string, Opt *ftpopt.Options) {
|
func AddFlagsPrefix(flagSet *pflag.FlagSet, prefix string, Opt *ftpopt.Options) {
|
||||||
|
rc.AddOption("ftp", &Opt)
|
||||||
flags.StringVarP(flagSet, &Opt.ListenAddr, prefix+"addr", "", Opt.ListenAddr, "IPaddress:Port or :Port to bind server to.")
|
flags.StringVarP(flagSet, &Opt.ListenAddr, prefix+"addr", "", Opt.ListenAddr, "IPaddress:Port or :Port to bind server to.")
|
||||||
flags.StringVarP(flagSet, &Opt.PassivePorts, prefix+"passive-port", "", Opt.PassivePorts, "Passive port range to use.")
|
flags.StringVarP(flagSet, &Opt.PassivePorts, prefix+"passive-port", "", Opt.PassivePorts, "Passive port range to use.")
|
||||||
flags.StringVarP(flagSet, &Opt.BasicUser, prefix+"user", "", Opt.BasicUser, "User name for authentication.")
|
flags.StringVarP(flagSet, &Opt.BasicUser, prefix+"user", "", Opt.BasicUser, "User name for authentication.")
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"html/template"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
@@ -12,9 +10,9 @@ import (
|
|||||||
"github.com/ncw/rclone/cmd"
|
"github.com/ncw/rclone/cmd"
|
||||||
"github.com/ncw/rclone/cmd/serve/httplib"
|
"github.com/ncw/rclone/cmd/serve/httplib"
|
||||||
"github.com/ncw/rclone/cmd/serve/httplib/httpflags"
|
"github.com/ncw/rclone/cmd/serve/httplib/httpflags"
|
||||||
|
"github.com/ncw/rclone/cmd/serve/httplib/serve"
|
||||||
"github.com/ncw/rclone/fs"
|
"github.com/ncw/rclone/fs"
|
||||||
"github.com/ncw/rclone/fs/accounting"
|
"github.com/ncw/rclone/fs/accounting"
|
||||||
"github.com/ncw/rclone/lib/rest"
|
|
||||||
"github.com/ncw/rclone/vfs"
|
"github.com/ncw/rclone/vfs"
|
||||||
"github.com/ncw/rclone/vfs/vfsflags"
|
"github.com/ncw/rclone/vfs/vfsflags"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -46,7 +44,11 @@ control the stats printing.
|
|||||||
f := cmd.NewFsSrc(args)
|
f := cmd.NewFsSrc(args)
|
||||||
cmd.Run(false, true, command, func() error {
|
cmd.Run(false, true, command, func() error {
|
||||||
s := newServer(f, &httpflags.Opt)
|
s := newServer(f, &httpflags.Opt)
|
||||||
s.serve()
|
err := s.Serve()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.Wait()
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -54,30 +56,32 @@ control the stats printing.
|
|||||||
|
|
||||||
// server contains everything to run the server
|
// server contains everything to run the server
|
||||||
type server struct {
|
type server struct {
|
||||||
|
*httplib.Server
|
||||||
f fs.Fs
|
f fs.Fs
|
||||||
vfs *vfs.VFS
|
vfs *vfs.VFS
|
||||||
srv *httplib.Server
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newServer(f fs.Fs, opt *httplib.Options) *server {
|
func newServer(f fs.Fs, opt *httplib.Options) *server {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
s := &server{
|
s := &server{
|
||||||
f: f,
|
Server: httplib.NewServer(mux, opt),
|
||||||
vfs: vfs.New(f, &vfsflags.Opt),
|
f: f,
|
||||||
srv: httplib.NewServer(mux, opt),
|
vfs: vfs.New(f, &vfsflags.Opt),
|
||||||
}
|
}
|
||||||
mux.HandleFunc("/", s.handler)
|
mux.HandleFunc("/", s.handler)
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
// serve runs the http server - doesn't return
|
// Serve runs the http server in the background.
|
||||||
func (s *server) serve() {
|
//
|
||||||
err := s.srv.Serve()
|
// Use s.Close() and s.Wait() to shutdown server
|
||||||
|
func (s *server) Serve() error {
|
||||||
|
err := s.Server.Serve()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fs.Errorf(s.f, "Opening listener: %v", err)
|
return err
|
||||||
}
|
}
|
||||||
fs.Logf(s.f, "Serving on %s", s.srv.URL())
|
fs.Logf(s.f, "Serving on %s", s.URL())
|
||||||
s.srv.Wait()
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// handler reads incoming requests and dispatches them
|
// handler reads incoming requests and dispatches them
|
||||||
@@ -99,62 +103,6 @@ func (s *server) handler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// entry is a directory entry
|
|
||||||
type entry struct {
|
|
||||||
remote string
|
|
||||||
URL string
|
|
||||||
Leaf string
|
|
||||||
}
|
|
||||||
|
|
||||||
// entries represents a directory
|
|
||||||
type entries []entry
|
|
||||||
|
|
||||||
// addEntry adds an entry to that directory
|
|
||||||
func (es *entries) addEntry(node interface {
|
|
||||||
Path() string
|
|
||||||
Name() string
|
|
||||||
IsDir() bool
|
|
||||||
}) {
|
|
||||||
remote := node.Path()
|
|
||||||
leaf := node.Name()
|
|
||||||
urlRemote := leaf
|
|
||||||
if node.IsDir() {
|
|
||||||
leaf += "/"
|
|
||||||
urlRemote += "/"
|
|
||||||
}
|
|
||||||
*es = append(*es, entry{remote: remote, URL: rest.URLPathEscape(urlRemote), Leaf: leaf})
|
|
||||||
}
|
|
||||||
|
|
||||||
// indexPage is a directory listing template
|
|
||||||
var indexPage = `<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>{{ .Title }}</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>{{ .Title }}</h1>
|
|
||||||
{{ range $i := .Entries }}<a href="{{ $i.URL }}">{{ $i.Leaf }}</a><br />
|
|
||||||
{{ end }}</body>
|
|
||||||
</html>
|
|
||||||
`
|
|
||||||
|
|
||||||
// indexTemplate is the instantiated indexPage
|
|
||||||
var indexTemplate = template.Must(template.New("index").Parse(indexPage))
|
|
||||||
|
|
||||||
// indexData is used to fill in the indexTemplate
|
|
||||||
type indexData struct {
|
|
||||||
Title string
|
|
||||||
Entries entries
|
|
||||||
}
|
|
||||||
|
|
||||||
// error returns an http.StatusInternalServerError and logs the error
|
|
||||||
func internalError(what interface{}, w http.ResponseWriter, text string, err error) {
|
|
||||||
fs.CountError(err)
|
|
||||||
fs.Errorf(what, "%s: %v", text, err)
|
|
||||||
http.Error(w, text+".", http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
|
|
||||||
// serveDir serves a directory index at dirRemote
|
// serveDir serves a directory index at dirRemote
|
||||||
func (s *server) serveDir(w http.ResponseWriter, r *http.Request, dirRemote string) {
|
func (s *server) serveDir(w http.ResponseWriter, r *http.Request, dirRemote string) {
|
||||||
// List the directory
|
// List the directory
|
||||||
@@ -163,7 +111,7 @@ func (s *server) serveDir(w http.ResponseWriter, r *http.Request, dirRemote stri
|
|||||||
http.Error(w, "Directory not found", http.StatusNotFound)
|
http.Error(w, "Directory not found", http.StatusNotFound)
|
||||||
return
|
return
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
internalError(dirRemote, w, "Failed to list directory", err)
|
serve.Error(dirRemote, w, "Failed to list directory", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !node.IsDir() {
|
if !node.IsDir() {
|
||||||
@@ -173,28 +121,17 @@ func (s *server) serveDir(w http.ResponseWriter, r *http.Request, dirRemote stri
|
|||||||
dir := node.(*vfs.Dir)
|
dir := node.(*vfs.Dir)
|
||||||
dirEntries, err := dir.ReadDirAll()
|
dirEntries, err := dir.ReadDirAll()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
internalError(dirRemote, w, "Failed to list directory", err)
|
serve.Error(dirRemote, w, "Failed to list directory", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var out entries
|
// Make the entries for display
|
||||||
|
directory := serve.NewDirectory(dirRemote)
|
||||||
for _, node := range dirEntries {
|
for _, node := range dirEntries {
|
||||||
out.addEntry(node)
|
directory.AddEntry(node.Path(), node.IsDir())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Account the transfer
|
directory.Serve(w, r)
|
||||||
accounting.Stats.Transferring(dirRemote)
|
|
||||||
defer accounting.Stats.DoneTransferring(dirRemote, true)
|
|
||||||
|
|
||||||
fs.Infof(dirRemote, "%s: Serving directory", r.RemoteAddr)
|
|
||||||
err = indexTemplate.Execute(w, indexData{
|
|
||||||
Entries: out,
|
|
||||||
Title: fmt.Sprintf("Directory listing of /%s", dirRemote),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
internalError(dirRemote, w, "Failed to render template", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// serveFile serves a file object at remote
|
// serveFile serves a file object at remote
|
||||||
@@ -205,7 +142,7 @@ func (s *server) serveFile(w http.ResponseWriter, r *http.Request, remote string
|
|||||||
http.Error(w, "File not found", http.StatusNotFound)
|
http.Error(w, "File not found", http.StatusNotFound)
|
||||||
return
|
return
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
internalError(remote, w, "Failed to find file", err)
|
serve.Error(remote, w, "Failed to find file", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !node.IsFile() {
|
if !node.IsFile() {
|
||||||
@@ -239,7 +176,7 @@ func (s *server) serveFile(w http.ResponseWriter, r *http.Request, remote string
|
|||||||
// open the object
|
// open the object
|
||||||
in, err := file.Open(os.O_RDONLY)
|
in, err := file.Open(os.O_RDONLY)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
internalError(remote, w, "Failed to open file", err)
|
serve.Error(remote, w, "Failed to open file", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -35,7 +34,7 @@ func startServer(t *testing.T, f fs.Fs) {
|
|||||||
opt := httplib.DefaultOpt
|
opt := httplib.DefaultOpt
|
||||||
opt.ListenAddr = testBindAddress
|
opt.ListenAddr = testBindAddress
|
||||||
httpServer = newServer(f, &opt)
|
httpServer = newServer(f, &opt)
|
||||||
go httpServer.serve()
|
assert.NoError(t, httpServer.Serve())
|
||||||
|
|
||||||
// try to connect to the test server
|
// try to connect to the test server
|
||||||
pause := time.Millisecond
|
pause := time.Millisecond
|
||||||
@@ -202,36 +201,7 @@ func TestGET(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type mockNode struct {
|
|
||||||
path string
|
|
||||||
isdir bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n mockNode) Path() string { return n.path }
|
|
||||||
func (n mockNode) Name() string {
|
|
||||||
if n.path == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return path.Base(n.path)
|
|
||||||
}
|
|
||||||
func (n mockNode) IsDir() bool { return n.isdir }
|
|
||||||
|
|
||||||
func TestAddEntry(t *testing.T) {
|
|
||||||
var es entries
|
|
||||||
es.addEntry(mockNode{path: "", isdir: true})
|
|
||||||
es.addEntry(mockNode{path: "dir", isdir: true})
|
|
||||||
es.addEntry(mockNode{path: "a/b/c/d.txt", isdir: false})
|
|
||||||
es.addEntry(mockNode{path: "a/b/c/colon:colon.txt", isdir: false})
|
|
||||||
es.addEntry(mockNode{path: "\"quotes\".txt", isdir: false})
|
|
||||||
assert.Equal(t, entries{
|
|
||||||
{remote: "", URL: "/", Leaf: "/"},
|
|
||||||
{remote: "dir", URL: "dir/", Leaf: "dir/"},
|
|
||||||
{remote: "a/b/c/d.txt", URL: "d.txt", Leaf: "d.txt"},
|
|
||||||
{remote: "a/b/c/colon:colon.txt", URL: "./colon:colon.txt", Leaf: "colon:colon.txt"},
|
|
||||||
{remote: "\"quotes\".txt", URL: "%22quotes%22.txt", Leaf: "\"quotes\".txt"},
|
|
||||||
}, es)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFinalise(t *testing.T) {
|
func TestFinalise(t *testing.T) {
|
||||||
httpServer.srv.Close()
|
httpServer.Close()
|
||||||
|
httpServer.Wait()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package httpflags
|
|||||||
import (
|
import (
|
||||||
"github.com/ncw/rclone/cmd/serve/httplib"
|
"github.com/ncw/rclone/cmd/serve/httplib"
|
||||||
"github.com/ncw/rclone/fs/config/flags"
|
"github.com/ncw/rclone/fs/config/flags"
|
||||||
|
"github.com/ncw/rclone/fs/rc"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -13,6 +14,7 @@ var (
|
|||||||
|
|
||||||
// AddFlagsPrefix adds flags for the httplib
|
// AddFlagsPrefix adds flags for the httplib
|
||||||
func AddFlagsPrefix(flagSet *pflag.FlagSet, prefix string, Opt *httplib.Options) {
|
func AddFlagsPrefix(flagSet *pflag.FlagSet, prefix string, Opt *httplib.Options) {
|
||||||
|
rc.AddOption(prefix+"http", &Opt)
|
||||||
flags.StringVarP(flagSet, &Opt.ListenAddr, prefix+"addr", "", Opt.ListenAddr, "IPaddress:Port or :Port to bind server to.")
|
flags.StringVarP(flagSet, &Opt.ListenAddr, prefix+"addr", "", Opt.ListenAddr, "IPaddress:Port or :Port to bind server to.")
|
||||||
flags.DurationVarP(flagSet, &Opt.ServerReadTimeout, prefix+"server-read-timeout", "", Opt.ServerReadTimeout, "Timeout for server reading data")
|
flags.DurationVarP(flagSet, &Opt.ServerReadTimeout, prefix+"server-read-timeout", "", Opt.ServerReadTimeout, "Timeout for server reading data")
|
||||||
flags.DurationVarP(flagSet, &Opt.ServerWriteTimeout, prefix+"server-write-timeout", "", Opt.ServerWriteTimeout, "Timeout for server writing data")
|
flags.DurationVarP(flagSet, &Opt.ServerWriteTimeout, prefix+"server-write-timeout", "", Opt.ServerWriteTimeout, "Timeout for server writing data")
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
|
|
||||||
auth "github.com/abbot/go-http-auth"
|
auth "github.com/abbot/go-http-auth"
|
||||||
"github.com/ncw/rclone/fs"
|
"github.com/ncw/rclone/fs"
|
||||||
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Globals
|
// Globals
|
||||||
@@ -105,6 +106,7 @@ type Server struct {
|
|||||||
httpServer *http.Server
|
httpServer *http.Server
|
||||||
basicPassHashed string
|
basicPassHashed string
|
||||||
useSSL bool // if server is configured for SSL/TLS
|
useSSL bool // if server is configured for SSL/TLS
|
||||||
|
usingAuth bool // set if authentication is configured
|
||||||
}
|
}
|
||||||
|
|
||||||
// singleUserProvider provides the encrypted password for a single user
|
// singleUserProvider provides the encrypted password for a single user
|
||||||
@@ -142,6 +144,7 @@ func NewServer(handler http.Handler, opt *Options) *Server {
|
|||||||
}
|
}
|
||||||
authenticator := auth.NewBasicAuthenticator(s.Opt.Realm, secretProvider)
|
authenticator := auth.NewBasicAuthenticator(s.Opt.Realm, secretProvider)
|
||||||
handler = auth.JustCheck(authenticator, handler.ServeHTTP)
|
handler = auth.JustCheck(authenticator, handler.ServeHTTP)
|
||||||
|
s.usingAuth = true
|
||||||
}
|
}
|
||||||
|
|
||||||
s.useSSL = s.Opt.SslKey != ""
|
s.useSSL = s.Opt.SslKey != ""
|
||||||
@@ -188,7 +191,7 @@ func NewServer(handler http.Handler, opt *Options) *Server {
|
|||||||
func (s *Server) Serve() error {
|
func (s *Server) Serve() error {
|
||||||
ln, err := net.Listen("tcp", s.httpServer.Addr)
|
ln, err := net.Listen("tcp", s.httpServer.Addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return errors.Wrapf(err, "start server failed")
|
||||||
}
|
}
|
||||||
s.listener = ln
|
s.listener = ln
|
||||||
s.waitChan = make(chan struct{})
|
s.waitChan = make(chan struct{})
|
||||||
@@ -254,3 +257,8 @@ func (s *Server) URL() string {
|
|||||||
}
|
}
|
||||||
return fmt.Sprintf("%s://%s/", proto, addr)
|
return fmt.Sprintf("%s://%s/", proto, addr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UsingAuth returns true if authentication is required
|
||||||
|
func (s *Server) UsingAuth() bool {
|
||||||
|
return s.usingAuth
|
||||||
|
}
|
||||||
|
|||||||
102
cmd/serve/httplib/serve/dir.go
Normal file
102
cmd/serve/httplib/serve/dir.go
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
package serve
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"github.com/ncw/rclone/fs"
|
||||||
|
"github.com/ncw/rclone/fs/accounting"
|
||||||
|
"github.com/ncw/rclone/lib/rest"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DirEntry is a directory entry
|
||||||
|
type DirEntry struct {
|
||||||
|
remote string
|
||||||
|
URL string
|
||||||
|
Leaf string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Directory represents a directory
|
||||||
|
type Directory struct {
|
||||||
|
DirRemote string
|
||||||
|
Title string
|
||||||
|
Entries []DirEntry
|
||||||
|
Query string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDirectory makes an empty Directory
|
||||||
|
func NewDirectory(dirRemote string) *Directory {
|
||||||
|
d := &Directory{
|
||||||
|
DirRemote: dirRemote,
|
||||||
|
Title: fmt.Sprintf("Directory listing of /%s", dirRemote),
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetQuery sets the query parameters for each URL
|
||||||
|
func (d *Directory) SetQuery(queryParams url.Values) *Directory {
|
||||||
|
d.Query = ""
|
||||||
|
if len(queryParams) > 0 {
|
||||||
|
d.Query = "?" + queryParams.Encode()
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddEntry adds an entry to that directory
|
||||||
|
func (d *Directory) AddEntry(remote string, isDir bool) {
|
||||||
|
leaf := path.Base(remote)
|
||||||
|
if leaf == "." {
|
||||||
|
leaf = ""
|
||||||
|
}
|
||||||
|
urlRemote := leaf
|
||||||
|
if isDir {
|
||||||
|
leaf += "/"
|
||||||
|
urlRemote += "/"
|
||||||
|
}
|
||||||
|
d.Entries = append(d.Entries, DirEntry{
|
||||||
|
remote: remote,
|
||||||
|
URL: rest.URLPathEscape(urlRemote) + d.Query,
|
||||||
|
Leaf: leaf,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error returns an http.StatusInternalServerError and logs the error
|
||||||
|
func Error(what interface{}, w http.ResponseWriter, text string, err error) {
|
||||||
|
fs.CountError(err)
|
||||||
|
fs.Errorf(what, "%s: %v", text, err)
|
||||||
|
http.Error(w, text+".", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serve serves a directory
|
||||||
|
func (d *Directory) Serve(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Account the transfer
|
||||||
|
accounting.Stats.Transferring(d.DirRemote)
|
||||||
|
defer accounting.Stats.DoneTransferring(d.DirRemote, true)
|
||||||
|
|
||||||
|
fs.Infof(d.DirRemote, "%s: Serving directory", r.RemoteAddr)
|
||||||
|
err := indexTemplate.Execute(w, d)
|
||||||
|
if err != nil {
|
||||||
|
Error(d.DirRemote, w, "Failed to render template", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// indexPage is a directory listing template
|
||||||
|
var indexPage = `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>{{ .Title }}</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>{{ .Title }}</h1>
|
||||||
|
{{ range $i := .Entries }}<a href="{{ $i.URL }}">{{ $i.Leaf }}</a><br />
|
||||||
|
{{ end }}</body>
|
||||||
|
</html>
|
||||||
|
`
|
||||||
|
|
||||||
|
// indexTemplate is the instantiated indexPage
|
||||||
|
var indexTemplate = template.Must(template.New("index").Parse(indexPage))
|
||||||
88
cmd/serve/httplib/serve/dir_test.go
Normal file
88
cmd/serve/httplib/serve/dir_test.go
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
package serve
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewDirectory(t *testing.T) {
|
||||||
|
d := NewDirectory("z")
|
||||||
|
assert.Equal(t, "z", d.DirRemote)
|
||||||
|
assert.Equal(t, "Directory listing of /z", d.Title)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetQuery(t *testing.T) {
|
||||||
|
d := NewDirectory("z")
|
||||||
|
assert.Equal(t, "", d.Query)
|
||||||
|
d.SetQuery(url.Values{"potato": []string{"42"}})
|
||||||
|
assert.Equal(t, "?potato=42", d.Query)
|
||||||
|
d.SetQuery(url.Values{})
|
||||||
|
assert.Equal(t, "", d.Query)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddEntry(t *testing.T) {
|
||||||
|
var d = NewDirectory("z")
|
||||||
|
d.AddEntry("", true)
|
||||||
|
d.AddEntry("dir", true)
|
||||||
|
d.AddEntry("a/b/c/d.txt", false)
|
||||||
|
d.AddEntry("a/b/c/colon:colon.txt", false)
|
||||||
|
d.AddEntry("\"quotes\".txt", false)
|
||||||
|
assert.Equal(t, []DirEntry{
|
||||||
|
{remote: "", URL: "/", Leaf: "/"},
|
||||||
|
{remote: "dir", URL: "dir/", Leaf: "dir/"},
|
||||||
|
{remote: "a/b/c/d.txt", URL: "d.txt", Leaf: "d.txt"},
|
||||||
|
{remote: "a/b/c/colon:colon.txt", URL: "./colon:colon.txt", Leaf: "colon:colon.txt"},
|
||||||
|
{remote: "\"quotes\".txt", URL: "%22quotes%22.txt", Leaf: "\"quotes\".txt"},
|
||||||
|
}, d.Entries)
|
||||||
|
|
||||||
|
// Now test with a query parameter
|
||||||
|
d = NewDirectory("z").SetQuery(url.Values{"potato": []string{"42"}})
|
||||||
|
d.AddEntry("file", false)
|
||||||
|
d.AddEntry("dir", true)
|
||||||
|
assert.Equal(t, []DirEntry{
|
||||||
|
{remote: "file", URL: "file?potato=42", Leaf: "file"},
|
||||||
|
{remote: "dir", URL: "dir/?potato=42", Leaf: "dir/"},
|
||||||
|
}, d.Entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestError(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
err := errors.New("help")
|
||||||
|
Error("potato", w, "sausage", err)
|
||||||
|
resp := w.Result()
|
||||||
|
assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
|
||||||
|
body, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
assert.Equal(t, "sausage.\n", string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServe(t *testing.T) {
|
||||||
|
d := NewDirectory("aDirectory")
|
||||||
|
d.AddEntry("file", false)
|
||||||
|
d.AddEntry("dir", true)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r := httptest.NewRequest("GET", "http://example.com/aDirectory/", nil)
|
||||||
|
d.Serve(w, r)
|
||||||
|
resp := w.Result()
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
body, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
assert.Equal(t, `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Directory listing of /aDirectory</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Directory listing of /aDirectory</h1>
|
||||||
|
<a href="file">file</a><br />
|
||||||
|
<a href="dir/">dir/</a><br />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`, string(body))
|
||||||
|
}
|
||||||
102
cmd/serve/httplib/serve/serve.go
Normal file
102
cmd/serve/httplib/serve/serve.go
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
// Package serve deals with serving objects over HTTP
|
||||||
|
package serve
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"path"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/ncw/rclone/fs"
|
||||||
|
"github.com/ncw/rclone/fs/accounting"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Object serves an fs.Object via HEAD or GET
|
||||||
|
func Object(w http.ResponseWriter, r *http.Request, o fs.Object) {
|
||||||
|
if r.Method != "HEAD" && r.Method != "GET" {
|
||||||
|
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show that we accept ranges
|
||||||
|
w.Header().Set("Accept-Ranges", "bytes")
|
||||||
|
|
||||||
|
// Set content length since we know how long the object is
|
||||||
|
if o.Size() >= 0 {
|
||||||
|
w.Header().Set("Content-Length", strconv.FormatInt(o.Size(), 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set content type
|
||||||
|
mimeType := fs.MimeType(o)
|
||||||
|
if mimeType == "application/octet-stream" && path.Ext(o.Remote()) == "" {
|
||||||
|
// Leave header blank so http server guesses
|
||||||
|
} else {
|
||||||
|
w.Header().Set("Content-Type", mimeType)
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Method == "HEAD" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode Range request if present
|
||||||
|
code := http.StatusOK
|
||||||
|
size := o.Size()
|
||||||
|
var options []fs.OpenOption
|
||||||
|
if rangeRequest := r.Header.Get("Range"); rangeRequest != "" {
|
||||||
|
//fs.Debugf(nil, "Range: request %q", rangeRequest)
|
||||||
|
option, err := fs.ParseRangeOption(rangeRequest)
|
||||||
|
if err != nil {
|
||||||
|
fs.Debugf(o, "Get request parse range request error: %v", err)
|
||||||
|
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
options = append(options, option)
|
||||||
|
offset, limit := option.Decode(o.Size())
|
||||||
|
end := o.Size() // exclusive
|
||||||
|
if limit >= 0 {
|
||||||
|
end = offset + limit
|
||||||
|
}
|
||||||
|
if end > o.Size() {
|
||||||
|
end = o.Size()
|
||||||
|
}
|
||||||
|
size = end - offset
|
||||||
|
// fs.Debugf(nil, "Range: offset=%d, limit=%d, end=%d, size=%d (object size %d)", offset, limit, end, size, o.Size())
|
||||||
|
// Content-Range: bytes 0-1023/146515
|
||||||
|
w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", offset, end-1, o.Size()))
|
||||||
|
// fs.Debugf(nil, "Range: Content-Range: %q", w.Header().Get("Content-Range"))
|
||||||
|
code = http.StatusPartialContent
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Length", strconv.FormatInt(size, 10))
|
||||||
|
|
||||||
|
file, err := o.Open(options...)
|
||||||
|
if err != nil {
|
||||||
|
fs.Debugf(o, "Get request open error: %v", err)
|
||||||
|
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
accounting.Stats.Transferring(o.Remote())
|
||||||
|
in := accounting.NewAccount(file, o) // account the transfer (no buffering)
|
||||||
|
defer func() {
|
||||||
|
closeErr := in.Close()
|
||||||
|
if closeErr != nil {
|
||||||
|
fs.Errorf(o, "Get request: close failed: %v", closeErr)
|
||||||
|
if err == nil {
|
||||||
|
err = closeErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ok := err == nil
|
||||||
|
accounting.Stats.DoneTransferring(o.Remote(), ok)
|
||||||
|
if !ok {
|
||||||
|
accounting.Stats.Error(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
w.WriteHeader(code)
|
||||||
|
|
||||||
|
n, err := io.Copy(w, in)
|
||||||
|
if err != nil {
|
||||||
|
fs.Errorf(o, "Didn't finish writing GET request (wrote %d/%d bytes): %v", n, size, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
76
cmd/serve/httplib/serve/serve_test.go
Normal file
76
cmd/serve/httplib/serve/serve_test.go
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
package serve
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ncw/rclone/fstest/mockobject"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestObjectBadMethod(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r := httptest.NewRequest("BADMETHOD", "http://example.com/aFile", nil)
|
||||||
|
o := mockobject.New("aFile")
|
||||||
|
Object(w, r, o)
|
||||||
|
resp := w.Result()
|
||||||
|
assert.Equal(t, http.StatusMethodNotAllowed, resp.StatusCode)
|
||||||
|
body, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
assert.Equal(t, "Method Not Allowed\n", string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestObjectHEAD(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r := httptest.NewRequest("HEAD", "http://example.com/aFile", nil)
|
||||||
|
o := mockobject.New("aFile").WithContent([]byte("hello"), mockobject.SeekModeNone)
|
||||||
|
Object(w, r, o)
|
||||||
|
resp := w.Result()
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
assert.Equal(t, "5", resp.Header.Get("Content-Length"))
|
||||||
|
assert.Equal(t, "bytes", resp.Header.Get("Accept-Ranges"))
|
||||||
|
body, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
assert.Equal(t, "", string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestObjectGET(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r := httptest.NewRequest("GET", "http://example.com/aFile", nil)
|
||||||
|
o := mockobject.New("aFile").WithContent([]byte("hello"), mockobject.SeekModeNone)
|
||||||
|
Object(w, r, o)
|
||||||
|
resp := w.Result()
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
assert.Equal(t, "5", resp.Header.Get("Content-Length"))
|
||||||
|
assert.Equal(t, "bytes", resp.Header.Get("Accept-Ranges"))
|
||||||
|
body, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
assert.Equal(t, "hello", string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestObjectRange(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r := httptest.NewRequest("GET", "http://example.com/aFile", nil)
|
||||||
|
r.Header.Add("Range", "bytes=3-5")
|
||||||
|
o := mockobject.New("aFile").WithContent([]byte("0123456789"), mockobject.SeekModeNone)
|
||||||
|
Object(w, r, o)
|
||||||
|
resp := w.Result()
|
||||||
|
assert.Equal(t, http.StatusPartialContent, resp.StatusCode)
|
||||||
|
assert.Equal(t, "3", resp.Header.Get("Content-Length"))
|
||||||
|
assert.Equal(t, "bytes", resp.Header.Get("Accept-Ranges"))
|
||||||
|
assert.Equal(t, "bytes 3-5/10", resp.Header.Get("Content-Range"))
|
||||||
|
body, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
assert.Equal(t, "345", string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestObjectBadRange(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r := httptest.NewRequest("GET", "http://example.com/aFile", nil)
|
||||||
|
r.Header.Add("Range", "xxxbytes=3-5")
|
||||||
|
o := mockobject.New("aFile").WithContent([]byte("0123456789"), mockobject.SeekModeNone)
|
||||||
|
Object(w, r, o)
|
||||||
|
resp := w.Result()
|
||||||
|
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||||
|
assert.Equal(t, "10", resp.Header.Get("Content-Length"))
|
||||||
|
body, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
assert.Equal(t, "Bad Request\n", string(body))
|
||||||
|
}
|
||||||
@@ -4,19 +4,17 @@ package restic
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ncw/rclone/cmd"
|
"github.com/ncw/rclone/cmd"
|
||||||
"github.com/ncw/rclone/cmd/serve/httplib"
|
"github.com/ncw/rclone/cmd/serve/httplib"
|
||||||
"github.com/ncw/rclone/cmd/serve/httplib/httpflags"
|
"github.com/ncw/rclone/cmd/serve/httplib/httpflags"
|
||||||
|
"github.com/ncw/rclone/cmd/serve/httplib/serve"
|
||||||
"github.com/ncw/rclone/fs"
|
"github.com/ncw/rclone/fs"
|
||||||
"github.com/ncw/rclone/fs/accounting"
|
"github.com/ncw/rclone/fs/accounting"
|
||||||
"github.com/ncw/rclone/fs/fserrors"
|
"github.com/ncw/rclone/fs/fserrors"
|
||||||
@@ -138,8 +136,11 @@ these **must** end with /. Eg
|
|||||||
httpSrv.ServeConn(conn, opts)
|
httpSrv.ServeConn(conn, opts)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
err := s.Serve()
|
||||||
s.serve()
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.Wait()
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -151,28 +152,30 @@ const (
|
|||||||
|
|
||||||
// server contains everything to run the server
|
// server contains everything to run the server
|
||||||
type server struct {
|
type server struct {
|
||||||
f fs.Fs
|
*httplib.Server
|
||||||
srv *httplib.Server
|
f fs.Fs
|
||||||
}
|
}
|
||||||
|
|
||||||
func newServer(f fs.Fs, opt *httplib.Options) *server {
|
func newServer(f fs.Fs, opt *httplib.Options) *server {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
s := &server{
|
s := &server{
|
||||||
f: f,
|
Server: httplib.NewServer(mux, opt),
|
||||||
srv: httplib.NewServer(mux, opt),
|
f: f,
|
||||||
}
|
}
|
||||||
mux.HandleFunc("/", s.handler)
|
mux.HandleFunc("/", s.handler)
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
// serve runs the http server - doesn't return
|
// Serve runs the http server in the background.
|
||||||
func (s *server) serve() {
|
//
|
||||||
err := s.srv.Serve()
|
// Use s.Close() and s.Wait() to shutdown server
|
||||||
|
func (s *server) Serve() error {
|
||||||
|
err := s.Server.Serve()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fs.Errorf(s.f, "Opening listener: %v", err)
|
return err
|
||||||
}
|
}
|
||||||
fs.Logf(s.f, "Serving restic REST API on %s", s.srv.URL())
|
fs.Logf(s.f, "Serving restic REST API on %s", s.URL())
|
||||||
s.srv.Wait()
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var matchData = regexp.MustCompile("(?:^|/)data/([^/]{2,})$")
|
var matchData = regexp.MustCompile("(?:^|/)data/([^/]{2,})$")
|
||||||
@@ -215,10 +218,8 @@ func (s *server) handler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case "GET":
|
case "GET", "HEAD":
|
||||||
s.getObject(w, r, remote)
|
s.serveObject(w, r, remote)
|
||||||
case "HEAD":
|
|
||||||
s.headObject(w, r, remote)
|
|
||||||
case "POST":
|
case "POST":
|
||||||
s.postObject(w, r, remote)
|
s.postObject(w, r, remote)
|
||||||
case "DELETE":
|
case "DELETE":
|
||||||
@@ -229,91 +230,15 @@ func (s *server) handler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// head request the remote
|
|
||||||
func (s *server) headObject(w http.ResponseWriter, r *http.Request, remote string) {
|
|
||||||
o, err := s.f.NewObject(remote)
|
|
||||||
if err != nil {
|
|
||||||
fs.Debugf(remote, "Head request error: %v", err)
|
|
||||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set content length since we know how long the object is
|
|
||||||
w.Header().Set("Content-Length", strconv.FormatInt(o.Size(), 10))
|
|
||||||
}
|
|
||||||
|
|
||||||
// get the remote
|
// get the remote
|
||||||
func (s *server) getObject(w http.ResponseWriter, r *http.Request, remote string) {
|
func (s *server) serveObject(w http.ResponseWriter, r *http.Request, remote string) {
|
||||||
o, err := s.f.NewObject(remote)
|
o, err := s.f.NewObject(remote)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fs.Debugf(remote, "Get request error: %v", err)
|
fs.Debugf(remote, "%s request error: %v", r.Method, err)
|
||||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
serve.Object(w, r, o)
|
||||||
// Set content length since we know how long the object is
|
|
||||||
w.Header().Set("Content-Length", strconv.FormatInt(o.Size(), 10))
|
|
||||||
|
|
||||||
// Decode Range request if present
|
|
||||||
code := http.StatusOK
|
|
||||||
size := o.Size()
|
|
||||||
var options []fs.OpenOption
|
|
||||||
if rangeRequest := r.Header.Get("Range"); rangeRequest != "" {
|
|
||||||
//fs.Debugf(nil, "Range: request %q", rangeRequest)
|
|
||||||
option, err := fs.ParseRangeOption(rangeRequest)
|
|
||||||
if err != nil {
|
|
||||||
fs.Debugf(remote, "Get request parse range request error: %v", err)
|
|
||||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
options = append(options, option)
|
|
||||||
offset, limit := option.Decode(o.Size())
|
|
||||||
end := o.Size() // exclusive
|
|
||||||
if limit >= 0 {
|
|
||||||
end = offset + limit
|
|
||||||
}
|
|
||||||
if end > o.Size() {
|
|
||||||
end = o.Size()
|
|
||||||
}
|
|
||||||
size = end - offset
|
|
||||||
// fs.Debugf(nil, "Range: offset=%d, limit=%d, end=%d, size=%d (object size %d)", offset, limit, end, size, o.Size())
|
|
||||||
// Content-Range: bytes 0-1023/146515
|
|
||||||
w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", offset, end-1, o.Size()))
|
|
||||||
// fs.Debugf(nil, "Range: Content-Range: %q", w.Header().Get("Content-Range"))
|
|
||||||
code = http.StatusPartialContent
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Length", strconv.FormatInt(size, 10))
|
|
||||||
|
|
||||||
file, err := o.Open(options...)
|
|
||||||
if err != nil {
|
|
||||||
fs.Debugf(remote, "Get request open error: %v", err)
|
|
||||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
accounting.Stats.Transferring(o.Remote())
|
|
||||||
in := accounting.NewAccount(file, o) // account the transfer (no buffering)
|
|
||||||
defer func() {
|
|
||||||
closeErr := in.Close()
|
|
||||||
if closeErr != nil {
|
|
||||||
fs.Errorf(remote, "Get request: close failed: %v", closeErr)
|
|
||||||
if err == nil {
|
|
||||||
err = closeErr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ok := err == nil
|
|
||||||
accounting.Stats.DoneTransferring(o.Remote(), ok)
|
|
||||||
if !ok {
|
|
||||||
accounting.Stats.Error(err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
w.WriteHeader(code)
|
|
||||||
|
|
||||||
n, err := io.Copy(w, in)
|
|
||||||
if err != nil {
|
|
||||||
fs.Errorf(remote, "Didn't finish writing GET request (wrote %d/%d bytes): %v", n, size, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// postObject posts an object to the repository
|
// postObject posts an object to the repository
|
||||||
|
|||||||
@@ -41,8 +41,11 @@ func TestRestic(t *testing.T) {
|
|||||||
|
|
||||||
// Start the server
|
// Start the server
|
||||||
w := newServer(fremote, &opt)
|
w := newServer(fremote, &opt)
|
||||||
go w.serve()
|
assert.NoError(t, w.Serve())
|
||||||
defer w.srv.Close()
|
defer func() {
|
||||||
|
w.Close()
|
||||||
|
w.Wait()
|
||||||
|
}()
|
||||||
|
|
||||||
// Change directory to run the tests
|
// Change directory to run the tests
|
||||||
err = os.Chdir(resticSource)
|
err = os.Chdir(resticSource)
|
||||||
|
|||||||
@@ -68,8 +68,12 @@ Use "rclone hashsum" to see the full list.
|
|||||||
fs.Debugf(f, "Using hash %v for ETag", hashType)
|
fs.Debugf(f, "Using hash %v for ETag", hashType)
|
||||||
}
|
}
|
||||||
cmd.Run(false, false, command, func() error {
|
cmd.Run(false, false, command, func() error {
|
||||||
w := newWebDAV(f, &httpflags.Opt)
|
s := newWebDAV(f, &httpflags.Opt)
|
||||||
w.serve()
|
err := s.serve()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.Wait()
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
return nil
|
return nil
|
||||||
@@ -89,9 +93,9 @@ Use "rclone hashsum" to see the full list.
|
|||||||
// might apply". In particular, whether or not renaming a file or directory
|
// might apply". In particular, whether or not renaming a file or directory
|
||||||
// overwriting another existing file or directory is an error is OS-dependent.
|
// overwriting another existing file or directory is an error is OS-dependent.
|
||||||
type WebDAV struct {
|
type WebDAV struct {
|
||||||
|
*httplib.Server
|
||||||
f fs.Fs
|
f fs.Fs
|
||||||
vfs *vfs.VFS
|
vfs *vfs.VFS
|
||||||
srv *httplib.Server
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// check interface
|
// check interface
|
||||||
@@ -110,18 +114,20 @@ func newWebDAV(f fs.Fs, opt *httplib.Options) *WebDAV {
|
|||||||
Logger: w.logRequest, // FIXME
|
Logger: w.logRequest, // FIXME
|
||||||
}
|
}
|
||||||
|
|
||||||
w.srv = httplib.NewServer(handler, opt)
|
w.Server = httplib.NewServer(handler, opt)
|
||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
|
|
||||||
// serve runs the http server - doesn't return
|
// serve runs the http server in the background.
|
||||||
func (w *WebDAV) serve() {
|
//
|
||||||
err := w.srv.Serve()
|
// Use s.Close() and s.Wait() to shutdown server
|
||||||
|
func (w *WebDAV) serve() error {
|
||||||
|
err := w.Serve()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fs.Errorf(w.f, "Opening listener: %v", err)
|
return err
|
||||||
}
|
}
|
||||||
fs.Logf(w.f, "WebDav Server started on %s", w.srv.URL())
|
fs.Logf(w.f, "WebDav Server started on %s", w.URL())
|
||||||
w.srv.Wait()
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// logRequest is called by the webdav module on every request
|
// logRequest is called by the webdav module on every request
|
||||||
|
|||||||
@@ -48,8 +48,11 @@ func TestWebDav(t *testing.T) {
|
|||||||
|
|
||||||
// Start the server
|
// Start the server
|
||||||
w := newWebDAV(fremote, &opt)
|
w := newWebDAV(fremote, &opt)
|
||||||
go w.serve()
|
assert.NoError(t, w.serve())
|
||||||
defer w.srv.Close()
|
defer func() {
|
||||||
|
w.Close()
|
||||||
|
w.Wait()
|
||||||
|
}()
|
||||||
|
|
||||||
// Change directory to run the tests
|
// Change directory to run the tests
|
||||||
err = os.Chdir("../../../backend/webdav")
|
err = os.Chdir("../../../backend/webdav")
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ extended explanation in the ` + "`" + `copy` + "`" + ` command above if unsure.
|
|||||||
|
|
||||||
If dest:path doesn't exist, it is created and the source:path contents
|
If dest:path doesn't exist, it is created and the source:path contents
|
||||||
go there.
|
go there.
|
||||||
|
|
||||||
|
**Note**: Use the ` + "`-P`" + `/` + "`--progress`" + ` flag to view real-time transfer statistics
|
||||||
`,
|
`,
|
||||||
Run: func(command *cobra.Command, args []string) {
|
Run: func(command *cobra.Command, args []string) {
|
||||||
cmd.CheckArgs(2, 2, command, args)
|
cmd.CheckArgs(2, 2, command, args)
|
||||||
|
|||||||
@@ -4,13 +4,12 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ncw/rclone/cmd"
|
"github.com/ncw/rclone/cmd"
|
||||||
"github.com/ncw/rclone/fs"
|
"github.com/ncw/rclone/fs"
|
||||||
|
"github.com/ncw/rclone/fs/version"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
@@ -66,63 +65,8 @@ Or
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var parseVersion = regexp.MustCompile(`^(?:rclone )?v(\d+)\.(\d+)(?:\.(\d+))?(?:-(\d+)(?:-(g[\wβ-]+))?)?$`)
|
|
||||||
|
|
||||||
type version []int
|
|
||||||
|
|
||||||
func newVersion(in string) (v version, err error) {
|
|
||||||
r := parseVersion.FindStringSubmatch(in)
|
|
||||||
if r == nil {
|
|
||||||
return v, errors.Errorf("failed to match version string %q", in)
|
|
||||||
}
|
|
||||||
atoi := func(s string) int {
|
|
||||||
i, err := strconv.Atoi(s)
|
|
||||||
if err != nil {
|
|
||||||
fs.Errorf(nil, "Failed to parse %q as int from %q: %v", s, in, err)
|
|
||||||
}
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
v = version{
|
|
||||||
atoi(r[1]), // major
|
|
||||||
atoi(r[2]), // minor
|
|
||||||
}
|
|
||||||
if r[3] != "" {
|
|
||||||
v = append(v, atoi(r[3])) // patch
|
|
||||||
} else if r[4] != "" {
|
|
||||||
v = append(v, 0) // patch
|
|
||||||
}
|
|
||||||
if r[4] != "" {
|
|
||||||
v = append(v, atoi(r[4])) // dev
|
|
||||||
}
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// String converts v to a string
|
|
||||||
func (v version) String() string {
|
|
||||||
var out []string
|
|
||||||
for _, vv := range v {
|
|
||||||
out = append(out, fmt.Sprint(vv))
|
|
||||||
}
|
|
||||||
return strings.Join(out, ".")
|
|
||||||
}
|
|
||||||
|
|
||||||
// cmp compares two versions returning >0, <0 or 0
|
|
||||||
func (v version) cmp(o version) (d int) {
|
|
||||||
n := len(v)
|
|
||||||
if n > len(o) {
|
|
||||||
n = len(o)
|
|
||||||
}
|
|
||||||
for i := 0; i < n; i++ {
|
|
||||||
d = v[i] - o[i]
|
|
||||||
if d != 0 {
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return len(v) - len(o)
|
|
||||||
}
|
|
||||||
|
|
||||||
// getVersion gets the version by checking the download repository passed in
|
// getVersion gets the version by checking the download repository passed in
|
||||||
func getVersion(url string) (v version, vs string, date time.Time, err error) {
|
func getVersion(url string) (v version.Version, vs string, date time.Time, err error) {
|
||||||
resp, err := http.Get(url)
|
resp, err := http.Get(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return v, vs, date, err
|
return v, vs, date, err
|
||||||
@@ -144,26 +88,17 @@ func getVersion(url string) (v version, vs string, date time.Time, err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return v, vs, date, err
|
return v, vs, date, err
|
||||||
}
|
}
|
||||||
v, err = newVersion(vs)
|
v, err = version.New(vs)
|
||||||
return v, vs, date, err
|
return v, vs, date, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// check the current version against available versions
|
// check the current version against available versions
|
||||||
func checkVersion() {
|
func checkVersion() {
|
||||||
// Get Current version
|
// Get Current version
|
||||||
currentVersion := fs.Version
|
vCurrent, err := version.New(fs.Version)
|
||||||
currentIsGit := strings.HasSuffix(currentVersion, "-DEV")
|
|
||||||
if currentIsGit {
|
|
||||||
currentVersion = currentVersion[:len(currentVersion)-4]
|
|
||||||
}
|
|
||||||
vCurrent, err := newVersion(currentVersion)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fs.Errorf(nil, "Failed to get parse version: %v", err)
|
fs.Errorf(nil, "Failed to get parse version: %v", err)
|
||||||
}
|
}
|
||||||
if currentIsGit {
|
|
||||||
vCurrent = append(vCurrent, 999, 999)
|
|
||||||
}
|
|
||||||
|
|
||||||
const timeFormat = "2006-01-02"
|
const timeFormat = "2006-01-02"
|
||||||
|
|
||||||
printVersion := func(what, url string) {
|
printVersion := func(what, url string) {
|
||||||
@@ -177,7 +112,7 @@ func checkVersion() {
|
|||||||
v,
|
v,
|
||||||
"(released "+t.Format(timeFormat)+")",
|
"(released "+t.Format(timeFormat)+")",
|
||||||
)
|
)
|
||||||
if v.cmp(vCurrent) > 0 {
|
if v.Cmp(vCurrent) > 0 {
|
||||||
fmt.Printf(" upgrade: %s\n", url+vs)
|
fmt.Printf(" upgrade: %s\n", url+vs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -190,7 +125,7 @@ func checkVersion() {
|
|||||||
"beta",
|
"beta",
|
||||||
"https://beta.rclone.org/",
|
"https://beta.rclone.org/",
|
||||||
)
|
)
|
||||||
if currentIsGit {
|
if vCurrent.IsGit() {
|
||||||
fmt.Println("Your version is compiled from git so comparisons may be wrong.")
|
fmt.Println("Your version is compiled from git so comparisons may be wrong.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package version
|
package version
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
@@ -46,65 +45,3 @@ func TestVersionWorksWithoutAccessibleConfigFile(t *testing.T) {
|
|||||||
// assert.NoError(t, cmd.Root.Execute())
|
// assert.NoError(t, cmd.Root.Execute())
|
||||||
// })
|
// })
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestVersionNew(t *testing.T) {
|
|
||||||
for _, test := range []struct {
|
|
||||||
in string
|
|
||||||
want version
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{"v1.41", version{1, 41}, false},
|
|
||||||
{"rclone v1.41", version{1, 41}, false},
|
|
||||||
{"rclone v1.41.23", version{1, 41, 23}, false},
|
|
||||||
{"rclone v1.41.23-100", version{1, 41, 23, 100}, false},
|
|
||||||
{"rclone v1.41-100", version{1, 41, 0, 100}, false},
|
|
||||||
{"rclone v1.41.23-100-g12312a", version{1, 41, 23, 100}, false},
|
|
||||||
{"rclone v1.41-100-g12312a", version{1, 41, 0, 100}, false},
|
|
||||||
{"rclone v1.42-005-g56e1e820β", version{1, 42, 0, 5}, false},
|
|
||||||
{"rclone v1.42-005-g56e1e820-feature-branchβ", version{1, 42, 0, 5}, false},
|
|
||||||
|
|
||||||
{"v1.41s", nil, true},
|
|
||||||
{"rclone v1-41", nil, true},
|
|
||||||
{"rclone v1.41.2c3", nil, true},
|
|
||||||
{"rclone v1.41.23-100 potato", nil, true},
|
|
||||||
{"rclone 1.41-100", nil, true},
|
|
||||||
{"rclone v1.41.23-100-12312a", nil, true},
|
|
||||||
} {
|
|
||||||
what := fmt.Sprintf("in=%q", test.in)
|
|
||||||
got, err := newVersion(test.in)
|
|
||||||
if test.wantErr {
|
|
||||||
assert.Error(t, err, what)
|
|
||||||
} else {
|
|
||||||
assert.NoError(t, err, what)
|
|
||||||
}
|
|
||||||
assert.Equal(t, test.want, got, what)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestVersionCmp(t *testing.T) {
|
|
||||||
for _, test := range []struct {
|
|
||||||
a, b version
|
|
||||||
want int
|
|
||||||
}{
|
|
||||||
{version{1}, version{1}, 0},
|
|
||||||
{version{1}, version{2}, -1},
|
|
||||||
{version{2}, version{1}, 1},
|
|
||||||
{version{2}, version{2, 1}, -1},
|
|
||||||
{version{2, 1}, version{2}, 1},
|
|
||||||
{version{2, 1}, version{2, 1}, 0},
|
|
||||||
{version{2, 1}, version{2, 2}, -1},
|
|
||||||
{version{2, 2}, version{2, 1}, 1},
|
|
||||||
} {
|
|
||||||
got := test.a.cmp(test.b)
|
|
||||||
if got < 0 {
|
|
||||||
got = -1
|
|
||||||
} else if got > 0 {
|
|
||||||
got = 1
|
|
||||||
}
|
|
||||||
assert.Equal(t, test.want, got, fmt.Sprintf("%v cmp %v", test.a, test.b))
|
|
||||||
// test the reverse
|
|
||||||
got = -test.b.cmp(test.a)
|
|
||||||
assert.Equal(t, test.want, got, fmt.Sprintf("%v cmp %v", test.b, test.a))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -208,3 +208,5 @@ Contributors
|
|||||||
* David Haguenauer <ml@kurokatta.org>
|
* David Haguenauer <ml@kurokatta.org>
|
||||||
* teresy <hi.teresy@gmail.com>
|
* teresy <hi.teresy@gmail.com>
|
||||||
* buergi <patbuergi@gmx.de>
|
* buergi <patbuergi@gmx.de>
|
||||||
|
* Florian Gamboeck <mail@floga.de>
|
||||||
|
* Ralf Hemberger <10364191+rhemberger@users.noreply.github.com>
|
||||||
|
|||||||
@@ -293,6 +293,10 @@ This reads a list of file names from the file passed in and **only**
|
|||||||
these files are transferred. The **filtering rules are ignored**
|
these files are transferred. The **filtering rules are ignored**
|
||||||
completely if you use this option.
|
completely if you use this option.
|
||||||
|
|
||||||
|
Rclone will not scan any directories if you use `--files-from` it will
|
||||||
|
just look at the files specified. Rclone will not error if any of the
|
||||||
|
files are missing from the source.
|
||||||
|
|
||||||
This option can be repeated to read from more than one file. These
|
This option can be repeated to read from more than one file. These
|
||||||
are read in the order that they are placed on the command line.
|
are read in the order that they are placed on the command line.
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,9 @@ d) Delete this remote
|
|||||||
y/e/d> y
|
y/e/d> y
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**NOTE:** The encryption keys need to have been already generated after a regular login
|
||||||
|
via the browser, otherwise attempting to use the credentials in `rclone` will fail.
|
||||||
|
|
||||||
Once configured you can then use `rclone` like this,
|
Once configured you can then use `rclone` like this,
|
||||||
|
|
||||||
List directories in top level of your Mega
|
List directories in top level of your Mega
|
||||||
@@ -152,11 +155,9 @@ permanently delete objects instead.
|
|||||||
|
|
||||||
### Limitations ###
|
### Limitations ###
|
||||||
|
|
||||||
This backend uses the [go-mega go
|
This backend uses the [go-mega go library](https://github.com/t3rm1n4l/go-mega) which is an opensource
|
||||||
library](https://github.com/t3rm1n4l/go-mega) which is an opensource
|
|
||||||
go library implementing the Mega API. There doesn't appear to be any
|
go library implementing the Mega API. There doesn't appear to be any
|
||||||
documentation for the mega protocol beyond the [mega C++
|
documentation for the mega protocol beyond the [mega C++ SDK](https://github.com/meganz/sdk) source code
|
||||||
SDK](https://github.com/meganz/sdk) source code so there are likely
|
so there are likely quite a few errors still remaining in this library.
|
||||||
quite a few errors still remaining in this library.
|
|
||||||
|
|
||||||
Mega allows duplicate files which may confuse rclone.
|
Mega allows duplicate files which may confuse rclone.
|
||||||
|
|||||||
@@ -9,46 +9,95 @@ date: "2018-03-05"
|
|||||||
If rclone is run with the `--rc` flag then it starts an http server
|
If rclone is run with the `--rc` flag then it starts an http server
|
||||||
which can be used to remote control rclone.
|
which can be used to remote control rclone.
|
||||||
|
|
||||||
|
If you just want to run a remote control then see the [rcd command](/commands/rclone_rcd/).
|
||||||
|
|
||||||
**NB** this is experimental and everything here is subject to change!
|
**NB** this is experimental and everything here is subject to change!
|
||||||
|
|
||||||
## Supported parameters
|
## Supported parameters
|
||||||
|
|
||||||
#### --rc ####
|
### --rc
|
||||||
|
|
||||||
Flag to start the http server listen on remote requests
|
Flag to start the http server listen on remote requests
|
||||||
|
|
||||||
#### --rc-addr=IP ####
|
### --rc-addr=IP
|
||||||
|
|
||||||
IPaddress:Port or :Port to bind server to. (default "localhost:5572")
|
IPaddress:Port or :Port to bind server to. (default "localhost:5572")
|
||||||
|
|
||||||
#### --rc-cert=KEY ####
|
### --rc-cert=KEY
|
||||||
SSL PEM key (concatenation of certificate and CA certificate)
|
SSL PEM key (concatenation of certificate and CA certificate)
|
||||||
|
|
||||||
#### --rc-client-ca=PATH ####
|
### --rc-client-ca=PATH
|
||||||
Client certificate authority to verify clients with
|
Client certificate authority to verify clients with
|
||||||
|
|
||||||
#### --rc-htpasswd=PATH ####
|
### --rc-htpasswd=PATH
|
||||||
|
|
||||||
htpasswd file - if not provided no authentication is done
|
htpasswd file - if not provided no authentication is done
|
||||||
|
|
||||||
#### --rc-key=PATH ####
|
### --rc-key=PATH
|
||||||
|
|
||||||
SSL PEM Private key
|
SSL PEM Private key
|
||||||
|
|
||||||
#### --rc-max-header-bytes=VALUE ####
|
### --rc-max-header-bytes=VALUE
|
||||||
|
|
||||||
Maximum size of request header (default 4096)
|
Maximum size of request header (default 4096)
|
||||||
|
|
||||||
#### --rc-user=VALUE ####
|
### --rc-user=VALUE
|
||||||
|
|
||||||
User name for authentication.
|
User name for authentication.
|
||||||
|
|
||||||
#### --rc-pass=VALUE ####
|
### --rc-pass=VALUE
|
||||||
|
|
||||||
Password for authentication.
|
Password for authentication.
|
||||||
|
|
||||||
#### --rc-realm=VALUE ####
|
### --rc-realm=VALUE
|
||||||
|
|
||||||
Realm for authentication (default "rclone")
|
Realm for authentication (default "rclone")
|
||||||
|
|
||||||
#### --rc-server-read-timeout=DURATION ####
|
### --rc-server-read-timeout=DURATION
|
||||||
|
|
||||||
Timeout for server reading data (default 1h0m0s)
|
Timeout for server reading data (default 1h0m0s)
|
||||||
|
|
||||||
#### --rc-server-write-timeout=DURATION ####
|
### --rc-server-write-timeout=DURATION
|
||||||
|
|
||||||
Timeout for server writing data (default 1h0m0s)
|
Timeout for server writing data (default 1h0m0s)
|
||||||
|
|
||||||
|
### --rc-serve
|
||||||
|
|
||||||
|
Enable the serving of remote objects via the HTTP interface. This
|
||||||
|
means objects will be accessible at http://127.0.0.1:5572/ by default,
|
||||||
|
so you can browse to http://127.0.0.1:5572/ or http://127.0.0.1:5572/*
|
||||||
|
to see a listing of the remotes. Objects may be requested from
|
||||||
|
remotes using this syntax http://127.0.0.1:5572/[remote:path]/path/to/object
|
||||||
|
|
||||||
|
Default Off.
|
||||||
|
|
||||||
|
### --rc-files /path/to/directory
|
||||||
|
|
||||||
|
Path to local files to serve on the HTTP server.
|
||||||
|
|
||||||
|
If this is set then rclone will serve the files in that directory. It
|
||||||
|
will also open the root in the web browser if specified. This is for
|
||||||
|
implementing browser based GUIs for rclone functions.
|
||||||
|
|
||||||
|
If `--rc-user` or `--rc-pass` is set then the URL that is opened will
|
||||||
|
have the authorization in the URL in the `http://user:pass@localhost/`
|
||||||
|
style.
|
||||||
|
|
||||||
|
Default Off.
|
||||||
|
|
||||||
|
### --rc-no-auth
|
||||||
|
|
||||||
|
By default rclone will require authorisation to have been set up on
|
||||||
|
the rc interface in order to use any methods which access any rclone
|
||||||
|
remotes. Eg `operations/list` is denied as it involved creating a
|
||||||
|
remote as is `sync/copy`.
|
||||||
|
|
||||||
|
If this is set then no authorisation will be required on the server to
|
||||||
|
use these methods. The alternative is to use `--rc-user` and
|
||||||
|
`--rc-pass` and use these credentials in the request.
|
||||||
|
|
||||||
|
Default Off.
|
||||||
|
|
||||||
## Accessing the remote control via the rclone rc command
|
## Accessing the remote control via the rclone rc command
|
||||||
|
|
||||||
Rclone itself implements the remote control protocol in its `rclone
|
Rclone itself implements the remote control protocol in its `rclone
|
||||||
@@ -67,6 +116,92 @@ $ rclone rc rc/noop param1=one param2=two
|
|||||||
Run `rclone rc` on its own to see the help for the installed remote
|
Run `rclone rc` on its own to see the help for the installed remote
|
||||||
control commands.
|
control commands.
|
||||||
|
|
||||||
|
`rclone rc` also supports a `--json` flag which can be used to send
|
||||||
|
more complicated input parameters.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ rclone rc --json '{ "p1": [1,"2",null,4], "p2": { "a":1, "b":2 } }' rc/noop
|
||||||
|
{
|
||||||
|
"p1": [
|
||||||
|
1,
|
||||||
|
"2",
|
||||||
|
null,
|
||||||
|
4
|
||||||
|
],
|
||||||
|
"p2": {
|
||||||
|
"a": 1,
|
||||||
|
"b": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Special parameters
|
||||||
|
|
||||||
|
The rc interface supports some special parameters which apply to
|
||||||
|
**all** commands. These start with `_` to show they are different.
|
||||||
|
|
||||||
|
### Running asynchronous jobs with _async = true
|
||||||
|
|
||||||
|
If `_async` has a true value when supplied to an rc call then it will
|
||||||
|
return immediately with a job id and the task will be run in the
|
||||||
|
background. The `job/status` call can be used to get information of
|
||||||
|
the background job. The job can be queried for up to 1 minute after
|
||||||
|
it has finished.
|
||||||
|
|
||||||
|
It is recommended that potentially long running jobs, eg `sync/sync`,
|
||||||
|
`sync/copy`, `sync/move`, `operations/purge` are run with the `_async`
|
||||||
|
flag to avoid any potential problems with the HTTP request and
|
||||||
|
response timing out.
|
||||||
|
|
||||||
|
Starting a job with the `_async` flag:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ rclone rc --json '{ "p1": [1,"2",null,4], "p2": { "a":1, "b":2 }, "_async": true }' rc/noop
|
||||||
|
{
|
||||||
|
"jobid": 2
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Query the status to see if the job has finished. For more information
|
||||||
|
on the meaning of these return parameters see the `job/status` call.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ rclone rc --json '{ "jobid":2 }' job/status
|
||||||
|
{
|
||||||
|
"duration": 0.000124163,
|
||||||
|
"endTime": "2018-10-27T11:38:07.911245881+01:00",
|
||||||
|
"error": "",
|
||||||
|
"finished": true,
|
||||||
|
"id": 2,
|
||||||
|
"output": {
|
||||||
|
"_async": true,
|
||||||
|
"p1": [
|
||||||
|
1,
|
||||||
|
"2",
|
||||||
|
null,
|
||||||
|
4
|
||||||
|
],
|
||||||
|
"p2": {
|
||||||
|
"a": 1,
|
||||||
|
"b": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"startTime": "2018-10-27T11:38:07.911121728+01:00",
|
||||||
|
"success": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`job/list` can be used to show the running or recently completed jobs
|
||||||
|
|
||||||
|
```
|
||||||
|
$ rclone rc job/list
|
||||||
|
{
|
||||||
|
"jobids": [
|
||||||
|
2
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Supported commands
|
## Supported commands
|
||||||
<!--- autogenerated start - run make rcdocs - don't edit here -->
|
<!--- autogenerated start - run make rcdocs - don't edit here -->
|
||||||
### cache/expire: Purge a remote from cache
|
### cache/expire: Purge a remote from cache
|
||||||
@@ -112,6 +247,90 @@ is used on top of the cache.
|
|||||||
|
|
||||||
Show statistics for the cache remote.
|
Show statistics for the cache remote.
|
||||||
|
|
||||||
|
### config/create: create the config for a remote.
|
||||||
|
|
||||||
|
This takes the following parameters
|
||||||
|
|
||||||
|
- name - name of remote
|
||||||
|
- type - type of new remote
|
||||||
|
- type - type of the new remote
|
||||||
|
|
||||||
|
|
||||||
|
See the [config create command](/commands/rclone_config_create/) command for more information on the above.
|
||||||
|
|
||||||
|
Authentication is required for this call.
|
||||||
|
|
||||||
|
### config/delete: Delete a remote in the config file.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- name - name of remote to delete
|
||||||
|
|
||||||
|
See the [config delete command](/commands/rclone_config_delete/) command for more information on the above.
|
||||||
|
|
||||||
|
Authentication is required for this call.
|
||||||
|
|
||||||
|
### config/dump: Dumps the config file.
|
||||||
|
|
||||||
|
Returns a JSON object:
|
||||||
|
- key: value
|
||||||
|
|
||||||
|
Where keys are remote names and values are the config parameters.
|
||||||
|
|
||||||
|
See the [config dump command](/commands/rclone_config_dump/) command for more information on the above.
|
||||||
|
|
||||||
|
Authentication is required for this call.
|
||||||
|
|
||||||
|
### config/get: Get a remote in the config file.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- name - name of remote to get
|
||||||
|
|
||||||
|
See the [config dump command](/commands/rclone_config_dump/) command for more information on the above.
|
||||||
|
|
||||||
|
Authentication is required for this call.
|
||||||
|
|
||||||
|
### config/listremotes: Lists the remotes in the config file.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
- remotes - array of remote names
|
||||||
|
|
||||||
|
See the [listremotes command](/commands/rclone_listremotes/) command for more information on the above.
|
||||||
|
|
||||||
|
Authentication is required for this call.
|
||||||
|
|
||||||
|
### config/password: password the config for a remote.
|
||||||
|
|
||||||
|
This takes the following parameters
|
||||||
|
|
||||||
|
- name - name of remote
|
||||||
|
- type - type of new remote
|
||||||
|
|
||||||
|
|
||||||
|
See the [config password command](/commands/rclone_config_password/) command for more information on the above.
|
||||||
|
|
||||||
|
Authentication is required for this call.
|
||||||
|
|
||||||
|
### config/providers: Shows how providers are configured in the config file.
|
||||||
|
|
||||||
|
Returns a JSON object:
|
||||||
|
- providers - array of objects
|
||||||
|
|
||||||
|
See the [config providers command](/commands/rclone_config_providers/) command for more information on the above.
|
||||||
|
|
||||||
|
Authentication is required for this call.
|
||||||
|
|
||||||
|
### config/update: update the config for a remote.
|
||||||
|
|
||||||
|
This takes the following parameters
|
||||||
|
|
||||||
|
- name - name of remote
|
||||||
|
- type - type of new remote
|
||||||
|
|
||||||
|
|
||||||
|
See the [config update command](/commands/rclone_config_update/) command for more information on the above.
|
||||||
|
|
||||||
|
Authentication is required for this call.
|
||||||
|
|
||||||
### core/bwlimit: Set the bandwidth limit.
|
### core/bwlimit: Set the bandwidth limit.
|
||||||
|
|
||||||
This sets the bandwidth limit to that passed in.
|
This sets the bandwidth limit to that passed in.
|
||||||
@@ -142,6 +361,14 @@ The most interesting values for most people are:
|
|||||||
* Sys: this is the total amount of memory requested from the OS
|
* Sys: this is the total amount of memory requested from the OS
|
||||||
* It is virtual memory so may include unused memory
|
* It is virtual memory so may include unused memory
|
||||||
|
|
||||||
|
### core/obscure: Obscures a string passed in.
|
||||||
|
|
||||||
|
Pass a clear string and rclone will obscure it for the config file:
|
||||||
|
- clear - string
|
||||||
|
|
||||||
|
Returns
|
||||||
|
- obscured - string
|
||||||
|
|
||||||
### core/pid: Return PID of current process
|
### core/pid: Return PID of current process
|
||||||
|
|
||||||
This returns PID of current process.
|
This returns PID of current process.
|
||||||
@@ -186,6 +413,230 @@ Returns the following values:
|
|||||||
Values for "transferring", "checking" and "lastError" are only assigned if data is available.
|
Values for "transferring", "checking" and "lastError" are only assigned if data is available.
|
||||||
The value for "eta" is null if an eta cannot be determined.
|
The value for "eta" is null if an eta cannot be determined.
|
||||||
|
|
||||||
|
### core/version: Shows the current version of rclone and the go runtime.
|
||||||
|
|
||||||
|
This shows the current version of go and the go runtime
|
||||||
|
- version - rclone version, eg "v1.44"
|
||||||
|
- decomposed - version number as [major, minor, patch, subpatch]
|
||||||
|
- note patch and subpatch will be 999 for a git compiled version
|
||||||
|
- isGit - boolean - true if this was compiled from the git version
|
||||||
|
- os - OS in use as according to Go
|
||||||
|
- arch - cpu architecture in use according to Go
|
||||||
|
- goVersion - version of Go runtime in use
|
||||||
|
|
||||||
|
### job/list: Lists the IDs of the running jobs
|
||||||
|
|
||||||
|
Parameters - None
|
||||||
|
|
||||||
|
Results
|
||||||
|
- jobids - array of integer job ids
|
||||||
|
|
||||||
|
### job/status: Reads the status of the job ID
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
- jobid - id of the job (integer)
|
||||||
|
|
||||||
|
Results
|
||||||
|
- finished - boolean
|
||||||
|
- duration - time in seconds that the job ran for
|
||||||
|
- endTime - time the job finished (eg "2018-10-26T18:50:20.528746884+01:00")
|
||||||
|
- error - error from the job or empty string for no error
|
||||||
|
- finished - boolean whether the job has finished or not
|
||||||
|
- id - as passed in above
|
||||||
|
- startTime - time the job started (eg "2018-10-26T18:50:20.528336039+01:00")
|
||||||
|
- success - boolean - true for success false otherwise
|
||||||
|
- output - output of the job as would have been returned if called synchronously
|
||||||
|
|
||||||
|
### operations/about: Return the space used on the remote
|
||||||
|
|
||||||
|
This takes the following parameters
|
||||||
|
|
||||||
|
- fs - a remote name string eg "drive:"
|
||||||
|
- remote - a path within that remote eg "dir"
|
||||||
|
|
||||||
|
The result is as returned from rclone about --json
|
||||||
|
|
||||||
|
Authentication is required for this call.
|
||||||
|
|
||||||
|
### operations/cleanup: Remove trashed files in the remote or path
|
||||||
|
|
||||||
|
This takes the following parameters
|
||||||
|
|
||||||
|
- fs - a remote name string eg "drive:"
|
||||||
|
|
||||||
|
See the [cleanup command](/commands/rclone_cleanup/) command for more information on the above.
|
||||||
|
|
||||||
|
Authentication is required for this call.
|
||||||
|
|
||||||
|
### operations/copyfile: Copy a file from source remote to destination remote
|
||||||
|
|
||||||
|
This takes the following parameters
|
||||||
|
|
||||||
|
- srcFs - a remote name string eg "drive:" for the source
|
||||||
|
- srcRemote - a path within that remote eg "file.txt" for the source
|
||||||
|
- dstFs - a remote name string eg "drive2:" for the destination
|
||||||
|
- dstRemote - a path within that remote eg "file2.txt" for the destination
|
||||||
|
|
||||||
|
This returns
|
||||||
|
- jobid - ID of async job to query with job/status
|
||||||
|
|
||||||
|
Authentication is required for this call.
|
||||||
|
|
||||||
|
### operations/copyurl: Copy the URL to the object
|
||||||
|
|
||||||
|
This takes the following parameters
|
||||||
|
|
||||||
|
- fs - a remote name string eg "drive:"
|
||||||
|
- remote - a path within that remote eg "dir"
|
||||||
|
- url - string, URL to read from
|
||||||
|
|
||||||
|
See the [copyurl command](/commands/rclone_copyurl/) command for more information on the above.
|
||||||
|
|
||||||
|
Authentication is required for this call.
|
||||||
|
|
||||||
|
### operations/delete: Remove files in the path
|
||||||
|
|
||||||
|
This takes the following parameters
|
||||||
|
|
||||||
|
- fs - a remote name string eg "drive:"
|
||||||
|
|
||||||
|
See the [delete command](/commands/rclone_delete/) command for more information on the above.
|
||||||
|
|
||||||
|
Authentication is required for this call.
|
||||||
|
|
||||||
|
### operations/deletefile: Remove the single file pointed to
|
||||||
|
|
||||||
|
This takes the following parameters
|
||||||
|
|
||||||
|
- fs - a remote name string eg "drive:"
|
||||||
|
- remote - a path within that remote eg "dir"
|
||||||
|
|
||||||
|
See the [deletefile command](/commands/rclone_deletefile/) command for more information on the above.
|
||||||
|
|
||||||
|
Authentication is required for this call.
|
||||||
|
|
||||||
|
### operations/list: List the given remote and path in JSON format
|
||||||
|
|
||||||
|
This takes the following parameters
|
||||||
|
|
||||||
|
- fs - a remote name string eg "drive:"
|
||||||
|
- remote - a path within that remote eg "dir"
|
||||||
|
- opt - a dictionary of options to control the listing (optional)
|
||||||
|
- recurse - If set recurse directories
|
||||||
|
- noModTime - If set return modification time
|
||||||
|
- showEncrypted - If set show decrypted names
|
||||||
|
- showOrigIDs - If set show the IDs for each item if known
|
||||||
|
- showHash - If set return a dictionary of hashes
|
||||||
|
|
||||||
|
The result is
|
||||||
|
|
||||||
|
- list
|
||||||
|
- This is an array of objects as described in the lsjson command
|
||||||
|
|
||||||
|
See the lsjson command for more information on the above and examples.
|
||||||
|
|
||||||
|
Authentication is required for this call.
|
||||||
|
|
||||||
|
### operations/mkdir: Make a destination directory or container
|
||||||
|
|
||||||
|
This takes the following parameters
|
||||||
|
|
||||||
|
- fs - a remote name string eg "drive:"
|
||||||
|
- remote - a path within that remote eg "dir"
|
||||||
|
|
||||||
|
See the [mkdir command](/commands/rclone_mkdir/) command for more information on the above.
|
||||||
|
|
||||||
|
Authentication is required for this call.
|
||||||
|
|
||||||
|
### operations/movefile: Move a file from source remote to destination remote
|
||||||
|
|
||||||
|
This takes the following parameters
|
||||||
|
|
||||||
|
- srcFs - a remote name string eg "drive:" for the source
|
||||||
|
- srcRemote - a path within that remote eg "file.txt" for the source
|
||||||
|
- dstFs - a remote name string eg "drive2:" for the destination
|
||||||
|
- dstRemote - a path within that remote eg "file2.txt" for the destination
|
||||||
|
|
||||||
|
This returns
|
||||||
|
- jobid - ID of async job to query with job/status
|
||||||
|
|
||||||
|
Authentication is required for this call.
|
||||||
|
|
||||||
|
### operations/purge: Remove a directory or container and all of its contents
|
||||||
|
|
||||||
|
This takes the following parameters
|
||||||
|
|
||||||
|
- fs - a remote name string eg "drive:"
|
||||||
|
- remote - a path within that remote eg "dir"
|
||||||
|
|
||||||
|
See the [purge command](/commands/rclone_purge/) command for more information on the above.
|
||||||
|
|
||||||
|
Authentication is required for this call.
|
||||||
|
|
||||||
|
### operations/rmdir: Remove an empty directory or container
|
||||||
|
|
||||||
|
This takes the following parameters
|
||||||
|
|
||||||
|
- fs - a remote name string eg "drive:"
|
||||||
|
- remote - a path within that remote eg "dir"
|
||||||
|
|
||||||
|
See the [rmdir command](/commands/rclone_rmdir/) command for more information on the above.
|
||||||
|
|
||||||
|
Authentication is required for this call.
|
||||||
|
|
||||||
|
### operations/rmdirs: Remove all the empty directories in the path
|
||||||
|
|
||||||
|
This takes the following parameters
|
||||||
|
|
||||||
|
- fs - a remote name string eg "drive:"
|
||||||
|
- remote - a path within that remote eg "dir"
|
||||||
|
- leaveRoot - boolean, set to true not to delete the root
|
||||||
|
|
||||||
|
See the [rmdirs command](/commands/rclone_rmdirs/) command for more information on the above.
|
||||||
|
|
||||||
|
Authentication is required for this call.
|
||||||
|
|
||||||
|
### operations/size: Count the number of bytes and files in remote
|
||||||
|
|
||||||
|
This takes the following parameters
|
||||||
|
|
||||||
|
- fs - a remote name string eg "drive:path/to/dir"
|
||||||
|
|
||||||
|
Returns
|
||||||
|
|
||||||
|
- count - number of files
|
||||||
|
- bytes - number of bytes in those files
|
||||||
|
|
||||||
|
See the [size command](/commands/rclone_size/) command for more information on the above.
|
||||||
|
|
||||||
|
Authentication is required for this call.
|
||||||
|
|
||||||
|
### options/blocks: List all the option blocks
|
||||||
|
|
||||||
|
Returns
|
||||||
|
- options - a list of the options block names
|
||||||
|
|
||||||
|
### options/get: Get all the options
|
||||||
|
|
||||||
|
Returns an object where keys are option block names and values are an
|
||||||
|
object with the current option values in.
|
||||||
|
|
||||||
|
This shows the internal names of the option within rclone which should
|
||||||
|
map to the external options very easily with a few exceptions.
|
||||||
|
|
||||||
|
### options/set: Set an option
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
|
||||||
|
- option block name containing an object with
|
||||||
|
- key: value
|
||||||
|
|
||||||
|
Repeated as often as required.
|
||||||
|
|
||||||
|
Only supply the options you wish to change. If an option is unknown
|
||||||
|
it will be silently ignored. Not all options will have an effect when
|
||||||
|
changed like this.
|
||||||
|
|
||||||
### rc/error: This returns an error
|
### rc/error: This returns an error
|
||||||
|
|
||||||
This returns an error with the input as part of its error string.
|
This returns an error with the input as part of its error string.
|
||||||
@@ -202,6 +653,57 @@ This echoes the input parameters to the output parameters for testing
|
|||||||
purposes. It can be used to check that rclone is still alive and to
|
purposes. It can be used to check that rclone is still alive and to
|
||||||
check that parameter passing is working properly.
|
check that parameter passing is working properly.
|
||||||
|
|
||||||
|
### rc/noopauth: Echo the input to the output parameters requiring auth
|
||||||
|
|
||||||
|
This echoes the input parameters to the output parameters for testing
|
||||||
|
purposes. It can be used to check that rclone is still alive and to
|
||||||
|
check that parameter passing is working properly.
|
||||||
|
|
||||||
|
Authentication is required for this call.
|
||||||
|
|
||||||
|
### sync/copy: copy a directory from source remote to destination remote
|
||||||
|
|
||||||
|
This takes the following parameters
|
||||||
|
|
||||||
|
- srcFs - a remote name string eg "drive:src" for the source
|
||||||
|
- dstFs - a remote name string eg "drive:dst" for the destination
|
||||||
|
|
||||||
|
This returns
|
||||||
|
- jobid - ID of async job to query with job/status
|
||||||
|
|
||||||
|
See the [copy command](/commands/rclone_copy/) command for more information on the above.
|
||||||
|
|
||||||
|
Authentication is required for this call.
|
||||||
|
|
||||||
|
### sync/move: move a directory from source remote to destination remote
|
||||||
|
|
||||||
|
This takes the following parameters
|
||||||
|
|
||||||
|
- srcFs - a remote name string eg "drive:src" for the source
|
||||||
|
- dstFs - a remote name string eg "drive:dst" for the destination
|
||||||
|
- deleteEmptySrcDirs - delete empty src directories if set
|
||||||
|
|
||||||
|
This returns
|
||||||
|
- jobid - ID of async job to query with job/status
|
||||||
|
|
||||||
|
See the [move command](/commands/rclone_move/) command for more information on the above.
|
||||||
|
|
||||||
|
Authentication is required for this call.
|
||||||
|
|
||||||
|
### sync/sync: sync a directory from source remote to destination remote
|
||||||
|
|
||||||
|
This takes the following parameters
|
||||||
|
|
||||||
|
- srcFs - a remote name string eg "drive:src" for the source
|
||||||
|
- dstFs - a remote name string eg "drive:dst" for the destination
|
||||||
|
|
||||||
|
This returns
|
||||||
|
- jobid - ID of async job to query with job/status
|
||||||
|
|
||||||
|
See the [sync command](/commands/rclone_sync/) command for more information on the above.
|
||||||
|
|
||||||
|
Authentication is required for this call.
|
||||||
|
|
||||||
### vfs/forget: Forget files or directories in the directory cache.
|
### vfs/forget: Forget files or directories in the directory cache.
|
||||||
|
|
||||||
This forgets the paths in the directory cache causing them to be
|
This forgets the paths in the directory cache causing them to be
|
||||||
@@ -276,9 +778,31 @@ blob in the body. There are examples of these below using `curl`.
|
|||||||
The response will be a JSON blob in the body of the response. This is
|
The response will be a JSON blob in the body of the response. This is
|
||||||
formatted to be reasonably human readable.
|
formatted to be reasonably human readable.
|
||||||
|
|
||||||
If an error occurs then there will be an HTTP error status (usually
|
### Error returns
|
||||||
400) and the body of the response will contain a JSON encoded error
|
|
||||||
object.
|
If an error occurs then there will be an HTTP error status (eg 500)
|
||||||
|
and the body of the response will contain a JSON encoded error object,
|
||||||
|
eg
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"error": "Expecting string value for key \"remote\" (was float64)",
|
||||||
|
"input": {
|
||||||
|
"fs": "/tmp",
|
||||||
|
"remote": 3
|
||||||
|
},
|
||||||
|
"status": 400
|
||||||
|
"path": "operations/rmdir",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The keys in the error response are
|
||||||
|
- error - error string
|
||||||
|
- input - the input parameters to the call
|
||||||
|
- status - the HTTP status code
|
||||||
|
- path - the path of the call
|
||||||
|
|
||||||
|
### CORS
|
||||||
|
|
||||||
The sever implements basic CORS support and allows all origins for that.
|
The sever implements basic CORS support and allows all origins for that.
|
||||||
The response to a preflight OPTIONS request will echo the requested "Access-Control-Request-Headers" back.
|
The response to a preflight OPTIONS request will echo the requested "Access-Control-Request-Headers" back.
|
||||||
@@ -286,7 +810,7 @@ The response to a preflight OPTIONS request will echo the requested "Access-Cont
|
|||||||
### Using POST with URL parameters only
|
### Using POST with URL parameters only
|
||||||
|
|
||||||
```
|
```
|
||||||
curl -X POST 'http://localhost:5572/rc/noop/?potato=1&sausage=2'
|
curl -X POST 'http://localhost:5572/rc/noop?potato=1&sausage=2'
|
||||||
```
|
```
|
||||||
|
|
||||||
Response
|
Response
|
||||||
@@ -301,7 +825,7 @@ Response
|
|||||||
Here is what an error response looks like:
|
Here is what an error response looks like:
|
||||||
|
|
||||||
```
|
```
|
||||||
curl -X POST 'http://localhost:5572/rc/error/?potato=1&sausage=2'
|
curl -X POST 'http://localhost:5572/rc/error?potato=1&sausage=2'
|
||||||
```
|
```
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -317,7 +841,7 @@ curl -X POST 'http://localhost:5572/rc/error/?potato=1&sausage=2'
|
|||||||
Note that curl doesn't return errors to the shell unless you use the `-f` option
|
Note that curl doesn't return errors to the shell unless you use the `-f` option
|
||||||
|
|
||||||
```
|
```
|
||||||
$ curl -f -X POST 'http://localhost:5572/rc/error/?potato=1&sausage=2'
|
$ curl -f -X POST 'http://localhost:5572/rc/error?potato=1&sausage=2'
|
||||||
curl: (22) The requested URL returned error: 400 Bad Request
|
curl: (22) The requested URL returned error: 400 Bad Request
|
||||||
$ echo $?
|
$ echo $?
|
||||||
22
|
22
|
||||||
@@ -326,7 +850,7 @@ $ echo $?
|
|||||||
### Using POST with a form
|
### Using POST with a form
|
||||||
|
|
||||||
```
|
```
|
||||||
curl --data "potato=1" --data "sausage=2" http://localhost:5572/rc/noop/
|
curl --data "potato=1" --data "sausage=2" http://localhost:5572/rc/noop
|
||||||
```
|
```
|
||||||
|
|
||||||
Response
|
Response
|
||||||
@@ -342,7 +866,7 @@ Note that you can combine these with URL parameters too with the POST
|
|||||||
parameters taking precedence.
|
parameters taking precedence.
|
||||||
|
|
||||||
```
|
```
|
||||||
curl --data "potato=1" --data "sausage=2" "http://localhost:5572/rc/noop/?rutabaga=3&sausage=4"
|
curl --data "potato=1" --data "sausage=2" "http://localhost:5572/rc/noop?rutabaga=3&sausage=4"
|
||||||
```
|
```
|
||||||
|
|
||||||
Response
|
Response
|
||||||
@@ -359,7 +883,7 @@ Response
|
|||||||
### Using POST with a JSON blob
|
### Using POST with a JSON blob
|
||||||
|
|
||||||
```
|
```
|
||||||
curl -H "Content-Type: application/json" -X POST -d '{"potato":2,"sausage":1}' http://localhost:5572/rc/noop/
|
curl -H "Content-Type: application/json" -X POST -d '{"potato":2,"sausage":1}' http://localhost:5572/rc/noop
|
||||||
```
|
```
|
||||||
|
|
||||||
response
|
response
|
||||||
@@ -375,7 +899,7 @@ This can be combined with URL parameters too if required. The JSON
|
|||||||
blob takes precedence.
|
blob takes precedence.
|
||||||
|
|
||||||
```
|
```
|
||||||
curl -H "Content-Type: application/json" -X POST -d '{"potato":2,"sausage":1}' 'http://localhost:5572/rc/noop/?rutabaga=3&potato=4'
|
curl -H "Content-Type: application/json" -X POST -d '{"potato":2,"sausage":1}' 'http://localhost:5572/rc/noop?rutabaga=3&potato=4'
|
||||||
```
|
```
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import (
|
|||||||
"github.com/ncw/rclone/fs/driveletter"
|
"github.com/ncw/rclone/fs/driveletter"
|
||||||
"github.com/ncw/rclone/fs/fshttp"
|
"github.com/ncw/rclone/fs/fshttp"
|
||||||
"github.com/ncw/rclone/fs/fspath"
|
"github.com/ncw/rclone/fs/fspath"
|
||||||
|
"github.com/ncw/rclone/fs/rc"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"golang.org/x/crypto/nacl/secretbox"
|
"golang.org/x/crypto/nacl/secretbox"
|
||||||
"golang.org/x/text/unicode/norm"
|
"golang.org/x/text/unicode/norm"
|
||||||
@@ -445,6 +446,10 @@ func changeConfigPassword() {
|
|||||||
// if configKey has been set, the file will be encrypted.
|
// if configKey has been set, the file will be encrypted.
|
||||||
func saveConfig() error {
|
func saveConfig() error {
|
||||||
dir, name := filepath.Split(ConfigPath)
|
dir, name := filepath.Split(ConfigPath)
|
||||||
|
err := os.MkdirAll(dir, os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to create config directory")
|
||||||
|
}
|
||||||
f, err := ioutil.TempFile(dir, name)
|
f, err := ioutil.TempFile(dir, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Errorf("Failed to create temp file for new config: %v", err)
|
return errors.Errorf("Failed to create temp file for new config: %v", err)
|
||||||
@@ -897,18 +902,24 @@ func ChooseOption(o *fs.Option, name string) string {
|
|||||||
return in
|
return in
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Suppress the confirm prompts and return a function to undo that
|
||||||
|
func suppressConfirm() func() {
|
||||||
|
old := fs.Config.AutoConfirm
|
||||||
|
fs.Config.AutoConfirm = true
|
||||||
|
return func() {
|
||||||
|
fs.Config.AutoConfirm = old
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateRemote adds the keyValues passed in to the remote of name.
|
// UpdateRemote adds the keyValues passed in to the remote of name.
|
||||||
// keyValues should be key, value pairs.
|
// keyValues should be key, value pairs.
|
||||||
func UpdateRemote(name string, keyValues []string) error {
|
func UpdateRemote(name string, keyValues rc.Params) error {
|
||||||
if len(keyValues)%2 != 0 {
|
defer suppressConfirm()()
|
||||||
return errors.New("found key without value")
|
|
||||||
}
|
|
||||||
// Set the config
|
// Set the config
|
||||||
for i := 0; i < len(keyValues); i += 2 {
|
for k, v := range keyValues {
|
||||||
getConfigData().SetValue(name, keyValues[i], keyValues[i+1])
|
getConfigData().SetValue(name, k, fmt.Sprint(v))
|
||||||
}
|
}
|
||||||
RemoteConfig(name)
|
RemoteConfig(name)
|
||||||
ShowRemote(name)
|
|
||||||
SaveConfig()
|
SaveConfig()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -916,9 +927,7 @@ func UpdateRemote(name string, keyValues []string) error {
|
|||||||
// CreateRemote creates a new remote with name, provider and a list of
|
// CreateRemote creates a new remote with name, provider and a list of
|
||||||
// parameters which are key, value pairs. If update is set then it
|
// parameters which are key, value pairs. If update is set then it
|
||||||
// adds the new keys rather than replacing all of them.
|
// adds the new keys rather than replacing all of them.
|
||||||
func CreateRemote(name string, provider string, keyValues []string) error {
|
func CreateRemote(name string, provider string, keyValues rc.Params) error {
|
||||||
// Suppress Confirm
|
|
||||||
fs.Config.AutoConfirm = true
|
|
||||||
// Delete the old config if it exists
|
// Delete the old config if it exists
|
||||||
getConfigData().DeleteSection(name)
|
getConfigData().DeleteSection(name)
|
||||||
// Set the type
|
// Set the type
|
||||||
@@ -931,20 +940,12 @@ func CreateRemote(name string, provider string, keyValues []string) error {
|
|||||||
|
|
||||||
// PasswordRemote adds the keyValues passed in to the remote of name.
|
// PasswordRemote adds the keyValues passed in to the remote of name.
|
||||||
// keyValues should be key, value pairs.
|
// keyValues should be key, value pairs.
|
||||||
func PasswordRemote(name string, keyValues []string) error {
|
func PasswordRemote(name string, keyValues rc.Params) error {
|
||||||
if len(keyValues) != 2 {
|
defer suppressConfirm()()
|
||||||
return errors.New("found key without value")
|
for k, v := range keyValues {
|
||||||
|
keyValues[k] = obscure.MustObscure(fmt.Sprint(v))
|
||||||
}
|
}
|
||||||
// Suppress Confirm
|
return UpdateRemote(name, keyValues)
|
||||||
fs.Config.AutoConfirm = true
|
|
||||||
passwd := obscure.MustObscure(keyValues[1])
|
|
||||||
if passwd != "" {
|
|
||||||
getConfigData().SetValue(name, keyValues[0], passwd)
|
|
||||||
RemoteConfig(name)
|
|
||||||
ShowRemote(name)
|
|
||||||
SaveConfig()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// JSONListProviders prints all the providers and options in JSON format
|
// JSONListProviders prints all the providers and options in JSON format
|
||||||
@@ -1293,16 +1294,28 @@ func FileSections() []string {
|
|||||||
return sections
|
return sections
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DumpRcRemote dumps the config for a single remote
|
||||||
|
func DumpRcRemote(name string) (dump rc.Params) {
|
||||||
|
params := rc.Params{}
|
||||||
|
for _, key := range getConfigData().GetKeyList(name) {
|
||||||
|
params[key] = FileGet(name, key)
|
||||||
|
}
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
// DumpRcBlob dumps all the config as an unstructured blob suitable
|
||||||
|
// for the rc
|
||||||
|
func DumpRcBlob() (dump rc.Params) {
|
||||||
|
dump = rc.Params{}
|
||||||
|
for _, name := range getConfigData().GetSectionList() {
|
||||||
|
dump[name] = DumpRcRemote(name)
|
||||||
|
}
|
||||||
|
return dump
|
||||||
|
}
|
||||||
|
|
||||||
// Dump dumps all the config as a JSON file
|
// Dump dumps all the config as a JSON file
|
||||||
func Dump() error {
|
func Dump() error {
|
||||||
dump := make(map[string]map[string]string)
|
dump := DumpRcBlob()
|
||||||
for _, name := range getConfigData().GetSectionList() {
|
|
||||||
params := make(map[string]string)
|
|
||||||
for _, key := range getConfigData().GetKeyList(name) {
|
|
||||||
params[key] = FileGet(name, key)
|
|
||||||
}
|
|
||||||
dump[name] = params
|
|
||||||
}
|
|
||||||
b, err := json.MarshalIndent(dump, "", " ")
|
b, err := json.MarshalIndent(dump, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to marshal config dump")
|
return errors.Wrap(err, "failed to marshal config dump")
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/ncw/rclone/fs"
|
"github.com/ncw/rclone/fs"
|
||||||
"github.com/ncw/rclone/fs/config"
|
"github.com/ncw/rclone/fs/config"
|
||||||
"github.com/ncw/rclone/fs/config/flags"
|
"github.com/ncw/rclone/fs/config/flags"
|
||||||
|
"github.com/ncw/rclone/fs/rc"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -31,6 +32,7 @@ var (
|
|||||||
|
|
||||||
// AddFlags adds the non filing system specific flags to the command
|
// AddFlags adds the non filing system specific flags to the command
|
||||||
func AddFlags(flagSet *pflag.FlagSet) {
|
func AddFlags(flagSet *pflag.FlagSet) {
|
||||||
|
rc.AddOption("main", fs.Config)
|
||||||
// NB defaults which aren't the zero for the type should be set in fs/config.go NewConfig
|
// NB defaults which aren't the zero for the type should be set in fs/config.go NewConfig
|
||||||
flags.CountVarP(flagSet, &verbose, "verbose", "v", "Print lots more stuff (repeat for more)")
|
flags.CountVarP(flagSet, &verbose, "verbose", "v", "Print lots more stuff (repeat for more)")
|
||||||
flags.BoolVarP(flagSet, &quiet, "quiet", "q", false, "Print as little stuff as possible")
|
flags.BoolVarP(flagSet, &quiet, "quiet", "q", false, "Print as little stuff as possible")
|
||||||
|
|||||||
178
fs/config/rc.go
Normal file
178
fs/config/rc.go
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ncw/rclone/fs"
|
||||||
|
"github.com/ncw/rclone/fs/rc"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rc.Add(rc.Call{
|
||||||
|
Path: "config/dump",
|
||||||
|
Fn: rcDump,
|
||||||
|
Title: "Dumps the config file.",
|
||||||
|
AuthRequired: true,
|
||||||
|
Help: `
|
||||||
|
Returns a JSON object:
|
||||||
|
- key: value
|
||||||
|
|
||||||
|
Where keys are remote names and values are the config parameters.
|
||||||
|
|
||||||
|
See the [config dump command](/commands/rclone_config_dump/) command for more information on the above.
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the config file dump
|
||||||
|
func rcDump(in rc.Params) (out rc.Params, err error) {
|
||||||
|
return DumpRcBlob(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rc.Add(rc.Call{
|
||||||
|
Path: "config/get",
|
||||||
|
Fn: rcGet,
|
||||||
|
Title: "Get a remote in the config file.",
|
||||||
|
AuthRequired: true,
|
||||||
|
Help: `
|
||||||
|
Parameters:
|
||||||
|
- name - name of remote to get
|
||||||
|
|
||||||
|
See the [config dump command](/commands/rclone_config_dump/) command for more information on the above.
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the config file get
|
||||||
|
func rcGet(in rc.Params) (out rc.Params, err error) {
|
||||||
|
name, err := in.GetString("name")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return DumpRcRemote(name), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rc.Add(rc.Call{
|
||||||
|
Path: "config/listremotes",
|
||||||
|
Fn: rcListRemotes,
|
||||||
|
Title: "Lists the remotes in the config file.",
|
||||||
|
AuthRequired: true,
|
||||||
|
Help: `
|
||||||
|
Returns
|
||||||
|
- remotes - array of remote names
|
||||||
|
|
||||||
|
See the [listremotes command](/commands/rclone_listremotes/) command for more information on the above.
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the a list of remotes in the config file
|
||||||
|
func rcListRemotes(in rc.Params) (out rc.Params, err error) {
|
||||||
|
var remotes = []string{}
|
||||||
|
for _, remote := range getConfigData().GetSectionList() {
|
||||||
|
remotes = append(remotes, remote)
|
||||||
|
}
|
||||||
|
out = rc.Params{
|
||||||
|
"remotes": remotes,
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rc.Add(rc.Call{
|
||||||
|
Path: "config/providers",
|
||||||
|
Fn: rcProviders,
|
||||||
|
Title: "Shows how providers are configured in the config file.",
|
||||||
|
AuthRequired: true,
|
||||||
|
Help: `
|
||||||
|
Returns a JSON object:
|
||||||
|
- providers - array of objects
|
||||||
|
|
||||||
|
See the [config providers command](/commands/rclone_config_providers/) command for more information on the above.
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the config file providers
|
||||||
|
func rcProviders(in rc.Params) (out rc.Params, err error) {
|
||||||
|
out = rc.Params{
|
||||||
|
"providers": fs.Registry,
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
for _, name := range []string{"create", "update", "password"} {
|
||||||
|
name := name
|
||||||
|
extraHelp := ""
|
||||||
|
if name == "create" {
|
||||||
|
extraHelp = "- type - type of the new remote\n"
|
||||||
|
}
|
||||||
|
rc.Add(rc.Call{
|
||||||
|
Path: "config/" + name,
|
||||||
|
AuthRequired: true,
|
||||||
|
Fn: func(in rc.Params) (rc.Params, error) {
|
||||||
|
return rcConfig(in, name)
|
||||||
|
},
|
||||||
|
Title: name + " the config for a remote.",
|
||||||
|
Help: `This takes the following parameters
|
||||||
|
|
||||||
|
- name - name of remote
|
||||||
|
- type - type of new remote
|
||||||
|
` + extraHelp + `
|
||||||
|
|
||||||
|
See the [config ` + name + ` command](/commands/rclone_config_` + name + `/) command for more information on the above.`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manipulate the config file
|
||||||
|
func rcConfig(in rc.Params, what string) (out rc.Params, err error) {
|
||||||
|
name, err := in.GetString("name")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
parameters := rc.Params{}
|
||||||
|
err = in.GetStruct("parameters", ¶meters)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
switch what {
|
||||||
|
case "create":
|
||||||
|
remoteType, err := in.GetString("type")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return nil, CreateRemote(name, remoteType, parameters)
|
||||||
|
case "update":
|
||||||
|
return nil, UpdateRemote(name, parameters)
|
||||||
|
case "password":
|
||||||
|
return nil, PasswordRemote(name, parameters)
|
||||||
|
}
|
||||||
|
panic("unknown rcConfig type")
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rc.Add(rc.Call{
|
||||||
|
Path: "config/delete",
|
||||||
|
Fn: rcDelete,
|
||||||
|
Title: "Delete a remote in the config file.",
|
||||||
|
AuthRequired: true,
|
||||||
|
Help: `
|
||||||
|
Parameters:
|
||||||
|
- name - name of remote to delete
|
||||||
|
|
||||||
|
See the [config delete command](/commands/rclone_config_delete/) command for more information on the above.
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the config file delete
|
||||||
|
func rcDelete(in rc.Params) (out rc.Params, err error) {
|
||||||
|
name, err := in.GetString("name")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
DeleteRemote(name)
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
149
fs/config/rc_test.go
Normal file
149
fs/config/rc_test.go
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
_ "github.com/ncw/rclone/backend/local"
|
||||||
|
"github.com/ncw/rclone/fs"
|
||||||
|
"github.com/ncw/rclone/fs/config/obscure"
|
||||||
|
"github.com/ncw/rclone/fs/rc"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
const testName = "configTestNameForRc"
|
||||||
|
|
||||||
|
func TestRc(t *testing.T) {
|
||||||
|
// Create the test remote
|
||||||
|
call := rc.Calls.Get("config/create")
|
||||||
|
assert.NotNil(t, call)
|
||||||
|
in := rc.Params{
|
||||||
|
"name": testName,
|
||||||
|
"type": "local",
|
||||||
|
"parameters": rc.Params{
|
||||||
|
"test_key": "sausage",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
out, err := call.Fn(in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Nil(t, out)
|
||||||
|
assert.Equal(t, "local", FileGet(testName, "type"))
|
||||||
|
assert.Equal(t, "sausage", FileGet(testName, "test_key"))
|
||||||
|
|
||||||
|
// The sub tests rely on the remote created above but they can
|
||||||
|
// all be run independently
|
||||||
|
|
||||||
|
t.Run("Dump", func(t *testing.T) {
|
||||||
|
call := rc.Calls.Get("config/dump")
|
||||||
|
assert.NotNil(t, call)
|
||||||
|
in := rc.Params{}
|
||||||
|
out, err := call.Fn(in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, out)
|
||||||
|
|
||||||
|
require.NotNil(t, out[testName])
|
||||||
|
config := out[testName].(rc.Params)
|
||||||
|
|
||||||
|
assert.Equal(t, "local", config["type"])
|
||||||
|
assert.Equal(t, "sausage", config["test_key"])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Get", func(t *testing.T) {
|
||||||
|
call := rc.Calls.Get("config/get")
|
||||||
|
assert.NotNil(t, call)
|
||||||
|
in := rc.Params{
|
||||||
|
"name": testName,
|
||||||
|
}
|
||||||
|
out, err := call.Fn(in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, out)
|
||||||
|
|
||||||
|
assert.Equal(t, "local", out["type"])
|
||||||
|
assert.Equal(t, "sausage", out["test_key"])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ListRemotes", func(t *testing.T) {
|
||||||
|
call := rc.Calls.Get("config/listremotes")
|
||||||
|
assert.NotNil(t, call)
|
||||||
|
in := rc.Params{}
|
||||||
|
out, err := call.Fn(in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, out)
|
||||||
|
|
||||||
|
var remotes []string
|
||||||
|
err = out.GetStruct("remotes", &remotes)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Contains(t, remotes, testName)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Update", func(t *testing.T) {
|
||||||
|
call := rc.Calls.Get("config/update")
|
||||||
|
assert.NotNil(t, call)
|
||||||
|
in := rc.Params{
|
||||||
|
"name": testName,
|
||||||
|
"parameters": rc.Params{
|
||||||
|
"test_key": "rutabaga",
|
||||||
|
"test_key2": "cabbage",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
out, err := call.Fn(in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Nil(t, out)
|
||||||
|
|
||||||
|
assert.Equal(t, "local", FileGet(testName, "type"))
|
||||||
|
assert.Equal(t, "rutabaga", FileGet(testName, "test_key"))
|
||||||
|
assert.Equal(t, "cabbage", FileGet(testName, "test_key2"))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Password", func(t *testing.T) {
|
||||||
|
call := rc.Calls.Get("config/password")
|
||||||
|
assert.NotNil(t, call)
|
||||||
|
in := rc.Params{
|
||||||
|
"name": testName,
|
||||||
|
"parameters": rc.Params{
|
||||||
|
"test_key": "rutabaga",
|
||||||
|
"test_key2": "cabbage",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
out, err := call.Fn(in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Nil(t, out)
|
||||||
|
|
||||||
|
assert.Equal(t, "local", FileGet(testName, "type"))
|
||||||
|
assert.Equal(t, "rutabaga", obscure.MustReveal(FileGet(testName, "test_key")))
|
||||||
|
assert.Equal(t, "cabbage", obscure.MustReveal(FileGet(testName, "test_key2")))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Delete the test remote
|
||||||
|
call = rc.Calls.Get("config/delete")
|
||||||
|
assert.NotNil(t, call)
|
||||||
|
in = rc.Params{
|
||||||
|
"name": testName,
|
||||||
|
}
|
||||||
|
out, err = call.Fn(in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Nil(t, out)
|
||||||
|
assert.Equal(t, "", FileGet(testName, "type"))
|
||||||
|
assert.Equal(t, "", FileGet(testName, "test_key"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRcProviders(t *testing.T) {
|
||||||
|
call := rc.Calls.Get("config/providers")
|
||||||
|
assert.NotNil(t, call)
|
||||||
|
in := rc.Params{}
|
||||||
|
out, err := call.Fn(in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, out)
|
||||||
|
var registry []*fs.RegInfo
|
||||||
|
err = out.GetStruct("providers", ®istry)
|
||||||
|
require.NoError(t, err)
|
||||||
|
foundLocal := false
|
||||||
|
for _, provider := range registry {
|
||||||
|
if provider.Name == "local" {
|
||||||
|
foundLocal = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.True(t, foundLocal, "didn't find local provider")
|
||||||
|
}
|
||||||
@@ -496,3 +496,31 @@ func (f *Filter) DumpFilters() string {
|
|||||||
}
|
}
|
||||||
return strings.Join(rules, "\n")
|
return strings.Join(rules, "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HaveFilesFrom returns true if --files-from has been supplied
|
||||||
|
func (f *Filter) HaveFilesFrom() bool {
|
||||||
|
return f.files != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var errFilesFromNotSet = errors.New("--files-from not set so can't use Filter.ListR")
|
||||||
|
|
||||||
|
// MakeListR makes function to return all the files set using --files-from
|
||||||
|
func (f *Filter) MakeListR(NewObject func(remote string) (fs.Object, error)) fs.ListRFn {
|
||||||
|
return func(dir string, callback fs.ListRCallback) error {
|
||||||
|
if !f.HaveFilesFrom() {
|
||||||
|
return errFilesFromNotSet
|
||||||
|
}
|
||||||
|
var entries fs.DirEntries
|
||||||
|
for remote := range f.files {
|
||||||
|
entry, err := NewObject(remote)
|
||||||
|
if err == fs.ErrorObjectNotFound {
|
||||||
|
// Skip files that are not found
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
entries = append(entries, entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return callback(entries)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ncw/rclone/fs"
|
"github.com/ncw/rclone/fs"
|
||||||
|
"github.com/ncw/rclone/fstest/mockobject"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@@ -183,6 +184,83 @@ func TestNewFilterIncludeFilesDirs(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNewFilterHaveFilesFrom(t *testing.T) {
|
||||||
|
f, err := NewFilter(nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, false, f.HaveFilesFrom())
|
||||||
|
|
||||||
|
require.NoError(t, f.AddFile("file"))
|
||||||
|
|
||||||
|
assert.Equal(t, true, f.HaveFilesFrom())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewFilterMakeListR(t *testing.T) {
|
||||||
|
f, err := NewFilter(nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Check error if no files
|
||||||
|
listR := f.MakeListR(nil)
|
||||||
|
err = listR("", nil)
|
||||||
|
assert.EqualError(t, err, errFilesFromNotSet.Error())
|
||||||
|
|
||||||
|
// Add some files
|
||||||
|
for _, path := range []string{
|
||||||
|
"path/to/dir/file1.png",
|
||||||
|
"/path/to/dir/file2.png",
|
||||||
|
"/path/to/file3.png",
|
||||||
|
"/path/to/dir2/file4.png",
|
||||||
|
"notfound",
|
||||||
|
} {
|
||||||
|
err = f.AddFile(path)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, 5, len(f.files))
|
||||||
|
|
||||||
|
// NewObject function for MakeListR
|
||||||
|
newObjects := FilesMap{}
|
||||||
|
NewObject := func(remote string) (fs.Object, error) {
|
||||||
|
if remote == "notfound" {
|
||||||
|
return nil, fs.ErrorObjectNotFound
|
||||||
|
} else if remote == "error" {
|
||||||
|
return nil, assert.AnError
|
||||||
|
}
|
||||||
|
newObjects[remote] = struct{}{}
|
||||||
|
return mockobject.New(remote), nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callback for ListRFn
|
||||||
|
listRObjects := FilesMap{}
|
||||||
|
listRcallback := func(entries fs.DirEntries) error {
|
||||||
|
for _, entry := range entries {
|
||||||
|
listRObjects[entry.Remote()] = struct{}{}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make the listR and call it
|
||||||
|
listR = f.MakeListR(NewObject)
|
||||||
|
err = listR("", listRcallback)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Check that the correct objects were created and listed
|
||||||
|
want := FilesMap{
|
||||||
|
"path/to/dir/file1.png": {},
|
||||||
|
"path/to/dir/file2.png": {},
|
||||||
|
"path/to/file3.png": {},
|
||||||
|
"path/to/dir2/file4.png": {},
|
||||||
|
}
|
||||||
|
assert.Equal(t, want, newObjects)
|
||||||
|
assert.Equal(t, want, listRObjects)
|
||||||
|
|
||||||
|
// Now check an error is returned from NewObject
|
||||||
|
require.NoError(t, f.AddFile("error"))
|
||||||
|
err = listR("", listRcallback)
|
||||||
|
require.EqualError(t, err, assert.AnError.Error())
|
||||||
|
}
|
||||||
|
|
||||||
func TestNewFilterMinSize(t *testing.T) {
|
func TestNewFilterMinSize(t *testing.T) {
|
||||||
f, err := NewFilter(nil)
|
f, err := NewFilter(nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ package filterflags
|
|||||||
import (
|
import (
|
||||||
"github.com/ncw/rclone/fs/config/flags"
|
"github.com/ncw/rclone/fs/config/flags"
|
||||||
"github.com/ncw/rclone/fs/filter"
|
"github.com/ncw/rclone/fs/filter"
|
||||||
|
"github.com/ncw/rclone/fs/rc"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -14,6 +15,7 @@ var (
|
|||||||
|
|
||||||
// AddFlags adds the non filing system specific flags to the command
|
// AddFlags adds the non filing system specific flags to the command
|
||||||
func AddFlags(flagSet *pflag.FlagSet) {
|
func AddFlags(flagSet *pflag.FlagSet) {
|
||||||
|
rc.AddOption("filter", &Opt)
|
||||||
flags.BoolVarP(flagSet, &Opt.DeleteExcluded, "delete-excluded", "", false, "Delete files on dest excluded from sync")
|
flags.BoolVarP(flagSet, &Opt.DeleteExcluded, "delete-excluded", "", false, "Delete files on dest excluded from sync")
|
||||||
flags.StringArrayVarP(flagSet, &Opt.FilterRule, "filter", "f", nil, "Add a file-filtering rule")
|
flags.StringArrayVarP(flagSet, &Opt.FilterRule, "filter", "f", nil, "Add a file-filtering rule")
|
||||||
flags.StringArrayVarP(flagSet, &Opt.FilterFrom, "filter-from", "", nil, "Read filtering patterns from a file")
|
flags.StringArrayVarP(flagSet, &Opt.FilterFrom, "filter-from", "", nil, "Read filtering patterns from a file")
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ type listDirFn func(dir string) (entries fs.DirEntries, err error)
|
|||||||
|
|
||||||
// makeListDir makes a listing function for the given fs and includeAll flags
|
// makeListDir makes a listing function for the given fs and includeAll flags
|
||||||
func (m *March) makeListDir(f fs.Fs, includeAll bool) listDirFn {
|
func (m *March) makeListDir(f fs.Fs, includeAll bool) listDirFn {
|
||||||
if !fs.Config.UseListR || f.Features().ListR == nil {
|
if (!fs.Config.UseListR || f.Features().ListR == nil) && !filter.Active.HaveFilesFrom() {
|
||||||
return func(dir string) (entries fs.DirEntries, err error) {
|
return func(dir string) (entries fs.DirEntries, err error) {
|
||||||
return list.DirSorted(f, includeAll, dir)
|
return list.DirSorted(f, includeAll, dir)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,16 +18,32 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// dedupeRename renames the objs slice to different names
|
// dedupeRename renames the objs slice to different names
|
||||||
func dedupeRename(remote string, objs []fs.Object) {
|
func dedupeRename(f fs.Fs, remote string, objs []fs.Object) {
|
||||||
f := objs[0].Fs()
|
|
||||||
doMove := f.Features().Move
|
doMove := f.Features().Move
|
||||||
if doMove == nil {
|
if doMove == nil {
|
||||||
log.Fatalf("Fs %v doesn't support Move", f)
|
log.Fatalf("Fs %v doesn't support Move", f)
|
||||||
}
|
}
|
||||||
ext := path.Ext(remote)
|
ext := path.Ext(remote)
|
||||||
base := remote[:len(remote)-len(ext)]
|
base := remote[:len(remote)-len(ext)]
|
||||||
|
|
||||||
|
outer:
|
||||||
for i, o := range objs {
|
for i, o := range objs {
|
||||||
newName := fmt.Sprintf("%s-%d%s", base, i+1, ext)
|
suffix := 1
|
||||||
|
newName := fmt.Sprintf("%s-%d%s", base, i+suffix, ext)
|
||||||
|
_, err := f.NewObject(newName)
|
||||||
|
for ; err != fs.ErrorObjectNotFound; suffix++ {
|
||||||
|
if err != nil {
|
||||||
|
fs.CountError(err)
|
||||||
|
fs.Errorf(o, "Failed to check for existing object: %v", err)
|
||||||
|
continue outer
|
||||||
|
}
|
||||||
|
if suffix > 100 {
|
||||||
|
fs.Errorf(o, "Could not find an available new name")
|
||||||
|
continue outer
|
||||||
|
}
|
||||||
|
newName = fmt.Sprintf("%s-%d%s", base, i+suffix, ext)
|
||||||
|
_, err = f.NewObject(newName)
|
||||||
|
}
|
||||||
if !fs.Config.DryRun {
|
if !fs.Config.DryRun {
|
||||||
newObj, err := doMove(o, newName)
|
newObj, err := doMove(o, newName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -81,7 +97,7 @@ func dedupeDeleteIdentical(ht hash.Type, remote string, objs []fs.Object) (remai
|
|||||||
}
|
}
|
||||||
|
|
||||||
// dedupeInteractive interactively dedupes the slice of objects
|
// dedupeInteractive interactively dedupes the slice of objects
|
||||||
func dedupeInteractive(ht hash.Type, remote string, objs []fs.Object) {
|
func dedupeInteractive(f fs.Fs, ht hash.Type, remote string, objs []fs.Object) {
|
||||||
fmt.Printf("%s: %d duplicates remain\n", remote, len(objs))
|
fmt.Printf("%s: %d duplicates remain\n", remote, len(objs))
|
||||||
for i, o := range objs {
|
for i, o := range objs {
|
||||||
md5sum, err := o.Hash(ht)
|
md5sum, err := o.Hash(ht)
|
||||||
@@ -96,7 +112,7 @@ func dedupeInteractive(ht hash.Type, remote string, objs []fs.Object) {
|
|||||||
keep := config.ChooseNumber("Enter the number of the file to keep", 1, len(objs))
|
keep := config.ChooseNumber("Enter the number of the file to keep", 1, len(objs))
|
||||||
dedupeDeleteAllButOne(keep-1, remote, objs)
|
dedupeDeleteAllButOne(keep-1, remote, objs)
|
||||||
case 'r':
|
case 'r':
|
||||||
dedupeRename(remote, objs)
|
dedupeRename(f, remote, objs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,7 +292,7 @@ func Deduplicate(f fs.Fs, mode DeduplicateMode) error {
|
|||||||
}
|
}
|
||||||
switch mode {
|
switch mode {
|
||||||
case DeduplicateInteractive:
|
case DeduplicateInteractive:
|
||||||
dedupeInteractive(ht, remote, objs)
|
dedupeInteractive(f, ht, remote, objs)
|
||||||
case DeduplicateFirst:
|
case DeduplicateFirst:
|
||||||
dedupeDeleteAllButOne(0, remote, objs)
|
dedupeDeleteAllButOne(0, remote, objs)
|
||||||
case DeduplicateNewest:
|
case DeduplicateNewest:
|
||||||
@@ -286,7 +302,7 @@ func Deduplicate(f fs.Fs, mode DeduplicateMode) error {
|
|||||||
sort.Sort(objectsSortedByModTime(objs)) // sort oldest first
|
sort.Sort(objectsSortedByModTime(objs)) // sort oldest first
|
||||||
dedupeDeleteAllButOne(0, remote, objs)
|
dedupeDeleteAllButOne(0, remote, objs)
|
||||||
case DeduplicateRename:
|
case DeduplicateRename:
|
||||||
dedupeRename(remote, objs)
|
dedupeRename(f, remote, objs)
|
||||||
case DeduplicateLargest:
|
case DeduplicateLargest:
|
||||||
largest, largestIndex := int64(-1), -1
|
largest, largestIndex := int64(-1), -1
|
||||||
for i, obj := range objs {
|
for i, obj := range objs {
|
||||||
|
|||||||
@@ -155,7 +155,8 @@ func TestDeduplicateRename(t *testing.T) {
|
|||||||
file1 := r.WriteUncheckedObject("one.txt", "This is one", t1)
|
file1 := r.WriteUncheckedObject("one.txt", "This is one", t1)
|
||||||
file2 := r.WriteUncheckedObject("one.txt", "This is one too", t2)
|
file2 := r.WriteUncheckedObject("one.txt", "This is one too", t2)
|
||||||
file3 := r.WriteUncheckedObject("one.txt", "This is another one", t3)
|
file3 := r.WriteUncheckedObject("one.txt", "This is another one", t3)
|
||||||
r.CheckWithDuplicates(t, file1, file2, file3)
|
file4 := r.WriteUncheckedObject("one-1.txt", "This is not a duplicate", t1)
|
||||||
|
r.CheckWithDuplicates(t, file1, file2, file3, file4)
|
||||||
|
|
||||||
err := operations.Deduplicate(r.Fremote, operations.DeduplicateRename)
|
err := operations.Deduplicate(r.Fremote, operations.DeduplicateRename)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -168,13 +169,20 @@ func TestDeduplicateRename(t *testing.T) {
|
|||||||
remote := o.Remote()
|
remote := o.Remote()
|
||||||
if remote != "one-1.txt" &&
|
if remote != "one-1.txt" &&
|
||||||
remote != "one-2.txt" &&
|
remote != "one-2.txt" &&
|
||||||
remote != "one-3.txt" {
|
remote != "one-3.txt" &&
|
||||||
|
remote != "one-4.txt" {
|
||||||
t.Errorf("Bad file name after rename %q", remote)
|
t.Errorf("Bad file name after rename %q", remote)
|
||||||
}
|
}
|
||||||
size := o.Size()
|
size := o.Size()
|
||||||
if size != file1.Size && size != file2.Size && size != file3.Size {
|
if size != file1.Size &&
|
||||||
|
size != file2.Size &&
|
||||||
|
size != file3.Size &&
|
||||||
|
size != file4.Size {
|
||||||
t.Errorf("Size not one of the object sizes %d", size)
|
t.Errorf("Size not one of the object sizes %d", size)
|
||||||
}
|
}
|
||||||
|
if remote == "one-1.txt" && size != file4.Size {
|
||||||
|
t.Errorf("Existing non-duplicate file modified %q", remote)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
return nil
|
return nil
|
||||||
}))
|
}))
|
||||||
|
|||||||
141
fs/operations/lsjson.go
Normal file
141
fs/operations/lsjson.go
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
package operations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ncw/rclone/backend/crypt"
|
||||||
|
"github.com/ncw/rclone/fs"
|
||||||
|
"github.com/ncw/rclone/fs/walk"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ListJSONItem in the struct which gets marshalled for each line
|
||||||
|
type ListJSONItem struct {
|
||||||
|
Path string
|
||||||
|
Name string
|
||||||
|
Encrypted string `json:",omitempty"`
|
||||||
|
Size int64
|
||||||
|
MimeType string `json:",omitempty"`
|
||||||
|
ModTime Timestamp //`json:",omitempty"`
|
||||||
|
IsDir bool
|
||||||
|
Hashes map[string]string `json:",omitempty"`
|
||||||
|
ID string `json:",omitempty"`
|
||||||
|
OrigID string `json:",omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timestamp a time in RFC3339 format with Nanosecond precision secongs
|
||||||
|
type Timestamp time.Time
|
||||||
|
|
||||||
|
// MarshalJSON turns a Timestamp into JSON
|
||||||
|
func (t Timestamp) MarshalJSON() (out []byte, err error) {
|
||||||
|
tt := time.Time(t)
|
||||||
|
if tt.IsZero() {
|
||||||
|
return []byte(`""`), nil
|
||||||
|
}
|
||||||
|
return []byte(`"` + tt.Format(time.RFC3339Nano) + `"`), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListJSONOpt describes the options for ListJSON
|
||||||
|
type ListJSONOpt struct {
|
||||||
|
Recurse bool `json:"recurse"`
|
||||||
|
NoModTime bool `json:"noModTime"`
|
||||||
|
ShowEncrypted bool `json:"showEncrypted"`
|
||||||
|
ShowOrigIDs bool `json:"showOrigIDs"`
|
||||||
|
ShowHash bool `json:"showHash"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListJSON lists fsrc using the options in opt calling callback for each item
|
||||||
|
func ListJSON(fsrc fs.Fs, remote string, opt *ListJSONOpt, callback func(*ListJSONItem) error) error {
|
||||||
|
var cipher crypt.Cipher
|
||||||
|
if opt.ShowEncrypted {
|
||||||
|
fsInfo, _, _, config, err := fs.ConfigFs(fsrc.Name() + ":" + fsrc.Root())
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "ListJSON failed to load config for crypt remote")
|
||||||
|
}
|
||||||
|
if fsInfo.Name != "crypt" {
|
||||||
|
return errors.New("The remote needs to be of type \"crypt\"")
|
||||||
|
}
|
||||||
|
cipher, err = crypt.NewCipher(config)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "ListJSON failed to make new crypt remote")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err := walk.Walk(fsrc, remote, false, ConfigMaxDepth(opt.Recurse), func(dirPath string, entries fs.DirEntries, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
fs.CountError(err)
|
||||||
|
fs.Errorf(dirPath, "error listing: %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, entry := range entries {
|
||||||
|
item := ListJSONItem{
|
||||||
|
Path: entry.Remote(),
|
||||||
|
Name: path.Base(entry.Remote()),
|
||||||
|
Size: entry.Size(),
|
||||||
|
MimeType: fs.MimeTypeDirEntry(entry),
|
||||||
|
}
|
||||||
|
if !opt.NoModTime {
|
||||||
|
item.ModTime = Timestamp(entry.ModTime())
|
||||||
|
}
|
||||||
|
if cipher != nil {
|
||||||
|
switch entry.(type) {
|
||||||
|
case fs.Directory:
|
||||||
|
item.Encrypted = cipher.EncryptDirName(path.Base(entry.Remote()))
|
||||||
|
case fs.Object:
|
||||||
|
item.Encrypted = cipher.EncryptFileName(path.Base(entry.Remote()))
|
||||||
|
default:
|
||||||
|
fs.Errorf(nil, "Unknown type %T in listing", entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if do, ok := entry.(fs.IDer); ok {
|
||||||
|
item.ID = do.ID()
|
||||||
|
}
|
||||||
|
if opt.ShowOrigIDs {
|
||||||
|
cur := entry
|
||||||
|
for {
|
||||||
|
u, ok := cur.(fs.ObjectUnWrapper)
|
||||||
|
if !ok {
|
||||||
|
break // not a wrapped object, use current id
|
||||||
|
}
|
||||||
|
next := u.UnWrap()
|
||||||
|
if next == nil {
|
||||||
|
break // no base object found, use current id
|
||||||
|
}
|
||||||
|
cur = next
|
||||||
|
}
|
||||||
|
if do, ok := cur.(fs.IDer); ok {
|
||||||
|
item.OrigID = do.ID()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch x := entry.(type) {
|
||||||
|
case fs.Directory:
|
||||||
|
item.IsDir = true
|
||||||
|
case fs.Object:
|
||||||
|
item.IsDir = false
|
||||||
|
if opt.ShowHash {
|
||||||
|
item.Hashes = make(map[string]string)
|
||||||
|
for _, hashType := range x.Fs().Hashes().Array() {
|
||||||
|
hash, err := x.Hash(hashType)
|
||||||
|
if err != nil {
|
||||||
|
fs.Errorf(x, "Failed to read hash: %v", err)
|
||||||
|
} else if hash != "" {
|
||||||
|
item.Hashes[hashType.String()] = hash
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
fs.Errorf(nil, "Unknown type %T in listing in ListJSON", entry)
|
||||||
|
}
|
||||||
|
err = callback(&item)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "callback failed in ListJSON")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "error in ListJSON")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
"path"
|
"path"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -975,7 +976,7 @@ func Purge(f fs.Fs, dir string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = Rmdirs(f, "", false)
|
err = Rmdirs(f, dir, false)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fs.CountError(err)
|
fs.CountError(err)
|
||||||
@@ -1206,7 +1207,7 @@ func PublicLink(f fs.Fs, remote string) (string, error) {
|
|||||||
// containing empty directories) under f, including f.
|
// containing empty directories) under f, including f.
|
||||||
func Rmdirs(f fs.Fs, dir string, leaveRoot bool) error {
|
func Rmdirs(f fs.Fs, dir string, leaveRoot bool) error {
|
||||||
dirEmpty := make(map[string]bool)
|
dirEmpty := make(map[string]bool)
|
||||||
dirEmpty[""] = !leaveRoot
|
dirEmpty[dir] = !leaveRoot
|
||||||
err := walk.Walk(f, dir, true, fs.Config.MaxDepth, func(dirPath string, entries fs.DirEntries, err error) error {
|
err := walk.Walk(f, dir, true, fs.Config.MaxDepth, func(dirPath string, entries fs.DirEntries, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fs.CountError(err)
|
fs.CountError(err)
|
||||||
@@ -1359,6 +1360,16 @@ func RcatSize(fdst fs.Fs, dstFileName string, in io.ReadCloser, size int64, modT
|
|||||||
return obj, nil
|
return obj, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CopyURL copies the data from the url to (fdst, dstFileName)
|
||||||
|
func CopyURL(fdst fs.Fs, dstFileName string, url string) (dst fs.Object, err error) {
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer fs.CheckClose(resp.Body, &err)
|
||||||
|
return RcatSize(fdst, dstFileName, resp.Body, resp.ContentLength, time.Now())
|
||||||
|
}
|
||||||
|
|
||||||
// moveOrCopyFile moves or copies a single file possibly to a new name
|
// moveOrCopyFile moves or copies a single file possibly to a new name
|
||||||
func moveOrCopyFile(fdst fs.Fs, fsrc fs.Fs, dstFileName string, srcFileName string, cp bool) (err error) {
|
func moveOrCopyFile(fdst fs.Fs, fsrc fs.Fs, dstFileName string, srcFileName string, cp bool) (err error) {
|
||||||
dstFilePath := path.Join(fdst.Root(), dstFileName)
|
dstFilePath := path.Join(fdst.Root(), dstFileName)
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -96,6 +98,33 @@ func TestLs(t *testing.T) {
|
|||||||
assert.Contains(t, res, " 60 potato2\n")
|
assert.Contains(t, res, " 60 potato2\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLsWithFilesFrom(t *testing.T) {
|
||||||
|
r := fstest.NewRun(t)
|
||||||
|
defer r.Finalise()
|
||||||
|
file1 := r.WriteBoth("potato2", "------------------------------------------------------------", t1)
|
||||||
|
file2 := r.WriteBoth("empty space", "", t2)
|
||||||
|
|
||||||
|
fstest.CheckItems(t, r.Fremote, file1, file2)
|
||||||
|
|
||||||
|
// Set the --files-from equivalent
|
||||||
|
f, err := filter.NewFilter(nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, f.AddFile("potato2"))
|
||||||
|
require.NoError(t, f.AddFile("notfound"))
|
||||||
|
|
||||||
|
// Monkey patch the active filter
|
||||||
|
oldFilter := filter.Active
|
||||||
|
filter.Active = f
|
||||||
|
defer func() {
|
||||||
|
filter.Active = oldFilter
|
||||||
|
}()
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err = operations.List(r.Fremote, &buf)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, " 60 potato2\n", buf.String())
|
||||||
|
}
|
||||||
|
|
||||||
func TestLsLong(t *testing.T) {
|
func TestLsLong(t *testing.T) {
|
||||||
r := fstest.NewRun(t)
|
r := fstest.NewRun(t)
|
||||||
defer r.Finalise()
|
defer r.Finalise()
|
||||||
@@ -374,6 +403,78 @@ func TestRcat(t *testing.T) {
|
|||||||
check(false)
|
check(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPurge(t *testing.T) {
|
||||||
|
r := fstest.NewRun(t)
|
||||||
|
defer r.Finalise()
|
||||||
|
r.Mkdir(r.Fremote)
|
||||||
|
|
||||||
|
// Make some files and dirs
|
||||||
|
r.ForceMkdir(r.Fremote)
|
||||||
|
file1 := r.WriteObject("A1/B1/C1/one", "aaa", t1)
|
||||||
|
//..and dirs we expect to delete
|
||||||
|
require.NoError(t, operations.Mkdir(r.Fremote, "A2"))
|
||||||
|
require.NoError(t, operations.Mkdir(r.Fremote, "A1/B2"))
|
||||||
|
require.NoError(t, operations.Mkdir(r.Fremote, "A1/B2/C2"))
|
||||||
|
require.NoError(t, operations.Mkdir(r.Fremote, "A1/B1/C3"))
|
||||||
|
require.NoError(t, operations.Mkdir(r.Fremote, "A3"))
|
||||||
|
require.NoError(t, operations.Mkdir(r.Fremote, "A3/B3"))
|
||||||
|
require.NoError(t, operations.Mkdir(r.Fremote, "A3/B3/C4"))
|
||||||
|
//..and one more file at the end
|
||||||
|
file2 := r.WriteObject("A1/two", "bbb", t2)
|
||||||
|
|
||||||
|
fstest.CheckListingWithPrecision(
|
||||||
|
t,
|
||||||
|
r.Fremote,
|
||||||
|
[]fstest.Item{
|
||||||
|
file1, file2,
|
||||||
|
},
|
||||||
|
[]string{
|
||||||
|
"A1",
|
||||||
|
"A1/B1",
|
||||||
|
"A1/B1/C1",
|
||||||
|
"A2",
|
||||||
|
"A1/B2",
|
||||||
|
"A1/B2/C2",
|
||||||
|
"A1/B1/C3",
|
||||||
|
"A3",
|
||||||
|
"A3/B3",
|
||||||
|
"A3/B3/C4",
|
||||||
|
},
|
||||||
|
fs.GetModifyWindow(r.Fremote),
|
||||||
|
)
|
||||||
|
|
||||||
|
require.NoError(t, operations.Purge(r.Fremote, "A1/B1"))
|
||||||
|
|
||||||
|
fstest.CheckListingWithPrecision(
|
||||||
|
t,
|
||||||
|
r.Fremote,
|
||||||
|
[]fstest.Item{
|
||||||
|
file2,
|
||||||
|
},
|
||||||
|
[]string{
|
||||||
|
"A1",
|
||||||
|
"A2",
|
||||||
|
"A1/B2",
|
||||||
|
"A1/B2/C2",
|
||||||
|
"A3",
|
||||||
|
"A3/B3",
|
||||||
|
"A3/B3/C4",
|
||||||
|
},
|
||||||
|
fs.GetModifyWindow(r.Fremote),
|
||||||
|
)
|
||||||
|
|
||||||
|
require.NoError(t, operations.Purge(r.Fremote, ""))
|
||||||
|
|
||||||
|
fstest.CheckListingWithPrecision(
|
||||||
|
t,
|
||||||
|
r.Fremote,
|
||||||
|
[]fstest.Item{},
|
||||||
|
[]string{},
|
||||||
|
fs.GetModifyWindow(r.Fremote),
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func TestRmdirsNoLeaveRoot(t *testing.T) {
|
func TestRmdirsNoLeaveRoot(t *testing.T) {
|
||||||
r := fstest.NewRun(t)
|
r := fstest.NewRun(t)
|
||||||
defer r.Finalise()
|
defer r.Finalise()
|
||||||
@@ -414,6 +515,28 @@ func TestRmdirsNoLeaveRoot(t *testing.T) {
|
|||||||
fs.GetModifyWindow(r.Fremote),
|
fs.GetModifyWindow(r.Fremote),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
require.NoError(t, operations.Rmdirs(r.Fremote, "A3/B3/C4", false))
|
||||||
|
|
||||||
|
fstest.CheckListingWithPrecision(
|
||||||
|
t,
|
||||||
|
r.Fremote,
|
||||||
|
[]fstest.Item{
|
||||||
|
file1, file2,
|
||||||
|
},
|
||||||
|
[]string{
|
||||||
|
"A1",
|
||||||
|
"A1/B1",
|
||||||
|
"A1/B1/C1",
|
||||||
|
"A2",
|
||||||
|
"A1/B2",
|
||||||
|
"A1/B2/C2",
|
||||||
|
"A1/B1/C3",
|
||||||
|
"A3",
|
||||||
|
"A3/B3",
|
||||||
|
},
|
||||||
|
fs.GetModifyWindow(r.Fremote),
|
||||||
|
)
|
||||||
|
|
||||||
require.NoError(t, operations.Rmdirs(r.Fremote, "", false))
|
require.NoError(t, operations.Rmdirs(r.Fremote, "", false))
|
||||||
|
|
||||||
fstest.CheckListingWithPrecision(
|
fstest.CheckListingWithPrecision(
|
||||||
@@ -494,6 +617,28 @@ func TestRcatSize(t *testing.T) {
|
|||||||
fstest.CheckItems(t, r.Fremote, file1, file2)
|
fstest.CheckItems(t, r.Fremote, file1, file2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCopyURL(t *testing.T) {
|
||||||
|
r := fstest.NewRun(t)
|
||||||
|
defer r.Finalise()
|
||||||
|
|
||||||
|
contents := "file1 contents\n"
|
||||||
|
file1 := r.WriteFile("file1", contents, t1)
|
||||||
|
r.Mkdir(r.Fremote)
|
||||||
|
fstest.CheckItems(t, r.Fremote)
|
||||||
|
|
||||||
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
_, err := w.Write([]byte(contents))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}))
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
o, err := operations.CopyURL(r.Fremote, "file1", ts.URL)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(len(contents)), o.Size())
|
||||||
|
|
||||||
|
fstest.CheckListingWithPrecision(t, r.Fremote, []fstest.Item{file1}, nil, fs.ModTimeNotSupported)
|
||||||
|
}
|
||||||
|
|
||||||
func TestMoveFile(t *testing.T) {
|
func TestMoveFile(t *testing.T) {
|
||||||
r := fstest.NewRun(t)
|
r := fstest.NewRun(t)
|
||||||
defer r.Finalise()
|
defer r.Finalise()
|
||||||
|
|||||||
260
fs/operations/rc.go
Normal file
260
fs/operations/rc.go
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
package operations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ncw/rclone/fs"
|
||||||
|
"github.com/ncw/rclone/fs/rc"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rc.Add(rc.Call{
|
||||||
|
Path: "operations/list",
|
||||||
|
AuthRequired: true,
|
||||||
|
Fn: rcList,
|
||||||
|
Title: "List the given remote and path in JSON format",
|
||||||
|
Help: `This takes the following parameters
|
||||||
|
|
||||||
|
- fs - a remote name string eg "drive:"
|
||||||
|
- remote - a path within that remote eg "dir"
|
||||||
|
- opt - a dictionary of options to control the listing (optional)
|
||||||
|
- recurse - If set recurse directories
|
||||||
|
- noModTime - If set return modification time
|
||||||
|
- showEncrypted - If set show decrypted names
|
||||||
|
- showOrigIDs - If set show the IDs for each item if known
|
||||||
|
- showHash - If set return a dictionary of hashes
|
||||||
|
|
||||||
|
The result is
|
||||||
|
|
||||||
|
- list
|
||||||
|
- This is an array of objects as described in the lsjson command
|
||||||
|
|
||||||
|
See the lsjson command for more information on the above and examples.
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// List the directory
|
||||||
|
func rcList(in rc.Params) (out rc.Params, err error) {
|
||||||
|
f, remote, err := rc.GetFsAndRemote(in)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var opt ListJSONOpt
|
||||||
|
err = in.GetStruct("opt", &opt)
|
||||||
|
if rc.NotErrParamNotFound(err) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var list = []*ListJSONItem{}
|
||||||
|
err = ListJSON(f, remote, &opt, func(item *ListJSONItem) error {
|
||||||
|
list = append(list, item)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out = make(rc.Params)
|
||||||
|
out["list"] = list
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rc.Add(rc.Call{
|
||||||
|
Path: "operations/about",
|
||||||
|
AuthRequired: true,
|
||||||
|
Fn: rcAbout,
|
||||||
|
Title: "Return the space used on the remote",
|
||||||
|
Help: `This takes the following parameters
|
||||||
|
|
||||||
|
- fs - a remote name string eg "drive:"
|
||||||
|
- remote - a path within that remote eg "dir"
|
||||||
|
|
||||||
|
The result is as returned from rclone about --json
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// About the remote
|
||||||
|
func rcAbout(in rc.Params) (out rc.Params, err error) {
|
||||||
|
f, err := rc.GetFs(in)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
doAbout := f.Features().About
|
||||||
|
if doAbout == nil {
|
||||||
|
return nil, errors.Errorf("%v doesn't support about", f)
|
||||||
|
}
|
||||||
|
u, err := doAbout()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "about call failed")
|
||||||
|
}
|
||||||
|
err = rc.Reshape(&out, u)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "about Reshape failed")
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
for _, copy := range []bool{false, true} {
|
||||||
|
copy := copy
|
||||||
|
name := "Move"
|
||||||
|
if copy {
|
||||||
|
name = "Copy"
|
||||||
|
}
|
||||||
|
rc.Add(rc.Call{
|
||||||
|
Path: "operations/" + strings.ToLower(name) + "file",
|
||||||
|
AuthRequired: true,
|
||||||
|
Fn: func(in rc.Params) (rc.Params, error) {
|
||||||
|
return rcMoveOrCopyFile(in, copy)
|
||||||
|
},
|
||||||
|
Title: name + " a file from source remote to destination remote",
|
||||||
|
Help: `This takes the following parameters
|
||||||
|
|
||||||
|
- srcFs - a remote name string eg "drive:" for the source
|
||||||
|
- srcRemote - a path within that remote eg "file.txt" for the source
|
||||||
|
- dstFs - a remote name string eg "drive2:" for the destination
|
||||||
|
- dstRemote - a path within that remote eg "file2.txt" for the destination
|
||||||
|
|
||||||
|
This returns
|
||||||
|
- jobid - ID of async job to query with job/status
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy a file
|
||||||
|
func rcMoveOrCopyFile(in rc.Params, cp bool) (out rc.Params, err error) {
|
||||||
|
srcFs, srcRemote, err := rc.GetFsAndRemoteNamed(in, "srcFs", "srcRemote")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dstFs, dstRemote, err := rc.GetFsAndRemoteNamed(in, "dstFs", "dstRemote")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return nil, moveOrCopyFile(dstFs, srcFs, dstRemote, srcRemote, cp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
for _, op := range []struct {
|
||||||
|
name string
|
||||||
|
title string
|
||||||
|
help string
|
||||||
|
noRemote bool
|
||||||
|
}{
|
||||||
|
{name: "mkdir", title: "Make a destination directory or container"},
|
||||||
|
{name: "rmdir", title: "Remove an empty directory or container"},
|
||||||
|
{name: "purge", title: "Remove a directory or container and all of its contents"},
|
||||||
|
{name: "rmdirs", title: "Remove all the empty directories in the path", help: "- leaveRoot - boolean, set to true not to delete the root\n"},
|
||||||
|
{name: "delete", title: "Remove files in the path", noRemote: true},
|
||||||
|
{name: "deletefile", title: "Remove the single file pointed to"},
|
||||||
|
{name: "copyurl", title: "Copy the URL to the object", help: "- url - string, URL to read from\n"},
|
||||||
|
{name: "cleanup", title: "Remove trashed files in the remote or path", noRemote: true},
|
||||||
|
} {
|
||||||
|
op := op
|
||||||
|
remote := "- remote - a path within that remote eg \"dir\"\n"
|
||||||
|
if op.noRemote {
|
||||||
|
remote = ""
|
||||||
|
}
|
||||||
|
rc.Add(rc.Call{
|
||||||
|
Path: "operations/" + op.name,
|
||||||
|
AuthRequired: true,
|
||||||
|
Fn: func(in rc.Params) (rc.Params, error) {
|
||||||
|
return rcSingleCommand(in, op.name, op.noRemote)
|
||||||
|
},
|
||||||
|
Title: op.title,
|
||||||
|
Help: `This takes the following parameters
|
||||||
|
|
||||||
|
- fs - a remote name string eg "drive:"
|
||||||
|
` + remote + op.help + `
|
||||||
|
See the [` + op.name + ` command](/commands/rclone_` + op.name + `/) command for more information on the above.
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mkdir a directory
|
||||||
|
func rcSingleCommand(in rc.Params, name string, noRemote bool) (out rc.Params, err error) {
|
||||||
|
var (
|
||||||
|
f fs.Fs
|
||||||
|
remote string
|
||||||
|
)
|
||||||
|
if noRemote {
|
||||||
|
f, err = rc.GetFs(in)
|
||||||
|
} else {
|
||||||
|
f, remote, err = rc.GetFsAndRemote(in)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
switch name {
|
||||||
|
case "mkdir":
|
||||||
|
return nil, Mkdir(f, remote)
|
||||||
|
case "rmdir":
|
||||||
|
return nil, Rmdir(f, remote)
|
||||||
|
case "purge":
|
||||||
|
return nil, Purge(f, remote)
|
||||||
|
case "rmdirs":
|
||||||
|
leaveRoot, err := in.GetBool("leaveRoot")
|
||||||
|
if rc.NotErrParamNotFound(err) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return nil, Rmdirs(f, remote, leaveRoot)
|
||||||
|
case "delete":
|
||||||
|
return nil, Delete(f)
|
||||||
|
case "deletefile":
|
||||||
|
o, err := f.NewObject(remote)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return nil, DeleteFile(o)
|
||||||
|
case "copyurl":
|
||||||
|
url, err := in.GetString("url")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, err = CopyURL(f, remote, url)
|
||||||
|
return nil, err
|
||||||
|
case "cleanup":
|
||||||
|
return nil, CleanUp(f)
|
||||||
|
}
|
||||||
|
panic("unknown rcSingleCommand type")
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rc.Add(rc.Call{
|
||||||
|
Path: "operations/size",
|
||||||
|
AuthRequired: true,
|
||||||
|
Fn: rcSize,
|
||||||
|
Title: "Count the number of bytes and files in remote",
|
||||||
|
Help: `This takes the following parameters
|
||||||
|
|
||||||
|
- fs - a remote name string eg "drive:path/to/dir"
|
||||||
|
|
||||||
|
Returns
|
||||||
|
|
||||||
|
- count - number of files
|
||||||
|
- bytes - number of bytes in those files
|
||||||
|
|
||||||
|
See the [size command](/commands/rclone_size/) command for more information on the above.
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mkdir a directory
|
||||||
|
func rcSize(in rc.Params) (out rc.Params, err error) {
|
||||||
|
f, err := rc.GetFs(in)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
count, bytes, err := Count(f)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out = make(rc.Params)
|
||||||
|
out["count"] = count
|
||||||
|
out["bytes"] = bytes
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
358
fs/operations/rc_test.go
Normal file
358
fs/operations/rc_test.go
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
package operations_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ncw/rclone/fs"
|
||||||
|
"github.com/ncw/rclone/fs/operations"
|
||||||
|
"github.com/ncw/rclone/fs/rc"
|
||||||
|
"github.com/ncw/rclone/fstest"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func rcNewRun(t *testing.T, method string) (*fstest.Run, *rc.Call) {
|
||||||
|
if *fstest.RemoteName != "" {
|
||||||
|
t.Skip("Skipping test on non local remote")
|
||||||
|
}
|
||||||
|
r := fstest.NewRun(t)
|
||||||
|
call := rc.Calls.Get(method)
|
||||||
|
assert.NotNil(t, call)
|
||||||
|
rc.PutCachedFs(r.LocalName, r.Flocal)
|
||||||
|
rc.PutCachedFs(r.FremoteName, r.Fremote)
|
||||||
|
return r, call
|
||||||
|
}
|
||||||
|
|
||||||
|
// operations/about: Return the space used on the remote
|
||||||
|
func TestRcAbout(t *testing.T) {
|
||||||
|
r, call := rcNewRun(t, "operations/about")
|
||||||
|
defer r.Finalise()
|
||||||
|
r.Mkdir(r.Fremote)
|
||||||
|
|
||||||
|
// Will get an error if remote doesn't support About
|
||||||
|
expectedErr := r.Fremote.Features().About == nil
|
||||||
|
|
||||||
|
in := rc.Params{
|
||||||
|
"fs": r.FremoteName,
|
||||||
|
}
|
||||||
|
out, err := call.Fn(in)
|
||||||
|
if expectedErr {
|
||||||
|
assert.Error(t, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Can't really check the output much!
|
||||||
|
assert.NotEqual(t, int64(0), out["Total"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// operations/cleanup: Remove trashed files in the remote or path
|
||||||
|
func TestRcCleanup(t *testing.T) {
|
||||||
|
r, call := rcNewRun(t, "operations/cleanup")
|
||||||
|
defer r.Finalise()
|
||||||
|
|
||||||
|
in := rc.Params{
|
||||||
|
"fs": r.LocalName,
|
||||||
|
}
|
||||||
|
out, err := call.Fn(in)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Equal(t, rc.Params(nil), out)
|
||||||
|
assert.Contains(t, err.Error(), "doesn't support cleanup")
|
||||||
|
}
|
||||||
|
|
||||||
|
// operations/copyfile: Copy a file from source remote to destination remote
|
||||||
|
func TestRcCopyfile(t *testing.T) {
|
||||||
|
r, call := rcNewRun(t, "operations/copyfile")
|
||||||
|
defer r.Finalise()
|
||||||
|
file1 := r.WriteFile("file1", "file1 contents", t1)
|
||||||
|
r.Mkdir(r.Fremote)
|
||||||
|
fstest.CheckItems(t, r.Flocal, file1)
|
||||||
|
fstest.CheckItems(t, r.Fremote)
|
||||||
|
|
||||||
|
in := rc.Params{
|
||||||
|
"srcFs": r.LocalName,
|
||||||
|
"srcRemote": "file1",
|
||||||
|
"dstFs": r.FremoteName,
|
||||||
|
"dstRemote": "file1-renamed",
|
||||||
|
}
|
||||||
|
out, err := call.Fn(in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, rc.Params(nil), out)
|
||||||
|
|
||||||
|
fstest.CheckItems(t, r.Flocal, file1)
|
||||||
|
file1.Path = "file1-renamed"
|
||||||
|
fstest.CheckItems(t, r.Fremote, file1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// operations/copyurl: Copy the URL to the object
|
||||||
|
func TestRcCopyurl(t *testing.T) {
|
||||||
|
r, call := rcNewRun(t, "operations/copyurl")
|
||||||
|
defer r.Finalise()
|
||||||
|
contents := "file1 contents\n"
|
||||||
|
file1 := r.WriteFile("file1", contents, t1)
|
||||||
|
r.Mkdir(r.Fremote)
|
||||||
|
fstest.CheckItems(t, r.Fremote)
|
||||||
|
|
||||||
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
_, err := w.Write([]byte(contents))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}))
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
in := rc.Params{
|
||||||
|
"fs": r.FremoteName,
|
||||||
|
"remote": "file1",
|
||||||
|
"url": ts.URL,
|
||||||
|
}
|
||||||
|
out, err := call.Fn(in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, rc.Params(nil), out)
|
||||||
|
|
||||||
|
fstest.CheckListingWithPrecision(t, r.Fremote, []fstest.Item{file1}, nil, fs.ModTimeNotSupported)
|
||||||
|
}
|
||||||
|
|
||||||
|
// operations/delete: Remove files in the path
|
||||||
|
func TestRcDelete(t *testing.T) {
|
||||||
|
r, call := rcNewRun(t, "operations/delete")
|
||||||
|
defer r.Finalise()
|
||||||
|
|
||||||
|
file1 := r.WriteObject("small", "1234567890", t2) // 10 bytes
|
||||||
|
file2 := r.WriteObject("medium", "------------------------------------------------------------", t1) // 60 bytes
|
||||||
|
file3 := r.WriteObject("large", "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", t1) // 100 bytes
|
||||||
|
fstest.CheckItems(t, r.Fremote, file1, file2, file3)
|
||||||
|
|
||||||
|
in := rc.Params{
|
||||||
|
"fs": r.FremoteName,
|
||||||
|
}
|
||||||
|
out, err := call.Fn(in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, rc.Params(nil), out)
|
||||||
|
|
||||||
|
fstest.CheckItems(t, r.Fremote)
|
||||||
|
}
|
||||||
|
|
||||||
|
// operations/deletefile: Remove the single file pointed to
|
||||||
|
func TestRcDeletefile(t *testing.T) {
|
||||||
|
r, call := rcNewRun(t, "operations/deletefile")
|
||||||
|
defer r.Finalise()
|
||||||
|
|
||||||
|
file1 := r.WriteObject("small", "1234567890", t2) // 10 bytes
|
||||||
|
file2 := r.WriteObject("medium", "------------------------------------------------------------", t1) // 60 bytes
|
||||||
|
fstest.CheckItems(t, r.Fremote, file1, file2)
|
||||||
|
|
||||||
|
in := rc.Params{
|
||||||
|
"fs": r.FremoteName,
|
||||||
|
"remote": "small",
|
||||||
|
}
|
||||||
|
out, err := call.Fn(in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, rc.Params(nil), out)
|
||||||
|
|
||||||
|
fstest.CheckItems(t, r.Fremote, file2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// operations/list: List the given remote and path in JSON format
|
||||||
|
func TestRcList(t *testing.T) {
|
||||||
|
r, call := rcNewRun(t, "operations/list")
|
||||||
|
defer r.Finalise()
|
||||||
|
|
||||||
|
file1 := r.WriteObject("a", "a", t1)
|
||||||
|
file2 := r.WriteObject("subdir/b", "bb", t2)
|
||||||
|
|
||||||
|
fstest.CheckItems(t, r.Fremote, file1, file2)
|
||||||
|
|
||||||
|
in := rc.Params{
|
||||||
|
"fs": r.FremoteName,
|
||||||
|
"remote": "",
|
||||||
|
}
|
||||||
|
out, err := call.Fn(in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
list := out["list"].([]*operations.ListJSONItem)
|
||||||
|
assert.Equal(t, 2, len(list))
|
||||||
|
|
||||||
|
checkFile1 := func(got *operations.ListJSONItem) {
|
||||||
|
assert.WithinDuration(t, t1, time.Time(got.ModTime), time.Second)
|
||||||
|
assert.Equal(t, "a", got.Path)
|
||||||
|
assert.Equal(t, "a", got.Name)
|
||||||
|
assert.Equal(t, int64(1), got.Size)
|
||||||
|
assert.Equal(t, "application/octet-stream", got.MimeType)
|
||||||
|
assert.Equal(t, false, got.IsDir)
|
||||||
|
}
|
||||||
|
checkFile1(list[0])
|
||||||
|
|
||||||
|
checkSubdir := func(got *operations.ListJSONItem) {
|
||||||
|
assert.Equal(t, "subdir", got.Path)
|
||||||
|
assert.Equal(t, "subdir", got.Name)
|
||||||
|
assert.Equal(t, int64(-1), got.Size)
|
||||||
|
assert.Equal(t, "inode/directory", got.MimeType)
|
||||||
|
assert.Equal(t, true, got.IsDir)
|
||||||
|
}
|
||||||
|
checkSubdir(list[1])
|
||||||
|
|
||||||
|
in = rc.Params{
|
||||||
|
"fs": r.FremoteName,
|
||||||
|
"remote": "",
|
||||||
|
"opt": rc.Params{
|
||||||
|
"recurse": true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
out, err = call.Fn(in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
list = out["list"].([]*operations.ListJSONItem)
|
||||||
|
assert.Equal(t, 3, len(list))
|
||||||
|
checkFile1(list[0])
|
||||||
|
checkSubdir(list[1])
|
||||||
|
|
||||||
|
checkFile2 := func(got *operations.ListJSONItem) {
|
||||||
|
assert.WithinDuration(t, t2, time.Time(got.ModTime), time.Second)
|
||||||
|
assert.Equal(t, "subdir/b", got.Path)
|
||||||
|
assert.Equal(t, "b", got.Name)
|
||||||
|
assert.Equal(t, int64(2), got.Size)
|
||||||
|
assert.Equal(t, "application/octet-stream", got.MimeType)
|
||||||
|
assert.Equal(t, false, got.IsDir)
|
||||||
|
}
|
||||||
|
checkFile2(list[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
// operations/mkdir: Make a destination directory or container
|
||||||
|
func TestRcMkdir(t *testing.T) {
|
||||||
|
r, call := rcNewRun(t, "operations/mkdir")
|
||||||
|
defer r.Finalise()
|
||||||
|
r.Mkdir(r.Fremote)
|
||||||
|
|
||||||
|
fstest.CheckListingWithPrecision(t, r.Fremote, []fstest.Item{}, []string{}, fs.GetModifyWindow(r.Fremote))
|
||||||
|
|
||||||
|
in := rc.Params{
|
||||||
|
"fs": r.FremoteName,
|
||||||
|
"remote": "subdir",
|
||||||
|
}
|
||||||
|
out, err := call.Fn(in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, rc.Params(nil), out)
|
||||||
|
|
||||||
|
fstest.CheckListingWithPrecision(t, r.Fremote, []fstest.Item{}, []string{"subdir"}, fs.GetModifyWindow(r.Fremote))
|
||||||
|
}
|
||||||
|
|
||||||
|
// operations/movefile: Move a file from source remote to destination remote
|
||||||
|
func TestRcMovefile(t *testing.T) {
|
||||||
|
r, call := rcNewRun(t, "operations/movefile")
|
||||||
|
defer r.Finalise()
|
||||||
|
file1 := r.WriteFile("file1", "file1 contents", t1)
|
||||||
|
r.Mkdir(r.Fremote)
|
||||||
|
fstest.CheckItems(t, r.Flocal, file1)
|
||||||
|
fstest.CheckItems(t, r.Fremote)
|
||||||
|
|
||||||
|
in := rc.Params{
|
||||||
|
"srcFs": r.LocalName,
|
||||||
|
"srcRemote": "file1",
|
||||||
|
"dstFs": r.FremoteName,
|
||||||
|
"dstRemote": "file1-renamed",
|
||||||
|
}
|
||||||
|
out, err := call.Fn(in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, rc.Params(nil), out)
|
||||||
|
|
||||||
|
fstest.CheckItems(t, r.Flocal)
|
||||||
|
file1.Path = "file1-renamed"
|
||||||
|
fstest.CheckItems(t, r.Fremote, file1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// operations/purge: Remove a directory or container and all of its contents
|
||||||
|
func TestRcPurge(t *testing.T) {
|
||||||
|
r, call := rcNewRun(t, "operations/purge")
|
||||||
|
defer r.Finalise()
|
||||||
|
file1 := r.WriteObject("subdir/file1", "subdir/file1 contents", t1)
|
||||||
|
|
||||||
|
fstest.CheckListingWithPrecision(t, r.Fremote, []fstest.Item{file1}, []string{"subdir"}, fs.GetModifyWindow(r.Fremote))
|
||||||
|
|
||||||
|
in := rc.Params{
|
||||||
|
"fs": r.FremoteName,
|
||||||
|
"remote": "subdir",
|
||||||
|
}
|
||||||
|
out, err := call.Fn(in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, rc.Params(nil), out)
|
||||||
|
|
||||||
|
fstest.CheckListingWithPrecision(t, r.Fremote, []fstest.Item{}, []string{}, fs.GetModifyWindow(r.Fremote))
|
||||||
|
}
|
||||||
|
|
||||||
|
// operations/rmdir: Remove an empty directory or container
|
||||||
|
func TestRcRmdir(t *testing.T) {
|
||||||
|
r, call := rcNewRun(t, "operations/rmdir")
|
||||||
|
defer r.Finalise()
|
||||||
|
r.Mkdir(r.Fremote)
|
||||||
|
assert.NoError(t, r.Fremote.Mkdir("subdir"))
|
||||||
|
|
||||||
|
fstest.CheckListingWithPrecision(t, r.Fremote, []fstest.Item{}, []string{"subdir"}, fs.GetModifyWindow(r.Fremote))
|
||||||
|
|
||||||
|
in := rc.Params{
|
||||||
|
"fs": r.FremoteName,
|
||||||
|
"remote": "subdir",
|
||||||
|
}
|
||||||
|
out, err := call.Fn(in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, rc.Params(nil), out)
|
||||||
|
|
||||||
|
fstest.CheckListingWithPrecision(t, r.Fremote, []fstest.Item{}, []string{}, fs.GetModifyWindow(r.Fremote))
|
||||||
|
}
|
||||||
|
|
||||||
|
// operations/rmdirs: Remove all the empty directories in the path
|
||||||
|
func TestRcRmdirs(t *testing.T) {
|
||||||
|
r, call := rcNewRun(t, "operations/rmdirs")
|
||||||
|
defer r.Finalise()
|
||||||
|
r.Mkdir(r.Fremote)
|
||||||
|
assert.NoError(t, r.Fremote.Mkdir("subdir"))
|
||||||
|
assert.NoError(t, r.Fremote.Mkdir("subdir/subsubdir"))
|
||||||
|
|
||||||
|
fstest.CheckListingWithPrecision(t, r.Fremote, []fstest.Item{}, []string{"subdir", "subdir/subsubdir"}, fs.GetModifyWindow(r.Fremote))
|
||||||
|
|
||||||
|
in := rc.Params{
|
||||||
|
"fs": r.FremoteName,
|
||||||
|
"remote": "subdir",
|
||||||
|
}
|
||||||
|
out, err := call.Fn(in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, rc.Params(nil), out)
|
||||||
|
|
||||||
|
fstest.CheckListingWithPrecision(t, r.Fremote, []fstest.Item{}, []string{}, fs.GetModifyWindow(r.Fremote))
|
||||||
|
|
||||||
|
assert.NoError(t, r.Fremote.Mkdir("subdir"))
|
||||||
|
assert.NoError(t, r.Fremote.Mkdir("subdir/subsubdir"))
|
||||||
|
|
||||||
|
in = rc.Params{
|
||||||
|
"fs": r.FremoteName,
|
||||||
|
"remote": "subdir",
|
||||||
|
"leaveRoot": true,
|
||||||
|
}
|
||||||
|
out, err = call.Fn(in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, rc.Params(nil), out)
|
||||||
|
|
||||||
|
fstest.CheckListingWithPrecision(t, r.Fremote, []fstest.Item{}, []string{"subdir"}, fs.GetModifyWindow(r.Fremote))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// operations/size: Count the number of bytes and files in remote
|
||||||
|
func TestRcSize(t *testing.T) {
|
||||||
|
r, call := rcNewRun(t, "operations/size")
|
||||||
|
defer r.Finalise()
|
||||||
|
file1 := r.WriteObject("small", "1234567890", t2) // 10 bytes
|
||||||
|
file2 := r.WriteObject("subdir/medium", "------------------------------------------------------------", t1) // 60 bytes
|
||||||
|
file3 := r.WriteObject("subdir/subsubdir/large", "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", t1) // 50 bytes
|
||||||
|
fstest.CheckItems(t, r.Fremote, file1, file2, file3)
|
||||||
|
|
||||||
|
in := rc.Params{
|
||||||
|
"fs": r.FremoteName,
|
||||||
|
}
|
||||||
|
out, err := call.Fn(in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, rc.Params{
|
||||||
|
"count": int64(3),
|
||||||
|
"bytes": int64(120),
|
||||||
|
}, out)
|
||||||
|
}
|
||||||
117
fs/rc/cache.go
Normal file
117
fs/rc/cache.go
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
// This implements the Fs cache
|
||||||
|
|
||||||
|
package rc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ncw/rclone/fs"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
fsCacheMu sync.Mutex
|
||||||
|
fsCache = map[string]*cacheEntry{}
|
||||||
|
fsNewFs = fs.NewFs // for tests
|
||||||
|
expireRunning = false
|
||||||
|
cacheExpireDuration = 300 * time.Second // expire the cache entry when it is older than this
|
||||||
|
cacheExpireInterval = 60 * time.Second // interval to run the cache expire
|
||||||
|
)
|
||||||
|
|
||||||
|
type cacheEntry struct {
|
||||||
|
f fs.Fs
|
||||||
|
fsString string
|
||||||
|
lastUsed time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCachedFs gets a fs.Fs named fsString either from the cache or creates it afresh
|
||||||
|
func GetCachedFs(fsString string) (f fs.Fs, err error) {
|
||||||
|
fsCacheMu.Lock()
|
||||||
|
defer fsCacheMu.Unlock()
|
||||||
|
entry, ok := fsCache[fsString]
|
||||||
|
if !ok {
|
||||||
|
f, err = fsNewFs(fsString)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
entry = &cacheEntry{
|
||||||
|
f: f,
|
||||||
|
fsString: fsString,
|
||||||
|
}
|
||||||
|
fsCache[fsString] = entry
|
||||||
|
}
|
||||||
|
entry.lastUsed = time.Now()
|
||||||
|
if !expireRunning {
|
||||||
|
time.AfterFunc(cacheExpireInterval, cacheExpire)
|
||||||
|
expireRunning = true
|
||||||
|
}
|
||||||
|
return entry.f, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutCachedFs puts an fs.Fs named fsString into the cache
|
||||||
|
func PutCachedFs(fsString string, f fs.Fs) {
|
||||||
|
fsCacheMu.Lock()
|
||||||
|
defer fsCacheMu.Unlock()
|
||||||
|
fsCache[fsString] = &cacheEntry{
|
||||||
|
f: f,
|
||||||
|
fsString: fsString,
|
||||||
|
lastUsed: time.Now(),
|
||||||
|
}
|
||||||
|
if !expireRunning {
|
||||||
|
time.AfterFunc(cacheExpireInterval, cacheExpire)
|
||||||
|
expireRunning = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cacheExpire expires any entries that haven't been used recently
|
||||||
|
func cacheExpire() {
|
||||||
|
fsCacheMu.Lock()
|
||||||
|
defer fsCacheMu.Unlock()
|
||||||
|
now := time.Now()
|
||||||
|
for fsString, entry := range fsCache {
|
||||||
|
if now.Sub(entry.lastUsed) > cacheExpireDuration {
|
||||||
|
delete(fsCache, fsString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(fsCache) != 0 {
|
||||||
|
time.AfterFunc(cacheExpireInterval, cacheExpire)
|
||||||
|
expireRunning = true
|
||||||
|
} else {
|
||||||
|
expireRunning = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFsNamed gets a fs.Fs named fsName either from the cache or creates it afresh
|
||||||
|
func GetFsNamed(in Params, fsName string) (f fs.Fs, err error) {
|
||||||
|
fsString, err := in.GetString(fsName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetCachedFs(fsString)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFs gets a fs.Fs named "fs" either from the cache or creates it afresh
|
||||||
|
func GetFs(in Params) (f fs.Fs, err error) {
|
||||||
|
return GetFsNamed(in, "fs")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFsAndRemoteNamed gets the fsName parameter from in, makes a
|
||||||
|
// remote or fetches it from the cache then gets the remoteName
|
||||||
|
// parameter from in too.
|
||||||
|
func GetFsAndRemoteNamed(in Params, fsName, remoteName string) (f fs.Fs, remote string, err error) {
|
||||||
|
remote, err = in.GetString(remoteName)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f, err = GetFsNamed(in, fsName)
|
||||||
|
return
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFsAndRemote gets the `fs` parameter from in, makes a remote or
|
||||||
|
// fetches it from the cache then gets the `remote` parameter from in
|
||||||
|
// too.
|
||||||
|
func GetFsAndRemote(in Params) (f fs.Fs, remote string, err error) {
|
||||||
|
return GetFsAndRemoteNamed(in, "fs", "remote")
|
||||||
|
}
|
||||||
138
fs/rc/cache_test.go
Normal file
138
fs/rc/cache_test.go
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
package rc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ncw/rclone/fs"
|
||||||
|
"github.com/ncw/rclone/fstest/mockfs"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
var called = 0
|
||||||
|
|
||||||
|
func mockNewFs(t *testing.T) func() {
|
||||||
|
called = 0
|
||||||
|
oldFsNewFs := fsNewFs
|
||||||
|
fsNewFs = func(path string) (fs.Fs, error) {
|
||||||
|
assert.Equal(t, 0, called)
|
||||||
|
called++
|
||||||
|
assert.Equal(t, "/", path)
|
||||||
|
return mockfs.NewFs("mock", "mock"), nil
|
||||||
|
}
|
||||||
|
return func() {
|
||||||
|
fsNewFs = oldFsNewFs
|
||||||
|
fsCacheMu.Lock()
|
||||||
|
fsCache = map[string]*cacheEntry{}
|
||||||
|
expireRunning = false
|
||||||
|
fsCacheMu.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetCachedFs(t *testing.T) {
|
||||||
|
defer mockNewFs(t)()
|
||||||
|
|
||||||
|
assert.Equal(t, 0, len(fsCache))
|
||||||
|
|
||||||
|
f, err := GetCachedFs("/")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, 1, len(fsCache))
|
||||||
|
|
||||||
|
f2, err := GetCachedFs("/")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, f, f2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCacheExpire(t *testing.T) {
|
||||||
|
defer mockNewFs(t)()
|
||||||
|
|
||||||
|
cacheExpireInterval = time.Millisecond
|
||||||
|
assert.Equal(t, false, expireRunning)
|
||||||
|
|
||||||
|
_, err := GetCachedFs("/")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
fsCacheMu.Lock()
|
||||||
|
entry := fsCache["/"]
|
||||||
|
|
||||||
|
assert.Equal(t, 1, len(fsCache))
|
||||||
|
fsCacheMu.Unlock()
|
||||||
|
cacheExpire()
|
||||||
|
fsCacheMu.Lock()
|
||||||
|
assert.Equal(t, 1, len(fsCache))
|
||||||
|
entry.lastUsed = time.Now().Add(-cacheExpireDuration - 60*time.Second)
|
||||||
|
assert.Equal(t, true, expireRunning)
|
||||||
|
fsCacheMu.Unlock()
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
fsCacheMu.Lock()
|
||||||
|
assert.Equal(t, false, expireRunning)
|
||||||
|
assert.Equal(t, 0, len(fsCache))
|
||||||
|
fsCacheMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetFsNamed(t *testing.T) {
|
||||||
|
defer mockNewFs(t)()
|
||||||
|
|
||||||
|
in := Params{
|
||||||
|
"potato": "/",
|
||||||
|
}
|
||||||
|
f, err := GetFsNamed(in, "potato")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotNil(t, f)
|
||||||
|
|
||||||
|
in = Params{
|
||||||
|
"sausage": "/",
|
||||||
|
}
|
||||||
|
f, err = GetFsNamed(in, "potato")
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Nil(t, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetFs(t *testing.T) {
|
||||||
|
defer mockNewFs(t)()
|
||||||
|
|
||||||
|
in := Params{
|
||||||
|
"fs": "/",
|
||||||
|
}
|
||||||
|
f, err := GetFs(in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotNil(t, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetFsAndRemoteNamed(t *testing.T) {
|
||||||
|
defer mockNewFs(t)()
|
||||||
|
|
||||||
|
in := Params{
|
||||||
|
"fs": "/",
|
||||||
|
"remote": "hello",
|
||||||
|
}
|
||||||
|
f, remote, err := GetFsAndRemoteNamed(in, "fs", "remote")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotNil(t, f)
|
||||||
|
assert.Equal(t, "hello", remote)
|
||||||
|
|
||||||
|
f, remote, err = GetFsAndRemoteNamed(in, "fsX", "remote")
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Nil(t, f)
|
||||||
|
|
||||||
|
f, remote, err = GetFsAndRemoteNamed(in, "fs", "remoteX")
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Nil(t, f)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetFsAndRemote(t *testing.T) {
|
||||||
|
defer mockNewFs(t)()
|
||||||
|
|
||||||
|
in := Params{
|
||||||
|
"fs": "/",
|
||||||
|
"remote": "hello",
|
||||||
|
}
|
||||||
|
f, remote, err := GetFsAndRemote(in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotNil(t, f)
|
||||||
|
assert.Equal(t, "hello", remote)
|
||||||
|
}
|
||||||
95
fs/rc/config.go
Normal file
95
fs/rc/config.go
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
// Implement config options reading and writing
|
||||||
|
//
|
||||||
|
// This is done here rather than in fs/fs.go so we don't cause a circular dependency
|
||||||
|
|
||||||
|
package rc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var optionBlock = map[string]interface{}{}
|
||||||
|
|
||||||
|
// AddOption adds an option set
|
||||||
|
func AddOption(name string, option interface{}) {
|
||||||
|
optionBlock[name] = option
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Add(Call{
|
||||||
|
Path: "options/blocks",
|
||||||
|
Fn: rcOptionsBlocks,
|
||||||
|
Title: "List all the option blocks",
|
||||||
|
Help: `Returns
|
||||||
|
- options - a list of the options block names`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the list of all the option blocks
|
||||||
|
func rcOptionsBlocks(in Params) (out Params, err error) {
|
||||||
|
options := []string{}
|
||||||
|
for name := range optionBlock {
|
||||||
|
options = append(options, name)
|
||||||
|
}
|
||||||
|
out = make(Params)
|
||||||
|
out["options"] = options
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Add(Call{
|
||||||
|
Path: "options/get",
|
||||||
|
Fn: rcOptionsGet,
|
||||||
|
Title: "Get all the options",
|
||||||
|
Help: `Returns an object where keys are option block names and values are an
|
||||||
|
object with the current option values in.
|
||||||
|
|
||||||
|
This shows the internal names of the option within rclone which should
|
||||||
|
map to the external options very easily with a few exceptions.
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the list of all the option blocks
|
||||||
|
func rcOptionsGet(in Params) (out Params, err error) {
|
||||||
|
out = make(Params)
|
||||||
|
for name, options := range optionBlock {
|
||||||
|
out[name] = options
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Add(Call{
|
||||||
|
Path: "options/set",
|
||||||
|
Fn: rcOptionsSet,
|
||||||
|
Title: "Set an option",
|
||||||
|
Help: `Parameters
|
||||||
|
|
||||||
|
- option block name containing an object with
|
||||||
|
- key: value
|
||||||
|
|
||||||
|
Repeated as often as required.
|
||||||
|
|
||||||
|
Only supply the options you wish to change. If an option is unknown
|
||||||
|
it will be silently ignored. Not all options will have an effect when
|
||||||
|
changed like this.
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set an option in an option block
|
||||||
|
func rcOptionsSet(in Params) (out Params, err error) {
|
||||||
|
for name, options := range in {
|
||||||
|
current := optionBlock[name]
|
||||||
|
if current == nil {
|
||||||
|
return nil, errors.Errorf("unknown option block %q", name)
|
||||||
|
}
|
||||||
|
err := Reshape(current, options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "failed to write options from block %q", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
88
fs/rc/config_test.go
Normal file
88
fs/rc/config_test.go
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
package rc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func clearOptionBlock() {
|
||||||
|
optionBlock = map[string]interface{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var testOptions = struct {
|
||||||
|
String string
|
||||||
|
Int int
|
||||||
|
}{
|
||||||
|
String: "hello",
|
||||||
|
Int: 42,
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddOption(t *testing.T) {
|
||||||
|
defer clearOptionBlock()
|
||||||
|
assert.Equal(t, len(optionBlock), 0)
|
||||||
|
AddOption("potato", &testOptions)
|
||||||
|
assert.Equal(t, len(optionBlock), 1)
|
||||||
|
assert.Equal(t, &testOptions, optionBlock["potato"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOptionsBlocks(t *testing.T) {
|
||||||
|
defer clearOptionBlock()
|
||||||
|
AddOption("potato", &testOptions)
|
||||||
|
call := Calls.Get("options/blocks")
|
||||||
|
require.NotNil(t, call)
|
||||||
|
in := Params{}
|
||||||
|
out, err := call.Fn(in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, out)
|
||||||
|
assert.Equal(t, Params{"options": []string{"potato"}}, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOptionsGet(t *testing.T) {
|
||||||
|
defer clearOptionBlock()
|
||||||
|
AddOption("potato", &testOptions)
|
||||||
|
call := Calls.Get("options/get")
|
||||||
|
require.NotNil(t, call)
|
||||||
|
in := Params{}
|
||||||
|
out, err := call.Fn(in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, out)
|
||||||
|
assert.Equal(t, Params{"potato": &testOptions}, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOptionsSet(t *testing.T) {
|
||||||
|
defer clearOptionBlock()
|
||||||
|
AddOption("potato", &testOptions)
|
||||||
|
call := Calls.Get("options/set")
|
||||||
|
require.NotNil(t, call)
|
||||||
|
|
||||||
|
in := Params{
|
||||||
|
"potato": Params{
|
||||||
|
"Int": 50,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
out, err := call.Fn(in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Nil(t, out)
|
||||||
|
assert.Equal(t, 50, testOptions.Int)
|
||||||
|
assert.Equal(t, "hello", testOptions.String)
|
||||||
|
|
||||||
|
// unknown option block
|
||||||
|
in = Params{
|
||||||
|
"sausage": Params{
|
||||||
|
"Int": 50,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
out, err = call.Fn(in)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "unknown option block")
|
||||||
|
|
||||||
|
// bad shape
|
||||||
|
in = Params{
|
||||||
|
"potato": []string{"a", "b"},
|
||||||
|
}
|
||||||
|
out, err = call.Fn(in)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "failed to write options")
|
||||||
|
}
|
||||||
@@ -6,10 +6,23 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/ncw/rclone/fs"
|
||||||
|
"github.com/ncw/rclone/fs/config/obscure"
|
||||||
|
"github.com/ncw/rclone/fs/version"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
Add(Call{
|
||||||
|
Path: "rc/noopauth",
|
||||||
|
AuthRequired: true,
|
||||||
|
Fn: rcNoop,
|
||||||
|
Title: "Echo the input to the output parameters requiring auth",
|
||||||
|
Help: `
|
||||||
|
This echoes the input parameters to the output parameters for testing
|
||||||
|
purposes. It can be used to check that rclone is still alive and to
|
||||||
|
check that parameter passing is working properly.`,
|
||||||
|
})
|
||||||
Add(Call{
|
Add(Call{
|
||||||
Path: "rc/noop",
|
Path: "rc/noop",
|
||||||
Fn: rcNoop,
|
Fn: rcNoop,
|
||||||
@@ -19,6 +32,14 @@ This echoes the input parameters to the output parameters for testing
|
|||||||
purposes. It can be used to check that rclone is still alive and to
|
purposes. It can be used to check that rclone is still alive and to
|
||||||
check that parameter passing is working properly.`,
|
check that parameter passing is working properly.`,
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Echo the input to the ouput parameters
|
||||||
|
func rcNoop(in Params) (out Params, err error) {
|
||||||
|
return in, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
Add(Call{
|
Add(Call{
|
||||||
Path: "rc/error",
|
Path: "rc/error",
|
||||||
Fn: rcError,
|
Fn: rcError,
|
||||||
@@ -27,6 +48,14 @@ check that parameter passing is working properly.`,
|
|||||||
This returns an error with the input as part of its error string.
|
This returns an error with the input as part of its error string.
|
||||||
Useful for testing error handling.`,
|
Useful for testing error handling.`,
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return an error regardless
|
||||||
|
func rcError(in Params) (out Params, err error) {
|
||||||
|
return nil, errors.Errorf("arbitrary error on input %+v", in)
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
Add(Call{
|
Add(Call{
|
||||||
Path: "rc/list",
|
Path: "rc/list",
|
||||||
Fn: rcList,
|
Fn: rcList,
|
||||||
@@ -35,6 +64,16 @@ Useful for testing error handling.`,
|
|||||||
This lists all the registered remote control commands as a JSON map in
|
This lists all the registered remote control commands as a JSON map in
|
||||||
the commands response.`,
|
the commands response.`,
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// List the registered commands
|
||||||
|
func rcList(in Params) (out Params, err error) {
|
||||||
|
out = make(Params)
|
||||||
|
out["commands"] = Calls.List()
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
Add(Call{
|
Add(Call{
|
||||||
Path: "core/pid",
|
Path: "core/pid",
|
||||||
Fn: rcPid,
|
Fn: rcPid,
|
||||||
@@ -43,6 +82,16 @@ the commands response.`,
|
|||||||
This returns PID of current process.
|
This returns PID of current process.
|
||||||
Useful for stopping rclone process.`,
|
Useful for stopping rclone process.`,
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return PID of current process
|
||||||
|
func rcPid(in Params) (out Params, err error) {
|
||||||
|
out = make(Params)
|
||||||
|
out["pid"] = os.Getpid()
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
Add(Call{
|
Add(Call{
|
||||||
Path: "core/memstats",
|
Path: "core/memstats",
|
||||||
Fn: rcMemStats,
|
Fn: rcMemStats,
|
||||||
@@ -59,40 +108,6 @@ The most interesting values for most people are:
|
|||||||
* It is virtual memory so may include unused memory
|
* It is virtual memory so may include unused memory
|
||||||
`,
|
`,
|
||||||
})
|
})
|
||||||
Add(Call{
|
|
||||||
Path: "core/gc",
|
|
||||||
Fn: rcGc,
|
|
||||||
Title: "Runs a garbage collection.",
|
|
||||||
Help: `
|
|
||||||
This tells the go runtime to do a garbage collection run. It isn't
|
|
||||||
necessary to call this normally, but it can be useful for debugging
|
|
||||||
memory problems.
|
|
||||||
`,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Echo the input to the ouput parameters
|
|
||||||
func rcNoop(in Params) (out Params, err error) {
|
|
||||||
return in, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return an error regardless
|
|
||||||
func rcError(in Params) (out Params, err error) {
|
|
||||||
return nil, errors.Errorf("arbitrary error on input %+v", in)
|
|
||||||
}
|
|
||||||
|
|
||||||
// List the registered commands
|
|
||||||
func rcList(in Params) (out Params, err error) {
|
|
||||||
out = make(Params)
|
|
||||||
out["commands"] = registry.list()
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return PID of current process
|
|
||||||
func rcPid(in Params) (out Params, err error) {
|
|
||||||
out = make(Params)
|
|
||||||
out["pid"] = os.Getpid()
|
|
||||||
return out, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the memory statistics
|
// Return the memory statistics
|
||||||
@@ -123,9 +138,88 @@ func rcMemStats(in Params) (out Params, err error) {
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Add(Call{
|
||||||
|
Path: "core/gc",
|
||||||
|
Fn: rcGc,
|
||||||
|
Title: "Runs a garbage collection.",
|
||||||
|
Help: `
|
||||||
|
This tells the go runtime to do a garbage collection run. It isn't
|
||||||
|
necessary to call this normally, but it can be useful for debugging
|
||||||
|
memory problems.
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Do a garbage collection run
|
// Do a garbage collection run
|
||||||
func rcGc(in Params) (out Params, err error) {
|
func rcGc(in Params) (out Params, err error) {
|
||||||
out = make(Params)
|
|
||||||
runtime.GC()
|
runtime.GC()
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Add(Call{
|
||||||
|
Path: "core/version",
|
||||||
|
Fn: rcVersion,
|
||||||
|
Title: "Shows the current version of rclone and the go runtime.",
|
||||||
|
Help: `
|
||||||
|
This shows the current version of go and the go runtime
|
||||||
|
- version - rclone version, eg "v1.44"
|
||||||
|
- decomposed - version number as [major, minor, patch, subpatch]
|
||||||
|
- note patch and subpatch will be 999 for a git compiled version
|
||||||
|
- isGit - boolean - true if this was compiled from the git version
|
||||||
|
- os - OS in use as according to Go
|
||||||
|
- arch - cpu architecture in use according to Go
|
||||||
|
- goVersion - version of Go runtime in use
|
||||||
|
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return version info
|
||||||
|
func rcVersion(in Params) (out Params, err error) {
|
||||||
|
decomposed, err := version.New(fs.Version)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out = Params{
|
||||||
|
"version": fs.Version,
|
||||||
|
"decomposed": decomposed,
|
||||||
|
"isGit": decomposed.IsGit(),
|
||||||
|
"os": runtime.GOOS,
|
||||||
|
"arch": runtime.GOARCH,
|
||||||
|
"goVersion": runtime.Version(),
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Add(Call{
|
||||||
|
Path: "core/obscure",
|
||||||
|
Fn: rcObscure,
|
||||||
|
Title: "Obscures a string passed in.",
|
||||||
|
Help: `
|
||||||
|
Pass a clear string and rclone will obscure it for the config file:
|
||||||
|
- clear - string
|
||||||
|
|
||||||
|
Returns
|
||||||
|
- obscured - string
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return obscured string
|
||||||
|
func rcObscure(in Params) (out Params, err error) {
|
||||||
|
clear, err := in.GetString("clear")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
obscured, err := obscure.Obscure(clear)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out = Params{
|
||||||
|
"obscured": obscured,
|
||||||
|
}
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|||||||
108
fs/rc/internal_test.go
Normal file
108
fs/rc/internal_test.go
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
package rc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ncw/rclone/fs"
|
||||||
|
"github.com/ncw/rclone/fs/config/obscure"
|
||||||
|
"github.com/ncw/rclone/fs/version"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInternalNoop(t *testing.T) {
|
||||||
|
call := Calls.Get("rc/noop")
|
||||||
|
assert.NotNil(t, call)
|
||||||
|
in := Params{
|
||||||
|
"String": "hello",
|
||||||
|
"Int": 42,
|
||||||
|
}
|
||||||
|
out, err := call.Fn(in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, out)
|
||||||
|
assert.Equal(t, in, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInternalError(t *testing.T) {
|
||||||
|
call := Calls.Get("rc/error")
|
||||||
|
assert.NotNil(t, call)
|
||||||
|
in := Params{}
|
||||||
|
out, err := call.Fn(in)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Nil(t, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInternalList(t *testing.T) {
|
||||||
|
call := Calls.Get("rc/list")
|
||||||
|
assert.NotNil(t, call)
|
||||||
|
in := Params{}
|
||||||
|
out, err := call.Fn(in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, out)
|
||||||
|
assert.Equal(t, Params{"commands": Calls.List()}, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCorePid(t *testing.T) {
|
||||||
|
call := Calls.Get("core/pid")
|
||||||
|
assert.NotNil(t, call)
|
||||||
|
in := Params{}
|
||||||
|
out, err := call.Fn(in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, out)
|
||||||
|
pid := out["pid"]
|
||||||
|
assert.NotEqual(t, nil, pid)
|
||||||
|
_, ok := pid.(int)
|
||||||
|
assert.Equal(t, true, ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCoreMemstats(t *testing.T) {
|
||||||
|
call := Calls.Get("core/memstats")
|
||||||
|
assert.NotNil(t, call)
|
||||||
|
in := Params{}
|
||||||
|
out, err := call.Fn(in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, out)
|
||||||
|
sys := out["Sys"]
|
||||||
|
assert.NotEqual(t, nil, sys)
|
||||||
|
_, ok := sys.(uint64)
|
||||||
|
assert.Equal(t, true, ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCoreGC(t *testing.T) {
|
||||||
|
call := Calls.Get("core/gc")
|
||||||
|
assert.NotNil(t, call)
|
||||||
|
in := Params{}
|
||||||
|
out, err := call.Fn(in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Nil(t, out)
|
||||||
|
assert.Equal(t, Params(nil), out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCoreVersion(t *testing.T) {
|
||||||
|
call := Calls.Get("core/version")
|
||||||
|
assert.NotNil(t, call)
|
||||||
|
in := Params{}
|
||||||
|
out, err := call.Fn(in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, out)
|
||||||
|
assert.Equal(t, fs.Version, out["version"])
|
||||||
|
assert.Equal(t, runtime.GOOS, out["os"])
|
||||||
|
assert.Equal(t, runtime.GOARCH, out["arch"])
|
||||||
|
assert.Equal(t, runtime.Version(), out["goVersion"])
|
||||||
|
_ = out["isGit"].(bool)
|
||||||
|
v := out["decomposed"].(version.Version)
|
||||||
|
assert.True(t, len(v) >= 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCoreObscure(t *testing.T) {
|
||||||
|
call := Calls.Get("core/obscure")
|
||||||
|
assert.NotNil(t, call)
|
||||||
|
in := Params{
|
||||||
|
"clear": "potato",
|
||||||
|
}
|
||||||
|
out, err := call.Fn(in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, out)
|
||||||
|
assert.Equal(t, in["clear"], obscure.MustReveal(out["obscured"].(string)))
|
||||||
|
}
|
||||||
215
fs/rc/job.go
Normal file
215
fs/rc/job.go
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
// Manage background jobs that the rc is running
|
||||||
|
|
||||||
|
package rc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// expire the job when it is finished and older than this
|
||||||
|
expireDuration = 60 * time.Second
|
||||||
|
// inteval to run the expire cache
|
||||||
|
expireInterval = 10 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
// Job describes a asynchronous task started via the rc package
|
||||||
|
type Job struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
StartTime time.Time `json:"startTime"`
|
||||||
|
EndTime time.Time `json:"endTime"`
|
||||||
|
Error string `json:"error"`
|
||||||
|
Finished bool `json:"finished"`
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Duration float64 `json:"duration"`
|
||||||
|
Output Params `json:"output"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jobs describes a collection of running tasks
|
||||||
|
type Jobs struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
jobs map[int64]*Job
|
||||||
|
expireInterval time.Duration
|
||||||
|
expireRunning bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
running = newJobs()
|
||||||
|
jobID = int64(0)
|
||||||
|
)
|
||||||
|
|
||||||
|
// newJobs makes a new Jobs structure
|
||||||
|
func newJobs() *Jobs {
|
||||||
|
return &Jobs{
|
||||||
|
jobs: map[int64]*Job{},
|
||||||
|
expireInterval: expireInterval,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// kickExpire makes sure Expire is running
|
||||||
|
func (jobs *Jobs) kickExpire() {
|
||||||
|
jobs.mu.Lock()
|
||||||
|
defer jobs.mu.Unlock()
|
||||||
|
if !jobs.expireRunning {
|
||||||
|
time.AfterFunc(jobs.expireInterval, jobs.Expire)
|
||||||
|
jobs.expireRunning = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expire expires any jobs that haven't been collected
|
||||||
|
func (jobs *Jobs) Expire() {
|
||||||
|
jobs.mu.Lock()
|
||||||
|
defer jobs.mu.Unlock()
|
||||||
|
now := time.Now()
|
||||||
|
for ID, job := range jobs.jobs {
|
||||||
|
job.mu.Lock()
|
||||||
|
if job.Finished && now.Sub(job.EndTime) > expireDuration {
|
||||||
|
delete(jobs.jobs, ID)
|
||||||
|
}
|
||||||
|
job.mu.Unlock()
|
||||||
|
}
|
||||||
|
if len(jobs.jobs) != 0 {
|
||||||
|
time.AfterFunc(jobs.expireInterval, jobs.Expire)
|
||||||
|
jobs.expireRunning = true
|
||||||
|
} else {
|
||||||
|
jobs.expireRunning = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IDs returns the IDs of the running jobs
|
||||||
|
func (jobs *Jobs) IDs() (IDs []int64) {
|
||||||
|
jobs.mu.RLock()
|
||||||
|
defer jobs.mu.RUnlock()
|
||||||
|
IDs = []int64{}
|
||||||
|
for ID := range jobs.jobs {
|
||||||
|
IDs = append(IDs, ID)
|
||||||
|
}
|
||||||
|
return IDs
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a job with a given ID or nil if it doesn't exist
|
||||||
|
func (jobs *Jobs) Get(ID int64) *Job {
|
||||||
|
jobs.mu.RLock()
|
||||||
|
defer jobs.mu.RUnlock()
|
||||||
|
return jobs.jobs[ID]
|
||||||
|
}
|
||||||
|
|
||||||
|
// mark the job as finished
|
||||||
|
func (job *Job) finish(out Params, err error) {
|
||||||
|
job.mu.Lock()
|
||||||
|
job.EndTime = time.Now()
|
||||||
|
if out == nil {
|
||||||
|
out = make(Params)
|
||||||
|
}
|
||||||
|
job.Output = out
|
||||||
|
job.Duration = job.EndTime.Sub(job.StartTime).Seconds()
|
||||||
|
if err != nil {
|
||||||
|
job.Error = err.Error()
|
||||||
|
job.Success = false
|
||||||
|
} else {
|
||||||
|
job.Error = ""
|
||||||
|
job.Success = true
|
||||||
|
}
|
||||||
|
job.Finished = true
|
||||||
|
job.mu.Unlock()
|
||||||
|
running.kickExpire() // make sure this job gets expired
|
||||||
|
}
|
||||||
|
|
||||||
|
// run the job until completion writing the return status
|
||||||
|
func (job *Job) run(fn Func, in Params) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
job.finish(nil, errors.Errorf("panic received: %v", r))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
job.finish(fn(in))
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewJob start a new Job off
|
||||||
|
func (jobs *Jobs) NewJob(fn Func, in Params) *Job {
|
||||||
|
job := &Job{
|
||||||
|
ID: atomic.AddInt64(&jobID, 1),
|
||||||
|
StartTime: time.Now(),
|
||||||
|
}
|
||||||
|
go job.run(fn, in)
|
||||||
|
jobs.mu.Lock()
|
||||||
|
jobs.jobs[job.ID] = job
|
||||||
|
jobs.mu.Unlock()
|
||||||
|
return job
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartJob starts a new job and returns a Param suitable for output
|
||||||
|
func StartJob(fn Func, in Params) (Params, error) {
|
||||||
|
job := running.NewJob(fn, in)
|
||||||
|
out := make(Params)
|
||||||
|
out["jobid"] = job.ID
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Add(Call{
|
||||||
|
Path: "job/status",
|
||||||
|
Fn: rcJobStatus,
|
||||||
|
Title: "Reads the status of the job ID",
|
||||||
|
Help: `Parameters
|
||||||
|
- jobid - id of the job (integer)
|
||||||
|
|
||||||
|
Results
|
||||||
|
- finished - boolean
|
||||||
|
- duration - time in seconds that the job ran for
|
||||||
|
- endTime - time the job finished (eg "2018-10-26T18:50:20.528746884+01:00")
|
||||||
|
- error - error from the job or empty string for no error
|
||||||
|
- finished - boolean whether the job has finished or not
|
||||||
|
- id - as passed in above
|
||||||
|
- startTime - time the job started (eg "2018-10-26T18:50:20.528336039+01:00")
|
||||||
|
- success - boolean - true for success false otherwise
|
||||||
|
- output - output of the job as would have been returned if called synchronously
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the status of a job
|
||||||
|
func rcJobStatus(in Params) (out Params, err error) {
|
||||||
|
jobID, err := in.GetInt64("jobid")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
job := running.Get(jobID)
|
||||||
|
if job == nil {
|
||||||
|
return nil, errors.New("job not found")
|
||||||
|
}
|
||||||
|
job.mu.Lock()
|
||||||
|
defer job.mu.Unlock()
|
||||||
|
out = make(Params)
|
||||||
|
err = Reshape(&out, job)
|
||||||
|
if job == nil {
|
||||||
|
return nil, errors.New("Reshape failed in job status")
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Add(Call{
|
||||||
|
Path: "job/list",
|
||||||
|
Fn: rcJobList,
|
||||||
|
Title: "Lists the IDs of the running jobs",
|
||||||
|
Help: `Parameters - None
|
||||||
|
|
||||||
|
Results
|
||||||
|
- jobids - array of integer job ids
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the status of a job
|
||||||
|
func rcJobList(in Params) (out Params, err error) {
|
||||||
|
out = make(Params)
|
||||||
|
out["jobids"] = running.IDs()
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
217
fs/rc/job_test.go
Normal file
217
fs/rc/job_test.go
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
package rc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewJobs(t *testing.T) {
|
||||||
|
jobs := newJobs()
|
||||||
|
assert.Equal(t, 0, len(jobs.jobs))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJobsKickExpire(t *testing.T) {
|
||||||
|
jobs := newJobs()
|
||||||
|
jobs.expireInterval = time.Millisecond
|
||||||
|
assert.Equal(t, false, jobs.expireRunning)
|
||||||
|
jobs.kickExpire()
|
||||||
|
jobs.mu.Lock()
|
||||||
|
assert.Equal(t, true, jobs.expireRunning)
|
||||||
|
jobs.mu.Unlock()
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
jobs.mu.Lock()
|
||||||
|
assert.Equal(t, false, jobs.expireRunning)
|
||||||
|
jobs.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJobsExpire(t *testing.T) {
|
||||||
|
wait := make(chan struct{})
|
||||||
|
jobs := newJobs()
|
||||||
|
jobs.expireInterval = time.Millisecond
|
||||||
|
assert.Equal(t, false, jobs.expireRunning)
|
||||||
|
job := jobs.NewJob(func(in Params) (Params, error) {
|
||||||
|
defer close(wait)
|
||||||
|
return in, nil
|
||||||
|
}, Params{})
|
||||||
|
<-wait
|
||||||
|
assert.Equal(t, 1, len(jobs.jobs))
|
||||||
|
jobs.Expire()
|
||||||
|
assert.Equal(t, 1, len(jobs.jobs))
|
||||||
|
jobs.mu.Lock()
|
||||||
|
job.EndTime = time.Now().Add(-expireDuration - 60*time.Second)
|
||||||
|
assert.Equal(t, true, jobs.expireRunning)
|
||||||
|
jobs.mu.Unlock()
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
jobs.mu.Lock()
|
||||||
|
assert.Equal(t, false, jobs.expireRunning)
|
||||||
|
assert.Equal(t, 0, len(jobs.jobs))
|
||||||
|
jobs.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
var noopFn = func(in Params) (Params, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJobsIDs(t *testing.T) {
|
||||||
|
jobs := newJobs()
|
||||||
|
job1 := jobs.NewJob(noopFn, Params{})
|
||||||
|
job2 := jobs.NewJob(noopFn, Params{})
|
||||||
|
wantIDs := []int64{job1.ID, job2.ID}
|
||||||
|
gotIDs := jobs.IDs()
|
||||||
|
require.Equal(t, 2, len(gotIDs))
|
||||||
|
if gotIDs[0] != wantIDs[0] {
|
||||||
|
gotIDs[0], gotIDs[1] = gotIDs[1], gotIDs[0]
|
||||||
|
}
|
||||||
|
assert.Equal(t, wantIDs, gotIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJobsGet(t *testing.T) {
|
||||||
|
jobs := newJobs()
|
||||||
|
job := jobs.NewJob(noopFn, Params{})
|
||||||
|
assert.Equal(t, job, jobs.Get(job.ID))
|
||||||
|
assert.Nil(t, jobs.Get(123123123123))
|
||||||
|
}
|
||||||
|
|
||||||
|
var longFn = func(in Params) (Params, error) {
|
||||||
|
time.Sleep(1 * time.Hour)
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJobFinish(t *testing.T) {
|
||||||
|
jobs := newJobs()
|
||||||
|
job := jobs.NewJob(longFn, Params{})
|
||||||
|
|
||||||
|
assert.Equal(t, true, job.EndTime.IsZero())
|
||||||
|
assert.Equal(t, Params(nil), job.Output)
|
||||||
|
assert.Equal(t, 0.0, job.Duration)
|
||||||
|
assert.Equal(t, "", job.Error)
|
||||||
|
assert.Equal(t, false, job.Success)
|
||||||
|
assert.Equal(t, false, job.Finished)
|
||||||
|
|
||||||
|
wantOut := Params{"a": 1}
|
||||||
|
job.finish(wantOut, nil)
|
||||||
|
|
||||||
|
assert.Equal(t, false, job.EndTime.IsZero())
|
||||||
|
assert.Equal(t, wantOut, job.Output)
|
||||||
|
assert.NotEqual(t, 0.0, job.Duration)
|
||||||
|
assert.Equal(t, "", job.Error)
|
||||||
|
assert.Equal(t, true, job.Success)
|
||||||
|
assert.Equal(t, true, job.Finished)
|
||||||
|
|
||||||
|
job = jobs.NewJob(longFn, Params{})
|
||||||
|
job.finish(nil, nil)
|
||||||
|
|
||||||
|
assert.Equal(t, false, job.EndTime.IsZero())
|
||||||
|
assert.Equal(t, Params{}, job.Output)
|
||||||
|
assert.NotEqual(t, 0.0, job.Duration)
|
||||||
|
assert.Equal(t, "", job.Error)
|
||||||
|
assert.Equal(t, true, job.Success)
|
||||||
|
assert.Equal(t, true, job.Finished)
|
||||||
|
|
||||||
|
job = jobs.NewJob(longFn, Params{})
|
||||||
|
job.finish(wantOut, errors.New("potato"))
|
||||||
|
|
||||||
|
assert.Equal(t, false, job.EndTime.IsZero())
|
||||||
|
assert.Equal(t, wantOut, job.Output)
|
||||||
|
assert.NotEqual(t, 0.0, job.Duration)
|
||||||
|
assert.Equal(t, "potato", job.Error)
|
||||||
|
assert.Equal(t, false, job.Success)
|
||||||
|
assert.Equal(t, true, job.Finished)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We've tested the functionality of run() already as it is
|
||||||
|
// part of NewJob, now just test the panic catching
|
||||||
|
func TestJobRunPanic(t *testing.T) {
|
||||||
|
wait := make(chan struct{})
|
||||||
|
boom := func(in Params) (Params, error) {
|
||||||
|
defer close(wait)
|
||||||
|
panic("boom")
|
||||||
|
}
|
||||||
|
|
||||||
|
jobs := newJobs()
|
||||||
|
job := jobs.NewJob(boom, Params{})
|
||||||
|
<-wait
|
||||||
|
runtime.Gosched() // yield to make sure job is updated
|
||||||
|
|
||||||
|
// Wait a short time for the panic to propagate
|
||||||
|
for i := uint(0); i < 10; i++ {
|
||||||
|
job.mu.Lock()
|
||||||
|
e := job.Error
|
||||||
|
job.mu.Unlock()
|
||||||
|
if e != "" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(time.Millisecond << i)
|
||||||
|
}
|
||||||
|
|
||||||
|
job.mu.Lock()
|
||||||
|
assert.Equal(t, false, job.EndTime.IsZero())
|
||||||
|
assert.Equal(t, Params{}, job.Output)
|
||||||
|
assert.NotEqual(t, 0.0, job.Duration)
|
||||||
|
assert.Equal(t, "panic received: boom", job.Error)
|
||||||
|
assert.Equal(t, false, job.Success)
|
||||||
|
assert.Equal(t, true, job.Finished)
|
||||||
|
job.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJobsNewJob(t *testing.T) {
|
||||||
|
jobID = 0
|
||||||
|
jobs := newJobs()
|
||||||
|
job := jobs.NewJob(noopFn, Params{})
|
||||||
|
assert.Equal(t, int64(1), job.ID)
|
||||||
|
assert.Equal(t, job, jobs.Get(1))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStartJob(t *testing.T) {
|
||||||
|
jobID = 0
|
||||||
|
out, err := StartJob(longFn, Params{})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, Params{"jobid": int64(1)}, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRcJobStatus(t *testing.T) {
|
||||||
|
jobID = 0
|
||||||
|
_, err := StartJob(longFn, Params{})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
call := Calls.Get("job/status")
|
||||||
|
assert.NotNil(t, call)
|
||||||
|
in := Params{"jobid": 1}
|
||||||
|
out, err := call.Fn(in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, out)
|
||||||
|
assert.Equal(t, float64(1), out["id"])
|
||||||
|
assert.Equal(t, "", out["error"])
|
||||||
|
assert.Equal(t, false, out["finished"])
|
||||||
|
assert.Equal(t, false, out["success"])
|
||||||
|
|
||||||
|
in = Params{"jobid": 123123123}
|
||||||
|
_, err = call.Fn(in)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "job not found")
|
||||||
|
|
||||||
|
in = Params{"jobidx": 123123123}
|
||||||
|
_, err = call.Fn(in)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "Didn't find key")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRcJobList(t *testing.T) {
|
||||||
|
jobID = 0
|
||||||
|
_, err := StartJob(longFn, Params{})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
call := Calls.Get("job/list")
|
||||||
|
assert.NotNil(t, call)
|
||||||
|
in := Params{}
|
||||||
|
out, err := call.Fn(in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, out)
|
||||||
|
assert.Equal(t, Params{"jobids": []int64{1}}, out)
|
||||||
|
}
|
||||||
204
fs/rc/params.go
Normal file
204
fs/rc/params.go
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
// Parameter parsing
|
||||||
|
|
||||||
|
package rc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Params is the input and output type for the Func
|
||||||
|
type Params map[string]interface{}
|
||||||
|
|
||||||
|
// ErrParamNotFound - this is returned from the Get* functions if the
|
||||||
|
// parameter isn't found along with a zero value of the requested
|
||||||
|
// item.
|
||||||
|
//
|
||||||
|
// Returning an error of this type from an rc.Func will cause the http
|
||||||
|
// method to return http.StatusBadRequest
|
||||||
|
type ErrParamNotFound string
|
||||||
|
|
||||||
|
// Error turns this error into a string
|
||||||
|
func (e ErrParamNotFound) Error() string {
|
||||||
|
return fmt.Sprintf("Didn't find key %q in input", string(e))
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsErrParamNotFound returns whether err is ErrParamNotFound
|
||||||
|
func IsErrParamNotFound(err error) bool {
|
||||||
|
_, isNotFound := err.(ErrParamNotFound)
|
||||||
|
return isNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotErrParamNotFound returns true if err != nil and
|
||||||
|
// !IsErrParamNotFound(err)
|
||||||
|
//
|
||||||
|
// This is for checking error returns of the Get* functions to ignore
|
||||||
|
// error not found returns and take the default value.
|
||||||
|
func NotErrParamNotFound(err error) bool {
|
||||||
|
return err != nil && !IsErrParamNotFound(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrParamInvalid - this is returned from the Get* functions if the
|
||||||
|
// parameter is invalid.
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Returning an error of this type from an rc.Func will cause the http
|
||||||
|
// method to return http.StatusBadRequest
|
||||||
|
type ErrParamInvalid struct {
|
||||||
|
error
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsErrParamInvalid returns whether err is ErrParamInvalid
|
||||||
|
func IsErrParamInvalid(err error) bool {
|
||||||
|
_, isInvalid := err.(ErrParamInvalid)
|
||||||
|
return isInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reshape reshapes one blob of data into another via json serialization
|
||||||
|
//
|
||||||
|
// out should be a pointer type
|
||||||
|
//
|
||||||
|
// This isn't a very efficient way of dealing with this!
|
||||||
|
func Reshape(out interface{}, in interface{}) error {
|
||||||
|
b, err := json.Marshal(in)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "Reshape failed to Marshal")
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(b, out)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "Reshape failed to Unmarshal")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get gets a parameter from the input
|
||||||
|
//
|
||||||
|
// If the parameter isn't found then error will be of type
|
||||||
|
// ErrParamNotFound and the returned value will be nil.
|
||||||
|
func (p Params) Get(key string) (interface{}, error) {
|
||||||
|
value, ok := p[key]
|
||||||
|
if !ok {
|
||||||
|
return nil, ErrParamNotFound(key)
|
||||||
|
}
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetString gets a string parameter from the input
|
||||||
|
//
|
||||||
|
// If the parameter isn't found then error will be of type
|
||||||
|
// ErrParamNotFound and the returned value will be "".
|
||||||
|
func (p Params) GetString(key string) (string, error) {
|
||||||
|
value, err := p.Get(key)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
str, ok := value.(string)
|
||||||
|
if !ok {
|
||||||
|
return "", ErrParamInvalid{errors.Errorf("expecting string value for key %q (was %T)", key, value)}
|
||||||
|
}
|
||||||
|
return str, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInt64 gets a int64 parameter from the input
|
||||||
|
//
|
||||||
|
// If the parameter isn't found then error will be of type
|
||||||
|
// ErrParamNotFound and the returned value will be 0.
|
||||||
|
func (p Params) GetInt64(key string) (int64, error) {
|
||||||
|
value, err := p.Get(key)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
switch x := value.(type) {
|
||||||
|
case int:
|
||||||
|
return int64(x), nil
|
||||||
|
case int64:
|
||||||
|
return x, nil
|
||||||
|
case float64:
|
||||||
|
if x > math.MaxInt64 || x < math.MinInt64 {
|
||||||
|
return 0, ErrParamInvalid{errors.Errorf("key %q (%v) overflows int64 ", key, value)}
|
||||||
|
}
|
||||||
|
return int64(x), nil
|
||||||
|
case string:
|
||||||
|
i, err := strconv.ParseInt(x, 10, 0)
|
||||||
|
if err != nil {
|
||||||
|
return 0, ErrParamInvalid{errors.Wrapf(err, "couldn't parse key %q (%v) as int64", key, value)}
|
||||||
|
}
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
|
return 0, ErrParamInvalid{errors.Errorf("expecting int64 value for key %q (was %T)", key, value)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFloat64 gets a float64 parameter from the input
|
||||||
|
//
|
||||||
|
// If the parameter isn't found then error will be of type
|
||||||
|
// ErrParamNotFound and the returned value will be 0.
|
||||||
|
func (p Params) GetFloat64(key string) (float64, error) {
|
||||||
|
value, err := p.Get(key)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
switch x := value.(type) {
|
||||||
|
case float64:
|
||||||
|
return x, nil
|
||||||
|
case int:
|
||||||
|
return float64(x), nil
|
||||||
|
case int64:
|
||||||
|
return float64(x), nil
|
||||||
|
case string:
|
||||||
|
f, err := strconv.ParseFloat(x, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, ErrParamInvalid{errors.Wrapf(err, "couldn't parse key %q (%v) as float64", key, value)}
|
||||||
|
}
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
return 0, ErrParamInvalid{errors.Errorf("expecting float64 value for key %q (was %T)", key, value)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBool gets a boolean parameter from the input
|
||||||
|
//
|
||||||
|
// If the parameter isn't found then error will be of type
|
||||||
|
// ErrParamNotFound and the returned value will be false.
|
||||||
|
func (p Params) GetBool(key string) (bool, error) {
|
||||||
|
value, err := p.Get(key)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
switch x := value.(type) {
|
||||||
|
case int:
|
||||||
|
return x != 0, nil
|
||||||
|
case int64:
|
||||||
|
return x != 0, nil
|
||||||
|
case float64:
|
||||||
|
return x != 0, nil
|
||||||
|
case bool:
|
||||||
|
return x, nil
|
||||||
|
case string:
|
||||||
|
b, err := strconv.ParseBool(x)
|
||||||
|
if err != nil {
|
||||||
|
return false, ErrParamInvalid{errors.Wrapf(err, "couldn't parse key %q (%v) as bool", key, value)}
|
||||||
|
}
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
return false, ErrParamInvalid{errors.Errorf("expecting bool value for key %q (was %T)", key, value)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStruct gets a struct from key from the input into the struct
|
||||||
|
// pointed to by out. out must be a pointer type.
|
||||||
|
//
|
||||||
|
// If the parameter isn't found then error will be of type
|
||||||
|
// ErrParamNotFound and out will be unchanged.
|
||||||
|
func (p Params) GetStruct(key string, out interface{}) error {
|
||||||
|
value, err := p.Get(key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = Reshape(out, value)
|
||||||
|
if err != nil {
|
||||||
|
return ErrParamInvalid{errors.Wrapf(err, "key %q", key)}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
251
fs/rc/params_test.go
Normal file
251
fs/rc/params_test.go
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
package rc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestErrParamNotFoundError(t *testing.T) {
|
||||||
|
e := ErrParamNotFound("key")
|
||||||
|
assert.Equal(t, "Didn't find key \"key\" in input", e.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsErrParamNotFound(t *testing.T) {
|
||||||
|
assert.Equal(t, true, IsErrParamNotFound(ErrParamNotFound("key")))
|
||||||
|
assert.Equal(t, false, IsErrParamNotFound(nil))
|
||||||
|
assert.Equal(t, false, IsErrParamNotFound(errors.New("potato")))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNotErrParamNotFound(t *testing.T) {
|
||||||
|
assert.Equal(t, false, NotErrParamNotFound(ErrParamNotFound("key")))
|
||||||
|
assert.Equal(t, false, NotErrParamNotFound(nil))
|
||||||
|
assert.Equal(t, true, NotErrParamNotFound(errors.New("potato")))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsErrParamInvalid(t *testing.T) {
|
||||||
|
e := ErrParamInvalid{errors.New("potato")}
|
||||||
|
assert.Equal(t, true, IsErrParamInvalid(e))
|
||||||
|
assert.Equal(t, false, IsErrParamInvalid(nil))
|
||||||
|
assert.Equal(t, false, IsErrParamInvalid(errors.New("potato")))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReshape(t *testing.T) {
|
||||||
|
in := Params{
|
||||||
|
"String": "hello",
|
||||||
|
"Float": 4.2,
|
||||||
|
}
|
||||||
|
var out struct {
|
||||||
|
String string
|
||||||
|
Float float64
|
||||||
|
}
|
||||||
|
require.NoError(t, Reshape(&out, in))
|
||||||
|
assert.Equal(t, "hello", out.String)
|
||||||
|
assert.Equal(t, 4.2, out.Float)
|
||||||
|
var inCopy = Params{}
|
||||||
|
require.NoError(t, Reshape(&inCopy, out))
|
||||||
|
assert.Equal(t, in, inCopy)
|
||||||
|
|
||||||
|
// Now a failure to marshal
|
||||||
|
var in2 func()
|
||||||
|
require.Error(t, Reshape(&inCopy, in2))
|
||||||
|
|
||||||
|
// Now a failure to unmarshal
|
||||||
|
require.Error(t, Reshape(&out, "string"))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParamsGet(t *testing.T) {
|
||||||
|
in := Params{
|
||||||
|
"ok": 1,
|
||||||
|
}
|
||||||
|
v1, e1 := in.Get("ok")
|
||||||
|
assert.NoError(t, e1)
|
||||||
|
assert.Equal(t, 1, v1)
|
||||||
|
v2, e2 := in.Get("notOK")
|
||||||
|
assert.Error(t, e2)
|
||||||
|
assert.Equal(t, nil, v2)
|
||||||
|
assert.Equal(t, ErrParamNotFound("notOK"), e2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParamsGetString(t *testing.T) {
|
||||||
|
in := Params{
|
||||||
|
"string": "one",
|
||||||
|
"notString": 17,
|
||||||
|
}
|
||||||
|
v1, e1 := in.GetString("string")
|
||||||
|
assert.NoError(t, e1)
|
||||||
|
assert.Equal(t, "one", v1)
|
||||||
|
v2, e2 := in.GetString("notOK")
|
||||||
|
assert.Error(t, e2)
|
||||||
|
assert.Equal(t, "", v2)
|
||||||
|
assert.Equal(t, ErrParamNotFound("notOK"), e2)
|
||||||
|
v3, e3 := in.GetString("notString")
|
||||||
|
assert.Error(t, e3)
|
||||||
|
assert.Equal(t, "", v3)
|
||||||
|
assert.Equal(t, true, IsErrParamInvalid(e3), e3.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParamsGetInt64(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
value interface{}
|
||||||
|
result int64
|
||||||
|
errString string
|
||||||
|
}{
|
||||||
|
{"123", 123, ""},
|
||||||
|
{"123x", 0, "couldn't parse"},
|
||||||
|
{int(12), 12, ""},
|
||||||
|
{int64(13), 13, ""},
|
||||||
|
{float64(14), 14, ""},
|
||||||
|
{float64(9.3E18), 0, "overflows int64"},
|
||||||
|
{float64(-9.3E18), 0, "overflows int64"},
|
||||||
|
} {
|
||||||
|
t.Run(fmt.Sprintf("%T=%v", test.value, test.value), func(t *testing.T) {
|
||||||
|
in := Params{
|
||||||
|
"key": test.value,
|
||||||
|
}
|
||||||
|
v1, e1 := in.GetInt64("key")
|
||||||
|
if test.errString == "" {
|
||||||
|
require.NoError(t, e1)
|
||||||
|
assert.Equal(t, test.result, v1)
|
||||||
|
} else {
|
||||||
|
require.NotNil(t, e1)
|
||||||
|
require.Error(t, e1)
|
||||||
|
assert.Contains(t, e1.Error(), test.errString)
|
||||||
|
assert.Equal(t, int64(0), v1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
in := Params{
|
||||||
|
"notInt64": []string{"a", "b"},
|
||||||
|
}
|
||||||
|
v2, e2 := in.GetInt64("notOK")
|
||||||
|
assert.Error(t, e2)
|
||||||
|
assert.Equal(t, int64(0), v2)
|
||||||
|
assert.Equal(t, ErrParamNotFound("notOK"), e2)
|
||||||
|
v3, e3 := in.GetInt64("notInt64")
|
||||||
|
assert.Error(t, e3)
|
||||||
|
assert.Equal(t, int64(0), v3)
|
||||||
|
assert.Equal(t, true, IsErrParamInvalid(e3), e3.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParamsGetFloat64(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
value interface{}
|
||||||
|
result float64
|
||||||
|
errString string
|
||||||
|
}{
|
||||||
|
{"123.1", 123.1, ""},
|
||||||
|
{"123x1", 0, "couldn't parse"},
|
||||||
|
{int(12), 12, ""},
|
||||||
|
{int64(13), 13, ""},
|
||||||
|
{float64(14), 14, ""},
|
||||||
|
} {
|
||||||
|
t.Run(fmt.Sprintf("%T=%v", test.value, test.value), func(t *testing.T) {
|
||||||
|
in := Params{
|
||||||
|
"key": test.value,
|
||||||
|
}
|
||||||
|
v1, e1 := in.GetFloat64("key")
|
||||||
|
if test.errString == "" {
|
||||||
|
require.NoError(t, e1)
|
||||||
|
assert.Equal(t, test.result, v1)
|
||||||
|
} else {
|
||||||
|
require.NotNil(t, e1)
|
||||||
|
require.Error(t, e1)
|
||||||
|
assert.Contains(t, e1.Error(), test.errString)
|
||||||
|
assert.Equal(t, float64(0), v1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
in := Params{
|
||||||
|
"notFloat64": []string{"a", "b"},
|
||||||
|
}
|
||||||
|
v2, e2 := in.GetFloat64("notOK")
|
||||||
|
assert.Error(t, e2)
|
||||||
|
assert.Equal(t, float64(0), v2)
|
||||||
|
assert.Equal(t, ErrParamNotFound("notOK"), e2)
|
||||||
|
v3, e3 := in.GetFloat64("notFloat64")
|
||||||
|
assert.Error(t, e3)
|
||||||
|
assert.Equal(t, float64(0), v3)
|
||||||
|
assert.Equal(t, true, IsErrParamInvalid(e3), e3.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParamsGetBool(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
value interface{}
|
||||||
|
result bool
|
||||||
|
errString string
|
||||||
|
}{
|
||||||
|
{true, true, ""},
|
||||||
|
{false, false, ""},
|
||||||
|
{"true", true, ""},
|
||||||
|
{"false", false, ""},
|
||||||
|
{"fasle", false, "couldn't parse"},
|
||||||
|
{int(12), true, ""},
|
||||||
|
{int(0), false, ""},
|
||||||
|
{int64(13), true, ""},
|
||||||
|
{int64(0), false, ""},
|
||||||
|
{float64(14), true, ""},
|
||||||
|
{float64(0), false, ""},
|
||||||
|
} {
|
||||||
|
t.Run(fmt.Sprintf("%T=%v", test.value, test.value), func(t *testing.T) {
|
||||||
|
in := Params{
|
||||||
|
"key": test.value,
|
||||||
|
}
|
||||||
|
v1, e1 := in.GetBool("key")
|
||||||
|
if test.errString == "" {
|
||||||
|
require.NoError(t, e1)
|
||||||
|
assert.Equal(t, test.result, v1)
|
||||||
|
} else {
|
||||||
|
require.NotNil(t, e1)
|
||||||
|
require.Error(t, e1)
|
||||||
|
assert.Contains(t, e1.Error(), test.errString)
|
||||||
|
assert.Equal(t, false, v1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
in := Params{
|
||||||
|
"notBool": []string{"a", "b"},
|
||||||
|
}
|
||||||
|
v2, e2 := Params{}.GetBool("notOK")
|
||||||
|
assert.Error(t, e2)
|
||||||
|
assert.Equal(t, false, v2)
|
||||||
|
assert.Equal(t, ErrParamNotFound("notOK"), e2)
|
||||||
|
v3, e3 := in.GetBool("notBool")
|
||||||
|
assert.Error(t, e3)
|
||||||
|
assert.Equal(t, false, v3)
|
||||||
|
assert.Equal(t, true, IsErrParamInvalid(e3), e3.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParamsGetStruct(t *testing.T) {
|
||||||
|
in := Params{
|
||||||
|
"struct": Params{
|
||||||
|
"String": "one",
|
||||||
|
"Float": 4.2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
var out struct {
|
||||||
|
String string
|
||||||
|
Float float64
|
||||||
|
}
|
||||||
|
e1 := in.GetStruct("struct", &out)
|
||||||
|
assert.NoError(t, e1)
|
||||||
|
assert.Equal(t, "one", out.String)
|
||||||
|
assert.Equal(t, 4.2, out.Float)
|
||||||
|
|
||||||
|
e2 := in.GetStruct("notOK", &out)
|
||||||
|
assert.Error(t, e2)
|
||||||
|
assert.Equal(t, "one", out.String)
|
||||||
|
assert.Equal(t, 4.2, out.Float)
|
||||||
|
assert.Equal(t, ErrParamNotFound("notOK"), e2)
|
||||||
|
|
||||||
|
in["struct"] = "string"
|
||||||
|
e3 := in.GetStruct("struct", &out)
|
||||||
|
assert.Error(t, e3)
|
||||||
|
assert.Equal(t, "one", out.String)
|
||||||
|
assert.Equal(t, 4.2, out.Float)
|
||||||
|
assert.Equal(t, true, IsErrParamInvalid(e3), e3.Error())
|
||||||
|
}
|
||||||
140
fs/rc/rc.go
140
fs/rc/rc.go
@@ -10,19 +10,18 @@ package rc
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
|
||||||
_ "net/http/pprof" // install the pprof http handlers
|
_ "net/http/pprof" // install the pprof http handlers
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/ncw/rclone/cmd/serve/httplib"
|
"github.com/ncw/rclone/cmd/serve/httplib"
|
||||||
"github.com/ncw/rclone/fs"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Options contains options for the remote control server
|
// Options contains options for the remote control server
|
||||||
type Options struct {
|
type Options struct {
|
||||||
HTTPOptions httplib.Options
|
HTTPOptions httplib.Options
|
||||||
Enabled bool
|
Enabled bool // set to enable the server
|
||||||
|
Serve bool // set to serve files from remotes
|
||||||
|
Files string // set to enable serving files locally
|
||||||
|
NoAuth bool // set to disable auth checks on AuthRequired methods
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultOpt is the default values used for Options
|
// DefaultOpt is the default values used for Options
|
||||||
@@ -35,140 +34,9 @@ func init() {
|
|||||||
DefaultOpt.HTTPOptions.ListenAddr = "localhost:5572"
|
DefaultOpt.HTTPOptions.ListenAddr = "localhost:5572"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start the remote control server if configured
|
|
||||||
func Start(opt *Options) {
|
|
||||||
if opt.Enabled {
|
|
||||||
s := newServer(opt)
|
|
||||||
go s.serve()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// server contains everything to run the server
|
|
||||||
type server struct {
|
|
||||||
srv *httplib.Server
|
|
||||||
}
|
|
||||||
|
|
||||||
func newServer(opt *Options) *server {
|
|
||||||
// Serve on the DefaultServeMux so can have global registrations appear
|
|
||||||
mux := http.DefaultServeMux
|
|
||||||
s := &server{
|
|
||||||
srv: httplib.NewServer(mux, &opt.HTTPOptions),
|
|
||||||
}
|
|
||||||
mux.HandleFunc("/", s.handler)
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// serve runs the http server - doesn't return
|
|
||||||
func (s *server) serve() {
|
|
||||||
err := s.srv.Serve()
|
|
||||||
if err != nil {
|
|
||||||
fs.Errorf(nil, "Opening listener: %v", err)
|
|
||||||
}
|
|
||||||
fs.Logf(nil, "Serving remote control on %s", s.srv.URL())
|
|
||||||
s.srv.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteJSON writes JSON in out to w
|
// WriteJSON writes JSON in out to w
|
||||||
func WriteJSON(w io.Writer, out Params) error {
|
func WriteJSON(w io.Writer, out Params) error {
|
||||||
enc := json.NewEncoder(w)
|
enc := json.NewEncoder(w)
|
||||||
enc.SetIndent("", "\t")
|
enc.SetIndent("", "\t")
|
||||||
return enc.Encode(out)
|
return enc.Encode(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
// handler reads incoming requests and dispatches them
|
|
||||||
func (s *server) handler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
path := strings.Trim(r.URL.Path, "/")
|
|
||||||
in := make(Params)
|
|
||||||
|
|
||||||
writeError := func(err error, status int) {
|
|
||||||
fs.Errorf(nil, "rc: %q: error: %v", path, err)
|
|
||||||
w.WriteHeader(status)
|
|
||||||
err = WriteJSON(w, Params{
|
|
||||||
"error": err.Error(),
|
|
||||||
"input": in,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
// can't return the error at this point
|
|
||||||
fs.Errorf(nil, "rc: failed to write JSON output: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the POST and URL parameters into r.Form, for others r.Form will be empty value
|
|
||||||
err := r.ParseForm()
|
|
||||||
if err != nil {
|
|
||||||
writeError(errors.Wrap(err, "failed to parse form/URL parameters"), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read the POST and URL parameters into in
|
|
||||||
for k, vs := range r.Form {
|
|
||||||
if len(vs) > 0 {
|
|
||||||
in[k] = vs[len(vs)-1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse a JSON blob from the input
|
|
||||||
if r.Header.Get("Content-Type") == "application/json" {
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&in)
|
|
||||||
if err != nil {
|
|
||||||
writeError(errors.Wrap(err, "failed to read input JSON"), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.Debugf(nil, "form = %+v", r.Form)
|
|
||||||
|
|
||||||
w.Header().Add("Access-Control-Allow-Origin", "*")
|
|
||||||
//echo back headers client needs
|
|
||||||
reqAccessHeaders := r.Header.Get("Access-Control-Request-Headers")
|
|
||||||
w.Header().Add("Access-Control-Allow-Headers", reqAccessHeaders)
|
|
||||||
|
|
||||||
switch r.Method {
|
|
||||||
case "POST":
|
|
||||||
s.handlePost(w, r, path, in)
|
|
||||||
case "OPTIONS":
|
|
||||||
s.handleOptions(w, r, in)
|
|
||||||
default:
|
|
||||||
writeError(errors.Errorf("method %q not allowed - POST or OPTIONS required", r.Method), http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *server) handlePost(w http.ResponseWriter, r *http.Request, path string, in Params) {
|
|
||||||
writeError := func(err error, status int) {
|
|
||||||
fs.Errorf(nil, "rc: %q: error: %v", path, err)
|
|
||||||
w.WriteHeader(status)
|
|
||||||
err = WriteJSON(w, Params{
|
|
||||||
"error": err.Error(),
|
|
||||||
"input": in,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
// can't return the error at this point
|
|
||||||
fs.Errorf(nil, "rc: failed to write JSON output: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the call
|
|
||||||
call := registry.get(path)
|
|
||||||
if call == nil {
|
|
||||||
writeError(errors.Errorf("couldn't find method %q", path), http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.Debugf(nil, "rc: %q: with parameters %+v", path, in)
|
|
||||||
out, err := call.Fn(in)
|
|
||||||
if err != nil {
|
|
||||||
writeError(errors.Wrap(err, "remote control command failed"), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.Debugf(nil, "rc: %q: reply %+v: %v", path, out, err)
|
|
||||||
err = WriteJSON(w, out)
|
|
||||||
if err != nil {
|
|
||||||
// can't return the error at this point
|
|
||||||
fs.Errorf(nil, "rc: failed to write JSON output: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func (s *server) handleOptions(w http.ResponseWriter, r *http.Request, in Params) {
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|||||||
23
fs/rc/rc_test.go
Normal file
23
fs/rc/rc_test.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package rc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWriteJSON(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err := WriteJSON(&buf, Params{
|
||||||
|
"String": "hello",
|
||||||
|
"Int": 42,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, `{
|
||||||
|
"Int": 42,
|
||||||
|
"String": "hello"
|
||||||
|
}
|
||||||
|
`, buf.String())
|
||||||
|
}
|
||||||
@@ -15,6 +15,10 @@ var (
|
|||||||
|
|
||||||
// AddFlags adds the remote control flags to the flagSet
|
// AddFlags adds the remote control flags to the flagSet
|
||||||
func AddFlags(flagSet *pflag.FlagSet) {
|
func AddFlags(flagSet *pflag.FlagSet) {
|
||||||
|
rc.AddOption("rc", &Opt)
|
||||||
flags.BoolVarP(flagSet, &Opt.Enabled, "rc", "", false, "Enable the remote control server.")
|
flags.BoolVarP(flagSet, &Opt.Enabled, "rc", "", false, "Enable the remote control server.")
|
||||||
|
flags.StringVarP(flagSet, &Opt.Files, "rc-files", "", "", "Path to local files to serve on the HTTP server.")
|
||||||
|
flags.BoolVarP(flagSet, &Opt.Serve, "rc-serve", "", false, "Enable the serving of remote objects.")
|
||||||
|
flags.BoolVarP(flagSet, &Opt.NoAuth, "rc-no-auth", "", false, "Don't require auth for certain methods.")
|
||||||
httpflags.AddFlagsPrefix(flagSet, "rc-", &Opt.HTTPOptions)
|
httpflags.AddFlagsPrefix(flagSet, "rc-", &Opt.HTTPOptions)
|
||||||
}
|
}
|
||||||
|
|||||||
279
fs/rc/rcserver/rcserver.go
Normal file
279
fs/rc/rcserver/rcserver.go
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
// Package rcserver implements the HTTP endpoint to serve the remote control
|
||||||
|
package rcserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"mime"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ncw/rclone/cmd/serve/httplib"
|
||||||
|
"github.com/ncw/rclone/cmd/serve/httplib/serve"
|
||||||
|
"github.com/ncw/rclone/fs"
|
||||||
|
"github.com/ncw/rclone/fs/config"
|
||||||
|
"github.com/ncw/rclone/fs/list"
|
||||||
|
"github.com/ncw/rclone/fs/rc"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/skratchdot/open-golang/open"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Start the remote control server if configured
|
||||||
|
//
|
||||||
|
// If the server wasn't configured the *Server returned may be nil
|
||||||
|
func Start(opt *rc.Options) (*Server, error) {
|
||||||
|
if opt.Enabled {
|
||||||
|
// Serve on the DefaultServeMux so can have global registrations appear
|
||||||
|
s := newServer(opt, http.DefaultServeMux)
|
||||||
|
return s, s.Serve()
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server contains everything to run the rc server
|
||||||
|
type Server struct {
|
||||||
|
*httplib.Server
|
||||||
|
files http.Handler
|
||||||
|
opt *rc.Options
|
||||||
|
}
|
||||||
|
|
||||||
|
func newServer(opt *rc.Options, mux *http.ServeMux) *Server {
|
||||||
|
s := &Server{
|
||||||
|
Server: httplib.NewServer(mux, &opt.HTTPOptions),
|
||||||
|
opt: opt,
|
||||||
|
}
|
||||||
|
mux.HandleFunc("/", s.handler)
|
||||||
|
|
||||||
|
// Add some more mime types which are often missing
|
||||||
|
_ = mime.AddExtensionType(".wasm", "application/wasm")
|
||||||
|
_ = mime.AddExtensionType(".js", "application/javascript")
|
||||||
|
|
||||||
|
// File handling
|
||||||
|
if opt.Files != "" {
|
||||||
|
fs.Logf(nil, "Serving files from %q", opt.Files)
|
||||||
|
s.files = http.FileServer(http.Dir(opt.Files))
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serve runs the http server in the background.
|
||||||
|
//
|
||||||
|
// Use s.Close() and s.Wait() to shutdown server
|
||||||
|
func (s *Server) Serve() error {
|
||||||
|
err := s.Server.Serve()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fs.Logf(nil, "Serving remote control on %s", s.URL())
|
||||||
|
// Open the files in the browser if set
|
||||||
|
if s.files != nil {
|
||||||
|
openURL, err := url.Parse(s.URL())
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "invalid serving URL")
|
||||||
|
}
|
||||||
|
// Add username, password into the URL if they are set
|
||||||
|
user, pass := s.opt.HTTPOptions.BasicUser, s.opt.HTTPOptions.BasicPass
|
||||||
|
if user != "" || pass != "" {
|
||||||
|
openURL.User = url.UserPassword(user, pass)
|
||||||
|
}
|
||||||
|
_ = open.Start(openURL.String())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeError writes a formatted error to the output
|
||||||
|
func writeError(path string, in rc.Params, w http.ResponseWriter, err error, status int) {
|
||||||
|
fs.Errorf(nil, "rc: %q: error: %v", path, err)
|
||||||
|
// Adjust the error return for some well known errors
|
||||||
|
errOrig := errors.Cause(err)
|
||||||
|
switch {
|
||||||
|
case errOrig == fs.ErrorDirNotFound || errOrig == fs.ErrorObjectNotFound:
|
||||||
|
status = http.StatusNotFound
|
||||||
|
case rc.IsErrParamInvalid(err) || rc.IsErrParamNotFound(err):
|
||||||
|
status = http.StatusBadRequest
|
||||||
|
}
|
||||||
|
w.WriteHeader(status)
|
||||||
|
err = rc.WriteJSON(w, rc.Params{
|
||||||
|
"status": status,
|
||||||
|
"error": err.Error(),
|
||||||
|
"input": in,
|
||||||
|
"path": path,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
// can't return the error at this point
|
||||||
|
fs.Errorf(nil, "rc: failed to write JSON output: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handler reads incoming requests and dispatches them
|
||||||
|
func (s *Server) handler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
path := strings.TrimLeft(r.URL.Path, "/")
|
||||||
|
|
||||||
|
w.Header().Add("Access-Control-Allow-Origin", "*")
|
||||||
|
|
||||||
|
// echo back access control headers client needs
|
||||||
|
reqAccessHeaders := r.Header.Get("Access-Control-Request-Headers")
|
||||||
|
w.Header().Add("Access-Control-Allow-Headers", reqAccessHeaders)
|
||||||
|
|
||||||
|
switch r.Method {
|
||||||
|
case "POST":
|
||||||
|
s.handlePost(w, r, path)
|
||||||
|
case "OPTIONS":
|
||||||
|
s.handleOptions(w, r, path)
|
||||||
|
case "GET", "HEAD":
|
||||||
|
s.handleGet(w, r, path)
|
||||||
|
default:
|
||||||
|
writeError(path, nil, w, errors.Errorf("method %q not allowed", r.Method), http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handlePost(w http.ResponseWriter, r *http.Request, path string) {
|
||||||
|
contentType := r.Header.Get("Content-Type")
|
||||||
|
|
||||||
|
values := r.URL.Query()
|
||||||
|
if contentType == "application/x-www-form-urlencoded" {
|
||||||
|
// Parse the POST and URL parameters into r.Form, for others r.Form will be empty value
|
||||||
|
err := r.ParseForm()
|
||||||
|
if err != nil {
|
||||||
|
writeError(path, nil, w, errors.Wrap(err, "failed to parse form/URL parameters"), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
values = r.Form
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the POST and URL parameters into in
|
||||||
|
in := make(rc.Params)
|
||||||
|
for k, vs := range values {
|
||||||
|
if len(vs) > 0 {
|
||||||
|
in[k] = vs[len(vs)-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse a JSON blob from the input
|
||||||
|
if contentType == "application/json" {
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&in)
|
||||||
|
if err != nil {
|
||||||
|
writeError(path, in, w, errors.Wrap(err, "failed to read input JSON"), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the call
|
||||||
|
call := rc.Calls.Get(path)
|
||||||
|
if call == nil {
|
||||||
|
writeError(path, in, w, errors.Errorf("couldn't find method %q", path), http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check to see if it requires authorisation
|
||||||
|
if !s.opt.NoAuth && call.AuthRequired && !s.UsingAuth() {
|
||||||
|
writeError(path, in, w, errors.Errorf("authentication must be set up on the rc server to use %q or the --rc-no-auth flag must be in use", path), http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check to see if it is async or not
|
||||||
|
isAsync, err := in.GetBool("_async")
|
||||||
|
if rc.NotErrParamNotFound(err) {
|
||||||
|
writeError(path, in, w, err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.Debugf(nil, "rc: %q: with parameters %+v", path, in)
|
||||||
|
var out rc.Params
|
||||||
|
if isAsync {
|
||||||
|
out, err = rc.StartJob(call.Fn, in)
|
||||||
|
} else {
|
||||||
|
out, err = call.Fn(in)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
writeError(path, in, w, err, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if out == nil {
|
||||||
|
out = make(rc.Params)
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.Debugf(nil, "rc: %q: reply %+v: %v", path, out, err)
|
||||||
|
err = rc.WriteJSON(w, out)
|
||||||
|
if err != nil {
|
||||||
|
// can't return the error at this point
|
||||||
|
fs.Errorf(nil, "rc: failed to write JSON output: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleOptions(w http.ResponseWriter, r *http.Request, path string) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) serveRoot(w http.ResponseWriter, r *http.Request) {
|
||||||
|
remotes := config.FileSections()
|
||||||
|
sort.Strings(remotes)
|
||||||
|
directory := serve.NewDirectory("")
|
||||||
|
directory.Title = "List of all rclone remotes."
|
||||||
|
q := url.Values{}
|
||||||
|
for _, remote := range remotes {
|
||||||
|
q.Set("fs", remote)
|
||||||
|
directory.AddEntry("["+remote+":]", true)
|
||||||
|
}
|
||||||
|
directory.Serve(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) serveRemote(w http.ResponseWriter, r *http.Request, path string, fsName string) {
|
||||||
|
f, err := rc.GetCachedFs(fsName)
|
||||||
|
if err != nil {
|
||||||
|
writeError(path, nil, w, errors.Wrap(err, "failed to make Fs"), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if path == "" || strings.HasSuffix(path, "/") {
|
||||||
|
path = strings.Trim(path, "/")
|
||||||
|
entries, err := list.DirSorted(f, false, path)
|
||||||
|
if err != nil {
|
||||||
|
writeError(path, nil, w, errors.Wrap(err, "failed to list directory"), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Make the entries for display
|
||||||
|
directory := serve.NewDirectory(path)
|
||||||
|
for _, entry := range entries {
|
||||||
|
_, isDir := entry.(fs.Directory)
|
||||||
|
directory.AddEntry(entry.Remote(), isDir)
|
||||||
|
}
|
||||||
|
directory.Serve(w, r)
|
||||||
|
} else {
|
||||||
|
o, err := f.NewObject(path)
|
||||||
|
if err != nil {
|
||||||
|
writeError(path, nil, w, errors.Wrap(err, "failed to find object"), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
serve.Object(w, r, o)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match URLS of the form [fs]/remote
|
||||||
|
var fsMatch = regexp.MustCompile(`^\[(.*?)\](.*)$`)
|
||||||
|
|
||||||
|
func (s *Server) handleGet(w http.ResponseWriter, r *http.Request, path string) {
|
||||||
|
// Look to see if this has an fs in the path
|
||||||
|
match := fsMatch.FindStringSubmatch(path)
|
||||||
|
switch {
|
||||||
|
case match != nil && s.opt.Serve:
|
||||||
|
// Serve /[fs]/remote files
|
||||||
|
s.serveRemote(w, r, match[2], match[1])
|
||||||
|
return
|
||||||
|
case path == "*" && s.opt.Serve:
|
||||||
|
// Serve /* as the remote listing
|
||||||
|
s.serveRoot(w, r)
|
||||||
|
return
|
||||||
|
case s.files != nil:
|
||||||
|
// Serve the files
|
||||||
|
s.files.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
case path == "" && s.opt.Serve:
|
||||||
|
// Serve the root as a remote listing
|
||||||
|
s.serveRoot(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||||
|
}
|
||||||
632
fs/rc/rcserver/rcserver_test.go
Normal file
632
fs/rc/rcserver/rcserver_test.go
Normal file
@@ -0,0 +1,632 @@
|
|||||||
|
// +build go1.8
|
||||||
|
|
||||||
|
package rcserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"regexp"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
_ "github.com/ncw/rclone/backend/local"
|
||||||
|
"github.com/ncw/rclone/fs/rc"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
testBindAddress = "localhost:51781"
|
||||||
|
testURL = "http://" + testBindAddress + "/"
|
||||||
|
testFs = "testdata/files"
|
||||||
|
remoteURL = "[" + testFs + "]/" // initial URL path to fetch from that remote
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test the RC server runs and we can do HTTP fetches from it.
|
||||||
|
// We'll do the majority of the testing with the httptest framework
|
||||||
|
func TestRcServer(t *testing.T) {
|
||||||
|
opt := rc.DefaultOpt
|
||||||
|
opt.HTTPOptions.ListenAddr = testBindAddress
|
||||||
|
opt.Enabled = true
|
||||||
|
opt.Serve = true
|
||||||
|
opt.Files = testFs
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
rcServer := newServer(&opt, mux)
|
||||||
|
assert.NoError(t, rcServer.Serve())
|
||||||
|
defer func() {
|
||||||
|
rcServer.Close()
|
||||||
|
rcServer.Wait()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Do the simplest possible test to check the server is alive
|
||||||
|
// Do it a few times to wait for the server to start
|
||||||
|
var resp *http.Response
|
||||||
|
var err error
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
resp, err = http.Get(testURL + "file.txt")
|
||||||
|
if err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, resp.Body.Close())
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
assert.Equal(t, "this is file1.txt\n", string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
type testRun struct {
|
||||||
|
Name string
|
||||||
|
URL string
|
||||||
|
Status int
|
||||||
|
Method string
|
||||||
|
Range string
|
||||||
|
Body string
|
||||||
|
ContentType string
|
||||||
|
Expected string
|
||||||
|
Contains *regexp.Regexp
|
||||||
|
Headers map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run a suite of tests
|
||||||
|
func testServer(t *testing.T, tests []testRun, opt *rc.Options) {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
rcServer := newServer(opt, mux)
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.Name, func(t *testing.T) {
|
||||||
|
method := test.Method
|
||||||
|
if method == "" {
|
||||||
|
method = "GET"
|
||||||
|
}
|
||||||
|
var inBody io.Reader
|
||||||
|
if test.Body != "" {
|
||||||
|
buf := bytes.NewBufferString(test.Body)
|
||||||
|
inBody = buf
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest(method, "http://1.2.3.4/"+test.URL, inBody)
|
||||||
|
require.NoError(t, err)
|
||||||
|
if test.Range != "" {
|
||||||
|
req.Header.Add("Range", test.Range)
|
||||||
|
}
|
||||||
|
if test.ContentType != "" {
|
||||||
|
req.Header.Add("Content-Type", test.ContentType)
|
||||||
|
}
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
rcServer.handler(w, req)
|
||||||
|
resp := w.Result()
|
||||||
|
|
||||||
|
assert.Equal(t, test.Status, resp.StatusCode)
|
||||||
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
if test.Contains == nil {
|
||||||
|
assert.Equal(t, test.Expected, string(body))
|
||||||
|
} else {
|
||||||
|
assert.True(t, test.Contains.Match(body), fmt.Sprintf("body didn't match: %v: %v", test.Contains, string(body)))
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range test.Headers {
|
||||||
|
assert.Equal(t, v, resp.Header.Get(k), k)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// return an enabled rc
|
||||||
|
func newTestOpt() rc.Options {
|
||||||
|
opt := rc.DefaultOpt
|
||||||
|
opt.Enabled = true
|
||||||
|
return opt
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFileServing(t *testing.T) {
|
||||||
|
tests := []testRun{{
|
||||||
|
Name: "index",
|
||||||
|
URL: "",
|
||||||
|
Status: http.StatusOK,
|
||||||
|
Expected: `<pre>
|
||||||
|
<a href="dir/">dir/</a>
|
||||||
|
<a href="file.txt">file.txt</a>
|
||||||
|
</pre>
|
||||||
|
`,
|
||||||
|
}, {
|
||||||
|
Name: "notfound",
|
||||||
|
URL: "notfound",
|
||||||
|
Status: http.StatusNotFound,
|
||||||
|
Expected: "404 page not found\n",
|
||||||
|
}, {
|
||||||
|
Name: "dirnotfound",
|
||||||
|
URL: "dirnotfound/",
|
||||||
|
Status: http.StatusNotFound,
|
||||||
|
Expected: "404 page not found\n",
|
||||||
|
}, {
|
||||||
|
Name: "dir",
|
||||||
|
URL: "dir/",
|
||||||
|
Status: http.StatusOK,
|
||||||
|
Expected: `<pre>
|
||||||
|
<a href="file2.txt">file2.txt</a>
|
||||||
|
</pre>
|
||||||
|
`,
|
||||||
|
}, {
|
||||||
|
Name: "file",
|
||||||
|
URL: "file.txt",
|
||||||
|
Status: http.StatusOK,
|
||||||
|
Expected: "this is file1.txt\n",
|
||||||
|
Headers: map[string]string{
|
||||||
|
"Content-Length": "18",
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
Name: "file2",
|
||||||
|
URL: "dir/file2.txt",
|
||||||
|
Status: http.StatusOK,
|
||||||
|
Expected: "this is dir/file2.txt\n",
|
||||||
|
}, {
|
||||||
|
Name: "file-head",
|
||||||
|
URL: "file.txt",
|
||||||
|
Method: "HEAD",
|
||||||
|
Status: http.StatusOK,
|
||||||
|
Expected: ``,
|
||||||
|
Headers: map[string]string{
|
||||||
|
"Content-Length": "18",
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
Name: "file-range",
|
||||||
|
URL: "file.txt",
|
||||||
|
Status: http.StatusPartialContent,
|
||||||
|
Range: "bytes=8-12",
|
||||||
|
Expected: `file1`,
|
||||||
|
}}
|
||||||
|
opt := newTestOpt()
|
||||||
|
opt.Serve = true
|
||||||
|
opt.Files = testFs
|
||||||
|
testServer(t, tests, &opt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoteServing(t *testing.T) {
|
||||||
|
tests := []testRun{
|
||||||
|
// Test serving files from the test remote
|
||||||
|
{
|
||||||
|
Name: "index",
|
||||||
|
URL: remoteURL + "",
|
||||||
|
Status: http.StatusOK,
|
||||||
|
Expected: `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Directory listing of /</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Directory listing of /</h1>
|
||||||
|
<a href="dir/">dir/</a><br />
|
||||||
|
<a href="file.txt">file.txt</a><br />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`,
|
||||||
|
}, {
|
||||||
|
Name: "notfound-index",
|
||||||
|
URL: "[notfound]/",
|
||||||
|
Status: http.StatusNotFound,
|
||||||
|
Expected: `{
|
||||||
|
"error": "failed to list directory: directory not found",
|
||||||
|
"input": null,
|
||||||
|
"path": "",
|
||||||
|
"status": 404
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
}, {
|
||||||
|
Name: "notfound",
|
||||||
|
URL: remoteURL + "notfound",
|
||||||
|
Status: http.StatusNotFound,
|
||||||
|
Expected: `{
|
||||||
|
"error": "failed to find object: object not found",
|
||||||
|
"input": null,
|
||||||
|
"path": "/notfound",
|
||||||
|
"status": 404
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
}, {
|
||||||
|
Name: "dirnotfound",
|
||||||
|
URL: remoteURL + "dirnotfound/",
|
||||||
|
Status: http.StatusNotFound,
|
||||||
|
Expected: `{
|
||||||
|
"error": "failed to list directory: directory not found",
|
||||||
|
"input": null,
|
||||||
|
"path": "dirnotfound",
|
||||||
|
"status": 404
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
}, {
|
||||||
|
Name: "dir",
|
||||||
|
URL: remoteURL + "dir/",
|
||||||
|
Status: http.StatusOK,
|
||||||
|
Expected: `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Directory listing of /dir</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Directory listing of /dir</h1>
|
||||||
|
<a href="file2.txt">file2.txt</a><br />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`,
|
||||||
|
}, {
|
||||||
|
Name: "file",
|
||||||
|
URL: remoteURL + "file.txt",
|
||||||
|
Status: http.StatusOK,
|
||||||
|
Expected: "this is file1.txt\n",
|
||||||
|
Headers: map[string]string{
|
||||||
|
"Content-Length": "18",
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
Name: "file2",
|
||||||
|
URL: remoteURL + "dir/file2.txt",
|
||||||
|
Status: http.StatusOK,
|
||||||
|
Expected: "this is dir/file2.txt\n",
|
||||||
|
}, {
|
||||||
|
Name: "file-head",
|
||||||
|
URL: remoteURL + "file.txt",
|
||||||
|
Method: "HEAD",
|
||||||
|
Status: http.StatusOK,
|
||||||
|
Expected: ``,
|
||||||
|
Headers: map[string]string{
|
||||||
|
"Content-Length": "18",
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
Name: "file-range",
|
||||||
|
URL: remoteURL + "file.txt",
|
||||||
|
Status: http.StatusPartialContent,
|
||||||
|
Range: "bytes=8-12",
|
||||||
|
Expected: `file1`,
|
||||||
|
}, {
|
||||||
|
Name: "bad-remote",
|
||||||
|
URL: "[notfoundremote:]/",
|
||||||
|
Status: http.StatusInternalServerError,
|
||||||
|
Expected: `{
|
||||||
|
"error": "failed to make Fs: didn't find section in config file",
|
||||||
|
"input": null,
|
||||||
|
"path": "/",
|
||||||
|
"status": 500
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
}}
|
||||||
|
opt := newTestOpt()
|
||||||
|
opt.Serve = true
|
||||||
|
opt.Files = testFs
|
||||||
|
testServer(t, tests, &opt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRC(t *testing.T) {
|
||||||
|
tests := []testRun{{
|
||||||
|
Name: "rc-root",
|
||||||
|
URL: "",
|
||||||
|
Method: "POST",
|
||||||
|
Status: http.StatusNotFound,
|
||||||
|
Expected: `{
|
||||||
|
"error": "couldn't find method \"\"",
|
||||||
|
"input": {},
|
||||||
|
"path": "",
|
||||||
|
"status": 404
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
}, {
|
||||||
|
Name: "rc-noop",
|
||||||
|
URL: "rc/noop",
|
||||||
|
Method: "POST",
|
||||||
|
Status: http.StatusOK,
|
||||||
|
Expected: "{}\n",
|
||||||
|
}, {
|
||||||
|
Name: "rc-error",
|
||||||
|
URL: "rc/error",
|
||||||
|
Method: "POST",
|
||||||
|
Status: http.StatusInternalServerError,
|
||||||
|
Expected: `{
|
||||||
|
"error": "arbitrary error on input map[]",
|
||||||
|
"input": {},
|
||||||
|
"path": "rc/error",
|
||||||
|
"status": 500
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
}, {
|
||||||
|
Name: "core-gc",
|
||||||
|
URL: "core/gc", // returns nil, nil so check it is made into {}
|
||||||
|
Method: "POST",
|
||||||
|
Status: http.StatusOK,
|
||||||
|
Expected: "{}\n",
|
||||||
|
}, {
|
||||||
|
Name: "url-params",
|
||||||
|
URL: "rc/noop?param1=potato¶m2=sausage",
|
||||||
|
Method: "POST",
|
||||||
|
Status: http.StatusOK,
|
||||||
|
Expected: `{
|
||||||
|
"param1": "potato",
|
||||||
|
"param2": "sausage"
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
}, {
|
||||||
|
Name: "json",
|
||||||
|
URL: "rc/noop",
|
||||||
|
Method: "POST",
|
||||||
|
Body: `{ "param1":"string", "param2":true }`,
|
||||||
|
ContentType: "application/json",
|
||||||
|
Status: http.StatusOK,
|
||||||
|
Expected: `{
|
||||||
|
"param1": "string",
|
||||||
|
"param2": true
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
}, {
|
||||||
|
Name: "json-and-url-params",
|
||||||
|
URL: "rc/noop?param1=potato¶m2=sausage",
|
||||||
|
Method: "POST",
|
||||||
|
Body: `{ "param1":"string", "param3":true }`,
|
||||||
|
ContentType: "application/json",
|
||||||
|
Status: http.StatusOK,
|
||||||
|
Expected: `{
|
||||||
|
"param1": "string",
|
||||||
|
"param2": "sausage",
|
||||||
|
"param3": true
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
}, {
|
||||||
|
Name: "json-bad",
|
||||||
|
URL: "rc/noop?param1=potato¶m2=sausage",
|
||||||
|
Method: "POST",
|
||||||
|
Body: `{ param1":"string", "param3":true }`,
|
||||||
|
ContentType: "application/json",
|
||||||
|
Status: http.StatusBadRequest,
|
||||||
|
Expected: `{
|
||||||
|
"error": "failed to read input JSON: invalid character 'p' looking for beginning of object key string",
|
||||||
|
"input": {
|
||||||
|
"param1": "potato",
|
||||||
|
"param2": "sausage"
|
||||||
|
},
|
||||||
|
"path": "rc/noop",
|
||||||
|
"status": 400
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
}, {
|
||||||
|
Name: "form",
|
||||||
|
URL: "rc/noop",
|
||||||
|
Method: "POST",
|
||||||
|
Body: `param1=string¶m2=true`,
|
||||||
|
ContentType: "application/x-www-form-urlencoded",
|
||||||
|
Status: http.StatusOK,
|
||||||
|
Expected: `{
|
||||||
|
"param1": "string",
|
||||||
|
"param2": "true"
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
}, {
|
||||||
|
Name: "form-and-url-params",
|
||||||
|
URL: "rc/noop?param1=potato¶m2=sausage",
|
||||||
|
Method: "POST",
|
||||||
|
Body: `param1=string¶m3=true`,
|
||||||
|
ContentType: "application/x-www-form-urlencoded",
|
||||||
|
Status: http.StatusOK,
|
||||||
|
Expected: `{
|
||||||
|
"param1": "potato",
|
||||||
|
"param2": "sausage",
|
||||||
|
"param3": "true"
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
}, {
|
||||||
|
Name: "form-bad",
|
||||||
|
URL: "rc/noop?param1=potato¶m2=sausage",
|
||||||
|
Method: "POST",
|
||||||
|
Body: `%zz`,
|
||||||
|
ContentType: "application/x-www-form-urlencoded",
|
||||||
|
Status: http.StatusBadRequest,
|
||||||
|
Expected: `{
|
||||||
|
"error": "failed to parse form/URL parameters: invalid URL escape \"%zz\"",
|
||||||
|
"input": null,
|
||||||
|
"path": "rc/noop",
|
||||||
|
"status": 400
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
}}
|
||||||
|
opt := newTestOpt()
|
||||||
|
opt.Serve = true
|
||||||
|
opt.Files = testFs
|
||||||
|
testServer(t, tests, &opt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMethods(t *testing.T) {
|
||||||
|
tests := []testRun{{
|
||||||
|
Name: "options",
|
||||||
|
URL: "",
|
||||||
|
Method: "OPTIONS",
|
||||||
|
Status: http.StatusOK,
|
||||||
|
Expected: "",
|
||||||
|
Headers: map[string]string{
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
"Access-Control-Allow-Headers": "",
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
Name: "bad",
|
||||||
|
URL: "",
|
||||||
|
Method: "POTATO",
|
||||||
|
Status: http.StatusMethodNotAllowed,
|
||||||
|
Expected: `{
|
||||||
|
"error": "method \"POTATO\" not allowed",
|
||||||
|
"input": null,
|
||||||
|
"path": "",
|
||||||
|
"status": 405
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
}}
|
||||||
|
opt := newTestOpt()
|
||||||
|
opt.Serve = true
|
||||||
|
opt.Files = testFs
|
||||||
|
testServer(t, tests, &opt)
|
||||||
|
}
|
||||||
|
|
||||||
|
var matchRemoteDirListing = regexp.MustCompile(`<title>List of all rclone remotes.</title>`)
|
||||||
|
|
||||||
|
func TestServingRoot(t *testing.T) {
|
||||||
|
tests := []testRun{{
|
||||||
|
Name: "rootlist",
|
||||||
|
URL: "*",
|
||||||
|
Status: http.StatusOK,
|
||||||
|
Contains: matchRemoteDirListing,
|
||||||
|
}}
|
||||||
|
opt := newTestOpt()
|
||||||
|
opt.Serve = true
|
||||||
|
opt.Files = testFs
|
||||||
|
testServer(t, tests, &opt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServingRootNoFiles(t *testing.T) {
|
||||||
|
tests := []testRun{{
|
||||||
|
Name: "rootlist",
|
||||||
|
URL: "",
|
||||||
|
Status: http.StatusOK,
|
||||||
|
Contains: matchRemoteDirListing,
|
||||||
|
}}
|
||||||
|
opt := newTestOpt()
|
||||||
|
opt.Serve = true
|
||||||
|
opt.Files = ""
|
||||||
|
testServer(t, tests, &opt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoFiles(t *testing.T) {
|
||||||
|
tests := []testRun{{
|
||||||
|
Name: "file",
|
||||||
|
URL: "file.txt",
|
||||||
|
Status: http.StatusNotFound,
|
||||||
|
Expected: "Not Found\n",
|
||||||
|
}, {
|
||||||
|
Name: "dir",
|
||||||
|
URL: "dir/",
|
||||||
|
Status: http.StatusNotFound,
|
||||||
|
Expected: "Not Found\n",
|
||||||
|
}}
|
||||||
|
opt := newTestOpt()
|
||||||
|
opt.Serve = true
|
||||||
|
opt.Files = ""
|
||||||
|
testServer(t, tests, &opt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoServe(t *testing.T) {
|
||||||
|
tests := []testRun{{
|
||||||
|
Name: "file",
|
||||||
|
URL: remoteURL + "file.txt",
|
||||||
|
Status: http.StatusNotFound,
|
||||||
|
Expected: "404 page not found\n",
|
||||||
|
}, {
|
||||||
|
Name: "dir",
|
||||||
|
URL: remoteURL + "dir/",
|
||||||
|
Status: http.StatusNotFound,
|
||||||
|
Expected: "404 page not found\n",
|
||||||
|
}}
|
||||||
|
opt := newTestOpt()
|
||||||
|
opt.Serve = false
|
||||||
|
opt.Files = testFs
|
||||||
|
testServer(t, tests, &opt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthRequired(t *testing.T) {
|
||||||
|
tests := []testRun{{
|
||||||
|
Name: "auth",
|
||||||
|
URL: "rc/noopauth",
|
||||||
|
Method: "POST",
|
||||||
|
Body: `{}`,
|
||||||
|
ContentType: "application/javascript",
|
||||||
|
Status: http.StatusForbidden,
|
||||||
|
Expected: `{
|
||||||
|
"error": "authentication must be set up on the rc server to use \"rc/noopauth\" or the --rc-no-auth flag must be in use",
|
||||||
|
"input": {},
|
||||||
|
"path": "rc/noopauth",
|
||||||
|
"status": 403
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
}}
|
||||||
|
opt := newTestOpt()
|
||||||
|
opt.Serve = false
|
||||||
|
opt.Files = ""
|
||||||
|
opt.NoAuth = false
|
||||||
|
testServer(t, tests, &opt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoAuth(t *testing.T) {
|
||||||
|
tests := []testRun{{
|
||||||
|
Name: "auth",
|
||||||
|
URL: "rc/noopauth",
|
||||||
|
Method: "POST",
|
||||||
|
Body: `{}`,
|
||||||
|
ContentType: "application/javascript",
|
||||||
|
Status: http.StatusOK,
|
||||||
|
Expected: "{}\n",
|
||||||
|
}}
|
||||||
|
opt := newTestOpt()
|
||||||
|
opt.Serve = false
|
||||||
|
opt.Files = ""
|
||||||
|
opt.NoAuth = true
|
||||||
|
testServer(t, tests, &opt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithUserPass(t *testing.T) {
|
||||||
|
tests := []testRun{{
|
||||||
|
Name: "auth",
|
||||||
|
URL: "rc/noopauth",
|
||||||
|
Method: "POST",
|
||||||
|
Body: `{}`,
|
||||||
|
ContentType: "application/javascript",
|
||||||
|
Status: http.StatusOK,
|
||||||
|
Expected: "{}\n",
|
||||||
|
}}
|
||||||
|
opt := newTestOpt()
|
||||||
|
opt.Serve = false
|
||||||
|
opt.Files = ""
|
||||||
|
opt.NoAuth = false
|
||||||
|
opt.HTTPOptions.BasicUser = "user"
|
||||||
|
opt.HTTPOptions.BasicPass = "pass"
|
||||||
|
testServer(t, tests, &opt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRCAsync(t *testing.T) {
|
||||||
|
tests := []testRun{{
|
||||||
|
Name: "ok",
|
||||||
|
URL: "rc/noop",
|
||||||
|
Method: "POST",
|
||||||
|
ContentType: "application/json",
|
||||||
|
Body: `{ "_async":true }`,
|
||||||
|
Status: http.StatusOK,
|
||||||
|
Expected: `{
|
||||||
|
"jobid": 1
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
}, {
|
||||||
|
Name: "bad",
|
||||||
|
URL: "rc/noop",
|
||||||
|
Method: "POST",
|
||||||
|
ContentType: "application/json",
|
||||||
|
Body: `{ "_async":"truthy" }`,
|
||||||
|
Status: http.StatusBadRequest,
|
||||||
|
Expected: `{
|
||||||
|
"error": "couldn't parse key \"_async\" (truthy) as bool: strconv.ParseBool: parsing \"truthy\": invalid syntax",
|
||||||
|
"input": {
|
||||||
|
"_async": "truthy"
|
||||||
|
},
|
||||||
|
"path": "rc/noop",
|
||||||
|
"status": 400
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
}}
|
||||||
|
opt := newTestOpt()
|
||||||
|
opt.Serve = true
|
||||||
|
opt.Files = ""
|
||||||
|
testServer(t, tests, &opt)
|
||||||
|
}
|
||||||
1
fs/rc/rcserver/testdata/files/dir/file2.txt
vendored
Normal file
1
fs/rc/rcserver/testdata/files/dir/file2.txt
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
this is dir/file2.txt
|
||||||
1
fs/rc/rcserver/testdata/files/file.txt
vendored
Normal file
1
fs/rc/rcserver/testdata/files/file.txt
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
this is file1.txt
|
||||||
@@ -10,19 +10,17 @@ import (
|
|||||||
"github.com/ncw/rclone/fs"
|
"github.com/ncw/rclone/fs"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Params is the input and output type for the Func
|
|
||||||
type Params map[string]interface{}
|
|
||||||
|
|
||||||
// Func defines a type for a remote control function
|
// Func defines a type for a remote control function
|
||||||
type Func func(in Params) (out Params, err error)
|
type Func func(in Params) (out Params, err error)
|
||||||
|
|
||||||
// Call defines info about a remote control function and is used in
|
// Call defines info about a remote control function and is used in
|
||||||
// the Add function to create new entry points.
|
// the Add function to create new entry points.
|
||||||
type Call struct {
|
type Call struct {
|
||||||
Path string // path to activate this RC
|
Path string // path to activate this RC
|
||||||
Fn Func `json:"-"` // function to call
|
Fn Func `json:"-"` // function to call
|
||||||
Title string // help for the function
|
Title string // help for the function
|
||||||
Help string // multi-line markdown formatted help
|
AuthRequired bool // if set then this call requires authorisation to be set
|
||||||
|
Help string // multi-line markdown formatted help
|
||||||
}
|
}
|
||||||
|
|
||||||
// Registry holds the list of all the registered remote control functions
|
// Registry holds the list of all the registered remote control functions
|
||||||
@@ -39,7 +37,7 @@ func NewRegistry() *Registry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add a call to the registry
|
// Add a call to the registry
|
||||||
func (r *Registry) add(call Call) {
|
func (r *Registry) Add(call Call) {
|
||||||
r.mu.Lock()
|
r.mu.Lock()
|
||||||
defer r.mu.Unlock()
|
defer r.mu.Unlock()
|
||||||
call.Path = strings.Trim(call.Path, "/")
|
call.Path = strings.Trim(call.Path, "/")
|
||||||
@@ -48,15 +46,15 @@ func (r *Registry) add(call Call) {
|
|||||||
r.call[call.Path] = &call
|
r.call[call.Path] = &call
|
||||||
}
|
}
|
||||||
|
|
||||||
// get a Call from a path or nil
|
// Get a Call from a path or nil
|
||||||
func (r *Registry) get(path string) *Call {
|
func (r *Registry) Get(path string) *Call {
|
||||||
r.mu.RLock()
|
r.mu.RLock()
|
||||||
defer r.mu.RUnlock()
|
defer r.mu.RUnlock()
|
||||||
return r.call[path]
|
return r.call[path]
|
||||||
}
|
}
|
||||||
|
|
||||||
// get a list of all calls in alphabetical order
|
// List of all calls in alphabetical order
|
||||||
func (r *Registry) list() (out []*Call) {
|
func (r *Registry) List() (out []*Call) {
|
||||||
r.mu.RLock()
|
r.mu.RLock()
|
||||||
defer r.mu.RUnlock()
|
defer r.mu.RUnlock()
|
||||||
var keys []string
|
var keys []string
|
||||||
@@ -70,10 +68,10 @@ func (r *Registry) list() (out []*Call) {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// The global registry
|
// Calls is the global registry of Call objects
|
||||||
var registry = NewRegistry()
|
var Calls = NewRegistry()
|
||||||
|
|
||||||
// Add a function to the global registry
|
// Add a function to the global registry
|
||||||
func Add(call Call) {
|
func Add(call Call) {
|
||||||
registry.add(call)
|
Calls.Add(call)
|
||||||
}
|
}
|
||||||
|
|||||||
57
fs/sync/rc.go
Normal file
57
fs/sync/rc.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package sync
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ncw/rclone/fs/rc"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
for _, name := range []string{"sync", "copy", "move"} {
|
||||||
|
name := name
|
||||||
|
moveHelp := ""
|
||||||
|
if name == "move" {
|
||||||
|
moveHelp = "- deleteEmptySrcDirs - delete empty src directories if set\n"
|
||||||
|
}
|
||||||
|
rc.Add(rc.Call{
|
||||||
|
Path: "sync/" + name,
|
||||||
|
AuthRequired: true,
|
||||||
|
Fn: func(in rc.Params) (rc.Params, error) {
|
||||||
|
return rcSyncCopyMove(in, name)
|
||||||
|
},
|
||||||
|
Title: name + " a directory from source remote to destination remote",
|
||||||
|
Help: `This takes the following parameters
|
||||||
|
|
||||||
|
- srcFs - a remote name string eg "drive:src" for the source
|
||||||
|
- dstFs - a remote name string eg "drive:dst" for the destination
|
||||||
|
` + moveHelp + `
|
||||||
|
This returns
|
||||||
|
- jobid - ID of async job to query with job/status
|
||||||
|
|
||||||
|
See the [` + name + ` command](/commands/rclone_` + name + `/) command for more information on the above.`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync/Copy/Move a file
|
||||||
|
func rcSyncCopyMove(in rc.Params, name string) (out rc.Params, err error) {
|
||||||
|
srcFs, err := rc.GetFsNamed(in, "srcFs")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dstFs, err := rc.GetFsNamed(in, "dstFs")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
switch name {
|
||||||
|
case "sync":
|
||||||
|
return nil, Sync(dstFs, srcFs)
|
||||||
|
case "copy":
|
||||||
|
return nil, CopyDir(dstFs, srcFs)
|
||||||
|
case "move":
|
||||||
|
deleteEmptySrcDirs, err := in.GetBool("deleteEmptySrcDirs")
|
||||||
|
if rc.NotErrParamNotFound(err) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return nil, MoveDir(dstFs, srcFs, deleteEmptySrcDirs)
|
||||||
|
}
|
||||||
|
panic("unknown rcSyncCopyMove type")
|
||||||
|
}
|
||||||
97
fs/sync/rc_test.go
Normal file
97
fs/sync/rc_test.go
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
package sync
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ncw/rclone/fs/rc"
|
||||||
|
"github.com/ncw/rclone/fstest"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func rcNewRun(t *testing.T, method string) (*fstest.Run, *rc.Call) {
|
||||||
|
if *fstest.RemoteName != "" {
|
||||||
|
t.Skip("Skipping test on non local remote")
|
||||||
|
}
|
||||||
|
r := fstest.NewRun(t)
|
||||||
|
call := rc.Calls.Get(method)
|
||||||
|
assert.NotNil(t, call)
|
||||||
|
rc.PutCachedFs(r.LocalName, r.Flocal)
|
||||||
|
rc.PutCachedFs(r.FremoteName, r.Fremote)
|
||||||
|
return r, call
|
||||||
|
}
|
||||||
|
|
||||||
|
// sync/copy: copy a directory from source remote to destination remote
|
||||||
|
func TestRcCopy(t *testing.T) {
|
||||||
|
r, call := rcNewRun(t, "sync/copy")
|
||||||
|
defer r.Finalise()
|
||||||
|
r.Mkdir(r.Fremote)
|
||||||
|
|
||||||
|
file1 := r.WriteBoth("file1", "file1 contents", t1)
|
||||||
|
file2 := r.WriteFile("subdir/file2", "file2 contents", t2)
|
||||||
|
file3 := r.WriteObject("subdir/subsubdir/file3", "file3 contents", t3)
|
||||||
|
|
||||||
|
fstest.CheckItems(t, r.Flocal, file1, file2)
|
||||||
|
fstest.CheckItems(t, r.Fremote, file1, file3)
|
||||||
|
|
||||||
|
in := rc.Params{
|
||||||
|
"srcFs": r.LocalName,
|
||||||
|
"dstFs": r.FremoteName,
|
||||||
|
}
|
||||||
|
out, err := call.Fn(in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, rc.Params(nil), out)
|
||||||
|
|
||||||
|
fstest.CheckItems(t, r.Flocal, file1, file2)
|
||||||
|
fstest.CheckItems(t, r.Fremote, file1, file2, file3)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sync/move: move a directory from source remote to destination remote
|
||||||
|
func TestRcMove(t *testing.T) {
|
||||||
|
r, call := rcNewRun(t, "sync/move")
|
||||||
|
defer r.Finalise()
|
||||||
|
r.Mkdir(r.Fremote)
|
||||||
|
|
||||||
|
file1 := r.WriteBoth("file1", "file1 contents", t1)
|
||||||
|
file2 := r.WriteFile("subdir/file2", "file2 contents", t2)
|
||||||
|
file3 := r.WriteObject("subdir/subsubdir/file3", "file3 contents", t3)
|
||||||
|
|
||||||
|
fstest.CheckItems(t, r.Flocal, file1, file2)
|
||||||
|
fstest.CheckItems(t, r.Fremote, file1, file3)
|
||||||
|
|
||||||
|
in := rc.Params{
|
||||||
|
"srcFs": r.LocalName,
|
||||||
|
"dstFs": r.FremoteName,
|
||||||
|
}
|
||||||
|
out, err := call.Fn(in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, rc.Params(nil), out)
|
||||||
|
|
||||||
|
fstest.CheckItems(t, r.Flocal)
|
||||||
|
fstest.CheckItems(t, r.Fremote, file1, file2, file3)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sync/sync: sync a directory from source remote to destination remote
|
||||||
|
func TestRcSync(t *testing.T) {
|
||||||
|
r, call := rcNewRun(t, "sync/sync")
|
||||||
|
defer r.Finalise()
|
||||||
|
r.Mkdir(r.Fremote)
|
||||||
|
|
||||||
|
file1 := r.WriteBoth("file1", "file1 contents", t1)
|
||||||
|
file2 := r.WriteFile("subdir/file2", "file2 contents", t2)
|
||||||
|
file3 := r.WriteObject("subdir/subsubdir/file3", "file3 contents", t3)
|
||||||
|
|
||||||
|
fstest.CheckItems(t, r.Flocal, file1, file2)
|
||||||
|
fstest.CheckItems(t, r.Fremote, file1, file3)
|
||||||
|
|
||||||
|
in := rc.Params{
|
||||||
|
"srcFs": r.LocalName,
|
||||||
|
"dstFs": r.FremoteName,
|
||||||
|
}
|
||||||
|
out, err := call.Fn(in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, rc.Params(nil), out)
|
||||||
|
|
||||||
|
fstest.CheckItems(t, r.Flocal, file1, file2)
|
||||||
|
fstest.CheckItems(t, r.Fremote, file1, file2)
|
||||||
|
}
|
||||||
@@ -79,6 +79,35 @@ func TestCopyWithDepth(t *testing.T) {
|
|||||||
fstest.CheckItems(t, r.Fremote, file2)
|
fstest.CheckItems(t, r.Fremote, file2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test copy with files from
|
||||||
|
func TestCopyWithFilesFrom(t *testing.T) {
|
||||||
|
r := fstest.NewRun(t)
|
||||||
|
defer r.Finalise()
|
||||||
|
file1 := r.WriteFile("potato2", "hello world", t1)
|
||||||
|
file2 := r.WriteFile("hello world2", "hello world2", t2)
|
||||||
|
|
||||||
|
// Set the --files-from equivalent
|
||||||
|
f, err := filter.NewFilter(nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, f.AddFile("potato2"))
|
||||||
|
require.NoError(t, f.AddFile("notfound"))
|
||||||
|
|
||||||
|
// Monkey patch the active filter
|
||||||
|
oldFilter := filter.Active
|
||||||
|
filter.Active = f
|
||||||
|
unpatch := func() {
|
||||||
|
filter.Active = oldFilter
|
||||||
|
}
|
||||||
|
defer unpatch()
|
||||||
|
|
||||||
|
err = CopyDir(r.Fremote, r.Flocal)
|
||||||
|
require.NoError(t, err)
|
||||||
|
unpatch()
|
||||||
|
|
||||||
|
fstest.CheckItems(t, r.Flocal, file1, file2)
|
||||||
|
fstest.CheckItems(t, r.Fremote, file1)
|
||||||
|
}
|
||||||
|
|
||||||
// Test copy empty directories
|
// Test copy empty directories
|
||||||
func TestCopyEmptyDirectories(t *testing.T) {
|
func TestCopyEmptyDirectories(t *testing.T) {
|
||||||
r := fstest.NewRun(t)
|
r := fstest.NewRun(t)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package fs
|
package fs
|
||||||
|
|
||||||
// Version of rclone
|
// Version of rclone
|
||||||
var Version = "v1.44"
|
var Version = "v1.44-DEV"
|
||||||
|
|||||||
86
fs/version/version.go
Normal file
86
fs/version/version.go
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
package version
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ncw/rclone/fs"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Version represents a parsed rclone version number
|
||||||
|
type Version []int
|
||||||
|
|
||||||
|
var parseVersion = regexp.MustCompile(`^(?:rclone )?v(\d+)\.(\d+)(?:\.(\d+))?(?:-(\d+)(?:-(g[\wβ-]+))?)?$`)
|
||||||
|
|
||||||
|
// New parses a version number from a string
|
||||||
|
//
|
||||||
|
// This will be returned with up to 4 elements for major, minor,
|
||||||
|
// patch, subpatch release.
|
||||||
|
//
|
||||||
|
// If the version number represents a compiled from git version
|
||||||
|
// number, then it will be returned as major, minor, 999, 999
|
||||||
|
func New(in string) (v Version, err error) {
|
||||||
|
isGit := strings.HasSuffix(in, "-DEV")
|
||||||
|
if isGit {
|
||||||
|
in = in[:len(in)-4]
|
||||||
|
}
|
||||||
|
r := parseVersion.FindStringSubmatch(in)
|
||||||
|
if r == nil {
|
||||||
|
return v, errors.Errorf("failed to match version string %q", in)
|
||||||
|
}
|
||||||
|
atoi := func(s string) int {
|
||||||
|
i, err := strconv.Atoi(s)
|
||||||
|
if err != nil {
|
||||||
|
fs.Errorf(nil, "Failed to parse %q as int from %q: %v", s, in, err)
|
||||||
|
}
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
v = Version{
|
||||||
|
atoi(r[1]), // major
|
||||||
|
atoi(r[2]), // minor
|
||||||
|
}
|
||||||
|
if r[3] != "" {
|
||||||
|
v = append(v, atoi(r[3])) // patch
|
||||||
|
} else if r[4] != "" {
|
||||||
|
v = append(v, 0) // patch
|
||||||
|
}
|
||||||
|
if r[4] != "" {
|
||||||
|
v = append(v, atoi(r[4])) // dev
|
||||||
|
}
|
||||||
|
if isGit {
|
||||||
|
v = append(v, 999, 999)
|
||||||
|
}
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// String converts v to a string
|
||||||
|
func (v Version) String() string {
|
||||||
|
var out []string
|
||||||
|
for _, vv := range v {
|
||||||
|
out = append(out, fmt.Sprint(vv))
|
||||||
|
}
|
||||||
|
return strings.Join(out, ".")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cmp compares two versions returning >0, <0 or 0
|
||||||
|
func (v Version) Cmp(o Version) (d int) {
|
||||||
|
n := len(v)
|
||||||
|
if n > len(o) {
|
||||||
|
n = len(o)
|
||||||
|
}
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
d = v[i] - o[i]
|
||||||
|
if d != 0 {
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return len(v) - len(o)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsGit returns true if the current version was compiled from git
|
||||||
|
func (v Version) IsGit() bool {
|
||||||
|
return len(v) >= 4 && v[2] == 999 && v[3] == 999
|
||||||
|
}
|
||||||
89
fs/version/version_test.go
Normal file
89
fs/version/version_test.go
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
package version
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNew(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
in string
|
||||||
|
want Version
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{"v1.41", Version{1, 41}, false},
|
||||||
|
{"rclone v1.41", Version{1, 41}, false},
|
||||||
|
{"rclone v1.41.23", Version{1, 41, 23}, false},
|
||||||
|
{"rclone v1.41.23-100", Version{1, 41, 23, 100}, false},
|
||||||
|
{"rclone v1.41-100", Version{1, 41, 0, 100}, false},
|
||||||
|
{"rclone v1.41.23-100-g12312a", Version{1, 41, 23, 100}, false},
|
||||||
|
{"rclone v1.41-100-g12312a", Version{1, 41, 0, 100}, false},
|
||||||
|
{"rclone v1.42-005-g56e1e820β", Version{1, 42, 0, 5}, false},
|
||||||
|
{"rclone v1.42-005-g56e1e820-feature-branchβ", Version{1, 42, 0, 5}, false},
|
||||||
|
|
||||||
|
{"v1.41s", nil, true},
|
||||||
|
{"rclone v1-41", nil, true},
|
||||||
|
{"rclone v1.41.2c3", nil, true},
|
||||||
|
{"rclone v1.41.23-100 potato", nil, true},
|
||||||
|
{"rclone 1.41-100", nil, true},
|
||||||
|
{"rclone v1.41.23-100-12312a", nil, true},
|
||||||
|
|
||||||
|
{"v1.41-DEV", Version{1, 41, 999, 999}, false},
|
||||||
|
} {
|
||||||
|
what := fmt.Sprintf("in=%q", test.in)
|
||||||
|
got, err := New(test.in)
|
||||||
|
if test.wantErr {
|
||||||
|
assert.Error(t, err, what)
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err, what)
|
||||||
|
}
|
||||||
|
assert.Equal(t, test.want, got, what)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCmp(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
a, b Version
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{Version{1}, Version{1}, 0},
|
||||||
|
{Version{1}, Version{2}, -1},
|
||||||
|
{Version{2}, Version{1}, 1},
|
||||||
|
{Version{2}, Version{2, 1}, -1},
|
||||||
|
{Version{2, 1}, Version{2}, 1},
|
||||||
|
{Version{2, 1}, Version{2, 1}, 0},
|
||||||
|
{Version{2, 1}, Version{2, 2}, -1},
|
||||||
|
{Version{2, 2}, Version{2, 1}, 1},
|
||||||
|
} {
|
||||||
|
got := test.a.Cmp(test.b)
|
||||||
|
if got < 0 {
|
||||||
|
got = -1
|
||||||
|
} else if got > 0 {
|
||||||
|
got = 1
|
||||||
|
}
|
||||||
|
assert.Equal(t, test.want, got, fmt.Sprintf("%v cmp %v", test.a, test.b))
|
||||||
|
// test the reverse
|
||||||
|
got = -test.b.Cmp(test.a)
|
||||||
|
assert.Equal(t, test.want, got, fmt.Sprintf("%v cmp %v", test.b, test.a))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestString(t *testing.T) {
|
||||||
|
v, err := New("v1.44.1-2")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, "1.44.1.2", v.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsGit(t *testing.T) {
|
||||||
|
v, err := New("v1.44")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, false, v.IsGit())
|
||||||
|
|
||||||
|
v, err = New("v1.44-DEV")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, true, v.IsGit())
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
//+build !go1.7
|
//+build !go1.8
|
||||||
|
|
||||||
package fs
|
package fs
|
||||||
|
|
||||||
// Upgrade to Go version 1.7 to compile rclone - latest stable go
|
// Upgrade to Go version 1.8 to compile rclone - latest stable go
|
||||||
// compiler recommended.
|
// compiler recommended.
|
||||||
func init() { Go_version_1_7_required_for_compilation() }
|
func init() { Go_version_1_8_required_for_compilation() }
|
||||||
|
|||||||
@@ -54,8 +54,14 @@ type Func func(path string, entries fs.DirEntries, err error) error
|
|||||||
// This is implemented by WalkR if Config.UseRecursiveListing is true
|
// This is implemented by WalkR if Config.UseRecursiveListing is true
|
||||||
// and f supports it and level > 1, or WalkN otherwise.
|
// and f supports it and level > 1, or WalkN otherwise.
|
||||||
//
|
//
|
||||||
|
// If --files-from is set then a DirTree will be constructed with just
|
||||||
|
// those files in and then walked with WalkR
|
||||||
|
//
|
||||||
// NB (f, path) to be replaced by fs.Dir at some point
|
// NB (f, path) to be replaced by fs.Dir at some point
|
||||||
func Walk(f fs.Fs, path string, includeAll bool, maxLevel int, fn Func) error {
|
func Walk(f fs.Fs, path string, includeAll bool, maxLevel int, fn Func) error {
|
||||||
|
if filter.Active.HaveFilesFrom() {
|
||||||
|
return walkR(f, path, includeAll, maxLevel, fn, filter.Active.MakeListR(f.NewObject))
|
||||||
|
}
|
||||||
if (maxLevel < 0 || maxLevel > 1) && fs.Config.UseListR && f.Features().ListR != nil {
|
if (maxLevel < 0 || maxLevel > 1) && fs.Config.UseListR && f.Features().ListR != nil {
|
||||||
return walkListR(f, path, includeAll, maxLevel, fn)
|
return walkListR(f, path, includeAll, maxLevel, fn)
|
||||||
}
|
}
|
||||||
@@ -452,8 +458,14 @@ func walkNDirTree(f fs.Fs, path string, includeAll bool, maxLevel int, listDir l
|
|||||||
// This is implemented by WalkR if Config.UseRecursiveListing is true
|
// This is implemented by WalkR if Config.UseRecursiveListing is true
|
||||||
// and f supports it and level > 1, or WalkN otherwise.
|
// and f supports it and level > 1, or WalkN otherwise.
|
||||||
//
|
//
|
||||||
|
// If --files-from is set then a DirTree will be constructed with just
|
||||||
|
// those files in.
|
||||||
|
//
|
||||||
// NB (f, path) to be replaced by fs.Dir at some point
|
// NB (f, path) to be replaced by fs.Dir at some point
|
||||||
func NewDirTree(f fs.Fs, path string, includeAll bool, maxLevel int) (DirTree, error) {
|
func NewDirTree(f fs.Fs, path string, includeAll bool, maxLevel int) (DirTree, error) {
|
||||||
|
if filter.Active.HaveFilesFrom() {
|
||||||
|
return walkRDirTree(f, path, includeAll, maxLevel, filter.Active.MakeListR(f.NewObject))
|
||||||
|
}
|
||||||
if ListR := f.Features().ListR; (maxLevel < 0 || maxLevel > 1) && fs.Config.UseListR && ListR != nil {
|
if ListR := f.Features().ListR; (maxLevel < 0 || maxLevel > 1) && fs.Config.UseListR && ListR != nil {
|
||||||
return walkRDirTree(f, path, includeAll, maxLevel, ListR)
|
return walkRDirTree(f, path, includeAll, maxLevel, ListR)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -540,16 +540,13 @@ func Run(t *testing.T, opt *Opt) {
|
|||||||
minChunkSize = opt.ChunkedUpload.CeilChunkSize(minChunkSize)
|
minChunkSize = opt.ChunkedUpload.CeilChunkSize(minChunkSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
maxChunkSize := opt.ChunkedUpload.MaxChunkSize
|
maxChunkSize := 2 * fs.MebiByte
|
||||||
if maxChunkSize < minChunkSize {
|
if maxChunkSize < 2*minChunkSize {
|
||||||
if minChunkSize <= fs.MebiByte {
|
|
||||||
maxChunkSize = 2 * fs.MebiByte
|
|
||||||
} else {
|
|
||||||
maxChunkSize = 2 * minChunkSize
|
|
||||||
}
|
|
||||||
} else if maxChunkSize >= 2*minChunkSize {
|
|
||||||
maxChunkSize = 2 * minChunkSize
|
maxChunkSize = 2 * minChunkSize
|
||||||
}
|
}
|
||||||
|
if opt.ChunkedUpload.MaxChunkSize > 0 && maxChunkSize > opt.ChunkedUpload.MaxChunkSize {
|
||||||
|
maxChunkSize = opt.ChunkedUpload.MaxChunkSize
|
||||||
|
}
|
||||||
if opt.ChunkedUpload.CeilChunkSize != nil {
|
if opt.ChunkedUpload.CeilChunkSize != nil {
|
||||||
maxChunkSize = opt.ChunkedUpload.CeilChunkSize(maxChunkSize)
|
maxChunkSize = opt.ChunkedUpload.CeilChunkSize(maxChunkSize)
|
||||||
}
|
}
|
||||||
|
|||||||
106
fstest/mockfs/mockfs.go
Normal file
106
fstest/mockfs/mockfs.go
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
package mockfs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ncw/rclone/fs"
|
||||||
|
"github.com/ncw/rclone/fs/hash"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Fs is a minimal mock Fs
|
||||||
|
type Fs struct {
|
||||||
|
name string // the name of the remote
|
||||||
|
root string // The root directory (OS path)
|
||||||
|
features *fs.Features // optional features
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrNotImplemented is returned by unimplemented methods
|
||||||
|
var ErrNotImplemented = errors.New("not implemented")
|
||||||
|
|
||||||
|
// NewFs returns a new mock Fs
|
||||||
|
func NewFs(name, root string) *Fs {
|
||||||
|
f := &Fs{
|
||||||
|
name: name,
|
||||||
|
root: root,
|
||||||
|
}
|
||||||
|
f.features = (&fs.Features{}).Fill(f)
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name of the remote (as passed into NewFs)
|
||||||
|
func (f *Fs) Name() string {
|
||||||
|
return f.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Root of the remote (as passed into NewFs)
|
||||||
|
func (f *Fs) Root() string {
|
||||||
|
return f.root
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a description of the FS
|
||||||
|
func (f *Fs) String() string {
|
||||||
|
return fmt.Sprintf("Mock file system at %s", f.root)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Precision of the ModTimes in this Fs
|
||||||
|
func (f *Fs) Precision() time.Duration {
|
||||||
|
return time.Second
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hashes returns the supported hash types of the filesystem
|
||||||
|
func (f *Fs) Hashes() hash.Set {
|
||||||
|
return hash.NewHashSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Features returns the optional features of this Fs
|
||||||
|
func (f *Fs) Features() *fs.Features {
|
||||||
|
return f.features
|
||||||
|
}
|
||||||
|
|
||||||
|
// List the objects and directories in dir into entries. The
|
||||||
|
// entries can be returned in any order but should be for a
|
||||||
|
// complete directory.
|
||||||
|
//
|
||||||
|
// dir should be "" to list the root, and should not have
|
||||||
|
// trailing slashes.
|
||||||
|
//
|
||||||
|
// This should return ErrDirNotFound if the directory isn't
|
||||||
|
// found.
|
||||||
|
func (f *Fs) List(dir string) (entries fs.DirEntries, err error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewObject finds the Object at remote. If it can't be found
|
||||||
|
// it returns the error ErrorObjectNotFound.
|
||||||
|
func (f *Fs) NewObject(remote string) (fs.Object, error) {
|
||||||
|
return nil, fs.ErrorObjectNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put in to the remote path with the modTime given of the given size
|
||||||
|
//
|
||||||
|
// May create the object even if it returns an error - if so
|
||||||
|
// will return the object and the error, otherwise will return
|
||||||
|
// nil and the error
|
||||||
|
func (f *Fs) Put(in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
|
||||||
|
return nil, ErrNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mkdir makes the directory (container, bucket)
|
||||||
|
//
|
||||||
|
// Shouldn't return an error if it already exists
|
||||||
|
func (f *Fs) Mkdir(dir string) error {
|
||||||
|
return ErrNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rmdir removes the directory (container, bucket) if empty
|
||||||
|
//
|
||||||
|
// Return an error if it doesn't exist or isn't empty
|
||||||
|
func (f *Fs) Rmdir(dir string) error {
|
||||||
|
return ErrNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert it is the correct type
|
||||||
|
var _ fs.Fs = (*Fs)(nil)
|
||||||
62
fstest/test_all/clean.go
Normal file
62
fstest/test_all/clean.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
// Clean the left over test files
|
||||||
|
|
||||||
|
// +build go1.10
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/ncw/rclone/fs"
|
||||||
|
"github.com/ncw/rclone/fs/list"
|
||||||
|
"github.com/ncw/rclone/fs/operations"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MatchTestRemote matches the remote names used for testing (copied
|
||||||
|
// from fstest/fstest.go so we don't have to import that and get all
|
||||||
|
// its flags)
|
||||||
|
var MatchTestRemote = regexp.MustCompile(`^rclone-test-[abcdefghijklmnopqrstuvwxyz0123456789]{24}$`)
|
||||||
|
|
||||||
|
// cleanFs runs a single clean fs for left over directories
|
||||||
|
func cleanFs(remote string) error {
|
||||||
|
f, err := fs.NewFs(remote)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
entries, err := list.DirSorted(f, true, "")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return entries.ForDirError(func(dir fs.Directory) error {
|
||||||
|
dirPath := dir.Remote()
|
||||||
|
fullPath := remote + dirPath
|
||||||
|
if MatchTestRemote.MatchString(dirPath) {
|
||||||
|
if *dryRun {
|
||||||
|
log.Printf("Not Purging %s - -dry-run", fullPath)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
log.Printf("Purging %s", fullPath)
|
||||||
|
dir, err := fs.NewFs(fullPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return operations.Purge(dir, "")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanRemotes cleans the list of remotes passed in
|
||||||
|
func cleanRemotes(remotes []string) error {
|
||||||
|
var lastError error
|
||||||
|
for _, remote := range remotes {
|
||||||
|
log.Printf("%q - Cleaning", remote)
|
||||||
|
err := cleanFs(remote)
|
||||||
|
if err != nil {
|
||||||
|
lastError = err
|
||||||
|
log.Printf("Failed to purge %q: %v", remote, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lastError
|
||||||
|
}
|
||||||
165
fstest/test_all/config.go
Normal file
165
fstest/test_all/config.go
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
// Config handling
|
||||||
|
|
||||||
|
// +build go1.10
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test describes an integration test to run with `go test`
|
||||||
|
type Test struct {
|
||||||
|
Path string // path to the source directory
|
||||||
|
SubDir bool // if it is possible to add -sub-dir to tests
|
||||||
|
FastList bool // if it is possible to add -fast-list to tests
|
||||||
|
AddBackend bool // set if Path needs the current backend appending
|
||||||
|
NoRetries bool // set if no retries should be performed
|
||||||
|
NoBinary bool // set to not build a binary in advance
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backend describes a backend test
|
||||||
|
//
|
||||||
|
// FIXME make bucket based remotes set sub-dir automatically???
|
||||||
|
type Backend struct {
|
||||||
|
Backend string // name of the backend directory
|
||||||
|
Remote string // name of the test remote
|
||||||
|
SubDir bool // set to test with -sub-dir
|
||||||
|
FastList bool // set to test with -fast-list
|
||||||
|
OneOnly bool // set to run only one backend test at once
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeRuns creates Run objects the Backend and Test
|
||||||
|
//
|
||||||
|
// There can be several created, one for each combination of SubDir
|
||||||
|
// and FastList
|
||||||
|
func (b *Backend) MakeRuns(t *Test) (runs []*Run) {
|
||||||
|
subdirs := []bool{false}
|
||||||
|
if b.SubDir && t.SubDir {
|
||||||
|
subdirs = append(subdirs, true)
|
||||||
|
}
|
||||||
|
fastlists := []bool{false}
|
||||||
|
if b.FastList && t.FastList {
|
||||||
|
fastlists = append(fastlists, true)
|
||||||
|
}
|
||||||
|
for _, subdir := range subdirs {
|
||||||
|
for _, fastlist := range fastlists {
|
||||||
|
run := &Run{
|
||||||
|
Remote: b.Remote,
|
||||||
|
Backend: b.Backend,
|
||||||
|
Path: t.Path,
|
||||||
|
SubDir: subdir,
|
||||||
|
FastList: fastlist,
|
||||||
|
NoRetries: t.NoRetries,
|
||||||
|
OneOnly: b.OneOnly,
|
||||||
|
NoBinary: t.NoBinary,
|
||||||
|
}
|
||||||
|
if t.AddBackend {
|
||||||
|
run.Path = path.Join(run.Path, b.Backend)
|
||||||
|
}
|
||||||
|
runs = append(runs, run)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return runs
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config describes the config for this program
|
||||||
|
type Config struct {
|
||||||
|
Tests []Test
|
||||||
|
Backends []Backend
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConfig reads the config file
|
||||||
|
func NewConfig(configFile string) (*Config, error) {
|
||||||
|
d, err := ioutil.ReadFile(configFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to read config file")
|
||||||
|
}
|
||||||
|
config := &Config{}
|
||||||
|
err = yaml.Unmarshal(d, &config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to parse config file")
|
||||||
|
}
|
||||||
|
// d, err = yaml.Marshal(&config)
|
||||||
|
// if err != nil {
|
||||||
|
// log.Fatalf("error: %v", err)
|
||||||
|
// }
|
||||||
|
// fmt.Printf("--- m dump:\n%s\n\n", string(d))
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeRuns makes Run objects for each combination of Backend and Test
|
||||||
|
// in the config
|
||||||
|
func (c *Config) MakeRuns() (runs Runs) {
|
||||||
|
for _, backend := range c.Backends {
|
||||||
|
for _, test := range c.Tests {
|
||||||
|
runs = append(runs, backend.MakeRuns(&test)...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return runs
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter the Backends with the remotes passed in.
|
||||||
|
//
|
||||||
|
// If no backend is found with a remote is found then synthesize one
|
||||||
|
func (c *Config) filterBackendsByRemotes(remotes []string) {
|
||||||
|
var newBackends []Backend
|
||||||
|
for _, name := range remotes {
|
||||||
|
found := false
|
||||||
|
for i := range c.Backends {
|
||||||
|
if c.Backends[i].Remote == name {
|
||||||
|
newBackends = append(newBackends, c.Backends[i])
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
log.Printf("Remote %q not found - inserting with default flags", name)
|
||||||
|
newBackends = append(newBackends, Backend{Remote: name})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.Backends = newBackends
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter the Backends with the backendNames passed in
|
||||||
|
func (c *Config) filterBackendsByBackends(backendNames []string) {
|
||||||
|
var newBackends []Backend
|
||||||
|
for _, name := range backendNames {
|
||||||
|
for i := range c.Backends {
|
||||||
|
if c.Backends[i].Backend == name {
|
||||||
|
newBackends = append(newBackends, c.Backends[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.Backends = newBackends
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter the incoming tests into the backends selected
|
||||||
|
func (c *Config) filterTests(paths []string) {
|
||||||
|
var newTests []Test
|
||||||
|
for _, path := range paths {
|
||||||
|
for i := range c.Tests {
|
||||||
|
if c.Tests[i].Path == path {
|
||||||
|
newTests = append(newTests, c.Tests[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.Tests = newTests
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remotes returns the unique remotes
|
||||||
|
func (c *Config) Remotes() (remotes []string) {
|
||||||
|
found := map[string]struct{}{}
|
||||||
|
for _, backend := range c.Backends {
|
||||||
|
if _, ok := found[backend.Remote]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
remotes = append(remotes, backend.Remote)
|
||||||
|
found[backend.Remote] = struct{}{}
|
||||||
|
}
|
||||||
|
return remotes
|
||||||
|
}
|
||||||
109
fstest/test_all/config.yaml
Normal file
109
fstest/test_all/config.yaml
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
tests:
|
||||||
|
- path: backend
|
||||||
|
addbackend: true
|
||||||
|
noretries: true
|
||||||
|
nobinary: true
|
||||||
|
- path: fs/operations
|
||||||
|
subdir: true
|
||||||
|
fastlist: true
|
||||||
|
- path: fs/sync
|
||||||
|
subdir: true
|
||||||
|
fastlist: true
|
||||||
|
backends:
|
||||||
|
# - backend: "amazonclouddrive"
|
||||||
|
# remote: "TestAmazonCloudDrive:"
|
||||||
|
# subdir: false
|
||||||
|
# fastlist: false
|
||||||
|
- backend: "b2"
|
||||||
|
remote: "TestB2:"
|
||||||
|
subdir: true
|
||||||
|
fastlist: true
|
||||||
|
- backend: "crypt"
|
||||||
|
remote: "TestCryptDrive:"
|
||||||
|
subdir: false
|
||||||
|
fastlist: true
|
||||||
|
- backend: "crypt"
|
||||||
|
remote: "TestCryptSwift:"
|
||||||
|
subdir: false
|
||||||
|
fastlist: false
|
||||||
|
- backend: "drive"
|
||||||
|
remote: "TestDrive:"
|
||||||
|
subdir: false
|
||||||
|
fastlist: true
|
||||||
|
- backend: "dropbox"
|
||||||
|
remote: "TestDropbox:"
|
||||||
|
subdir: false
|
||||||
|
fastlist: false
|
||||||
|
- backend: "googlecloudstorage"
|
||||||
|
remote: "TestGoogleCloudStorage:"
|
||||||
|
subdir: true
|
||||||
|
fastlist: true
|
||||||
|
- backend: "hubic"
|
||||||
|
remote: "TestHubic:"
|
||||||
|
subdir: false
|
||||||
|
fastlist: false
|
||||||
|
- backend: "jottacloud"
|
||||||
|
remote: "TestJottacloud:"
|
||||||
|
subdir: false
|
||||||
|
fastlist: true
|
||||||
|
- backend: "onedrive"
|
||||||
|
remote: "TestOneDrive:"
|
||||||
|
subdir: false
|
||||||
|
fastlist: false
|
||||||
|
- backend: "s3"
|
||||||
|
remote: "TestS3:"
|
||||||
|
subdir: true
|
||||||
|
fastlist: true
|
||||||
|
- backend: "sftp"
|
||||||
|
remote: "TestSftp:"
|
||||||
|
subdir: false
|
||||||
|
fastlist: false
|
||||||
|
- backend: "swift"
|
||||||
|
remote: "TestSwift:"
|
||||||
|
subdir: true
|
||||||
|
fastlist: true
|
||||||
|
- backend: "yandex"
|
||||||
|
remote: "TestYandex:"
|
||||||
|
subdir: false
|
||||||
|
fastlist: false
|
||||||
|
- backend: "ftp"
|
||||||
|
remote: "TestFTP:"
|
||||||
|
subdir: false
|
||||||
|
fastlist: false
|
||||||
|
- backend: "box"
|
||||||
|
remote: "TestBox:"
|
||||||
|
subdir: false
|
||||||
|
fastlist: false
|
||||||
|
- backend: "qingstor"
|
||||||
|
remote: "TestQingStor:"
|
||||||
|
subdir: false
|
||||||
|
fastlist: false
|
||||||
|
oneonly: true
|
||||||
|
- backend: "azureblob"
|
||||||
|
remote: "TestAzureBlob:"
|
||||||
|
subdir: true
|
||||||
|
fastlist: true
|
||||||
|
- backend: "pcloud"
|
||||||
|
remote: "TestPcloud:"
|
||||||
|
subdir: false
|
||||||
|
fastlist: false
|
||||||
|
- backend: "webdav"
|
||||||
|
remote: "TestWebdav:"
|
||||||
|
subdir: false
|
||||||
|
fastlist: false
|
||||||
|
- backend: "cache"
|
||||||
|
remote: "TestCache:"
|
||||||
|
subdir: false
|
||||||
|
fastlist: false
|
||||||
|
- backend: "mega"
|
||||||
|
remote: "TestMega:"
|
||||||
|
subdir: false
|
||||||
|
fastlist: false
|
||||||
|
- backend: "opendrive"
|
||||||
|
remote: "TestOpenDrive:"
|
||||||
|
subdir: false
|
||||||
|
fastlist: false
|
||||||
|
- backend: "union"
|
||||||
|
remote: "TestUnion:"
|
||||||
|
subdir: false
|
||||||
|
fastlist: false
|
||||||
283
fstest/test_all/report.go
Normal file
283
fstest/test_all/report.go
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
// +build go1.10
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path"
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ncw/rclone/fs"
|
||||||
|
"github.com/skratchdot/open-golang/open"
|
||||||
|
)
|
||||||
|
|
||||||
|
const timeFormat = "2006-01-02-150405"
|
||||||
|
|
||||||
|
// Report holds the info to make a report on a series of test runs
|
||||||
|
type Report struct {
|
||||||
|
LogDir string // output directory for logs and report
|
||||||
|
StartTime time.Time // time started
|
||||||
|
DateTime string // directory name for output
|
||||||
|
Duration time.Duration // time the run took
|
||||||
|
Failed Runs // failed runs
|
||||||
|
Passed Runs // passed runs
|
||||||
|
Runs []ReportRun // runs to report
|
||||||
|
Version string // rclone version
|
||||||
|
Previous string // previous test name if known
|
||||||
|
IndexHTML string // path to the index.html file
|
||||||
|
URL string // online version
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReportRun is used in the templates to report on a test run
|
||||||
|
type ReportRun struct {
|
||||||
|
Name string
|
||||||
|
Runs Runs
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewReport initialises and returns a Report
|
||||||
|
func NewReport() *Report {
|
||||||
|
r := &Report{
|
||||||
|
StartTime: time.Now(),
|
||||||
|
Version: fs.Version,
|
||||||
|
}
|
||||||
|
r.DateTime = r.StartTime.Format(timeFormat)
|
||||||
|
|
||||||
|
// Find previous log directory if possible
|
||||||
|
names, err := ioutil.ReadDir(*outputDir)
|
||||||
|
if err == nil && len(names) > 0 {
|
||||||
|
r.Previous = names[len(names)-1].Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create output directory for logs and report
|
||||||
|
r.LogDir = path.Join(*outputDir, r.DateTime)
|
||||||
|
err = os.MkdirAll(r.LogDir, 0777)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to make log directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Online version
|
||||||
|
r.URL = *urlBase + r.DateTime + "/index.html"
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// End should be called when the tests are complete
|
||||||
|
func (r *Report) End() {
|
||||||
|
r.Duration = time.Since(r.StartTime)
|
||||||
|
sort.Sort(r.Failed)
|
||||||
|
sort.Sort(r.Passed)
|
||||||
|
r.Runs = []ReportRun{
|
||||||
|
{Name: "Failed", Runs: r.Failed},
|
||||||
|
{Name: "Passed", Runs: r.Passed},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllPassed returns true if there were no failed tests
|
||||||
|
func (r *Report) AllPassed() bool {
|
||||||
|
return len(r.Failed) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordResult should be called with a Run when it has finished to be
|
||||||
|
// recorded into the Report
|
||||||
|
func (r *Report) RecordResult(t *Run) {
|
||||||
|
if !t.passed() {
|
||||||
|
r.Failed = append(r.Failed, t)
|
||||||
|
} else {
|
||||||
|
r.Passed = append(r.Passed, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Title returns a human readable summary title for the Report
|
||||||
|
func (r *Report) Title() string {
|
||||||
|
if r.AllPassed() {
|
||||||
|
return fmt.Sprintf("PASS: All tests finished OK in %v", r.Duration)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("FAIL: %d tests failed in %v", len(r.Failed), r.Duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogSummary writes the summary to the log file
|
||||||
|
func (r *Report) LogSummary() {
|
||||||
|
log.Printf("Logs in %q", r.LogDir)
|
||||||
|
|
||||||
|
// Summarise results
|
||||||
|
log.Printf("SUMMARY")
|
||||||
|
log.Println(r.Title())
|
||||||
|
if !r.AllPassed() {
|
||||||
|
for _, t := range r.Failed {
|
||||||
|
log.Printf(" * %s", toShell(t.nextCmdLine()))
|
||||||
|
log.Printf(" * Failed tests: %v", t.failedTests)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogHTML writes the summary to index.html in LogDir
|
||||||
|
func (r *Report) LogHTML() {
|
||||||
|
r.IndexHTML = path.Join(r.LogDir, "index.html")
|
||||||
|
out, err := os.Create(r.IndexHTML)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to open index.html: %v", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
err := out.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to close index.html: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
err = reportTemplate.Execute(out, r)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to execute template: %v", err)
|
||||||
|
}
|
||||||
|
_ = open.Start("file://" + r.IndexHTML)
|
||||||
|
}
|
||||||
|
|
||||||
|
var reportHTML = `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>{{ .Title }}</title>
|
||||||
|
<style>
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-spacing: 0;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
table.tests {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
table, th, td {
|
||||||
|
border: 1px solid #264653;
|
||||||
|
}
|
||||||
|
.Failed {
|
||||||
|
color: #BE5B43;
|
||||||
|
}
|
||||||
|
.Passed {
|
||||||
|
color: #17564E;
|
||||||
|
}
|
||||||
|
.false {
|
||||||
|
font-weight: lighter;
|
||||||
|
}
|
||||||
|
.true {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
text-align: left;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:nth-child(even) {
|
||||||
|
background-color: #f2f2f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #5B1955;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
a:hover, a:focus {
|
||||||
|
color: #F4A261;
|
||||||
|
text-decoration:underline;
|
||||||
|
}
|
||||||
|
a:focus {
|
||||||
|
outline: thin dotted;
|
||||||
|
outline: 5px auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>{{ .Title }}</h1>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr><th>Version</th><td>{{ .Version }}</td></tr>
|
||||||
|
<tr><th>Test</th><td><a href="{{ .URL }}">{{ .DateTime}}</a></td></tr>
|
||||||
|
<tr><th>Duration</th><td>{{ .Duration }}</td></tr>
|
||||||
|
{{ if .Previous}}<tr><th>Previous</th><td><a href="../{{ .Previous }}/index.html">{{ .Previous }}</a></td></tr>{{ end }}
|
||||||
|
<tr><th>Up</th><td><a href="../">Older Tests</a></td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{{ range .Runs }}
|
||||||
|
{{ if .Runs }}
|
||||||
|
<h2 class="{{ .Name }}">{{ .Name }}: {{ len .Runs }}</h2>
|
||||||
|
<table class="{{ .Name }} tests">
|
||||||
|
<tr>
|
||||||
|
<th>Backend</th>
|
||||||
|
<th>Remote</th>
|
||||||
|
<th>Test</th>
|
||||||
|
<th>SubDir</th>
|
||||||
|
<th>FastList</th>
|
||||||
|
<th>Failed</th>
|
||||||
|
<th>Logs</th>
|
||||||
|
</tr>
|
||||||
|
{{ $prevBackend := "" }}
|
||||||
|
{{ $prevRemote := "" }}
|
||||||
|
{{ range .Runs}}
|
||||||
|
<tr>
|
||||||
|
<td>{{ if ne $prevBackend .Backend }}{{ .Backend }}{{ end }}{{ $prevBackend = .Backend }}</td>
|
||||||
|
<td>{{ if ne $prevRemote .Remote }}{{ .Remote }}{{ end }}{{ $prevRemote = .Remote }}</td>
|
||||||
|
<td>{{ .Path }}</td>
|
||||||
|
<td><span class="{{ .SubDir }}">{{ .SubDir }}</span></td>
|
||||||
|
<td><span class="{{ .FastList }}">{{ .FastList }}</span></td>
|
||||||
|
<td>{{ .FailedTests }}</td>
|
||||||
|
<td>{{ range $i, $v := .Logs }}<a href="{{ $v }}">#{{ $i }}</a> {{ end }}</td>
|
||||||
|
</tr>
|
||||||
|
{{ end }}
|
||||||
|
</table>
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`
|
||||||
|
|
||||||
|
var reportTemplate = template.Must(template.New("Report").Parse(reportHTML))
|
||||||
|
|
||||||
|
// EmailHTML sends the summary report to the email address supplied
|
||||||
|
func (r *Report) EmailHTML() {
|
||||||
|
if *emailReport == "" || r.IndexHTML == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("Sending email summary to %q", *emailReport)
|
||||||
|
cmdLine := []string{"mail", "-a", "Content-Type: text/html", *emailReport, "-s", "rclone integration tests: " + r.Title()}
|
||||||
|
cmd := exec.Command(cmdLine[0], cmdLine[1:]...)
|
||||||
|
in, err := os.Open(r.IndexHTML)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to open index.html: %v", err)
|
||||||
|
}
|
||||||
|
cmd.Stdin = in
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
err = cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to send email: %v", err)
|
||||||
|
}
|
||||||
|
_ = in.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// uploadTo uploads a copy of the report online to the dir given
|
||||||
|
func (r *Report) uploadTo(uploadDir string) {
|
||||||
|
dst := path.Join(*uploadPath, uploadDir)
|
||||||
|
log.Printf("Uploading results to %q", dst)
|
||||||
|
cmdLine := []string{"rclone", "sync", "--stats-log-level", "NOTICE", r.LogDir, dst}
|
||||||
|
cmd := exec.Command(cmdLine[0], cmdLine[1:]...)
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
err := cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to upload results: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload uploads a copy of the report online
|
||||||
|
func (r *Report) Upload() {
|
||||||
|
if *uploadPath == "" || r.IndexHTML == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Upload into dated directory
|
||||||
|
r.uploadTo(r.DateTime)
|
||||||
|
// And again into current
|
||||||
|
r.uploadTo("current")
|
||||||
|
}
|
||||||
356
fstest/test_all/run.go
Normal file
356
fstest/test_all/run.go
Normal file
@@ -0,0 +1,356 @@
|
|||||||
|
// Run a test
|
||||||
|
|
||||||
|
// +build go1.10
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"go/build"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path"
|
||||||
|
"regexp"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ncw/rclone/fs"
|
||||||
|
)
|
||||||
|
|
||||||
|
const testBase = "github.com/ncw/rclone/"
|
||||||
|
|
||||||
|
// Control concurrency per backend if required
|
||||||
|
var (
|
||||||
|
oneOnlyMu sync.Mutex
|
||||||
|
oneOnly = map[string]*sync.Mutex{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Run holds info about a running test
|
||||||
|
//
|
||||||
|
// A run just runs one command line, but it can be run multiple times
|
||||||
|
// if retries are needed.
|
||||||
|
type Run struct {
|
||||||
|
// Config
|
||||||
|
Remote string // name of the test remote
|
||||||
|
Backend string // name of the backend
|
||||||
|
Path string // path to the source directory
|
||||||
|
SubDir bool // add -sub-dir to tests
|
||||||
|
FastList bool // add -fast-list to tests
|
||||||
|
NoRetries bool // don't retry if set
|
||||||
|
OneOnly bool // only run test for this backend at once
|
||||||
|
NoBinary bool // set to not build a binary
|
||||||
|
// Internals
|
||||||
|
cmdLine []string
|
||||||
|
cmdString string
|
||||||
|
try int
|
||||||
|
err error
|
||||||
|
output []byte
|
||||||
|
failedTests []string
|
||||||
|
runFlag string
|
||||||
|
logDir string // directory to place the logs
|
||||||
|
trialName string // name/log file name of current trial
|
||||||
|
trialNames []string // list of all the trials
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runs records multiple Run objects
|
||||||
|
type Runs []*Run
|
||||||
|
|
||||||
|
// Sort interface
|
||||||
|
func (rs Runs) Len() int { return len(rs) }
|
||||||
|
func (rs Runs) Swap(i, j int) { rs[i], rs[j] = rs[j], rs[i] }
|
||||||
|
func (rs Runs) Less(i, j int) bool {
|
||||||
|
a, b := rs[i], rs[j]
|
||||||
|
if a.Backend < b.Backend {
|
||||||
|
return true
|
||||||
|
} else if a.Backend > b.Backend {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if a.Remote < b.Remote {
|
||||||
|
return true
|
||||||
|
} else if a.Remote > b.Remote {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if a.Path < b.Path {
|
||||||
|
return true
|
||||||
|
} else if a.Path > b.Path {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !a.SubDir && b.SubDir {
|
||||||
|
return true
|
||||||
|
} else if a.SubDir && !b.SubDir {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !a.FastList && b.FastList {
|
||||||
|
return true
|
||||||
|
} else if a.FastList && !b.FastList {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// dumpOutput prints the error output
|
||||||
|
func (r *Run) dumpOutput() {
|
||||||
|
log.Println("------------------------------------------------------------")
|
||||||
|
log.Printf("---- %q ----", r.cmdString)
|
||||||
|
log.Println(string(r.output))
|
||||||
|
log.Println("------------------------------------------------------------")
|
||||||
|
}
|
||||||
|
|
||||||
|
var failRe = regexp.MustCompile(`(?m)^\s*--- FAIL: (Test.*?) \(`)
|
||||||
|
|
||||||
|
// findFailures looks for all the tests which failed
|
||||||
|
func (r *Run) findFailures() {
|
||||||
|
oldFailedTests := r.failedTests
|
||||||
|
r.failedTests = nil
|
||||||
|
excludeParents := map[string]struct{}{}
|
||||||
|
for _, matches := range failRe.FindAllSubmatch(r.output, -1) {
|
||||||
|
failedTest := string(matches[1])
|
||||||
|
r.failedTests = append(r.failedTests, failedTest)
|
||||||
|
// Find all the parents of this test
|
||||||
|
parts := strings.Split(failedTest, "/")
|
||||||
|
for i := len(parts) - 1; i >= 1; i-- {
|
||||||
|
excludeParents[strings.Join(parts[:i], "/")] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Exclude the parents
|
||||||
|
var newTests = r.failedTests[:0]
|
||||||
|
for _, failedTest := range r.failedTests {
|
||||||
|
if _, excluded := excludeParents[failedTest]; !excluded {
|
||||||
|
newTests = append(newTests, failedTest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r.failedTests = newTests
|
||||||
|
if len(r.failedTests) != 0 {
|
||||||
|
r.runFlag = "^(" + strings.Join(r.failedTests, "|") + ")$"
|
||||||
|
} else {
|
||||||
|
r.runFlag = ""
|
||||||
|
}
|
||||||
|
if r.passed() && len(r.failedTests) != 0 {
|
||||||
|
log.Printf("%q - Expecting no errors but got: %v", r.cmdString, r.failedTests)
|
||||||
|
r.dumpOutput()
|
||||||
|
} else if !r.passed() && len(r.failedTests) == 0 {
|
||||||
|
log.Printf("%q - Expecting errors but got none: %v", r.cmdString, r.failedTests)
|
||||||
|
r.dumpOutput()
|
||||||
|
r.failedTests = oldFailedTests
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// nextCmdLine returns the next command line
|
||||||
|
func (r *Run) nextCmdLine() []string {
|
||||||
|
cmdLine := r.cmdLine
|
||||||
|
if r.runFlag != "" {
|
||||||
|
cmdLine = append(cmdLine, "-test.run", r.runFlag)
|
||||||
|
}
|
||||||
|
return cmdLine
|
||||||
|
}
|
||||||
|
|
||||||
|
// trial runs a single test
|
||||||
|
func (r *Run) trial() {
|
||||||
|
cmdLine := r.nextCmdLine()
|
||||||
|
cmdString := toShell(cmdLine)
|
||||||
|
msg := fmt.Sprintf("%q - Starting (try %d/%d)", cmdString, r.try, *maxTries)
|
||||||
|
log.Println(msg)
|
||||||
|
logName := path.Join(r.logDir, r.trialName)
|
||||||
|
out, err := os.Create(logName)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Couldn't create log file: %v", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
err := out.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to close log file: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
_, _ = fmt.Fprintln(out, msg)
|
||||||
|
|
||||||
|
// Early exit if --try-run
|
||||||
|
if *dryRun {
|
||||||
|
log.Printf("Not executing as --dry-run: %v", cmdLine)
|
||||||
|
_, _ = fmt.Fprintln(out, "--dry-run is set - not running")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal buffer
|
||||||
|
var b bytes.Buffer
|
||||||
|
multiOut := io.MultiWriter(out, &b)
|
||||||
|
|
||||||
|
cmd := exec.Command(cmdLine[0], cmdLine[1:]...)
|
||||||
|
cmd.Stderr = multiOut
|
||||||
|
cmd.Stdout = multiOut
|
||||||
|
cmd.Dir = r.Path
|
||||||
|
start := time.Now()
|
||||||
|
r.err = cmd.Run()
|
||||||
|
r.output = b.Bytes()
|
||||||
|
duration := time.Since(start)
|
||||||
|
r.findFailures()
|
||||||
|
if r.passed() {
|
||||||
|
msg = fmt.Sprintf("%q - Finished OK in %v (try %d/%d)", cmdString, duration, r.try, *maxTries)
|
||||||
|
} else {
|
||||||
|
msg = fmt.Sprintf("%q - Finished ERROR in %v (try %d/%d): %v: Failed %v", cmdString, duration, r.try, *maxTries, r.err, r.failedTests)
|
||||||
|
}
|
||||||
|
log.Println(msg)
|
||||||
|
_, _ = fmt.Fprintln(out, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// passed returns true if the test passed
|
||||||
|
func (r *Run) passed() bool {
|
||||||
|
return r.err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GOPATH returns the current GOPATH
|
||||||
|
func GOPATH() string {
|
||||||
|
gopath := os.Getenv("GOPATH")
|
||||||
|
if gopath == "" {
|
||||||
|
gopath = build.Default.GOPATH
|
||||||
|
}
|
||||||
|
return gopath
|
||||||
|
}
|
||||||
|
|
||||||
|
// BinaryName turns a package name into a binary name
|
||||||
|
func (r *Run) BinaryName() string {
|
||||||
|
binary := path.Base(r.Path) + ".test"
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
binary += ".exe"
|
||||||
|
}
|
||||||
|
return binary
|
||||||
|
}
|
||||||
|
|
||||||
|
// BinaryPath turns a package name into a binary path
|
||||||
|
func (r *Run) BinaryPath() string {
|
||||||
|
return path.Join(r.Path, r.BinaryName())
|
||||||
|
}
|
||||||
|
|
||||||
|
// PackagePath returns the path to the package
|
||||||
|
func (r *Run) PackagePath() string {
|
||||||
|
return path.Join(GOPATH(), "src", r.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeTestBinary makes the binary we will run
|
||||||
|
func (r *Run) MakeTestBinary() {
|
||||||
|
binary := r.BinaryPath()
|
||||||
|
binaryName := r.BinaryName()
|
||||||
|
log.Printf("%s: Making test binary %q", r.Path, binaryName)
|
||||||
|
cmdLine := []string{"go", "test", "-c"}
|
||||||
|
if *dryRun {
|
||||||
|
log.Printf("Not executing: %v", cmdLine)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cmd := exec.Command(cmdLine[0], cmdLine[1:]...)
|
||||||
|
cmd.Dir = r.Path
|
||||||
|
err := cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to make test binary: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(binary); err != nil {
|
||||||
|
log.Fatalf("Couldn't find test binary %q", binary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveTestBinary removes the binary made in makeTestBinary
|
||||||
|
func (r *Run) RemoveTestBinary() {
|
||||||
|
if *dryRun {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
binary := r.BinaryPath()
|
||||||
|
err := os.Remove(binary) // Delete the binary when finished
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error removing test binary %q: %v", binary, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the run name as a file name friendly string
|
||||||
|
func (r *Run) Name() string {
|
||||||
|
ns := []string{
|
||||||
|
r.Backend,
|
||||||
|
strings.Replace(r.Path, "/", ".", -1),
|
||||||
|
r.Remote,
|
||||||
|
}
|
||||||
|
if r.SubDir {
|
||||||
|
ns = append(ns, "subdir")
|
||||||
|
}
|
||||||
|
if r.FastList {
|
||||||
|
ns = append(ns, "fastlist")
|
||||||
|
}
|
||||||
|
ns = append(ns, fmt.Sprintf("%d", r.try))
|
||||||
|
s := strings.Join(ns, "-")
|
||||||
|
s = strings.Replace(s, ":", "", -1)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init the Run
|
||||||
|
func (r *Run) Init() {
|
||||||
|
prefix := "-test."
|
||||||
|
if r.NoBinary {
|
||||||
|
prefix = "-"
|
||||||
|
r.cmdLine = []string{"go", "test"}
|
||||||
|
} else {
|
||||||
|
r.cmdLine = []string{"./" + r.BinaryName()}
|
||||||
|
}
|
||||||
|
r.cmdLine = append(r.cmdLine, prefix+"v", prefix+"timeout", timeout.String(), "-remote", r.Remote)
|
||||||
|
r.try = 1
|
||||||
|
if *verbose {
|
||||||
|
r.cmdLine = append(r.cmdLine, "-verbose")
|
||||||
|
fs.Config.LogLevel = fs.LogLevelDebug
|
||||||
|
}
|
||||||
|
if *runOnly != "" {
|
||||||
|
r.cmdLine = append(r.cmdLine, prefix+"run", *runOnly)
|
||||||
|
}
|
||||||
|
if r.SubDir {
|
||||||
|
r.cmdLine = append(r.cmdLine, "-subdir")
|
||||||
|
}
|
||||||
|
if r.FastList {
|
||||||
|
r.cmdLine = append(r.cmdLine, "-fast-list")
|
||||||
|
}
|
||||||
|
r.cmdString = toShell(r.cmdLine)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logs returns all the log names
|
||||||
|
func (r *Run) Logs() []string {
|
||||||
|
return r.trialNames
|
||||||
|
}
|
||||||
|
|
||||||
|
// FailedTests returns the failed tests as a comma separated string, limiting the number
|
||||||
|
func (r *Run) FailedTests() string {
|
||||||
|
const maxTests = 5
|
||||||
|
ts := r.failedTests
|
||||||
|
if len(ts) > maxTests {
|
||||||
|
ts = ts[:maxTests:maxTests]
|
||||||
|
ts = append(ts, fmt.Sprintf("… (%d more)", len(r.failedTests)-maxTests))
|
||||||
|
}
|
||||||
|
return strings.Join(ts, ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run runs all the trials for this test
|
||||||
|
func (r *Run) Run(logDir string, result chan<- *Run) {
|
||||||
|
if r.OneOnly {
|
||||||
|
oneOnlyMu.Lock()
|
||||||
|
mu := oneOnly[r.Backend]
|
||||||
|
if mu == nil {
|
||||||
|
mu = new(sync.Mutex)
|
||||||
|
oneOnly[r.Backend] = mu
|
||||||
|
}
|
||||||
|
oneOnlyMu.Unlock()
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
}
|
||||||
|
r.Init()
|
||||||
|
r.logDir = logDir
|
||||||
|
for r.try = 1; r.try <= *maxTries; r.try++ {
|
||||||
|
r.trialName = r.Name() + ".txt"
|
||||||
|
r.trialNames = append(r.trialNames, r.trialName)
|
||||||
|
log.Printf("Starting run with log %q", r.trialName)
|
||||||
|
r.trial()
|
||||||
|
if r.passed() || r.NoRetries {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !r.passed() {
|
||||||
|
r.dumpOutput()
|
||||||
|
}
|
||||||
|
result <- r
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user