mirror of
https://github.com/rclone/rclone.git
synced 2025-12-22 19:23:40 +00:00
Compare commits
1 Commits
pasnox-sym
...
fix-vfs-la
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7cba2835d |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -15,4 +15,3 @@ fuzz-build.zip
|
||||
*.rej
|
||||
Thumbs.db
|
||||
__pycache__
|
||||
*.kate-swp
|
||||
|
||||
2580
MANUAL.html
generated
2580
MANUAL.html
generated
File diff suppressed because it is too large
Load Diff
2883
MANUAL.txt
generated
2883
MANUAL.txt
generated
File diff suppressed because it is too large
Load Diff
3
Makefile
3
Makefile
@@ -81,9 +81,6 @@ quicktest:
|
||||
racequicktest:
|
||||
RCLONE_CONFIG="/notfound" go test $(BUILDTAGS) -cpu=2 -race ./...
|
||||
|
||||
compiletest:
|
||||
RCLONE_CONFIG="/notfound" go test $(BUILDTAGS) -run XXX ./...
|
||||
|
||||
# Do source code quality checks
|
||||
check: rclone
|
||||
@echo "-- START CODE QUALITY REPORT -------------------------------"
|
||||
|
||||
@@ -50,7 +50,6 @@ Rclone *("rsync for cloud storage")* is a command-line program to sync files and
|
||||
* IBM COS S3 [:page_facing_up:](https://rclone.org/s3/#ibm-cos-s3)
|
||||
* IONOS Cloud [:page_facing_up:](https://rclone.org/s3/#ionos)
|
||||
* Koofr [:page_facing_up:](https://rclone.org/koofr/)
|
||||
* Liara Object Storage [:page_facing_up:](https://rclone.org/s3/#liara-object-storage)
|
||||
* Mail.ru Cloud [:page_facing_up:](https://rclone.org/mailru/)
|
||||
* Memset Memstore [:page_facing_up:](https://rclone.org/swift/)
|
||||
* Mega [:page_facing_up:](https://rclone.org/mega/)
|
||||
|
||||
@@ -74,7 +74,8 @@ Set vars
|
||||
First make the release branch. If this is a second point release then
|
||||
this will be done already.
|
||||
|
||||
* git co -b ${BASE_TAG}-stable ${BASE_TAG}.0
|
||||
* git branch ${BASE_TAG} ${BASE_TAG}-stable
|
||||
* git co ${BASE_TAG}-stable
|
||||
* make startstable
|
||||
|
||||
Now
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
//go:build !plan9 && !solaris && !js && go1.18
|
||||
// +build !plan9,!solaris,!js,go1.18
|
||||
//go:build !plan9 && !solaris && !js
|
||||
// +build !plan9,!solaris,!js
|
||||
|
||||
package azureblob
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
// Test AzureBlob filesystem interface
|
||||
|
||||
//go:build !plan9 && !solaris && !js && go1.18
|
||||
// +build !plan9,!solaris,!js,go1.18
|
||||
//go:build !plan9 && !solaris && !js
|
||||
// +build !plan9,!solaris,!js
|
||||
|
||||
package azureblob
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/rclone/rclone/fs"
|
||||
@@ -16,12 +17,10 @@ import (
|
||||
// TestIntegration runs integration tests against the remote
|
||||
func TestIntegration(t *testing.T) {
|
||||
fstests.Run(t, &fstests.Opt{
|
||||
RemoteName: "TestAzureBlob:",
|
||||
NilObject: (*Object)(nil),
|
||||
TiersToTest: []string{"Hot", "Cool"},
|
||||
ChunkedUpload: fstests.ChunkedUploadConfig{
|
||||
MinChunkSize: defaultChunkSize,
|
||||
},
|
||||
RemoteName: "TestAzureBlob:",
|
||||
NilObject: (*Object)(nil),
|
||||
TiersToTest: []string{"Hot", "Cool"},
|
||||
ChunkedUpload: fstests.ChunkedUploadConfig{},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -33,6 +32,36 @@ var (
|
||||
_ fstests.SetUploadChunkSizer = (*Fs)(nil)
|
||||
)
|
||||
|
||||
// TestServicePrincipalFileSuccess checks that, given a proper JSON file, we can create a token.
|
||||
func TestServicePrincipalFileSuccess(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
credentials := `
|
||||
{
|
||||
"appId": "my application (client) ID",
|
||||
"password": "my secret",
|
||||
"tenant": "my active directory tenant ID"
|
||||
}
|
||||
`
|
||||
tokenRefresher, err := newServicePrincipalTokenRefresher(ctx, []byte(credentials))
|
||||
if assert.NoError(t, err) {
|
||||
assert.NotNil(t, tokenRefresher)
|
||||
}
|
||||
}
|
||||
|
||||
// TestServicePrincipalFileFailure checks that, given a JSON file with a missing secret, it returns an error.
|
||||
func TestServicePrincipalFileFailure(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
credentials := `
|
||||
{
|
||||
"appId": "my application (client) ID",
|
||||
"tenant": "my active directory tenant ID"
|
||||
}
|
||||
`
|
||||
_, err := newServicePrincipalTokenRefresher(ctx, []byte(credentials))
|
||||
assert.Error(t, err)
|
||||
assert.EqualError(t, err, "error creating service principal token: parameter 'secret' cannot be empty")
|
||||
}
|
||||
|
||||
func TestValidateAccessTier(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
accessTier string
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Build for azureblob for unsupported platforms to stop go complaining
|
||||
// about "no buildable Go source files "
|
||||
|
||||
//go:build plan9 || solaris || js || !go1.18
|
||||
// +build plan9 solaris js !go1.18
|
||||
//go:build plan9 || solaris || js
|
||||
// +build plan9 solaris js
|
||||
|
||||
package azureblob
|
||||
|
||||
136
backend/azureblob/imds.go
Normal file
136
backend/azureblob/imds.go
Normal file
@@ -0,0 +1,136 @@
|
||||
//go:build !plan9 && !solaris && !js
|
||||
// +build !plan9,!solaris,!js
|
||||
|
||||
package azureblob
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/Azure/go-autorest/autorest/adal"
|
||||
"github.com/rclone/rclone/fs"
|
||||
"github.com/rclone/rclone/fs/fshttp"
|
||||
)
|
||||
|
||||
const (
|
||||
azureResource = "https://storage.azure.com"
|
||||
imdsAPIVersion = "2018-02-01"
|
||||
msiEndpointDefault = "http://169.254.169.254/metadata/identity/oauth2/token"
|
||||
)
|
||||
|
||||
// This custom type is used to add the port the test server has bound to
|
||||
// to the request context.
|
||||
type testPortKey string
|
||||
|
||||
type msiIdentifierType int
|
||||
|
||||
const (
|
||||
msiClientID msiIdentifierType = iota
|
||||
msiObjectID
|
||||
msiResourceID
|
||||
)
|
||||
|
||||
type userMSI struct {
|
||||
Type msiIdentifierType
|
||||
Value string
|
||||
}
|
||||
|
||||
type httpError struct {
|
||||
Response *http.Response
|
||||
}
|
||||
|
||||
func (e httpError) Error() string {
|
||||
return fmt.Sprintf("HTTP error %v (%v)", e.Response.StatusCode, e.Response.Status)
|
||||
}
|
||||
|
||||
// GetMSIToken attempts to obtain an MSI token from the Azure Instance
|
||||
// Metadata Service.
|
||||
func GetMSIToken(ctx context.Context, identity *userMSI) (adal.Token, error) {
|
||||
// Attempt to get an MSI token; silently continue if unsuccessful.
|
||||
// This code has been lovingly stolen from azcopy's OAuthTokenManager.
|
||||
result := adal.Token{}
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", msiEndpointDefault, nil)
|
||||
if err != nil {
|
||||
fs.Debugf(nil, "Failed to create request: %v", err)
|
||||
return result, err
|
||||
}
|
||||
params := req.URL.Query()
|
||||
params.Set("resource", azureResource)
|
||||
params.Set("api-version", imdsAPIVersion)
|
||||
|
||||
// Specify user-assigned identity if requested.
|
||||
if identity != nil {
|
||||
switch identity.Type {
|
||||
case msiClientID:
|
||||
params.Set("client_id", identity.Value)
|
||||
case msiObjectID:
|
||||
params.Set("object_id", identity.Value)
|
||||
case msiResourceID:
|
||||
params.Set("mi_res_id", identity.Value)
|
||||
default:
|
||||
// If this happens, the calling function and this one don't agree on
|
||||
// what valid ID types exist.
|
||||
return result, fmt.Errorf("unknown MSI identity type specified")
|
||||
}
|
||||
}
|
||||
req.URL.RawQuery = params.Encode()
|
||||
|
||||
// The Metadata header is required by all calls to IMDS.
|
||||
req.Header.Set("Metadata", "true")
|
||||
|
||||
// If this function is run in a test, query the test server instead of IMDS.
|
||||
testPort, isTest := ctx.Value(testPortKey("testPort")).(int)
|
||||
if isTest {
|
||||
req.URL.Host = fmt.Sprintf("localhost:%d", testPort)
|
||||
req.Host = req.URL.Host
|
||||
}
|
||||
|
||||
// Send request
|
||||
httpClient := fshttp.NewClient(ctx)
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("MSI is not enabled on this VM: %w", err)
|
||||
}
|
||||
defer func() { // resp and Body should not be nil
|
||||
_, err = io.Copy(io.Discard, resp.Body)
|
||||
if err != nil {
|
||||
fs.Debugf(nil, "Unable to drain IMDS response: %v", err)
|
||||
}
|
||||
err = resp.Body.Close()
|
||||
if err != nil {
|
||||
fs.Debugf(nil, "Unable to close IMDS response: %v", err)
|
||||
}
|
||||
}()
|
||||
// Check if the status code indicates success
|
||||
// The request returns 200 currently, add 201 and 202 as well for possible extension.
|
||||
switch resp.StatusCode {
|
||||
case 200, 201, 202:
|
||||
break
|
||||
default:
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
fs.Errorf(nil, "Couldn't obtain OAuth token from IMDS; server returned status code %d and body: %v", resp.StatusCode, string(body))
|
||||
return result, httpError{Response: resp}
|
||||
}
|
||||
|
||||
b, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("couldn't read IMDS response: %w", err)
|
||||
}
|
||||
// Remove BOM, if any. azcopy does this so I'm following along.
|
||||
b = bytes.TrimPrefix(b, []byte("\xef\xbb\xbf"))
|
||||
|
||||
// This would be a good place to persist the token if a large number of rclone
|
||||
// invocations are being made in a short amount of time. If the token is
|
||||
// persisted, the azureblob code will need to check for expiry before every
|
||||
// storage API call.
|
||||
err = json.Unmarshal(b, &result)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("couldn't unmarshal IMDS response: %w", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
118
backend/azureblob/imds_test.go
Normal file
118
backend/azureblob/imds_test.go
Normal file
@@ -0,0 +1,118 @@
|
||||
//go:build !plan9 && !solaris && !js
|
||||
// +build !plan9,!solaris,!js
|
||||
|
||||
package azureblob
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/Azure/go-autorest/autorest/adal"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func handler(t *testing.T, actual *map[string]string) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
parameters := r.URL.Query()
|
||||
(*actual)["path"] = r.URL.Path
|
||||
(*actual)["Metadata"] = r.Header.Get("Metadata")
|
||||
(*actual)["method"] = r.Method
|
||||
for paramName := range parameters {
|
||||
(*actual)[paramName] = parameters.Get(paramName)
|
||||
}
|
||||
// Make response.
|
||||
response := adal.Token{}
|
||||
responseBytes, err := json.Marshal(response)
|
||||
require.NoError(t, err)
|
||||
_, err = w.Write(responseBytes)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagedIdentity(t *testing.T) {
|
||||
// test user-assigned identity specifiers to use
|
||||
testMSIClientID := "d859b29f-5c9c-42f8-a327-ec1bc6408d79"
|
||||
testMSIObjectID := "9ffeb650-3ca0-4278-962b-5a38d520591a"
|
||||
testMSIResourceID := "/subscriptions/fe714c49-b8a4-4d49-9388-96a20daa318f/resourceGroups/somerg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/someidentity"
|
||||
tests := []struct {
|
||||
identity *userMSI
|
||||
identityParameterName string
|
||||
expectedAbsent []string
|
||||
}{
|
||||
{&userMSI{msiClientID, testMSIClientID}, "client_id", []string{"object_id", "mi_res_id"}},
|
||||
{&userMSI{msiObjectID, testMSIObjectID}, "object_id", []string{"client_id", "mi_res_id"}},
|
||||
{&userMSI{msiResourceID, testMSIResourceID}, "mi_res_id", []string{"object_id", "client_id"}},
|
||||
{nil, "(default)", []string{"object_id", "client_id", "mi_res_id"}},
|
||||
}
|
||||
alwaysExpected := map[string]string{
|
||||
"path": "/metadata/identity/oauth2/token",
|
||||
"resource": "https://storage.azure.com",
|
||||
"Metadata": "true",
|
||||
"api-version": "2018-02-01",
|
||||
"method": "GET",
|
||||
}
|
||||
for _, test := range tests {
|
||||
actual := make(map[string]string, 10)
|
||||
testServer := httptest.NewServer(handler(t, &actual))
|
||||
defer testServer.Close()
|
||||
testServerPort, err := strconv.Atoi(strings.Split(testServer.URL, ":")[2])
|
||||
require.NoError(t, err)
|
||||
ctx := context.WithValue(context.TODO(), testPortKey("testPort"), testServerPort)
|
||||
_, err = GetMSIToken(ctx, test.identity)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Validate expected query parameters present
|
||||
expected := make(map[string]string)
|
||||
for k, v := range alwaysExpected {
|
||||
expected[k] = v
|
||||
}
|
||||
if test.identity != nil {
|
||||
expected[test.identityParameterName] = test.identity.Value
|
||||
}
|
||||
|
||||
for key := range expected {
|
||||
value, exists := actual[key]
|
||||
if assert.Truef(t, exists, "test of %s: query parameter %s was not passed",
|
||||
test.identityParameterName, key) {
|
||||
assert.Equalf(t, expected[key], value,
|
||||
"test of %s: parameter %s has incorrect value", test.identityParameterName, key)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate unexpected query parameters absent
|
||||
for _, key := range test.expectedAbsent {
|
||||
_, exists := actual[key]
|
||||
assert.Falsef(t, exists, "query parameter %s was unexpectedly passed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func errorHandler(resultCode int) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "Test error generated", resultCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIMDSErrors(t *testing.T) {
|
||||
errorCodes := []int{404, 429, 500}
|
||||
for _, code := range errorCodes {
|
||||
testServer := httptest.NewServer(errorHandler(code))
|
||||
defer testServer.Close()
|
||||
testServerPort, err := strconv.Atoi(strings.Split(testServer.URL, ":")[2])
|
||||
require.NoError(t, err)
|
||||
ctx := context.WithValue(context.TODO(), testPortKey("testPort"), testServerPort)
|
||||
_, err = GetMSIToken(ctx, nil)
|
||||
require.Error(t, err)
|
||||
httpErr, ok := err.(httpError)
|
||||
require.Truef(t, ok, "HTTP error %d did not result in an httpError object", code)
|
||||
assert.Equalf(t, httpErr.Response.StatusCode, code, "desired error %d but didn't get it", code)
|
||||
}
|
||||
}
|
||||
61
backend/cache/cache_internal_test.go
vendored
61
backend/cache/cache_internal_test.go
vendored
@@ -101,12 +101,14 @@ func TestMain(m *testing.M) {
|
||||
|
||||
func TestInternalListRootAndInnerRemotes(t *testing.T) {
|
||||
id := fmt.Sprintf("tilrair%v", time.Now().Unix())
|
||||
rootFs, _ := runInstance.newCacheFs(t, remoteName, id, true, true, nil)
|
||||
rootFs, boltDb := runInstance.newCacheFs(t, remoteName, id, true, true, nil, nil)
|
||||
defer runInstance.cleanupFs(t, rootFs, boltDb)
|
||||
|
||||
// Instantiate inner fs
|
||||
innerFolder := "inner"
|
||||
runInstance.mkdir(t, rootFs, innerFolder)
|
||||
rootFs2, _ := runInstance.newCacheFs(t, remoteName, id+"/"+innerFolder, true, true, nil)
|
||||
rootFs2, boltDb2 := runInstance.newCacheFs(t, remoteName, id+"/"+innerFolder, true, true, nil, nil)
|
||||
defer runInstance.cleanupFs(t, rootFs2, boltDb2)
|
||||
|
||||
runInstance.writeObjectString(t, rootFs2, "one", "content")
|
||||
listRoot, err := runInstance.list(t, rootFs, "")
|
||||
@@ -223,7 +225,8 @@ func TestInternalVfsCache(t *testing.T) {
|
||||
|
||||
func TestInternalObjWrapFsFound(t *testing.T) {
|
||||
id := fmt.Sprintf("tiowff%v", time.Now().Unix())
|
||||
rootFs, _ := runInstance.newCacheFs(t, remoteName, id, true, true, nil)
|
||||
rootFs, boltDb := runInstance.newCacheFs(t, remoteName, id, true, true, nil, nil)
|
||||
defer runInstance.cleanupFs(t, rootFs, boltDb)
|
||||
|
||||
cfs, err := runInstance.getCacheFs(rootFs)
|
||||
require.NoError(t, err)
|
||||
@@ -255,7 +258,8 @@ func TestInternalObjWrapFsFound(t *testing.T) {
|
||||
|
||||
func TestInternalObjNotFound(t *testing.T) {
|
||||
id := fmt.Sprintf("tionf%v", time.Now().Unix())
|
||||
rootFs, _ := runInstance.newCacheFs(t, remoteName, id, false, true, nil)
|
||||
rootFs, boltDb := runInstance.newCacheFs(t, remoteName, id, false, true, nil, nil)
|
||||
defer runInstance.cleanupFs(t, rootFs, boltDb)
|
||||
|
||||
obj, err := rootFs.NewObject(context.Background(), "404")
|
||||
require.Error(t, err)
|
||||
@@ -265,7 +269,8 @@ func TestInternalObjNotFound(t *testing.T) {
|
||||
func TestInternalCachedWrittenContentMatches(t *testing.T) {
|
||||
testy.SkipUnreliable(t)
|
||||
id := fmt.Sprintf("ticwcm%v", time.Now().Unix())
|
||||
rootFs, _ := runInstance.newCacheFs(t, remoteName, id, false, true, nil)
|
||||
rootFs, boltDb := runInstance.newCacheFs(t, remoteName, id, false, true, nil, nil)
|
||||
defer runInstance.cleanupFs(t, rootFs, boltDb)
|
||||
|
||||
cfs, err := runInstance.getCacheFs(rootFs)
|
||||
require.NoError(t, err)
|
||||
@@ -292,7 +297,8 @@ func TestInternalDoubleWrittenContentMatches(t *testing.T) {
|
||||
t.Skip("Skip test on windows/386")
|
||||
}
|
||||
id := fmt.Sprintf("tidwcm%v", time.Now().Unix())
|
||||
rootFs, _ := runInstance.newCacheFs(t, remoteName, id, false, true, nil)
|
||||
rootFs, boltDb := runInstance.newCacheFs(t, remoteName, id, false, true, nil, nil)
|
||||
defer runInstance.cleanupFs(t, rootFs, boltDb)
|
||||
|
||||
// write the object
|
||||
runInstance.writeRemoteString(t, rootFs, "one", "one content")
|
||||
@@ -310,7 +316,8 @@ func TestInternalDoubleWrittenContentMatches(t *testing.T) {
|
||||
func TestInternalCachedUpdatedContentMatches(t *testing.T) {
|
||||
testy.SkipUnreliable(t)
|
||||
id := fmt.Sprintf("ticucm%v", time.Now().Unix())
|
||||
rootFs, _ := runInstance.newCacheFs(t, remoteName, id, false, true, nil)
|
||||
rootFs, boltDb := runInstance.newCacheFs(t, remoteName, id, false, true, nil, nil)
|
||||
defer runInstance.cleanupFs(t, rootFs, boltDb)
|
||||
var err error
|
||||
|
||||
// create some rand test data
|
||||
@@ -339,7 +346,8 @@ func TestInternalCachedUpdatedContentMatches(t *testing.T) {
|
||||
func TestInternalWrappedWrittenContentMatches(t *testing.T) {
|
||||
id := fmt.Sprintf("tiwwcm%v", time.Now().Unix())
|
||||
vfsflags.Opt.DirCacheTime = time.Second
|
||||
rootFs, _ := runInstance.newCacheFs(t, remoteName, id, true, true, nil)
|
||||
rootFs, boltDb := runInstance.newCacheFs(t, remoteName, id, true, true, nil, nil)
|
||||
defer runInstance.cleanupFs(t, rootFs, boltDb)
|
||||
if runInstance.rootIsCrypt {
|
||||
t.Skip("test skipped with crypt remote")
|
||||
}
|
||||
@@ -369,7 +377,8 @@ func TestInternalWrappedWrittenContentMatches(t *testing.T) {
|
||||
func TestInternalLargeWrittenContentMatches(t *testing.T) {
|
||||
id := fmt.Sprintf("tilwcm%v", time.Now().Unix())
|
||||
vfsflags.Opt.DirCacheTime = time.Second
|
||||
rootFs, _ := runInstance.newCacheFs(t, remoteName, id, true, true, nil)
|
||||
rootFs, boltDb := runInstance.newCacheFs(t, remoteName, id, true, true, nil, nil)
|
||||
defer runInstance.cleanupFs(t, rootFs, boltDb)
|
||||
if runInstance.rootIsCrypt {
|
||||
t.Skip("test skipped with crypt remote")
|
||||
}
|
||||
@@ -395,7 +404,8 @@ func TestInternalLargeWrittenContentMatches(t *testing.T) {
|
||||
|
||||
func TestInternalWrappedFsChangeNotSeen(t *testing.T) {
|
||||
id := fmt.Sprintf("tiwfcns%v", time.Now().Unix())
|
||||
rootFs, _ := runInstance.newCacheFs(t, remoteName, id, false, true, nil)
|
||||
rootFs, boltDb := runInstance.newCacheFs(t, remoteName, id, false, true, nil, nil)
|
||||
defer runInstance.cleanupFs(t, rootFs, boltDb)
|
||||
|
||||
cfs, err := runInstance.getCacheFs(rootFs)
|
||||
require.NoError(t, err)
|
||||
@@ -449,7 +459,8 @@ func TestInternalWrappedFsChangeNotSeen(t *testing.T) {
|
||||
|
||||
func TestInternalMoveWithNotify(t *testing.T) {
|
||||
id := fmt.Sprintf("timwn%v", time.Now().Unix())
|
||||
rootFs, _ := runInstance.newCacheFs(t, remoteName, id, false, true, nil)
|
||||
rootFs, boltDb := runInstance.newCacheFs(t, remoteName, id, false, true, nil, nil)
|
||||
defer runInstance.cleanupFs(t, rootFs, boltDb)
|
||||
if !runInstance.wrappedIsExternal {
|
||||
t.Skipf("Not external")
|
||||
}
|
||||
@@ -535,7 +546,8 @@ func TestInternalMoveWithNotify(t *testing.T) {
|
||||
|
||||
func TestInternalNotifyCreatesEmptyParts(t *testing.T) {
|
||||
id := fmt.Sprintf("tincep%v", time.Now().Unix())
|
||||
rootFs, boltDb := runInstance.newCacheFs(t, remoteName, id, false, true, nil)
|
||||
rootFs, boltDb := runInstance.newCacheFs(t, remoteName, id, false, true, nil, nil)
|
||||
defer runInstance.cleanupFs(t, rootFs, boltDb)
|
||||
if !runInstance.wrappedIsExternal {
|
||||
t.Skipf("Not external")
|
||||
}
|
||||
@@ -621,7 +633,8 @@ func TestInternalNotifyCreatesEmptyParts(t *testing.T) {
|
||||
|
||||
func TestInternalChangeSeenAfterDirCacheFlush(t *testing.T) {
|
||||
id := fmt.Sprintf("ticsadcf%v", time.Now().Unix())
|
||||
rootFs, _ := runInstance.newCacheFs(t, remoteName, id, false, true, nil)
|
||||
rootFs, boltDb := runInstance.newCacheFs(t, remoteName, id, false, true, nil, nil)
|
||||
defer runInstance.cleanupFs(t, rootFs, boltDb)
|
||||
|
||||
cfs, err := runInstance.getCacheFs(rootFs)
|
||||
require.NoError(t, err)
|
||||
@@ -653,7 +666,8 @@ func TestInternalChangeSeenAfterDirCacheFlush(t *testing.T) {
|
||||
|
||||
func TestInternalCacheWrites(t *testing.T) {
|
||||
id := "ticw"
|
||||
rootFs, boltDb := runInstance.newCacheFs(t, remoteName, id, false, true, map[string]string{"writes": "true"})
|
||||
rootFs, boltDb := runInstance.newCacheFs(t, remoteName, id, false, true, nil, map[string]string{"writes": "true"})
|
||||
defer runInstance.cleanupFs(t, rootFs, boltDb)
|
||||
|
||||
cfs, err := runInstance.getCacheFs(rootFs)
|
||||
require.NoError(t, err)
|
||||
@@ -674,7 +688,8 @@ func TestInternalMaxChunkSizeRespected(t *testing.T) {
|
||||
t.Skip("Skip test on windows/386")
|
||||
}
|
||||
id := fmt.Sprintf("timcsr%v", time.Now().Unix())
|
||||
rootFs, boltDb := runInstance.newCacheFs(t, remoteName, id, false, true, map[string]string{"workers": "1"})
|
||||
rootFs, boltDb := runInstance.newCacheFs(t, remoteName, id, false, true, nil, map[string]string{"workers": "1"})
|
||||
defer runInstance.cleanupFs(t, rootFs, boltDb)
|
||||
|
||||
cfs, err := runInstance.getCacheFs(rootFs)
|
||||
require.NoError(t, err)
|
||||
@@ -709,7 +724,8 @@ func TestInternalMaxChunkSizeRespected(t *testing.T) {
|
||||
func TestInternalExpiredEntriesRemoved(t *testing.T) {
|
||||
id := fmt.Sprintf("tieer%v", time.Now().Unix())
|
||||
vfsflags.Opt.DirCacheTime = time.Second * 4 // needs to be lower than the defined
|
||||
rootFs, _ := runInstance.newCacheFs(t, remoteName, id, true, true, nil)
|
||||
rootFs, boltDb := runInstance.newCacheFs(t, remoteName, id, true, true, map[string]string{"info_age": "5s"}, nil)
|
||||
defer runInstance.cleanupFs(t, rootFs, boltDb)
|
||||
cfs, err := runInstance.getCacheFs(rootFs)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -746,7 +762,9 @@ func TestInternalBug2117(t *testing.T) {
|
||||
vfsflags.Opt.DirCacheTime = time.Second * 10
|
||||
|
||||
id := fmt.Sprintf("tib2117%v", time.Now().Unix())
|
||||
rootFs, _ := runInstance.newCacheFs(t, remoteName, id, false, true, map[string]string{"info_age": "72h", "chunk_clean_interval": "15m"})
|
||||
rootFs, boltDb := runInstance.newCacheFs(t, remoteName, id, false, true, nil,
|
||||
map[string]string{"info_age": "72h", "chunk_clean_interval": "15m"})
|
||||
defer runInstance.cleanupFs(t, rootFs, boltDb)
|
||||
|
||||
if runInstance.rootIsCrypt {
|
||||
t.Skipf("skipping crypt")
|
||||
@@ -847,7 +865,7 @@ func (r *run) encryptRemoteIfNeeded(t *testing.T, remote string) string {
|
||||
return enc
|
||||
}
|
||||
|
||||
func (r *run) newCacheFs(t *testing.T, remote, id string, needRemote, purge bool, flags map[string]string) (fs.Fs, *cache.Persistent) {
|
||||
func (r *run) newCacheFs(t *testing.T, remote, id string, needRemote, purge bool, cfg map[string]string, flags map[string]string) (fs.Fs, *cache.Persistent) {
|
||||
fstest.Initialise()
|
||||
remoteExists := false
|
||||
for _, s := range config.FileSections() {
|
||||
@@ -940,15 +958,10 @@ func (r *run) newCacheFs(t *testing.T, remote, id string, needRemote, purge bool
|
||||
}
|
||||
err = f.Mkdir(context.Background(), "")
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
runInstance.cleanupFs(t, f)
|
||||
})
|
||||
|
||||
return f, boltDb
|
||||
}
|
||||
|
||||
func (r *run) cleanupFs(t *testing.T, f fs.Fs) {
|
||||
func (r *run) cleanupFs(t *testing.T, f fs.Fs, b *cache.Persistent) {
|
||||
err := f.Features().Purge(context.Background(), "")
|
||||
require.NoError(t, err)
|
||||
cfs, err := r.getCacheFs(f)
|
||||
|
||||
24
backend/cache/cache_upload_test.go
vendored
24
backend/cache/cache_upload_test.go
vendored
@@ -21,8 +21,10 @@ import (
|
||||
|
||||
func TestInternalUploadTempDirCreated(t *testing.T) {
|
||||
id := fmt.Sprintf("tiutdc%v", time.Now().Unix())
|
||||
runInstance.newCacheFs(t, remoteName, id, false, true,
|
||||
rootFs, boltDb := runInstance.newCacheFs(t, remoteName, id, false, true,
|
||||
nil,
|
||||
map[string]string{"tmp_upload_path": path.Join(runInstance.tmpUploadDir, id)})
|
||||
defer runInstance.cleanupFs(t, rootFs, boltDb)
|
||||
|
||||
_, err := os.Stat(path.Join(runInstance.tmpUploadDir, id))
|
||||
require.NoError(t, err)
|
||||
@@ -61,7 +63,9 @@ func testInternalUploadQueueOneFile(t *testing.T, id string, rootFs fs.Fs, boltD
|
||||
func TestInternalUploadQueueOneFileNoRest(t *testing.T) {
|
||||
id := fmt.Sprintf("tiuqofnr%v", time.Now().Unix())
|
||||
rootFs, boltDb := runInstance.newCacheFs(t, remoteName, id, true, true,
|
||||
nil,
|
||||
map[string]string{"tmp_upload_path": path.Join(runInstance.tmpUploadDir, id), "tmp_wait_time": "0s"})
|
||||
defer runInstance.cleanupFs(t, rootFs, boltDb)
|
||||
|
||||
testInternalUploadQueueOneFile(t, id, rootFs, boltDb)
|
||||
}
|
||||
@@ -69,15 +73,19 @@ func TestInternalUploadQueueOneFileNoRest(t *testing.T) {
|
||||
func TestInternalUploadQueueOneFileWithRest(t *testing.T) {
|
||||
id := fmt.Sprintf("tiuqofwr%v", time.Now().Unix())
|
||||
rootFs, boltDb := runInstance.newCacheFs(t, remoteName, id, true, true,
|
||||
nil,
|
||||
map[string]string{"tmp_upload_path": path.Join(runInstance.tmpUploadDir, id), "tmp_wait_time": "1m"})
|
||||
defer runInstance.cleanupFs(t, rootFs, boltDb)
|
||||
|
||||
testInternalUploadQueueOneFile(t, id, rootFs, boltDb)
|
||||
}
|
||||
|
||||
func TestInternalUploadMoveExistingFile(t *testing.T) {
|
||||
id := fmt.Sprintf("tiumef%v", time.Now().Unix())
|
||||
rootFs, _ := runInstance.newCacheFs(t, remoteName, id, true, true,
|
||||
rootFs, boltDb := runInstance.newCacheFs(t, remoteName, id, true, true,
|
||||
nil,
|
||||
map[string]string{"tmp_upload_path": path.Join(runInstance.tmpUploadDir, id), "tmp_wait_time": "3s"})
|
||||
defer runInstance.cleanupFs(t, rootFs, boltDb)
|
||||
|
||||
err := rootFs.Mkdir(context.Background(), "one")
|
||||
require.NoError(t, err)
|
||||
@@ -111,8 +119,10 @@ func TestInternalUploadMoveExistingFile(t *testing.T) {
|
||||
|
||||
func TestInternalUploadTempPathCleaned(t *testing.T) {
|
||||
id := fmt.Sprintf("tiutpc%v", time.Now().Unix())
|
||||
rootFs, _ := runInstance.newCacheFs(t, remoteName, id, true, true,
|
||||
rootFs, boltDb := runInstance.newCacheFs(t, remoteName, id, true, true,
|
||||
nil,
|
||||
map[string]string{"cache-tmp-upload-path": path.Join(runInstance.tmpUploadDir, id), "cache-tmp-wait-time": "5s"})
|
||||
defer runInstance.cleanupFs(t, rootFs, boltDb)
|
||||
|
||||
err := rootFs.Mkdir(context.Background(), "one")
|
||||
require.NoError(t, err)
|
||||
@@ -152,8 +162,10 @@ func TestInternalUploadTempPathCleaned(t *testing.T) {
|
||||
|
||||
func TestInternalUploadQueueMoreFiles(t *testing.T) {
|
||||
id := fmt.Sprintf("tiuqmf%v", time.Now().Unix())
|
||||
rootFs, _ := runInstance.newCacheFs(t, remoteName, id, true, true,
|
||||
rootFs, boltDb := runInstance.newCacheFs(t, remoteName, id, true, true,
|
||||
nil,
|
||||
map[string]string{"tmp_upload_path": path.Join(runInstance.tmpUploadDir, id), "tmp_wait_time": "1s"})
|
||||
defer runInstance.cleanupFs(t, rootFs, boltDb)
|
||||
|
||||
err := rootFs.Mkdir(context.Background(), "test")
|
||||
require.NoError(t, err)
|
||||
@@ -201,7 +213,9 @@ func TestInternalUploadQueueMoreFiles(t *testing.T) {
|
||||
func TestInternalUploadTempFileOperations(t *testing.T) {
|
||||
id := "tiutfo"
|
||||
rootFs, boltDb := runInstance.newCacheFs(t, remoteName, id, true, true,
|
||||
nil,
|
||||
map[string]string{"tmp_upload_path": path.Join(runInstance.tmpUploadDir, id), "tmp_wait_time": "1h"})
|
||||
defer runInstance.cleanupFs(t, rootFs, boltDb)
|
||||
|
||||
boltDb.PurgeTempUploads()
|
||||
|
||||
@@ -329,7 +343,9 @@ func TestInternalUploadTempFileOperations(t *testing.T) {
|
||||
func TestInternalUploadUploadingFileOperations(t *testing.T) {
|
||||
id := "tiuufo"
|
||||
rootFs, boltDb := runInstance.newCacheFs(t, remoteName, id, true, true,
|
||||
nil,
|
||||
map[string]string{"tmp_upload_path": path.Join(runInstance.tmpUploadDir, id), "tmp_wait_time": "1h"})
|
||||
defer runInstance.cleanupFs(t, rootFs, boltDb)
|
||||
|
||||
boltDb.PurgeTempUploads()
|
||||
|
||||
|
||||
@@ -631,7 +631,7 @@ func (f *Fs) put(ctx context.Context, in io.Reader, src fs.ObjectInfo, stream bo
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uSrc := fs.NewOverrideRemote(src, uRemote)
|
||||
uSrc := operations.NewOverrideRemote(src, uRemote)
|
||||
var o fs.Object
|
||||
if stream {
|
||||
o, err = u.f.Features().PutStream(ctx, in, uSrc, options...)
|
||||
|
||||
@@ -396,8 +396,6 @@ type putFn func(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ..
|
||||
|
||||
// put implements Put or PutStream
|
||||
func (f *Fs) put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options []fs.OpenOption, put putFn) (fs.Object, error) {
|
||||
ci := fs.GetConfig(ctx)
|
||||
|
||||
if f.opt.NoDataEncryption {
|
||||
o, err := put(ctx, in, f.newObjectInfo(src, nonce{}), options...)
|
||||
if err == nil && o != nil {
|
||||
@@ -415,9 +413,6 @@ func (f *Fs) put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options [
|
||||
// Find a hash the destination supports to compute a hash of
|
||||
// the encrypted data
|
||||
ht := f.Fs.Hashes().GetOne()
|
||||
if ci.IgnoreChecksum {
|
||||
ht = hash.None
|
||||
}
|
||||
var hasher *hash.MultiHasher
|
||||
if ht != hash.None {
|
||||
hasher, err = hash.NewMultiHasherTypes(hash.NewHashSet(ht))
|
||||
@@ -1052,11 +1047,10 @@ func (o *ObjectInfo) Hash(ctx context.Context, hash hash.Type) (string, error) {
|
||||
// Get the underlying object if there is one
|
||||
if srcObj, ok = o.ObjectInfo.(fs.Object); ok {
|
||||
// Prefer direct interface assertion
|
||||
} else if do, ok := o.ObjectInfo.(*fs.OverrideRemote); ok {
|
||||
// Unwrap if it is an operations.OverrideRemote
|
||||
} else if do, ok := o.ObjectInfo.(fs.ObjectUnWrapper); ok {
|
||||
// Otherwise likely is an operations.OverrideRemote
|
||||
srcObj = do.UnWrap()
|
||||
} else {
|
||||
// Otherwise don't unwrap any further
|
||||
return "", nil
|
||||
}
|
||||
// if this is wrapping a local object then we work out the hash
|
||||
|
||||
@@ -17,28 +17,41 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type testWrapper struct {
|
||||
fs.ObjectInfo
|
||||
}
|
||||
|
||||
// UnWrap returns the Object that this Object is wrapping or nil if it
|
||||
// isn't wrapping anything
|
||||
func (o testWrapper) UnWrap() fs.Object {
|
||||
if o, ok := o.ObjectInfo.(fs.Object); ok {
|
||||
return o
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a temporary local fs to upload things from
|
||||
|
||||
func makeTempLocalFs(t *testing.T) (localFs fs.Fs) {
|
||||
func makeTempLocalFs(t *testing.T) (localFs fs.Fs, cleanup func()) {
|
||||
localFs, err := fs.TemporaryLocalFs(context.Background())
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
cleanup = func() {
|
||||
require.NoError(t, localFs.Rmdir(context.Background(), ""))
|
||||
})
|
||||
return localFs
|
||||
}
|
||||
return localFs, cleanup
|
||||
}
|
||||
|
||||
// Upload a file to a remote
|
||||
func uploadFile(t *testing.T, f fs.Fs, remote, contents string) (obj fs.Object) {
|
||||
func uploadFile(t *testing.T, f fs.Fs, remote, contents string) (obj fs.Object, cleanup func()) {
|
||||
inBuf := bytes.NewBufferString(contents)
|
||||
t1 := time.Date(2012, time.December, 17, 18, 32, 31, 0, time.UTC)
|
||||
upSrc := object.NewStaticObjectInfo(remote, t1, int64(len(contents)), true, nil, nil)
|
||||
obj, err := f.Put(context.Background(), inBuf, upSrc)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
cleanup = func() {
|
||||
require.NoError(t, obj.Remove(context.Background()))
|
||||
})
|
||||
return obj
|
||||
}
|
||||
return obj, cleanup
|
||||
}
|
||||
|
||||
// Test the ObjectInfo
|
||||
@@ -52,9 +65,11 @@ func testObjectInfo(t *testing.T, f *Fs, wrap bool) {
|
||||
path = "_wrap"
|
||||
}
|
||||
|
||||
localFs := makeTempLocalFs(t)
|
||||
localFs, cleanupLocalFs := makeTempLocalFs(t)
|
||||
defer cleanupLocalFs()
|
||||
|
||||
obj := uploadFile(t, localFs, path, contents)
|
||||
obj, cleanupObj := uploadFile(t, localFs, path, contents)
|
||||
defer cleanupObj()
|
||||
|
||||
// encrypt the data
|
||||
inBuf := bytes.NewBufferString(contents)
|
||||
@@ -68,7 +83,7 @@ func testObjectInfo(t *testing.T, f *Fs, wrap bool) {
|
||||
var oi fs.ObjectInfo = obj
|
||||
if wrap {
|
||||
// wrap the object in an fs.ObjectUnwrapper if required
|
||||
oi = fs.NewOverrideRemote(oi, "new_remote")
|
||||
oi = testWrapper{oi}
|
||||
}
|
||||
|
||||
// wrap the object in a crypt for upload using the nonce we
|
||||
@@ -101,13 +116,16 @@ func testComputeHash(t *testing.T, f *Fs) {
|
||||
t.Skipf("%v: does not support hashes", f.Fs)
|
||||
}
|
||||
|
||||
localFs := makeTempLocalFs(t)
|
||||
localFs, cleanupLocalFs := makeTempLocalFs(t)
|
||||
defer cleanupLocalFs()
|
||||
|
||||
// Upload a file to localFs as a test object
|
||||
localObj := uploadFile(t, localFs, path, contents)
|
||||
localObj, cleanupLocalObj := uploadFile(t, localFs, path, contents)
|
||||
defer cleanupLocalObj()
|
||||
|
||||
// Upload the same data to the remote Fs also
|
||||
remoteObj := uploadFile(t, f, path, contents)
|
||||
remoteObj, cleanupRemoteObj := uploadFile(t, f, path, contents)
|
||||
defer cleanupRemoteObj()
|
||||
|
||||
// Calculate the expected Hash of the remote object
|
||||
computedHash, err := f.ComputeHash(ctx, remoteObj.(*Object), localObj, hashType)
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -3430,12 +3431,13 @@ func (f *Fs) Command(ctx context.Context, name string, arg []string, opt map[str
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
re := regexp.MustCompile(`[^\w_. -]+`)
|
||||
if _, ok := opt["config"]; ok {
|
||||
lines := []string{}
|
||||
upstreams := []string{}
|
||||
names := make(map[string]struct{}, len(drives))
|
||||
for i, drive := range drives {
|
||||
name := fspath.MakeConfigName(drive.Name)
|
||||
name := re.ReplaceAllString(drive.Name, "_")
|
||||
for {
|
||||
if _, found := names[name]; !found {
|
||||
break
|
||||
|
||||
@@ -70,7 +70,7 @@ func init() {
|
||||
When using implicit FTP over TLS the client connects using TLS
|
||||
right from the start which breaks compatibility with
|
||||
non-TLS-aware servers. This is usually served over port 990 rather
|
||||
than port 21. Cannot be used in combination with explicit FTPS.`,
|
||||
than port 21. Cannot be used in combination with explicit FTP.`,
|
||||
Default: false,
|
||||
}, {
|
||||
Name: "explicit_tls",
|
||||
@@ -78,7 +78,7 @@ than port 21. Cannot be used in combination with explicit FTPS.`,
|
||||
|
||||
When using explicit FTP over TLS the client explicitly requests
|
||||
security from the server in order to upgrade a plain text connection
|
||||
to an encrypted one. Cannot be used in combination with implicit FTPS.`,
|
||||
to an encrypted one. Cannot be used in combination with implicit FTP.`,
|
||||
Default: false,
|
||||
}, {
|
||||
Name: "concurrency",
|
||||
@@ -657,7 +657,8 @@ func (f *Fs) dirFromStandardPath(dir string) string {
|
||||
// findItem finds a directory entry for the name in its parent directory
|
||||
func (f *Fs) findItem(ctx context.Context, remote string) (entry *ftp.Entry, err error) {
|
||||
// defer fs.Trace(remote, "")("o=%v, err=%v", &o, &err)
|
||||
if remote == "" || remote == "." || remote == "/" {
|
||||
fullPath := path.Join(f.root, remote)
|
||||
if fullPath == "" || fullPath == "." || fullPath == "/" {
|
||||
// if root, assume exists and synthesize an entry
|
||||
return &ftp.Entry{
|
||||
Name: "",
|
||||
@@ -665,32 +666,13 @@ func (f *Fs) findItem(ctx context.Context, remote string) (entry *ftp.Entry, err
|
||||
Time: time.Now(),
|
||||
}, nil
|
||||
}
|
||||
dir := path.Dir(fullPath)
|
||||
base := path.Base(fullPath)
|
||||
|
||||
c, err := f.getFtpConnection(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("findItem: %w", err)
|
||||
}
|
||||
|
||||
// returns TRUE if MLST is supported which is required to call GetEntry
|
||||
if c.IsTimePreciseInList() {
|
||||
entry, err := c.GetEntry(f.opt.Enc.FromStandardPath(remote))
|
||||
f.putFtpConnection(&c, err)
|
||||
if err != nil {
|
||||
err = translateErrorFile(err)
|
||||
if err == fs.ErrorObjectNotFound {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if entry != nil {
|
||||
f.entryToStandard(entry)
|
||||
}
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
dir := path.Dir(remote)
|
||||
base := path.Base(remote)
|
||||
|
||||
files, err := c.List(f.dirFromStandardPath(dir))
|
||||
f.putFtpConnection(&c, err)
|
||||
if err != nil {
|
||||
@@ -709,7 +691,7 @@ func (f *Fs) findItem(ctx context.Context, remote string) (entry *ftp.Entry, err
|
||||
// it returns the error fs.ErrorObjectNotFound.
|
||||
func (f *Fs) NewObject(ctx context.Context, remote string) (o fs.Object, err error) {
|
||||
// defer fs.Trace(remote, "")("o=%v, err=%v", &o, &err)
|
||||
entry, err := f.findItem(ctx, path.Join(f.root, remote))
|
||||
entry, err := f.findItem(ctx, remote)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -731,7 +713,7 @@ func (f *Fs) NewObject(ctx context.Context, remote string) (o fs.Object, err err
|
||||
|
||||
// dirExists checks the directory pointed to by remote exists or not
|
||||
func (f *Fs) dirExists(ctx context.Context, remote string) (exists bool, err error) {
|
||||
entry, err := f.findItem(ctx, path.Join(f.root, remote))
|
||||
entry, err := f.findItem(ctx, remote)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("dirExists: %w", err)
|
||||
}
|
||||
@@ -875,18 +857,32 @@ func (f *Fs) PutStream(ctx context.Context, in io.Reader, src fs.ObjectInfo, opt
|
||||
// getInfo reads the FileInfo for a path
|
||||
func (f *Fs) getInfo(ctx context.Context, remote string) (fi *FileInfo, err error) {
|
||||
// defer fs.Trace(remote, "")("fi=%v, err=%v", &fi, &err)
|
||||
file, err := f.findItem(ctx, remote)
|
||||
dir := path.Dir(remote)
|
||||
base := path.Base(remote)
|
||||
|
||||
c, err := f.getFtpConnection(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if file != nil {
|
||||
info := &FileInfo{
|
||||
Name: remote,
|
||||
Size: file.Size,
|
||||
ModTime: file.Time,
|
||||
precise: f.fLstTime,
|
||||
IsDir: file.Type == ftp.EntryTypeFolder,
|
||||
return nil, fmt.Errorf("getInfo: %w", err)
|
||||
}
|
||||
files, err := c.List(f.dirFromStandardPath(dir))
|
||||
f.putFtpConnection(&c, err)
|
||||
if err != nil {
|
||||
return nil, translateErrorFile(err)
|
||||
}
|
||||
|
||||
for i := range files {
|
||||
file := files[i]
|
||||
f.entryToStandard(file)
|
||||
if file.Name == base {
|
||||
info := &FileInfo{
|
||||
Name: remote,
|
||||
Size: file.Size,
|
||||
ModTime: file.Time,
|
||||
precise: f.fLstTime,
|
||||
IsDir: file.Type == ftp.EntryTypeFolder,
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
return nil, fs.ErrorObjectNotFound
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
_ "github.com/rclone/rclone/backend/local"
|
||||
"github.com/rclone/rclone/fs"
|
||||
"github.com/rclone/rclone/fs/hash"
|
||||
"github.com/rclone/rclone/fs/operations"
|
||||
"github.com/rclone/rclone/fstest"
|
||||
"github.com/rclone/rclone/lib/random"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -55,7 +56,7 @@ func TestIntegration(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
in, err := srcObj.Open(ctx)
|
||||
require.NoError(t, err)
|
||||
dstObj, err := f.Put(ctx, in, fs.NewOverrideRemote(srcObj, remote))
|
||||
dstObj, err := f.Put(ctx, in, operations.NewOverrideRemote(srcObj, remote))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, remote, dstObj.Remote())
|
||||
_ = in.Close()
|
||||
@@ -220,7 +221,7 @@ func TestIntegration(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
in, err := srcObj.Open(ctx)
|
||||
require.NoError(t, err)
|
||||
dstObj, err := f.Put(ctx, in, fs.NewOverrideRemote(srcObj, remote))
|
||||
dstObj, err := f.Put(ctx, in, operations.NewOverrideRemote(srcObj, remote))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, remote, dstObj.Remote())
|
||||
_ = in.Close()
|
||||
|
||||
@@ -33,9 +33,8 @@ var (
|
||||
lineEndSize = 1
|
||||
)
|
||||
|
||||
// prepareServer prepares the test server and shuts it down automatically
|
||||
// when the test completes.
|
||||
func prepareServer(t *testing.T) configmap.Simple {
|
||||
// prepareServer the test server and return a function to tidy it up afterwards
|
||||
func prepareServer(t *testing.T) (configmap.Simple, func()) {
|
||||
// file server for test/files
|
||||
fileServer := http.FileServer(http.Dir(filesPath))
|
||||
|
||||
@@ -79,21 +78,20 @@ func prepareServer(t *testing.T) configmap.Simple {
|
||||
"url": ts.URL,
|
||||
"headers": strings.Join(headers, ","),
|
||||
}
|
||||
t.Cleanup(ts.Close)
|
||||
|
||||
return m
|
||||
// return a function to tidy up
|
||||
return m, ts.Close
|
||||
}
|
||||
|
||||
// prepare prepares the test server and shuts it down automatically
|
||||
// when the test completes.
|
||||
func prepare(t *testing.T) fs.Fs {
|
||||
m := prepareServer(t)
|
||||
// prepare the test server and return a function to tidy it up afterwards
|
||||
func prepare(t *testing.T) (fs.Fs, func()) {
|
||||
m, tidy := prepareServer(t)
|
||||
|
||||
// Instantiate it
|
||||
f, err := NewFs(context.Background(), remoteName, "", m)
|
||||
require.NoError(t, err)
|
||||
|
||||
return f
|
||||
return f, tidy
|
||||
}
|
||||
|
||||
func testListRoot(t *testing.T, f fs.Fs, noSlash bool) {
|
||||
@@ -136,19 +134,22 @@ func testListRoot(t *testing.T, f fs.Fs, noSlash bool) {
|
||||
}
|
||||
|
||||
func TestListRoot(t *testing.T) {
|
||||
f := prepare(t)
|
||||
f, tidy := prepare(t)
|
||||
defer tidy()
|
||||
testListRoot(t, f, false)
|
||||
}
|
||||
|
||||
func TestListRootNoSlash(t *testing.T) {
|
||||
f := prepare(t)
|
||||
f, tidy := prepare(t)
|
||||
f.(*Fs).opt.NoSlash = true
|
||||
defer tidy()
|
||||
|
||||
testListRoot(t, f, true)
|
||||
}
|
||||
|
||||
func TestListSubDir(t *testing.T) {
|
||||
f := prepare(t)
|
||||
f, tidy := prepare(t)
|
||||
defer tidy()
|
||||
|
||||
entries, err := f.List(context.Background(), "three")
|
||||
require.NoError(t, err)
|
||||
@@ -165,7 +166,8 @@ func TestListSubDir(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNewObject(t *testing.T) {
|
||||
f := prepare(t)
|
||||
f, tidy := prepare(t)
|
||||
defer tidy()
|
||||
|
||||
o, err := f.NewObject(context.Background(), "four/under four.txt")
|
||||
require.NoError(t, err)
|
||||
@@ -192,7 +194,8 @@ func TestNewObject(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestOpen(t *testing.T) {
|
||||
m := prepareServer(t)
|
||||
m, tidy := prepareServer(t)
|
||||
defer tidy()
|
||||
|
||||
for _, head := range []bool{false, true} {
|
||||
if !head {
|
||||
@@ -254,7 +257,8 @@ func TestOpen(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMimeType(t *testing.T) {
|
||||
f := prepare(t)
|
||||
f, tidy := prepare(t)
|
||||
defer tidy()
|
||||
|
||||
o, err := f.NewObject(context.Background(), "four/under four.txt")
|
||||
require.NoError(t, err)
|
||||
@@ -265,7 +269,8 @@ func TestMimeType(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestIsAFileRoot(t *testing.T) {
|
||||
m := prepareServer(t)
|
||||
m, tidy := prepareServer(t)
|
||||
defer tidy()
|
||||
|
||||
f, err := NewFs(context.Background(), remoteName, "one%.txt", m)
|
||||
assert.Equal(t, err, fs.ErrorIsFile)
|
||||
@@ -274,7 +279,8 @@ func TestIsAFileRoot(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestIsAFileSubDir(t *testing.T) {
|
||||
m := prepareServer(t)
|
||||
m, tidy := prepareServer(t)
|
||||
defer tidy()
|
||||
|
||||
f, err := NewFs(context.Background(), remoteName, "three/underthree.txt", m)
|
||||
assert.Equal(t, err, fs.ErrorIsFile)
|
||||
|
||||
@@ -32,6 +32,7 @@ import (
|
||||
|
||||
// Constants
|
||||
const devUnset = 0xdeadbeefcafebabe // a device id meaning it is unset
|
||||
const linkSuffix = ".rclonelink" // The suffix added to a translated symbolic link
|
||||
const useReadDir = (runtime.GOOS == "windows" || runtime.GOOS == "plan9") // these OSes read FileInfos directly
|
||||
|
||||
// Register with Fs
|
||||
@@ -71,7 +72,7 @@ supported by all file systems) under the "user.*" prefix.
|
||||
Advanced: true,
|
||||
}, {
|
||||
Name: "links",
|
||||
Help: "Translate symlinks to/from regular files with a '" + fs.LinkSuffix + "' extension.",
|
||||
Help: "Translate symlinks to/from regular files with a '" + linkSuffix + "' extension.",
|
||||
Default: false,
|
||||
NoPrefix: true,
|
||||
ShortOpt: "l",
|
||||
@@ -374,8 +375,8 @@ func (f *Fs) caseInsensitive() bool {
|
||||
//
|
||||
// for regular files, localPath is returned unchanged
|
||||
func translateLink(remote, localPath string) (newLocalPath string, isTranslatedLink bool) {
|
||||
isTranslatedLink = strings.HasSuffix(remote, fs.LinkSuffix)
|
||||
newLocalPath = strings.TrimSuffix(localPath, fs.LinkSuffix)
|
||||
isTranslatedLink = strings.HasSuffix(remote, linkSuffix)
|
||||
newLocalPath = strings.TrimSuffix(localPath, linkSuffix)
|
||||
return newLocalPath, isTranslatedLink
|
||||
}
|
||||
|
||||
@@ -502,7 +503,7 @@ func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err e
|
||||
continue
|
||||
}
|
||||
}
|
||||
err = fmt.Errorf("failed to read directory %q: %w", namepath, fierr)
|
||||
err = fmt.Errorf("failed to read directory %q: %w", namepath, err)
|
||||
fs.Errorf(dir, "%v", fierr)
|
||||
_ = accounting.Stats(ctx).Error(fserrors.NoRetryError(fierr)) // fail the sync
|
||||
continue
|
||||
@@ -550,7 +551,7 @@ func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err e
|
||||
} else {
|
||||
// Check whether this link should be translated
|
||||
if f.opt.TranslateSymlinks && fi.Mode()&os.ModeSymlink != 0 {
|
||||
newRemote += fs.LinkSuffix
|
||||
newRemote += linkSuffix
|
||||
}
|
||||
fso, err := f.newObjectWithInfo(newRemote, fi)
|
||||
if err != nil {
|
||||
|
||||
@@ -33,6 +33,7 @@ func TestMain(m *testing.M) {
|
||||
// Test copy with source file that's updating
|
||||
func TestUpdatingCheck(t *testing.T) {
|
||||
r := fstest.NewRun(t)
|
||||
defer r.Finalise()
|
||||
filePath := "sub dir/local test"
|
||||
r.WriteFile(filePath, "content", time.Now())
|
||||
|
||||
@@ -77,6 +78,7 @@ func TestUpdatingCheck(t *testing.T) {
|
||||
func TestSymlink(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
r := fstest.NewRun(t)
|
||||
defer r.Finalise()
|
||||
f := r.Flocal.(*Fs)
|
||||
dir := f.root
|
||||
|
||||
@@ -91,7 +93,7 @@ func TestSymlink(t *testing.T) {
|
||||
require.NoError(t, lChtimes(symlinkPath, modTime2, modTime2))
|
||||
|
||||
// Object viewed as symlink
|
||||
file2 := fstest.NewItem("symlink.txt"+fs.LinkSuffix, "file.txt", modTime2)
|
||||
file2 := fstest.NewItem("symlink.txt"+linkSuffix, "file.txt", modTime2)
|
||||
|
||||
// Object viewed as destination
|
||||
file2d := fstest.NewItem("symlink.txt", "hello", modTime1)
|
||||
@@ -120,7 +122,7 @@ func TestSymlink(t *testing.T) {
|
||||
|
||||
// Create a symlink
|
||||
modTime3 := fstest.Time("2002-03-03T04:05:10.123123123Z")
|
||||
file3 := r.WriteObjectTo(ctx, r.Flocal, "symlink2.txt"+fs.LinkSuffix, "file.txt", modTime3, false)
|
||||
file3 := r.WriteObjectTo(ctx, r.Flocal, "symlink2.txt"+linkSuffix, "file.txt", modTime3, false)
|
||||
fstest.CheckListingWithPrecision(t, r.Flocal, []fstest.Item{file1, file2, file3}, nil, fs.ModTimeNotSupported)
|
||||
if haveLChtimes {
|
||||
r.CheckLocalItems(t, file1, file2, file3)
|
||||
@@ -136,9 +138,9 @@ func TestSymlink(t *testing.T) {
|
||||
assert.Equal(t, "file.txt", linkText)
|
||||
|
||||
// Check that NewObject gets the correct object
|
||||
o, err := r.Flocal.NewObject(ctx, "symlink2.txt"+fs.LinkSuffix)
|
||||
o, err := r.Flocal.NewObject(ctx, "symlink2.txt"+linkSuffix)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "symlink2.txt"+fs.LinkSuffix, o.Remote())
|
||||
assert.Equal(t, "symlink2.txt"+linkSuffix, o.Remote())
|
||||
assert.Equal(t, int64(8), o.Size())
|
||||
|
||||
// Check that NewObject doesn't see the non suffixed version
|
||||
@@ -175,6 +177,7 @@ func TestSymlinkError(t *testing.T) {
|
||||
func TestHashOnUpdate(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
r := fstest.NewRun(t)
|
||||
defer r.Finalise()
|
||||
const filePath = "file.txt"
|
||||
when := time.Now()
|
||||
r.WriteFile(filePath, "content", when)
|
||||
@@ -205,6 +208,7 @@ func TestHashOnUpdate(t *testing.T) {
|
||||
func TestHashOnDelete(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
r := fstest.NewRun(t)
|
||||
defer r.Finalise()
|
||||
const filePath = "file.txt"
|
||||
when := time.Now()
|
||||
r.WriteFile(filePath, "content", when)
|
||||
@@ -233,6 +237,7 @@ func TestHashOnDelete(t *testing.T) {
|
||||
func TestMetadata(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
r := fstest.NewRun(t)
|
||||
defer r.Finalise()
|
||||
const filePath = "metafile.txt"
|
||||
when := time.Now()
|
||||
const dayLength = len("2001-01-01")
|
||||
@@ -367,6 +372,7 @@ func TestMetadata(t *testing.T) {
|
||||
func TestFilter(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
r := fstest.NewRun(t)
|
||||
defer r.Finalise()
|
||||
when := time.Now()
|
||||
r.WriteFile("included", "included file", when)
|
||||
r.WriteFile("excluded", "excluded file", when)
|
||||
|
||||
@@ -66,7 +66,7 @@ import (
|
||||
func init() {
|
||||
fs.Register(&fs.RegInfo{
|
||||
Name: "s3",
|
||||
Description: "Amazon S3 Compliant Storage Providers including AWS, Alibaba, Ceph, China Mobile, Cloudflare, ArvanCloud, DigitalOcean, Dreamhost, Huawei OBS, IBM COS, IDrive e2, IONOS Cloud, Liara, Lyve Cloud, Minio, Netease, RackCorp, Scaleway, SeaweedFS, StackPath, Storj, Tencent COS, Qiniu and Wasabi",
|
||||
Description: "Amazon S3 Compliant Storage Providers including AWS, Alibaba, Ceph, China Mobile, Cloudflare, ArvanCloud, Digital Ocean, Dreamhost, Huawei OBS, IBM COS, IDrive e2, IONOS Cloud, Lyve Cloud, Minio, Netease, RackCorp, Scaleway, SeaweedFS, StackPath, Storj, Tencent COS, Qiniu and Wasabi",
|
||||
NewFs: NewFs,
|
||||
CommandHelp: commandHelp,
|
||||
Config: func(ctx context.Context, name string, m configmap.Mapper, config fs.ConfigIn) (*fs.ConfigOut, error) {
|
||||
@@ -105,7 +105,7 @@ func init() {
|
||||
Help: "Arvan Cloud Object Storage (AOS)",
|
||||
}, {
|
||||
Value: "DigitalOcean",
|
||||
Help: "DigitalOcean Spaces",
|
||||
Help: "Digital Ocean Spaces",
|
||||
}, {
|
||||
Value: "Dreamhost",
|
||||
Help: "Dreamhost DreamObjects",
|
||||
@@ -124,9 +124,6 @@ func init() {
|
||||
}, {
|
||||
Value: "LyveCloud",
|
||||
Help: "Seagate Lyve Cloud",
|
||||
}, {
|
||||
Value: "Liara",
|
||||
Help: "Liara Object Storage",
|
||||
}, {
|
||||
Value: "Minio",
|
||||
Help: "Minio Object Storage",
|
||||
@@ -440,7 +437,7 @@ func init() {
|
||||
}, {
|
||||
Name: "region",
|
||||
Help: "Region to connect to.\n\nLeave blank if you are using an S3 clone and you don't have a region.",
|
||||
Provider: "!AWS,Alibaba,ChinaMobile,Cloudflare,IONOS,ArvanCloud,Liara,Qiniu,RackCorp,Scaleway,Storj,TencentCOS,HuaweiOBS,IDrive",
|
||||
Provider: "!AWS,Alibaba,ChinaMobile,Cloudflare,IONOS,ArvanCloud,Qiniu,RackCorp,Scaleway,Storj,TencentCOS,HuaweiOBS,IDrive",
|
||||
Examples: []fs.OptionExample{{
|
||||
Value: "",
|
||||
Help: "Use this if unsure.\nWill use v4 signatures and an empty region.",
|
||||
@@ -765,15 +762,6 @@ func init() {
|
||||
Value: "s3-eu-south-2.ionoscloud.com",
|
||||
Help: "Logrono, Spain",
|
||||
}},
|
||||
}, {
|
||||
// Liara endpoints: https://liara.ir/landing/object-storage
|
||||
Name: "endpoint",
|
||||
Help: "Endpoint for Liara Object Storage API.",
|
||||
Provider: "Liara",
|
||||
Examples: []fs.OptionExample{{
|
||||
Value: "storage.iran.liara.space",
|
||||
Help: "The default endpoint\nIran",
|
||||
}},
|
||||
}, {
|
||||
// oss endpoints: https://help.aliyun.com/document_detail/31837.html
|
||||
Name: "endpoint",
|
||||
@@ -936,11 +924,17 @@ func init() {
|
||||
}},
|
||||
}, {
|
||||
Name: "endpoint",
|
||||
Help: "Endpoint for Storj Gateway.",
|
||||
Help: "Endpoint of the Shared Gateway.",
|
||||
Provider: "Storj",
|
||||
Examples: []fs.OptionExample{{
|
||||
Value: "gateway.storjshare.io",
|
||||
Help: "Global Hosted Gateway",
|
||||
Value: "gateway.eu1.storjshare.io",
|
||||
Help: "EU1 Shared Gateway",
|
||||
}, {
|
||||
Value: "gateway.us1.storjshare.io",
|
||||
Help: "US1 Shared Gateway",
|
||||
}, {
|
||||
Value: "gateway.ap1.storjshare.io",
|
||||
Help: "Asia-Pacific Shared Gateway",
|
||||
}},
|
||||
}, {
|
||||
// cos endpoints: https://intl.cloud.tencent.com/document/product/436/6224
|
||||
@@ -1098,34 +1092,22 @@ func init() {
|
||||
}, {
|
||||
Name: "endpoint",
|
||||
Help: "Endpoint for S3 API.\n\nRequired when using an S3 clone.",
|
||||
Provider: "!AWS,IBMCOS,IDrive,IONOS,TencentCOS,HuaweiOBS,Alibaba,ChinaMobile,Liara,ArvanCloud,Scaleway,StackPath,Storj,RackCorp,Qiniu",
|
||||
Provider: "!AWS,IBMCOS,IDrive,IONOS,TencentCOS,HuaweiOBS,Alibaba,ChinaMobile,ArvanCloud,Scaleway,StackPath,Storj,RackCorp,Qiniu",
|
||||
Examples: []fs.OptionExample{{
|
||||
Value: "objects-us-east-1.dream.io",
|
||||
Help: "Dream Objects endpoint",
|
||||
Provider: "Dreamhost",
|
||||
}, {
|
||||
Value: "syd1.digitaloceanspaces.com",
|
||||
Help: "DigitalOcean Spaces Sydney 1",
|
||||
Provider: "DigitalOcean",
|
||||
}, {
|
||||
Value: "sfo3.digitaloceanspaces.com",
|
||||
Help: "DigitalOcean Spaces San Francisco 3",
|
||||
Provider: "DigitalOcean",
|
||||
}, {
|
||||
Value: "fra1.digitaloceanspaces.com",
|
||||
Help: "DigitalOcean Spaces Frankfurt 1",
|
||||
Provider: "DigitalOcean",
|
||||
}, {
|
||||
Value: "nyc3.digitaloceanspaces.com",
|
||||
Help: "DigitalOcean Spaces New York 3",
|
||||
Help: "Digital Ocean Spaces New York 3",
|
||||
Provider: "DigitalOcean",
|
||||
}, {
|
||||
Value: "ams3.digitaloceanspaces.com",
|
||||
Help: "DigitalOcean Spaces Amsterdam 3",
|
||||
Help: "Digital Ocean Spaces Amsterdam 3",
|
||||
Provider: "DigitalOcean",
|
||||
}, {
|
||||
Value: "sgp1.digitaloceanspaces.com",
|
||||
Help: "DigitalOcean Spaces Singapore 1",
|
||||
Help: "Digital Ocean Spaces Singapore 1",
|
||||
Provider: "DigitalOcean",
|
||||
}, {
|
||||
Value: "localhost:8333",
|
||||
@@ -1195,10 +1177,6 @@ func init() {
|
||||
Value: "s3.ap-southeast-2.wasabisys.com",
|
||||
Help: "Wasabi AP Southeast 2 (Sydney)",
|
||||
Provider: "Wasabi",
|
||||
}, {
|
||||
Value: "storage.iran.liara.space",
|
||||
Help: "Liara Iran endpoint",
|
||||
Provider: "Liara",
|
||||
}, {
|
||||
Value: "s3.ir-thr-at1.arvanstorage.com",
|
||||
Help: "ArvanCloud Tehran Iran (Asiatech) endpoint",
|
||||
@@ -1582,7 +1560,7 @@ func init() {
|
||||
}, {
|
||||
Name: "location_constraint",
|
||||
Help: "Location constraint - must be set to match the Region.\n\nLeave blank if not sure. Used when creating buckets only.",
|
||||
Provider: "!AWS,Alibaba,HuaweiOBS,ChinaMobile,Cloudflare,IBMCOS,IDrive,IONOS,Liara,ArvanCloud,Qiniu,RackCorp,Scaleway,StackPath,Storj,TencentCOS",
|
||||
Provider: "!AWS,Alibaba,HuaweiOBS,ChinaMobile,Cloudflare,IBMCOS,IDrive,IONOS,ArvanCloud,Qiniu,RackCorp,Scaleway,StackPath,Storj,TencentCOS",
|
||||
}, {
|
||||
Name: "acl",
|
||||
Help: `Canned ACL used when creating buckets and storing or copying objects.
|
||||
@@ -1815,15 +1793,6 @@ If you leave it blank, this is calculated automatically from the sse_customer_ke
|
||||
Value: "STANDARD_IA",
|
||||
Help: "Infrequent access storage mode",
|
||||
}},
|
||||
}, {
|
||||
// Mapping from here: https://liara.ir/landing/object-storage
|
||||
Name: "storage_class",
|
||||
Help: "The storage class to use when storing new objects in Liara",
|
||||
Provider: "Liara",
|
||||
Examples: []fs.OptionExample{{
|
||||
Value: "STANDARD",
|
||||
Help: "Standard storage class",
|
||||
}},
|
||||
}, {
|
||||
// Mapping from here: https://www.arvancloud.com/en/products/cloud-storage
|
||||
Name: "storage_class",
|
||||
@@ -2795,10 +2764,6 @@ func setQuirks(opt *Options) {
|
||||
// listObjectsV2 supported - https://api.ionos.com/docs/s3/#Basic-Operations-get-Bucket-list-type-2
|
||||
virtualHostStyle = false
|
||||
urlEncodeListings = false
|
||||
case "Liara":
|
||||
virtualHostStyle = false
|
||||
urlEncodeListings = false
|
||||
useMultipartEtag = false
|
||||
case "LyveCloud":
|
||||
useMultipartEtag = false // LyveCloud seems to calculate multipart Etags differently from AWS
|
||||
case "Minio":
|
||||
@@ -3062,17 +3027,6 @@ func (f *Fs) getMetaDataListing(ctx context.Context, wantRemote string) (info *s
|
||||
return info, versionID, nil
|
||||
}
|
||||
|
||||
// stringClonePointer clones the string pointed to by sp into new
|
||||
// memory. This is useful to stop us keeping references to small
|
||||
// strings carved out of large XML responses.
|
||||
func stringClonePointer(sp *string) *string {
|
||||
if sp == nil {
|
||||
return nil
|
||||
}
|
||||
var s = *sp
|
||||
return &s
|
||||
}
|
||||
|
||||
// Return an Object from a path
|
||||
//
|
||||
// If it can't be found it returns the error ErrorObjectNotFound.
|
||||
@@ -3098,8 +3052,8 @@ func (f *Fs) newObjectWithInfo(ctx context.Context, remote string, info *s3.Obje
|
||||
}
|
||||
o.setMD5FromEtag(aws.StringValue(info.ETag))
|
||||
o.bytes = aws.Int64Value(info.Size)
|
||||
o.storageClass = stringClonePointer(info.StorageClass)
|
||||
o.versionID = stringClonePointer(versionID)
|
||||
o.storageClass = info.StorageClass
|
||||
o.versionID = versionID
|
||||
} else if !o.fs.opt.NoHeadObject {
|
||||
err := o.readMetaData(ctx) // reads info and meta, returning an error
|
||||
if err != nil {
|
||||
@@ -3240,9 +3194,6 @@ func (ls *v2List) List(ctx context.Context) (resp *s3.ListObjectsV2Output, versi
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if aws.BoolValue(resp.IsTruncated) && (resp.NextContinuationToken == nil || *resp.NextContinuationToken == "") {
|
||||
return nil, nil, errors.New("s3 protocol error: received listing v2 with IsTruncated set and no NextContinuationToken. Should you be using `--s3-list-version 1`?")
|
||||
}
|
||||
ls.req.ContinuationToken = resp.NextContinuationToken
|
||||
return resp, nil, nil
|
||||
}
|
||||
@@ -5501,12 +5452,7 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Only record versionID if we are using --s3-versions or --s3-version-at
|
||||
if o.fs.opt.Versions || o.fs.opt.VersionAt.IsSet() {
|
||||
o.versionID = versionID
|
||||
} else {
|
||||
o.versionID = nil
|
||||
}
|
||||
o.versionID = versionID
|
||||
|
||||
// User requested we don't HEAD the object after uploading it
|
||||
// so make up the object as best we can assuming it got
|
||||
|
||||
@@ -1775,14 +1775,11 @@ func (o *Object) setMetadata(info os.FileInfo) {
|
||||
|
||||
// statRemote stats the file or directory at the remote given
|
||||
func (f *Fs) stat(ctx context.Context, remote string) (info os.FileInfo, err error) {
|
||||
absPath := remote
|
||||
if !strings.HasPrefix(remote, "/") {
|
||||
absPath = path.Join(f.absRoot, remote)
|
||||
}
|
||||
c, err := f.getSftpConnection(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("stat: %w", err)
|
||||
}
|
||||
absPath := path.Join(f.absRoot, remote)
|
||||
info, err = c.sftpClient.Stat(absPath)
|
||||
f.putSftpConnection(&c, err)
|
||||
return info, err
|
||||
|
||||
@@ -103,10 +103,6 @@ func (f *Fs) getSessions() int32 {
|
||||
|
||||
// Open a new connection to the SMB server.
|
||||
func (f *Fs) newConnection(ctx context.Context, share string) (c *conn, err error) {
|
||||
// As we are pooling these connections we need to decouple
|
||||
// them from the current context
|
||||
ctx = context.Background()
|
||||
|
||||
c, err = f.dial(ctx, "tcp", f.opt.Host+":"+f.opt.Port)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("couldn't connect SMB: %w", err)
|
||||
|
||||
@@ -478,15 +478,11 @@ func (f *Fs) makeEntryRelative(share, _path, relative string, stat os.FileInfo)
|
||||
}
|
||||
|
||||
func (f *Fs) ensureDirectory(ctx context.Context, share, _path string) error {
|
||||
dir := path.Dir(_path)
|
||||
if dir == "." {
|
||||
return nil
|
||||
}
|
||||
cn, err := f.getConnection(ctx, share)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = cn.smbShare.MkdirAll(f.toSambaPath(dir), 0o755)
|
||||
err = cn.smbShare.MkdirAll(f.toSambaPath(path.Dir(_path)), 0o755)
|
||||
f.putConnection(&cn)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -160,7 +160,6 @@ var (
|
||||
_ fs.ListRer = &Fs{}
|
||||
_ fs.PutStreamer = &Fs{}
|
||||
_ fs.Mover = &Fs{}
|
||||
_ fs.Copier = &Fs{}
|
||||
)
|
||||
|
||||
// NewFs creates a filesystem backed by Storj.
|
||||
@@ -721,43 +720,3 @@ func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object,
|
||||
// Read the new object
|
||||
return f.NewObject(ctx, remote)
|
||||
}
|
||||
|
||||
// Copy src to this remote using server-side copy operations.
|
||||
//
|
||||
// This is stored with the remote path given.
|
||||
//
|
||||
// It returns the destination Object and a possible error.
|
||||
//
|
||||
// Will only be called if src.Fs().Name() == f.Name()
|
||||
//
|
||||
// If it isn't possible then return fs.ErrorCantCopy
|
||||
func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object, error) {
|
||||
srcObj, ok := src.(*Object)
|
||||
if !ok {
|
||||
fs.Debugf(src, "Can't copy - not same remote type")
|
||||
return nil, fs.ErrorCantCopy
|
||||
}
|
||||
|
||||
// Copy parameters
|
||||
srcBucket, srcKey := bucket.Split(srcObj.absolute)
|
||||
dstBucket, dstKey := f.absolute(remote)
|
||||
options := uplink.CopyObjectOptions{}
|
||||
|
||||
// Do the copy
|
||||
newObject, err := f.project.CopyObject(ctx, srcBucket, srcKey, dstBucket, dstKey, &options)
|
||||
if err != nil {
|
||||
// Make sure destination bucket exists
|
||||
_, err := f.project.EnsureBucket(ctx, dstBucket)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("copy object failed to create destination bucket: %w", err)
|
||||
}
|
||||
// And try again
|
||||
newObject, err = f.project.CopyObject(ctx, srcBucket, srcKey, dstBucket, dstKey, &options)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("copy object failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Return the new object
|
||||
return newObjectFromUplink(f, remote, newObject), nil
|
||||
}
|
||||
|
||||
@@ -991,7 +991,6 @@ func (f *Fs) copyOrMove(ctx context.Context, src fs.Object, remote string, metho
|
||||
}
|
||||
return nil, fs.ErrorCantMove
|
||||
}
|
||||
srcFs := srcObj.fs
|
||||
dstPath := f.filePath(remote)
|
||||
err := f.mkParentDir(ctx, dstPath)
|
||||
if err != nil {
|
||||
@@ -1014,10 +1013,9 @@ func (f *Fs) copyOrMove(ctx context.Context, src fs.Object, remote string, metho
|
||||
if f.useOCMtime {
|
||||
opts.ExtraHeaders["X-OC-Mtime"] = fmt.Sprintf("%d", src.ModTime(ctx).Unix())
|
||||
}
|
||||
// Direct the MOVE/COPY to the source server
|
||||
err = srcFs.pacer.Call(func() (bool, error) {
|
||||
resp, err = srcFs.srv.Call(ctx, &opts)
|
||||
return srcFs.shouldRetry(ctx, resp, err)
|
||||
err = f.pacer.Call(func() (bool, error) {
|
||||
resp, err = f.srv.Call(ctx, &opts)
|
||||
return f.shouldRetry(ctx, resp, err)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Copy call failed: %w", err)
|
||||
@@ -1111,10 +1109,9 @@ func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string
|
||||
"Overwrite": "F",
|
||||
},
|
||||
}
|
||||
// Direct the MOVE/COPY to the source server
|
||||
err = srcFs.pacer.Call(func() (bool, error) {
|
||||
resp, err = srcFs.srv.Call(ctx, &opts)
|
||||
return srcFs.shouldRetry(ctx, resp, err)
|
||||
err = f.pacer.Call(func() (bool, error) {
|
||||
resp, err = f.srv.Call(ctx, &opts)
|
||||
return f.shouldRetry(ctx, resp, err)
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("DirMove MOVE call failed: %w", err)
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
#!/bin/bash
|
||||
# This adds the version each backend was released to its docs page
|
||||
set -e
|
||||
for backend in $( find backend -maxdepth 1 -type d ); do
|
||||
backend=$(basename $backend)
|
||||
if [[ "$backend" == "backend" || "$backend" == "vfs" || "$backend" == "all" || "$backend" == "azurefile" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
commit=$(git log --oneline -- $backend | tail -1 | cut -d' ' -f1)
|
||||
if [ "$commit" == "" ]; then
|
||||
commit=$(git log --oneline -- backend/$backend | tail -1 | cut -d' ' -f1)
|
||||
fi
|
||||
version=$(git tag --contains $commit | grep ^v | sort -n | head -1)
|
||||
echo $backend $version
|
||||
sed -i~ "4i versionIntroduced: \"$version\"" docs/content/${backend}.md
|
||||
done
|
||||
@@ -93,9 +93,6 @@ provided by a backend. Where the value is unlimited it is omitted.
|
||||
Some backends does not support the ` + "`rclone about`" + ` command at all,
|
||||
see complete list in [documentation](https://rclone.org/overview/#optional-features).
|
||||
`,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.41",
|
||||
},
|
||||
Run: func(command *cobra.Command, args []string) {
|
||||
cmd.CheckArgs(1, 1, command, args)
|
||||
f := cmd.NewFsSrc(args)
|
||||
|
||||
@@ -30,9 +30,6 @@ rclone config.
|
||||
|
||||
Use the --auth-no-open-browser to prevent rclone to open auth
|
||||
link in default browser automatically.`,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.27",
|
||||
},
|
||||
RunE: func(command *cobra.Command, args []string) error {
|
||||
cmd.CheckArgs(1, 3, command, args)
|
||||
return config.Authorize(context.Background(), args, noAutoBrowser)
|
||||
|
||||
@@ -58,9 +58,6 @@ Pass arguments to the backend by placing them on the end of the line
|
||||
Note to run these commands on a running backend then see
|
||||
[backend/command](/rc/#backend-command) in the rc docs.
|
||||
`,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.52",
|
||||
},
|
||||
RunE: func(command *cobra.Command, args []string) error {
|
||||
cmd.CheckArgs(2, 1e6, command, args)
|
||||
name, remote := args[0], args[1]
|
||||
|
||||
@@ -115,9 +115,6 @@ var commandDefinition = &cobra.Command{
|
||||
Use: "bisync remote1:path1 remote2:path2",
|
||||
Short: shortHelp,
|
||||
Long: longHelp,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.58",
|
||||
},
|
||||
RunE: func(command *cobra.Command, args []string) error {
|
||||
cmd.CheckArgs(2, 2, command, args)
|
||||
fs1, file1, fs2, file2 := cmd.NewFsSrcDstFiles(args)
|
||||
|
||||
@@ -25,10 +25,6 @@ var commandDefinition = &cobra.Command{
|
||||
Print cache stats for a remote in JSON format
|
||||
`,
|
||||
Hidden: true,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.39",
|
||||
"status": "Deprecated",
|
||||
},
|
||||
Run: func(command *cobra.Command, args []string) {
|
||||
cmd.CheckArgs(1, 1, command, args)
|
||||
fs.Logf(nil, `"rclone cachestats" is deprecated, use "rclone backend stats %s" instead`, args[0])
|
||||
|
||||
@@ -57,9 +57,6 @@ the end and |--offset| and |--count| to print a section in the middle.
|
||||
Note that if offset is negative it will count from the end, so
|
||||
|--offset -1 --count 1| is equivalent to |--tail 1|.
|
||||
`, "|", "`"),
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.33",
|
||||
},
|
||||
Run: func(command *cobra.Command, args []string) {
|
||||
usedOffset := offset != 0 || count >= 0
|
||||
usedHead := head > 0
|
||||
|
||||
@@ -37,9 +37,6 @@ that don't support hashes or if you really want to check all the data.
|
||||
|
||||
Note that hash values in the SUM file are treated as case insensitive.
|
||||
`, "|", "`") + check.FlagsHelp,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.56",
|
||||
},
|
||||
RunE: func(command *cobra.Command, args []string) error {
|
||||
cmd.CheckArgs(3, 3, command, args)
|
||||
var hashType hash.Type
|
||||
|
||||
@@ -20,9 +20,6 @@ var commandDefinition = &cobra.Command{
|
||||
Clean up the remote if possible. Empty the trash or delete old file
|
||||
versions. Not supported by all remotes.
|
||||
`,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.31",
|
||||
},
|
||||
Run: func(command *cobra.Command, args []string) {
|
||||
cmd.CheckArgs(1, 1, command, args)
|
||||
fsrc := cmd.NewFsSrc(args)
|
||||
|
||||
132
cmd/cmount/fs.go
132
cmd/cmount/fs.go
@@ -8,7 +8,6 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@@ -96,11 +95,8 @@ func (fsys *FS) closeHandle(fh uint64) (errc int) {
|
||||
}
|
||||
|
||||
// lookup a Node given a path
|
||||
func (fsys *FS) lookupNode(path string) (vfs.Node, int) {
|
||||
func (fsys *FS) lookupNode(path string) (node vfs.Node, errc int) {
|
||||
node, err := fsys.VFS.Stat(path)
|
||||
if err == vfs.ENOENT && fsys.VFS.Opt.Links {
|
||||
node, err = fsys.VFS.Stat(path + fs.LinkSuffix)
|
||||
}
|
||||
return node, translateError(err)
|
||||
}
|
||||
|
||||
@@ -121,13 +117,6 @@ func (fsys *FS) lookupDir(path string) (dir *vfs.Dir, errc int) {
|
||||
func (fsys *FS) lookupParentDir(filePath string) (leaf string, dir *vfs.Dir, errc int) {
|
||||
parentDir, leaf := path.Split(filePath)
|
||||
dir, errc = fsys.lookupDir(parentDir)
|
||||
// Try to get real leaf for symlinks
|
||||
if fsys.VFS.Opt.Links {
|
||||
node, e := fsys.lookupNode(filePath)
|
||||
if e == 0 {
|
||||
leaf = node.Name()
|
||||
}
|
||||
}
|
||||
return leaf, dir, errc
|
||||
}
|
||||
|
||||
@@ -164,9 +153,15 @@ func (fsys *FS) stat(node vfs.Node, stat *fuse.Stat_t) (errc int) {
|
||||
Size := uint64(node.Size())
|
||||
Blocks := (Size + 511) / 512
|
||||
modTime := node.ModTime()
|
||||
Mode := node.Mode().Perm()
|
||||
if node.IsDir() {
|
||||
Mode |= fuse.S_IFDIR
|
||||
} else {
|
||||
Mode |= fuse.S_IFREG
|
||||
}
|
||||
//stat.Dev = 1
|
||||
stat.Ino = node.Inode() // FIXME do we need to set the inode number?
|
||||
stat.Mode = getMode(node)
|
||||
stat.Mode = uint32(Mode)
|
||||
stat.Nlink = 1
|
||||
stat.Uid = fsys.VFS.Opt.UID
|
||||
stat.Gid = fsys.VFS.Opt.GID
|
||||
@@ -257,7 +252,7 @@ func (fsys *FS) Readdir(dirPath string,
|
||||
fill(".", nil, 0)
|
||||
fill("..", nil, 0)
|
||||
for _, node := range nodes {
|
||||
name, _ := fsys.VFS.TrimSymlink(node.Name())
|
||||
name := node.Name()
|
||||
if len(name) > mountlib.MaxLeafSize {
|
||||
fs.Errorf(dirPath, "Name too long (%d bytes) for FUSE, skipping: %s", len(name), name)
|
||||
continue
|
||||
@@ -335,15 +330,13 @@ func (fsys *FS) CreateEx(filePath string, mode uint32, fi *fuse.FileInfo_t) (err
|
||||
if errc != 0 {
|
||||
return errc
|
||||
}
|
||||
// translate the fuse flags to os flags
|
||||
osFlags := translateOpenFlags(fi.Flags) | os.O_CREATE
|
||||
// translate the fuse mode to os mode
|
||||
//osMode := getFileMode(mode)
|
||||
file, err := parentDir.Create(leaf, osFlags)
|
||||
file, err := parentDir.Create(leaf, fi.Flags)
|
||||
if err != nil {
|
||||
return translateError(err)
|
||||
}
|
||||
handle, err := file.Open(osFlags)
|
||||
// translate the fuse flags to os flags
|
||||
flags := translateOpenFlags(fi.Flags) | os.O_CREATE
|
||||
handle, err := file.Open(flags)
|
||||
if err != nil {
|
||||
return translateError(err)
|
||||
}
|
||||
@@ -463,18 +456,6 @@ func (fsys *FS) Rmdir(dirPath string) (errc int) {
|
||||
// Rename renames a file.
|
||||
func (fsys *FS) Rename(oldPath string, newPath string) (errc int) {
|
||||
defer log.Trace(oldPath, "newPath=%q", newPath)("errc=%d", &errc)
|
||||
|
||||
if fsys.VFS.Opt.Links {
|
||||
node, e := fsys.lookupNode(oldPath)
|
||||
|
||||
if e == 0 {
|
||||
if strings.HasSuffix(node.Name(), fs.LinkSuffix) {
|
||||
oldPath += fs.LinkSuffix
|
||||
newPath += fs.LinkSuffix
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return translateError(fsys.VFS.Rename(oldPath, newPath))
|
||||
}
|
||||
|
||||
@@ -524,58 +505,14 @@ func (fsys *FS) Link(oldpath string, newpath string) (errc int) {
|
||||
|
||||
// Symlink creates a symbolic link.
|
||||
func (fsys *FS) Symlink(target string, newpath string) (errc int) {
|
||||
defer log.Trace(fsys, "Requested to symlink newpath=%q, target=%q", newpath, target)("errc=%d", &errc)
|
||||
|
||||
if fsys.VFS.Opt.Links {
|
||||
// The user must NOT provide .rclonelink suffix
|
||||
if strings.HasSuffix(newpath, fs.LinkSuffix) {
|
||||
fs.Errorf(nil, "Invalid name suffix provided: %v", newpath)
|
||||
return translateError(vfs.EINVAL)
|
||||
}
|
||||
|
||||
newpath += fs.LinkSuffix
|
||||
} else {
|
||||
// The user must provide .rclonelink suffix
|
||||
if !strings.HasSuffix(newpath, fs.LinkSuffix) {
|
||||
fs.Errorf(nil, "Invalid name suffix provided: %v", newpath)
|
||||
return translateError(vfs.EINVAL)
|
||||
}
|
||||
}
|
||||
|
||||
// Add target suffix when linking to a link
|
||||
if !strings.HasSuffix(target, fs.LinkSuffix) {
|
||||
vnode, err := fsys.lookupNode(target)
|
||||
if err == 0 && strings.HasSuffix(vnode.Name(), fs.LinkSuffix) {
|
||||
target += fs.LinkSuffix
|
||||
}
|
||||
}
|
||||
|
||||
return translateError(fsys.VFS.Symlink(target, newpath))
|
||||
defer log.Trace(target, "newpath=%q", newpath)("errc=%d", &errc)
|
||||
return -fuse.ENOSYS
|
||||
}
|
||||
|
||||
// Readlink reads the target of a symbolic link.
|
||||
func (fsys *FS) Readlink(path string) (errc int, linkPath string) {
|
||||
defer log.Trace(fsys, "Requested to read link")("errc=%v, linkPath=%q", &errc, linkPath)
|
||||
|
||||
if fsys.VFS.Opt.Links {
|
||||
// The user must NOT provide .rclonelink suffix
|
||||
if strings.HasSuffix(path, fs.LinkSuffix) {
|
||||
fs.Errorf(nil, "Invalid name suffix provided: %v", path)
|
||||
return translateError(vfs.EINVAL), ""
|
||||
}
|
||||
|
||||
path += fs.LinkSuffix
|
||||
} else {
|
||||
// The user must provide .rclonelink suffix
|
||||
if !strings.HasSuffix(path, fs.LinkSuffix) {
|
||||
fs.Errorf(nil, "Invalid name suffix provided: %v", path)
|
||||
return translateError(vfs.EINVAL), ""
|
||||
}
|
||||
}
|
||||
|
||||
linkPath, err := fsys.VFS.Readlink(path)
|
||||
linkPath, _ = fsys.VFS.TrimSymlink(linkPath)
|
||||
return translateError(err), linkPath
|
||||
defer log.Trace(path, "")("linkPath=%q, errc=%d", &linkPath, &errc)
|
||||
return -fuse.ENOSYS, ""
|
||||
}
|
||||
|
||||
// Chmod changes the permission bits of a file.
|
||||
@@ -690,41 +627,6 @@ func translateOpenFlags(inFlags int) (outFlags int) {
|
||||
return outFlags
|
||||
}
|
||||
|
||||
// get the Mode from a vfs Node
|
||||
func getMode(node os.FileInfo) uint32 {
|
||||
vfsMode := node.Mode()
|
||||
Mode := vfsMode.Perm()
|
||||
if vfsMode&os.ModeDir != 0 {
|
||||
Mode |= fuse.S_IFDIR
|
||||
} else if vfsMode&os.ModeSymlink != 0 {
|
||||
Mode |= fuse.S_IFLNK
|
||||
} else if vfsMode&os.ModeNamedPipe != 0 {
|
||||
Mode |= fuse.S_IFIFO
|
||||
} else {
|
||||
Mode |= fuse.S_IFREG
|
||||
}
|
||||
return uint32(Mode)
|
||||
}
|
||||
|
||||
// convert fuse mode to os.FileMode
|
||||
// func getFileMode(mode uint32) os.FileMode {
|
||||
// osMode := os.FileMode(0)
|
||||
// if mode&fuse.S_IFDIR != 0 {
|
||||
// mode ^= fuse.S_IFDIR
|
||||
// osMode |= os.ModeDir
|
||||
// } else if mode&fuse.S_IFREG != 0 {
|
||||
// mode ^= fuse.S_IFREG
|
||||
// } else if mode&fuse.S_IFLNK != 0 {
|
||||
// mode ^= fuse.S_IFLNK
|
||||
// osMode |= os.ModeSymlink
|
||||
// } else if mode&fuse.S_IFIFO != 0 {
|
||||
// mode ^= fuse.S_IFIFO
|
||||
// osMode |= os.ModeNamedPipe
|
||||
// }
|
||||
// osMode |= os.FileMode(mode)
|
||||
// return osMode
|
||||
// }
|
||||
|
||||
// Make sure interfaces are satisfied
|
||||
var (
|
||||
_ fuse.FileSystemInterface = (*FS)(nil)
|
||||
|
||||
@@ -44,9 +44,6 @@ var configCommand = &cobra.Command{
|
||||
remotes and manage existing ones. You may also set or remove a
|
||||
password to protect your configuration.
|
||||
`,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.39",
|
||||
},
|
||||
RunE: func(command *cobra.Command, args []string) error {
|
||||
cmd.CheckArgs(0, 0, command, args)
|
||||
return config.EditConfig(context.Background())
|
||||
@@ -57,18 +54,12 @@ var configEditCommand = &cobra.Command{
|
||||
Use: "edit",
|
||||
Short: configCommand.Short,
|
||||
Long: configCommand.Long,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.39",
|
||||
},
|
||||
Run: configCommand.Run,
|
||||
Run: configCommand.Run,
|
||||
}
|
||||
|
||||
var configFileCommand = &cobra.Command{
|
||||
Use: "file",
|
||||
Short: `Show path of configuration file in use.`,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.38",
|
||||
},
|
||||
Run: func(command *cobra.Command, args []string) {
|
||||
cmd.CheckArgs(0, 0, command, args)
|
||||
config.ShowConfigLocation()
|
||||
@@ -78,9 +69,6 @@ var configFileCommand = &cobra.Command{
|
||||
var configTouchCommand = &cobra.Command{
|
||||
Use: "touch",
|
||||
Short: `Ensure configuration file exists.`,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.56",
|
||||
},
|
||||
Run: func(command *cobra.Command, args []string) {
|
||||
cmd.CheckArgs(0, 0, command, args)
|
||||
config.SaveConfig()
|
||||
@@ -90,9 +78,6 @@ var configTouchCommand = &cobra.Command{
|
||||
var configPathsCommand = &cobra.Command{
|
||||
Use: "paths",
|
||||
Short: `Show paths used for configuration, cache, temp etc.`,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.57",
|
||||
},
|
||||
Run: func(command *cobra.Command, args []string) {
|
||||
cmd.CheckArgs(0, 0, command, args)
|
||||
fmt.Printf("Config file: %s\n", config.GetConfigPath())
|
||||
@@ -104,9 +89,6 @@ var configPathsCommand = &cobra.Command{
|
||||
var configShowCommand = &cobra.Command{
|
||||
Use: "show [<remote>]",
|
||||
Short: `Print (decrypted) config file, or the config for a single remote.`,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.38",
|
||||
},
|
||||
Run: func(command *cobra.Command, args []string) {
|
||||
cmd.CheckArgs(0, 1, command, args)
|
||||
if len(args) == 0 {
|
||||
@@ -121,9 +103,6 @@ var configShowCommand = &cobra.Command{
|
||||
var configDumpCommand = &cobra.Command{
|
||||
Use: "dump",
|
||||
Short: `Dump the config file as JSON.`,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.39",
|
||||
},
|
||||
RunE: func(command *cobra.Command, args []string) error {
|
||||
cmd.CheckArgs(0, 0, command, args)
|
||||
return config.Dump()
|
||||
@@ -133,9 +112,6 @@ var configDumpCommand = &cobra.Command{
|
||||
var configProvidersCommand = &cobra.Command{
|
||||
Use: "providers",
|
||||
Short: `List in JSON format all the providers and options.`,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.39",
|
||||
},
|
||||
RunE: func(command *cobra.Command, args []string) error {
|
||||
cmd.CheckArgs(0, 0, command, args)
|
||||
return config.JSONListProviders()
|
||||
@@ -176,7 +152,7 @@ This will look something like (some irrelevant detail removed):
|
||||
"State": "*oauth-islocal,teamdrive,,",
|
||||
"Option": {
|
||||
"Name": "config_is_local",
|
||||
"Help": "Use web browser to automatically authenticate rclone with remote?\n * Say Y if the machine running rclone has a web browser you can use\n * Say N if running rclone on a (remote) machine without web browser access\nIf not sure try Y. If Y failed, try N.\n",
|
||||
"Help": "Use auto config?\n * Say Y if not sure\n * Say N if you are working on a remote or headless machine\n",
|
||||
"Default": true,
|
||||
"Examples": [
|
||||
{
|
||||
@@ -250,9 +226,6 @@ using remote authorization you would do this:
|
||||
|
||||
rclone config create mydrive drive config_is_local=false
|
||||
`, "|", "`") + configPasswordHelp,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.39",
|
||||
},
|
||||
RunE: func(command *cobra.Command, args []string) error {
|
||||
cmd.CheckArgs(2, 256, command, args)
|
||||
in, err := argsToMap(args[2:])
|
||||
@@ -316,9 +289,6 @@ require this add an extra parameter thus:
|
||||
|
||||
rclone config update myremote env_auth=true config_refresh_token=false
|
||||
`, "|", "`") + configPasswordHelp,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.39",
|
||||
},
|
||||
RunE: func(command *cobra.Command, args []string) error {
|
||||
cmd.CheckArgs(1, 256, command, args)
|
||||
in, err := argsToMap(args[1:])
|
||||
@@ -334,9 +304,6 @@ require this add an extra parameter thus:
|
||||
var configDeleteCommand = &cobra.Command{
|
||||
Use: "delete name",
|
||||
Short: "Delete an existing remote.",
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.39",
|
||||
},
|
||||
Run: func(command *cobra.Command, args []string) {
|
||||
cmd.CheckArgs(1, 1, command, args)
|
||||
config.DeleteRemote(args[0])
|
||||
@@ -359,9 +326,6 @@ For example, to set password of a remote of name myremote you would do:
|
||||
This command is obsolete now that "config update" and "config create"
|
||||
both support obscuring passwords directly.
|
||||
`, "|", "`"),
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.39",
|
||||
},
|
||||
RunE: func(command *cobra.Command, args []string) error {
|
||||
cmd.CheckArgs(1, 256, command, args)
|
||||
in, err := argsToMap(args[1:])
|
||||
|
||||
@@ -46,9 +46,6 @@ the destination.
|
||||
|
||||
**Note**: Use the ` + "`-P`" + `/` + "`--progress`" + ` flag to view real-time transfer statistics
|
||||
`,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.35",
|
||||
},
|
||||
Run: func(command *cobra.Command, args []string) {
|
||||
cmd.CheckArgs(2, 2, command, args)
|
||||
fsrc, srcFileName, fdst, dstFileName := cmd.NewFsSrcDstFiles(args)
|
||||
|
||||
@@ -51,9 +51,6 @@ destination if there is one with the same name.
|
||||
Setting ` + "`--stdout`" + ` or making the output file name ` + "`-`" + `
|
||||
will cause the output to be written to standard output.
|
||||
`,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.43",
|
||||
},
|
||||
RunE: func(command *cobra.Command, args []string) (err error) {
|
||||
cmd.CheckArgs(1, 2, command, args)
|
||||
|
||||
|
||||
@@ -47,9 +47,6 @@ the files in remote:path.
|
||||
|
||||
After it has run it will log the status of the encryptedremote:.
|
||||
` + check.FlagsHelp,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.36",
|
||||
},
|
||||
Run: func(command *cobra.Command, args []string) {
|
||||
cmd.CheckArgs(2, 2, command, args)
|
||||
fsrc, fdst := cmd.NewFsSrcDst(args)
|
||||
|
||||
@@ -41,9 +41,6 @@ use it like this
|
||||
Another way to accomplish this is by using the ` + "`rclone backend encode` (or `decode`)" + ` command.
|
||||
See the documentation on the [crypt](/crypt/) overlay for more info.
|
||||
`,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.38",
|
||||
},
|
||||
Run: func(command *cobra.Command, args []string) {
|
||||
cmd.CheckArgs(2, 11, command, args)
|
||||
cmd.Run(false, false, command, func() error {
|
||||
|
||||
@@ -135,9 +135,6 @@ Or
|
||||
|
||||
rclone dedupe rename "drive:Google Photos"
|
||||
`,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.27",
|
||||
},
|
||||
Run: func(command *cobra.Command, args []string) {
|
||||
cmd.CheckArgs(1, 2, command, args)
|
||||
if len(args) > 1 {
|
||||
|
||||
@@ -53,9 +53,6 @@ delete all files bigger than 100 MiB.
|
||||
**Important**: Since this can cause data loss, test first with the
|
||||
|--dry-run| or the |--interactive|/|-i| flag.
|
||||
`, "|", "`"),
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.27",
|
||||
},
|
||||
Run: func(command *cobra.Command, args []string) {
|
||||
cmd.CheckArgs(1, 1, command, args)
|
||||
fsrc := cmd.NewFsSrc(args)
|
||||
|
||||
@@ -22,9 +22,6 @@ Remove a single file from remote. Unlike ` + "`" + `delete` + "`" + ` it cannot
|
||||
remove a directory and it doesn't obey include/exclude filters - if the specified file exists,
|
||||
it will always be removed.
|
||||
`,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.42",
|
||||
},
|
||||
Run: func(command *cobra.Command, args []string) {
|
||||
cmd.CheckArgs(1, 1, command, args)
|
||||
fs, fileName := cmd.NewFsFile(args[0])
|
||||
|
||||
@@ -17,7 +17,4 @@ var completionDefinition = &cobra.Command{
|
||||
Generates a shell completion script for rclone.
|
||||
Run with ` + "`--help`" + ` to list the supported shells.
|
||||
`,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.33",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ type frontmatter struct {
|
||||
Slug string
|
||||
URL string
|
||||
Source string
|
||||
Annotations map[string]string
|
||||
}
|
||||
|
||||
var frontmatterTemplate = template.Must(template.New("frontmatter").Parse(`---
|
||||
@@ -39,9 +38,6 @@ title: "{{ .Title }}"
|
||||
description: "{{ .Description }}"
|
||||
slug: {{ .Slug }}
|
||||
url: {{ .URL }}
|
||||
{{- range $key, $value := .Annotations }}
|
||||
{{ $key }}: {{ $value }}
|
||||
{{- end }}
|
||||
# autogenerated - DO NOT EDIT, instead edit the source code in {{ .Source }} and as part of making a release run "make commanddocs"
|
||||
---
|
||||
`))
|
||||
@@ -53,9 +49,6 @@ var commandDefinition = &cobra.Command{
|
||||
This produces markdown docs for the rclone commands to the directory
|
||||
supplied. These are in a format suitable for hugo to render into the
|
||||
rclone.org website.`,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.33",
|
||||
},
|
||||
RunE: func(command *cobra.Command, args []string) error {
|
||||
cmd.CheckArgs(1, 1, command, args)
|
||||
now := time.Now().Format(time.RFC3339)
|
||||
@@ -82,24 +75,17 @@ rclone.org website.`,
|
||||
return err
|
||||
}
|
||||
|
||||
// Look up name => details for prepender
|
||||
type commandDetails struct {
|
||||
Short string
|
||||
Annotations map[string]string
|
||||
}
|
||||
var commands = map[string]commandDetails{}
|
||||
var addCommandDetails func(root *cobra.Command)
|
||||
addCommandDetails = func(root *cobra.Command) {
|
||||
// Look up name => description for prepender
|
||||
var description = map[string]string{}
|
||||
var addDescription func(root *cobra.Command)
|
||||
addDescription = func(root *cobra.Command) {
|
||||
name := strings.ReplaceAll(root.CommandPath(), " ", "_") + ".md"
|
||||
commands[name] = commandDetails{
|
||||
Short: root.Short,
|
||||
Annotations: root.Annotations,
|
||||
}
|
||||
description[name] = root.Short
|
||||
for _, c := range root.Commands() {
|
||||
addCommandDetails(c)
|
||||
addDescription(c)
|
||||
}
|
||||
}
|
||||
addCommandDetails(cmd.Root)
|
||||
addDescription(cmd.Root)
|
||||
|
||||
// markup for the docs files
|
||||
prepender := func(filename string) string {
|
||||
@@ -108,11 +94,10 @@ rclone.org website.`,
|
||||
data := frontmatter{
|
||||
Date: now,
|
||||
Title: strings.ReplaceAll(base, "_", " "),
|
||||
Description: commands[name].Short,
|
||||
Description: description[name],
|
||||
Slug: base,
|
||||
URL: "/commands/" + strings.ToLower(base) + "/",
|
||||
Source: strings.ReplaceAll(strings.ReplaceAll(base, "rclone", "cmd"), "_", "/") + "/",
|
||||
Annotations: commands[name].Annotations,
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
err := frontmatterTemplate.Execute(&buf, data)
|
||||
|
||||
@@ -112,9 +112,6 @@ Then
|
||||
|
||||
Note that hash names are case insensitive and values are output in lower case.
|
||||
`,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.41",
|
||||
},
|
||||
RunE: func(command *cobra.Command, args []string) error {
|
||||
cmd.CheckArgs(0, 2, command, args)
|
||||
if len(args) == 0 {
|
||||
|
||||
@@ -49,9 +49,6 @@ link. Exact capabilities depend on the remote, but the link will
|
||||
always by default be created with the least constraints – e.g. no
|
||||
expiry, no password protection, accessible without account.
|
||||
`,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.41",
|
||||
},
|
||||
Run: func(command *cobra.Command, args []string) {
|
||||
cmd.CheckArgs(1, 1, command, args)
|
||||
fsrc, remote := cmd.NewFsFile(args[0])
|
||||
|
||||
@@ -30,9 +30,6 @@ rclone listremotes lists all the available remotes from the config file.
|
||||
|
||||
When used with the ` + "`--long`" + ` flag it lists the types too.
|
||||
`,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.34",
|
||||
},
|
||||
Run: func(command *cobra.Command, args []string) {
|
||||
cmd.CheckArgs(0, 0, command, args)
|
||||
remotes := config.FileSections()
|
||||
|
||||
@@ -142,9 +142,6 @@ those only (without traversing the whole directory structure):
|
||||
rclone copy --files-from-raw new_files /path/to/local remote:path
|
||||
|
||||
` + lshelp.Help,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.40",
|
||||
},
|
||||
Run: func(command *cobra.Command, args []string) {
|
||||
cmd.CheckArgs(1, 1, command, args)
|
||||
fsrc := cmd.NewFsSrc(args)
|
||||
|
||||
@@ -112,9 +112,6 @@ will be shown ("2017-05-31T16:15:57+01:00").
|
||||
The whole output can be processed as a JSON blob, or alternatively it
|
||||
can be processed line by line as each item is written one to a line.
|
||||
` + lshelp.Help,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.37",
|
||||
},
|
||||
RunE: func(command *cobra.Command, args []string) error {
|
||||
cmd.CheckArgs(1, 1, command, args)
|
||||
var fsrc fs.Fs
|
||||
|
||||
@@ -31,9 +31,6 @@ Eg
|
||||
37600 2016-06-25 18:55:40.814629136 fubuwic
|
||||
|
||||
` + lshelp.Help,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.02",
|
||||
},
|
||||
Run: func(command *cobra.Command, args []string) {
|
||||
cmd.CheckArgs(1, 1, command, args)
|
||||
fsrc := cmd.NewFsSrc(args)
|
||||
|
||||
@@ -38,9 +38,6 @@ by not passing a remote:path, or by passing a hyphen as remote:path
|
||||
when there is data to read (if not, the hyphen will be treated literally,
|
||||
as a relative path).
|
||||
`,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.02",
|
||||
},
|
||||
RunE: func(command *cobra.Command, args []string) error {
|
||||
cmd.CheckArgs(0, 1, command, args)
|
||||
if found, err := hashsum.CreateFromStdinArg(hash.MD5, args, 0); found {
|
||||
|
||||
103
cmd/mount/dir.go
103
cmd/mount/dir.go
@@ -8,8 +8,6 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@@ -30,21 +28,13 @@ type Dir struct {
|
||||
// Check interface satisfied
|
||||
var _ fusefs.Node = (*Dir)(nil)
|
||||
|
||||
func fallbackStat(dir *vfs.Dir, leaf string) (node vfs.Node, err error) {
|
||||
node, err = dir.Stat(leaf)
|
||||
if err == vfs.ENOENT && dir.VFS().Opt.Links {
|
||||
node, err = dir.Stat(leaf + fs.LinkSuffix)
|
||||
}
|
||||
return node, err
|
||||
}
|
||||
|
||||
// Attr updates the attributes of a directory
|
||||
func (d *Dir) Attr(ctx context.Context, a *fuse.Attr) (err error) {
|
||||
defer log.Trace(d, "")("attr=%+v, err=%v", a, &err)
|
||||
a.Valid = d.fsys.opt.AttrTimeout
|
||||
a.Gid = d.VFS().Opt.GID
|
||||
a.Uid = d.VFS().Opt.UID
|
||||
a.Mode = d.Mode()
|
||||
a.Mode = os.ModeDir | d.VFS().Opt.DirPerms
|
||||
modTime := d.ModTime()
|
||||
a.Atime = modTime
|
||||
a.Mtime = modTime
|
||||
@@ -84,7 +74,7 @@ var _ fusefs.NodeRequestLookuper = (*Dir)(nil)
|
||||
// Lookup need not to handle the names "." and "..".
|
||||
func (d *Dir) Lookup(ctx context.Context, req *fuse.LookupRequest, resp *fuse.LookupResponse) (node fusefs.Node, err error) {
|
||||
defer log.Trace(d, "name=%q", req.Name)("node=%+v, err=%v", &node, &err)
|
||||
mnode, err := fallbackStat(d.Dir, req.Name)
|
||||
mnode, err := d.Dir.Stat(req.Name)
|
||||
if err != nil {
|
||||
return nil, translateError(err)
|
||||
}
|
||||
@@ -127,7 +117,7 @@ func (d *Dir) ReadDirAll(ctx context.Context) (dirents []fuse.Dirent, err error)
|
||||
Name: "..",
|
||||
})
|
||||
for _, node := range items {
|
||||
name, isLink := d.VFS().TrimSymlink(node.Name())
|
||||
name := node.Name()
|
||||
if len(name) > mountlib.MaxLeafSize {
|
||||
fs.Errorf(d, "Name too long (%d bytes) for FUSE, skipping: %s", len(name), name)
|
||||
continue
|
||||
@@ -137,9 +127,7 @@ func (d *Dir) ReadDirAll(ctx context.Context) (dirents []fuse.Dirent, err error)
|
||||
Type: fuse.DT_File,
|
||||
Name: name,
|
||||
}
|
||||
if isLink {
|
||||
dirent.Type = fuse.DT_Link
|
||||
} else if node.IsDir() {
|
||||
if node.IsDir() {
|
||||
dirent.Type = fuse.DT_Dir
|
||||
}
|
||||
dirents = append(dirents, dirent)
|
||||
@@ -153,13 +141,11 @@ var _ fusefs.NodeCreater = (*Dir)(nil)
|
||||
// Create makes a new file
|
||||
func (d *Dir) Create(ctx context.Context, req *fuse.CreateRequest, resp *fuse.CreateResponse) (node fusefs.Node, handle fusefs.Handle, err error) {
|
||||
defer log.Trace(d, "name=%q", req.Name)("node=%v, handle=%v, err=%v", &node, &handle, &err)
|
||||
// translate the fuse flags to os flags
|
||||
osFlags := int(req.Flags) | os.O_CREATE
|
||||
file, err := d.Dir.Create(req.Name, osFlags)
|
||||
file, err := d.Dir.Create(req.Name, int(req.Flags))
|
||||
if err != nil {
|
||||
return nil, nil, translateError(err)
|
||||
}
|
||||
fh, err := file.Open(osFlags)
|
||||
fh, err := file.Open(int(req.Flags) | os.O_CREATE)
|
||||
if err != nil {
|
||||
return nil, nil, translateError(err)
|
||||
}
|
||||
@@ -189,18 +175,7 @@ var _ fusefs.NodeRemover = (*Dir)(nil)
|
||||
// may correspond to a file (unlink) or to a directory (rmdir).
|
||||
func (d *Dir) Remove(ctx context.Context, req *fuse.RemoveRequest) (err error) {
|
||||
defer log.Trace(d, "name=%q", req.Name)("err=%v", &err)
|
||||
|
||||
name := req.Name
|
||||
|
||||
if d.VFS().Opt.Links {
|
||||
node, err := fallbackStat(d.Dir, name)
|
||||
|
||||
if err == nil {
|
||||
name = node.Name()
|
||||
}
|
||||
}
|
||||
|
||||
err = d.Dir.RemoveName(name)
|
||||
err = d.Dir.RemoveName(req.Name)
|
||||
if err != nil {
|
||||
return translateError(err)
|
||||
}
|
||||
@@ -227,22 +202,7 @@ func (d *Dir) Rename(ctx context.Context, req *fuse.RenameRequest, newDir fusefs
|
||||
return fmt.Errorf("unknown Dir type %T", newDir)
|
||||
}
|
||||
|
||||
oldName := req.OldName
|
||||
newName := req.NewName
|
||||
|
||||
if d.VFS().Opt.Links {
|
||||
node, err := fallbackStat(d.Dir, oldName)
|
||||
|
||||
if err == nil {
|
||||
oldName = node.Name()
|
||||
|
||||
if strings.HasSuffix(oldName, fs.LinkSuffix) {
|
||||
newName += fs.LinkSuffix
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = d.Dir.Rename(oldName, newName, destDir.Dir)
|
||||
err = d.Dir.Rename(req.OldName, req.NewName, destDir.Dir)
|
||||
if err != nil {
|
||||
return translateError(err)
|
||||
}
|
||||
@@ -280,53 +240,6 @@ func (d *Dir) Link(ctx context.Context, req *fuse.LinkRequest, old fusefs.Node)
|
||||
return nil, syscall.ENOSYS
|
||||
}
|
||||
|
||||
var _ fusefs.NodeSymlinker = (*Dir)(nil)
|
||||
|
||||
// Symlink create a symbolic link.
|
||||
func (d *Dir) Symlink(ctx context.Context, req *fuse.SymlinkRequest) (node fusefs.Node, err error) {
|
||||
defer log.Trace(d, "Requested to symlink newname=%v, target=%v", req.NewName, req.Target)("node=%v, err=%v", &node, &err)
|
||||
|
||||
newName := path.Join(d.Path(), req.NewName)
|
||||
target := req.Target
|
||||
|
||||
if d.VFS().Opt.Links {
|
||||
// The user must NOT provide .rclonelink suffix
|
||||
if strings.HasSuffix(newName, fs.LinkSuffix) {
|
||||
fs.Errorf(nil, "Invalid name suffix provided: %v", newName)
|
||||
return nil, vfs.EINVAL
|
||||
}
|
||||
|
||||
newName += fs.LinkSuffix
|
||||
} else {
|
||||
// The user must provide .rclonelink suffix
|
||||
if !strings.HasSuffix(newName, fs.LinkSuffix) {
|
||||
fs.Errorf(nil, "Invalid name suffix provided: %v", newName)
|
||||
return nil, vfs.EINVAL
|
||||
}
|
||||
}
|
||||
|
||||
// Add target suffix when linking to a link
|
||||
if !strings.HasSuffix(target, fs.LinkSuffix) {
|
||||
vnode, err := fallbackStat(d.Dir, target)
|
||||
if err == nil && strings.HasSuffix(vnode.Name(), fs.LinkSuffix) {
|
||||
target += fs.LinkSuffix
|
||||
}
|
||||
}
|
||||
|
||||
err = d.VFS().Symlink(target, newName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
n, err := d.Stat(path.Base(newName))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
node = &File{n.(*vfs.File), d.fsys}
|
||||
return node, nil
|
||||
}
|
||||
|
||||
// Check interface satisfied
|
||||
var _ fusefs.NodeMknoder = (*Dir)(nil)
|
||||
|
||||
|
||||
@@ -5,14 +5,11 @@ package mount
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"bazil.org/fuse"
|
||||
fusefs "bazil.org/fuse/fs"
|
||||
"github.com/rclone/rclone/fs"
|
||||
"github.com/rclone/rclone/fs/log"
|
||||
"github.com/rclone/rclone/vfs"
|
||||
)
|
||||
@@ -35,7 +32,7 @@ func (f *File) Attr(ctx context.Context, a *fuse.Attr) (err error) {
|
||||
Blocks := (Size + 511) / 512
|
||||
a.Gid = f.VFS().Opt.GID
|
||||
a.Uid = f.VFS().Opt.UID
|
||||
a.Mode = f.File.Mode() &^ os.ModeAppend
|
||||
a.Mode = f.VFS().Opt.FilePerms
|
||||
a.Size = Size
|
||||
a.Atime = modTime
|
||||
a.Mtime = modTime
|
||||
@@ -129,32 +126,3 @@ func (f *File) Removexattr(ctx context.Context, req *fuse.RemovexattrRequest) er
|
||||
}
|
||||
|
||||
var _ fusefs.NodeRemovexattrer = (*File)(nil)
|
||||
|
||||
var _ fusefs.NodeReadlinker = (*File)(nil)
|
||||
|
||||
// Readlink read symbolic link target.
|
||||
func (f *File) Readlink(ctx context.Context, req *fuse.ReadlinkRequest) (ret string, err error) {
|
||||
defer log.Trace(f, "Requested to read link")("ret=%v, err=%v", &ret, &err)
|
||||
|
||||
path := f.Path()
|
||||
|
||||
if f.VFS().Opt.Links {
|
||||
// The user must NOT provide .rclonelink suffix
|
||||
// if strings.HasSuffix(path, fs.LinkSuffix) {
|
||||
// fs.Errorf(nil, "Invalid name suffix provided: %v", path)
|
||||
// return "", vfs.EINVAL
|
||||
// }
|
||||
|
||||
// path += fs.LinkSuffix
|
||||
} else {
|
||||
// The user must provide .rclonelink suffix
|
||||
if !strings.HasSuffix(path, fs.LinkSuffix) {
|
||||
fs.Errorf(nil, "Invalid name suffix provided: %v", path)
|
||||
return "", vfs.EINVAL
|
||||
}
|
||||
}
|
||||
|
||||
ret, err = f.VFS().Readlink(path)
|
||||
ret, _ = f.VFS().TrimSymlink(ret)
|
||||
return ret, err
|
||||
}
|
||||
|
||||
@@ -51,39 +51,15 @@ func (f *FS) SetDebug(debug bool) {
|
||||
|
||||
// get the Mode from a vfs Node
|
||||
func getMode(node os.FileInfo) uint32 {
|
||||
vfsMode := node.Mode()
|
||||
Mode := vfsMode.Perm()
|
||||
if vfsMode&os.ModeDir != 0 {
|
||||
Mode := node.Mode().Perm()
|
||||
if node.IsDir() {
|
||||
Mode |= fuse.S_IFDIR
|
||||
} else if vfsMode&os.ModeSymlink != 0 {
|
||||
Mode |= fuse.S_IFLNK
|
||||
} else if vfsMode&os.ModeNamedPipe != 0 {
|
||||
Mode |= fuse.S_IFIFO
|
||||
} else {
|
||||
Mode |= fuse.S_IFREG
|
||||
}
|
||||
return uint32(Mode)
|
||||
}
|
||||
|
||||
// convert fuse mode to os.FileMode
|
||||
// func getFileMode(mode uint32) os.FileMode {
|
||||
// osMode := os.FileMode(0)
|
||||
// if mode&fuse.S_IFDIR != 0 {
|
||||
// mode ^= fuse.S_IFDIR
|
||||
// osMode |= os.ModeDir
|
||||
// } else if mode&fuse.S_IFREG != 0 {
|
||||
// mode ^= fuse.S_IFREG
|
||||
// } else if mode&fuse.S_IFLNK != 0 {
|
||||
// mode ^= fuse.S_IFLNK
|
||||
// osMode |= os.ModeSymlink
|
||||
// } else if mode&fuse.S_IFIFO != 0 {
|
||||
// mode ^= fuse.S_IFIFO
|
||||
// osMode |= os.ModeNamedPipe
|
||||
// }
|
||||
// osMode |= os.FileMode(mode)
|
||||
// return osMode
|
||||
// }
|
||||
|
||||
// fill in attr from node
|
||||
func setAttr(node vfs.Node, attr *fuse.Attr) {
|
||||
Size := uint64(node.Size())
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"context"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
fusefs "github.com/hanwen/go-fuse/v2/fs"
|
||||
@@ -57,9 +56,6 @@ func (n *Node) lookupVfsNodeInDir(leaf string) (vfsNode vfs.Node, errno syscall.
|
||||
return nil, syscall.ENOTDIR
|
||||
}
|
||||
vfsNode, err := dir.Stat(leaf)
|
||||
if err == vfs.ENOENT && dir.VFS().Opt.Links {
|
||||
vfsNode, err = dir.Stat(leaf + fs.LinkSuffix)
|
||||
}
|
||||
return vfsNode, translateError(err)
|
||||
}
|
||||
|
||||
@@ -223,7 +219,6 @@ func (n *Node) Opendir(ctx context.Context) syscall.Errno {
|
||||
var _ = (fusefs.NodeOpendirer)((*Node)(nil))
|
||||
|
||||
type dirStream struct {
|
||||
fsys *FS
|
||||
nodes []os.FileInfo
|
||||
i int
|
||||
}
|
||||
@@ -231,7 +226,7 @@ type dirStream struct {
|
||||
// HasNext indicates if there are further entries. HasNext
|
||||
// might be called on already closed streams.
|
||||
func (ds *dirStream) HasNext() bool {
|
||||
return ds.i < len(ds.nodes)+2
|
||||
return ds.i < len(ds.nodes)
|
||||
}
|
||||
|
||||
// Next retrieves the next entry. It is only called if HasNext
|
||||
@@ -239,30 +234,14 @@ func (ds *dirStream) HasNext() bool {
|
||||
// indicate I/O errors
|
||||
func (ds *dirStream) Next() (de fuse.DirEntry, errno syscall.Errno) {
|
||||
// defer log.Trace(nil, "")("de=%+v, errno=%v", &de, &errno)
|
||||
if ds.i == 0 {
|
||||
ds.i++
|
||||
return fuse.DirEntry{
|
||||
Mode: fuse.S_IFDIR,
|
||||
Name: ".",
|
||||
Ino: 0, // FIXME
|
||||
}, 0
|
||||
} else if ds.i == 1 {
|
||||
ds.i++
|
||||
return fuse.DirEntry{
|
||||
Mode: fuse.S_IFDIR,
|
||||
Name: "..",
|
||||
Ino: 0, // FIXME
|
||||
}, 0
|
||||
}
|
||||
fi := ds.nodes[ds.i-2]
|
||||
name, _ := ds.fsys.VFS.TrimSymlink(path.Base(fi.Name()))
|
||||
fi := ds.nodes[ds.i]
|
||||
de = fuse.DirEntry{
|
||||
// Mode is the file's mode. Only the high bits (e.g. S_IFDIR)
|
||||
// are considered.
|
||||
Mode: getMode(fi),
|
||||
|
||||
// Name is the basename of the file in the directory.
|
||||
Name: name,
|
||||
Name: path.Base(fi.Name()),
|
||||
|
||||
// Ino is the inode number.
|
||||
Ino: 0, // FIXME
|
||||
@@ -310,7 +289,6 @@ func (n *Node) Readdir(ctx context.Context) (ds fusefs.DirStream, errno syscall.
|
||||
}
|
||||
return &dirStream{
|
||||
nodes: items,
|
||||
fsys: n.fsys,
|
||||
}, 0
|
||||
}
|
||||
|
||||
@@ -348,8 +326,6 @@ func (n *Node) Create(ctx context.Context, name string, flags uint32, mode uint3
|
||||
}
|
||||
// translate the fuse flags to os flags
|
||||
osFlags := int(flags) | os.O_CREATE
|
||||
// translate the fuse mode to os mode
|
||||
//osMode := getFileMode(mode)
|
||||
file, err := dir.Create(name, osFlags)
|
||||
if err != nil {
|
||||
return nil, nil, 0, translateError(err)
|
||||
@@ -425,101 +401,7 @@ func (n *Node) Rename(ctx context.Context, oldName string, newParent fusefs.Inod
|
||||
if !ok {
|
||||
return syscall.ENOTDIR
|
||||
}
|
||||
if oldDir.VFS().Opt.Links {
|
||||
node, err := n.lookupVfsNodeInDir(oldName)
|
||||
|
||||
if err == 0 {
|
||||
oldName = node.Name()
|
||||
|
||||
if strings.HasSuffix(oldName, fs.LinkSuffix) {
|
||||
newName += fs.LinkSuffix
|
||||
}
|
||||
}
|
||||
}
|
||||
return translateError(oldDir.Rename(oldName, newName, newDir))
|
||||
}
|
||||
|
||||
var _ = (fusefs.NodeRenamer)((*Node)(nil))
|
||||
|
||||
var _ fusefs.NodeReadlinker = (*Node)(nil)
|
||||
|
||||
// Readlink read symbolic link target.
|
||||
func (n *Node) Readlink(ctx context.Context) (ret []byte, err syscall.Errno) {
|
||||
defer log.Trace(n, "Requested to read link")("ret=%v, err=%v", &ret, &err)
|
||||
|
||||
path := n.node.Path()
|
||||
|
||||
if n.node.VFS().Opt.Links {
|
||||
// The user must NOT provide .rclonelink suffix
|
||||
// if strings.HasSuffix(path, fs.LinkSuffix) {
|
||||
// fs.Errorf(nil, "Invalid name suffix provided: %v", path)
|
||||
// return nil, translateError(vfs.EINVAL)
|
||||
// }
|
||||
|
||||
// path += fs.LinkSuffix
|
||||
} else {
|
||||
// The user must provide .rclonelink suffix
|
||||
if !strings.HasSuffix(path, fs.LinkSuffix) {
|
||||
fs.Errorf(nil, "Invalid name suffix provided: %v", path)
|
||||
return nil, translateError(vfs.EINVAL)
|
||||
}
|
||||
}
|
||||
|
||||
s, serr := n.node.VFS().Readlink(path)
|
||||
if serr != nil {
|
||||
return nil, translateError(serr)
|
||||
}
|
||||
s, _ = n.node.VFS().TrimSymlink(s)
|
||||
return []byte(s), 0
|
||||
}
|
||||
|
||||
var _ fusefs.NodeSymlinker = (*Node)(nil)
|
||||
|
||||
// Symlink create symbolic link.
|
||||
func (n *Node) Symlink(ctx context.Context, target, name string, out *fuse.EntryOut) (node *fusefs.Inode, err syscall.Errno) {
|
||||
defer log.Trace(n, "Requested to symlink name=%v, target=%v", name, target)("node=%v, err=%v", &node, &err)
|
||||
|
||||
name = path.Join(n.node.Path(), name)
|
||||
|
||||
if n.node.VFS().Opt.Links {
|
||||
// The user must NOT provide .rclonelink suffix
|
||||
if strings.HasSuffix(name, fs.LinkSuffix) {
|
||||
fs.Errorf(nil, "Invalid name suffix provided: %v", name)
|
||||
return nil, translateError(vfs.EINVAL)
|
||||
}
|
||||
|
||||
name += fs.LinkSuffix
|
||||
} else {
|
||||
// The user must provide .rclonelink suffix
|
||||
if !strings.HasSuffix(name, fs.LinkSuffix) {
|
||||
fs.Errorf(nil, "Invalid name suffix provided: %v", name)
|
||||
return nil, translateError(vfs.EINVAL)
|
||||
}
|
||||
}
|
||||
|
||||
// Add target suffix when linking to a link
|
||||
if !strings.HasSuffix(target, fs.LinkSuffix) {
|
||||
vnode, err := n.lookupVfsNodeInDir(target)
|
||||
if err == 0 && strings.HasSuffix(vnode.Name(), fs.LinkSuffix) {
|
||||
target += fs.LinkSuffix
|
||||
}
|
||||
}
|
||||
|
||||
serr := n.node.VFS().Symlink(target, name)
|
||||
if serr != nil {
|
||||
return nil, translateError(serr)
|
||||
}
|
||||
|
||||
// Find the created node
|
||||
vfsNode, err := n.lookupVfsNodeInDir(path.Base(name))
|
||||
if err != 0 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
n.fsys.setEntryOut(vfsNode, out)
|
||||
newNode := newNode(n.fsys, vfsNode)
|
||||
fs.Debugf(nil, "attr=%#v", out.Attr)
|
||||
newInode := n.NewInode(ctx, newNode, fusefs.StableAttr{Mode: out.Attr.Mode})
|
||||
|
||||
return newInode, 0
|
||||
}
|
||||
|
||||
@@ -157,9 +157,6 @@ func NewMountCommand(commandName string, hidden bool, mount MountFn) *cobra.Comm
|
||||
Hidden: hidden,
|
||||
Short: `Mount the remote as file system on a mountpoint.`,
|
||||
Long: strings.ReplaceAll(strings.ReplaceAll(mountHelp, "|", "`"), "@", commandName) + vfs.Help,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.33",
|
||||
},
|
||||
Run: func(command *cobra.Command, args []string) {
|
||||
cmd.CheckArgs(2, 2, command, args)
|
||||
|
||||
|
||||
@@ -60,9 +60,6 @@ can speed transfers up greatly.
|
||||
|
||||
**Note**: Use the |-P|/|--progress| flag to view real-time transfer statistics.
|
||||
`, "|", "`"),
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.19",
|
||||
},
|
||||
Run: func(command *cobra.Command, args []string) {
|
||||
cmd.CheckArgs(2, 2, command, args)
|
||||
fsrc, srcFileName, fdst := cmd.NewFsSrcFileDst(args)
|
||||
|
||||
@@ -49,9 +49,6 @@ successful transfer.
|
||||
|
||||
**Note**: Use the ` + "`-P`" + `/` + "`--progress`" + ` flag to view real-time transfer statistics.
|
||||
`,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.35",
|
||||
},
|
||||
Run: func(command *cobra.Command, args []string) {
|
||||
cmd.CheckArgs(2, 2, command, args)
|
||||
fsrc, srcFileName, fdst, dstFileName := cmd.NewFsSrcDstFiles(args)
|
||||
|
||||
234
cmd/ncdu/ncdu.go
234
cmd/ncdu/ncdu.go
@@ -13,14 +13,13 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/atotto/clipboard"
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/gdamore/tcell/v2/termbox"
|
||||
runewidth "github.com/mattn/go-runewidth"
|
||||
"github.com/rclone/rclone/cmd"
|
||||
"github.com/rclone/rclone/cmd/ncdu/scan"
|
||||
"github.com/rclone/rclone/fs"
|
||||
"github.com/rclone/rclone/fs/fspath"
|
||||
"github.com/rclone/rclone/fs/operations"
|
||||
"github.com/rivo/uniseg"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -74,9 +73,6 @@ For a non-interactive listing of the remote, see the
|
||||
[tree](/commands/rclone_tree/) command. To just get the total size of
|
||||
the remote you can also use the [size](/commands/rclone_size/) command.
|
||||
`,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.37",
|
||||
},
|
||||
Run: func(command *cobra.Command, args []string) {
|
||||
cmd.CheckArgs(1, 1, command, args)
|
||||
fsrc := cmd.NewFsSrc(args)
|
||||
@@ -118,7 +114,6 @@ func helpText() (tr []string) {
|
||||
|
||||
// UI contains the state of the user interface
|
||||
type UI struct {
|
||||
s tcell.Screen
|
||||
f fs.Fs // fs being displayed
|
||||
fsName string // human name of Fs
|
||||
root *scan.Dir // root directory
|
||||
@@ -155,91 +150,77 @@ type dirPos struct {
|
||||
offset int
|
||||
}
|
||||
|
||||
// graphemeWidth returns the number of cells in rs.
|
||||
//
|
||||
// The original [runewidth.StringWidth] iterates through graphemes
|
||||
// and uses this same logic. To avoid iterating through graphemes
|
||||
// repeatedly, we separate that out into its own function.
|
||||
func graphemeWidth(rs []rune) (wd int) {
|
||||
// copied/adapted from [runewidth.StringWidth]
|
||||
for _, r := range rs {
|
||||
wd = runewidth.RuneWidth(r)
|
||||
if wd > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Print a string
|
||||
func (u *UI) Print(x, y int, style tcell.Style, msg string) {
|
||||
g := uniseg.NewGraphemes(msg)
|
||||
for g.Next() {
|
||||
rs := g.Runes()
|
||||
u.s.SetContent(x, y, rs[0], rs[1:], style)
|
||||
x += graphemeWidth(rs)
|
||||
func Print(x, y int, fg, bg termbox.Attribute, msg string) {
|
||||
for _, c := range msg {
|
||||
termbox.SetCell(x, y, c, fg, bg)
|
||||
x++
|
||||
}
|
||||
}
|
||||
|
||||
// Printf a string
|
||||
func (u *UI) Printf(x, y int, style tcell.Style, format string, args ...interface{}) {
|
||||
func Printf(x, y int, fg, bg termbox.Attribute, format string, args ...interface{}) {
|
||||
s := fmt.Sprintf(format, args...)
|
||||
u.Print(x, y, style, s)
|
||||
Print(x, y, fg, bg, s)
|
||||
}
|
||||
|
||||
// Line prints a string to given xmax, with given space
|
||||
func (u *UI) Line(x, y, xmax int, style tcell.Style, spacer rune, msg string) {
|
||||
g := uniseg.NewGraphemes(msg)
|
||||
for g.Next() {
|
||||
rs := g.Runes()
|
||||
u.s.SetContent(x, y, rs[0], rs[1:], style)
|
||||
x += graphemeWidth(rs)
|
||||
func Line(x, y, xmax int, fg, bg termbox.Attribute, spacer rune, msg string) {
|
||||
for _, c := range msg {
|
||||
termbox.SetCell(x, y, c, fg, bg)
|
||||
x += runewidth.RuneWidth(c)
|
||||
if x >= xmax {
|
||||
return
|
||||
}
|
||||
}
|
||||
for ; x < xmax; x++ {
|
||||
u.s.SetContent(x, y, spacer, nil, style)
|
||||
termbox.SetCell(x, y, spacer, fg, bg)
|
||||
}
|
||||
}
|
||||
|
||||
// Linef a string
|
||||
func (u *UI) Linef(x, y, xmax int, style tcell.Style, spacer rune, format string, args ...interface{}) {
|
||||
func Linef(x, y, xmax int, fg, bg termbox.Attribute, spacer rune, format string, args ...interface{}) {
|
||||
s := fmt.Sprintf(format, args...)
|
||||
u.Line(x, y, xmax, style, spacer, s)
|
||||
Line(x, y, xmax, fg, bg, spacer, s)
|
||||
}
|
||||
|
||||
// LineOptions Print line of selectable options
|
||||
func (u *UI) LineOptions(x, y, xmax int, style tcell.Style, options []string, selected int) {
|
||||
for x := x; x < xmax; x++ {
|
||||
u.s.SetContent(x, y, ' ', nil, style) // fill
|
||||
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)
|
||||
}
|
||||
x += ((xmax - x) - lineOptionLength(options)) / 2 // center
|
||||
for j := xmax - xoffset; j < xmax; j++ {
|
||||
termbox.SetCell(j, y, ' ', fg, bg)
|
||||
}
|
||||
x += xoffset
|
||||
|
||||
for i, o := range options {
|
||||
u.s.SetContent(x, y, ' ', nil, style)
|
||||
x++
|
||||
termbox.SetCell(x, y, ' ', fg, bg)
|
||||
|
||||
ostyle := style
|
||||
if i == selected {
|
||||
ostyle = tcell.StyleDefault
|
||||
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++
|
||||
}
|
||||
|
||||
u.s.SetContent(x, y, '<', nil, ostyle)
|
||||
x++
|
||||
termbox.SetCell(x, y, '>', fg, bg)
|
||||
bg = defaultBg
|
||||
fg = defaultFg
|
||||
|
||||
g := uniseg.NewGraphemes(o)
|
||||
for g.Next() {
|
||||
rs := g.Runes()
|
||||
u.s.SetContent(x, y, rs[0], rs[1:], ostyle)
|
||||
x += graphemeWidth(rs)
|
||||
}
|
||||
|
||||
u.s.SetContent(x, y, '>', nil, ostyle)
|
||||
x++
|
||||
|
||||
u.s.SetContent(x, y, ' ', nil, style)
|
||||
x++
|
||||
termbox.SetCell(x+1, y, ' ', fg, bg)
|
||||
x += 2
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,7 +234,7 @@ func lineOptionLength(o []string) int {
|
||||
|
||||
// Box the u.boxText onto the screen
|
||||
func (u *UI) Box() {
|
||||
w, h := u.s.Size()
|
||||
w, h := termbox.Size()
|
||||
|
||||
// Find dimensions of text
|
||||
boxWidth := 10
|
||||
@@ -279,31 +260,31 @@ func (u *UI) Box() {
|
||||
ymax := y + len(u.boxText)
|
||||
|
||||
// draw text
|
||||
style := tcell.StyleDefault.Background(tcell.ColorRed).Reverse(true)
|
||||
fg, bg := termbox.ColorRed, termbox.ColorWhite
|
||||
for i, s := range u.boxText {
|
||||
u.Line(x, y+i, xmax, style, ' ', s)
|
||||
style = tcell.StyleDefault.Reverse(true)
|
||||
Line(x, y+i, xmax, fg, bg, ' ', s)
|
||||
fg = termbox.ColorBlack
|
||||
}
|
||||
|
||||
if len(u.boxMenu) != 0 {
|
||||
u.LineOptions(x, ymax, xmax, style, u.boxMenu, u.boxMenuButton)
|
||||
ymax++
|
||||
LineOptions(x, ymax-1, xmax, fg, bg, u.boxMenu, u.boxMenuButton)
|
||||
}
|
||||
|
||||
// draw top border
|
||||
for i := y; i < ymax; i++ {
|
||||
u.s.SetContent(x-1, i, tcell.RuneVLine, nil, style)
|
||||
u.s.SetContent(xmax, i, tcell.RuneVLine, nil, style)
|
||||
termbox.SetCell(x-1, i, '│', fg, bg)
|
||||
termbox.SetCell(xmax, i, '│', fg, bg)
|
||||
}
|
||||
for j := x; j < xmax; j++ {
|
||||
u.s.SetContent(j, y-1, tcell.RuneHLine, nil, style)
|
||||
u.s.SetContent(j, ymax, tcell.RuneHLine, nil, style)
|
||||
termbox.SetCell(j, y-1, '─', fg, bg)
|
||||
termbox.SetCell(j, ymax, '─', fg, bg)
|
||||
}
|
||||
|
||||
u.s.SetContent(x-1, y-1, tcell.RuneULCorner, nil, style)
|
||||
u.s.SetContent(xmax, y-1, tcell.RuneURCorner, nil, style)
|
||||
u.s.SetContent(x-1, ymax, tcell.RuneLLCorner, nil, style)
|
||||
u.s.SetContent(xmax, ymax, tcell.RuneLRCorner, nil, style)
|
||||
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) {
|
||||
@@ -355,17 +336,17 @@ func (u *UI) hasEmptyDir() bool {
|
||||
// Draw the current screen
|
||||
func (u *UI) Draw() error {
|
||||
ctx := context.Background()
|
||||
w, h := u.s.Size()
|
||||
w, h := termbox.Size()
|
||||
u.dirListHeight = h - 3
|
||||
|
||||
// Plot
|
||||
u.s.Clear()
|
||||
termbox.Clear(termbox.ColorWhite, termbox.ColorBlack)
|
||||
|
||||
// Header line
|
||||
u.Linef(0, 0, w, tcell.StyleDefault.Reverse(true), ' ', "rclone ncdu %s - use the arrow keys to navigate, press ? for help", fs.Version)
|
||||
Linef(0, 0, w, termbox.ColorBlack, termbox.ColorWhite, ' ', "rclone ncdu %s - use the arrow keys to navigate, press ? for help", fs.Version)
|
||||
|
||||
// Directory line
|
||||
u.Linef(0, 1, w, tcell.StyleDefault, '-', "-- %s ", u.path)
|
||||
Linef(0, 1, w, termbox.ColorWhite, termbox.ColorBlack, '-', "-- %s ", u.path)
|
||||
|
||||
// graphs
|
||||
const (
|
||||
@@ -396,18 +377,20 @@ func (u *UI) Draw() error {
|
||||
attrs, err = u.d.AttrI(u.sortPerm[n])
|
||||
}
|
||||
_, isSelected := u.selectedEntries[entry.String()]
|
||||
style := tcell.StyleDefault
|
||||
fg := termbox.ColorWhite
|
||||
if attrs.EntriesHaveErrors {
|
||||
style = style.Foreground(tcell.ColorYellow)
|
||||
fg = termbox.ColorYellow
|
||||
}
|
||||
if err != nil {
|
||||
style = style.Foreground(tcell.ColorRed)
|
||||
fg = termbox.ColorRed
|
||||
}
|
||||
const colorLightYellow = termbox.ColorYellow + 8
|
||||
if isSelected {
|
||||
style = style.Foreground(tcell.ColorLightYellow)
|
||||
fg = colorLightYellow
|
||||
}
|
||||
bg := termbox.ColorBlack
|
||||
if n == dirPos.entry {
|
||||
style = style.Reverse(true)
|
||||
fg, bg = bg, fg
|
||||
}
|
||||
mark := ' '
|
||||
if attrs.IsDir {
|
||||
@@ -466,30 +449,31 @@ func (u *UI) Draw() error {
|
||||
}
|
||||
extras += "[" + graph[graphBars-bars:2*graphBars-bars] + "] "
|
||||
}
|
||||
u.Linef(0, y, w, style, ' ', "%c %s %s%c%s%s",
|
||||
fileFlag, operations.SizeStringField(attrs.Size, u.humanReadable, 12), extras, mark, path.Base(entry.Remote()), message)
|
||||
Linef(0, y, w, fg, bg, ' ', "%c %s %s%c%s%s", fileFlag, operations.SizeStringField(attrs.Size, u.humanReadable, 12), extras, mark, path.Base(entry.Remote()), message)
|
||||
y++
|
||||
}
|
||||
}
|
||||
|
||||
// Footer
|
||||
if u.d == nil {
|
||||
u.Line(0, h-1, w, tcell.StyleDefault.Reverse(true), ' ', "Waiting for root directory...")
|
||||
Line(0, h-1, w, termbox.ColorBlack, termbox.ColorWhite, ' ', "Waiting for root directory...")
|
||||
} else {
|
||||
message := ""
|
||||
if u.listing {
|
||||
message = " [listing in progress]"
|
||||
}
|
||||
size, count := u.d.Attr()
|
||||
u.Linef(0, h-1, w, tcell.StyleDefault.Reverse(true), ' ', "Total usage: %s, Objects: %s%s",
|
||||
operations.SizeString(size, u.humanReadable), operations.CountString(count, u.humanReadable), message)
|
||||
Linef(0, h-1, w, termbox.ColorBlack, termbox.ColorWhite, ' ', "Total usage: %s, Objects: %s%s", operations.SizeString(size, u.humanReadable), operations.CountString(count, u.humanReadable), message)
|
||||
}
|
||||
|
||||
// Show the box on top if required
|
||||
if u.showBox {
|
||||
u.Box()
|
||||
}
|
||||
u.s.Show()
|
||||
err := termbox.Flush()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to flush screen: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -902,34 +886,37 @@ func NewUI(f fs.Fs) *UI {
|
||||
|
||||
// Show shows the user interface
|
||||
func (u *UI) Show() error {
|
||||
var err error
|
||||
u.s, err = tcell.NewScreen()
|
||||
err := termbox.Init()
|
||||
if err != nil {
|
||||
return fmt.Errorf("screen new: %w", err)
|
||||
return fmt.Errorf("termbox init: %w", err)
|
||||
}
|
||||
err = u.s.Init()
|
||||
if err != nil {
|
||||
return fmt.Errorf("screen init: %w", err)
|
||||
}
|
||||
defer u.s.Fini()
|
||||
defer termbox.Close()
|
||||
|
||||
// scan the disk in the background
|
||||
u.listing = true
|
||||
rootChan, errChan, updated := scan.Scan(context.Background(), u.f)
|
||||
|
||||
// Poll the events into a channel
|
||||
events := make(chan tcell.Event)
|
||||
go u.s.ChannelEvents(events, nil)
|
||||
events := make(chan termbox.Event)
|
||||
doneWithEvent := make(chan bool)
|
||||
go func() {
|
||||
for {
|
||||
events <- termbox.PollEvent()
|
||||
<-doneWithEvent
|
||||
}
|
||||
}()
|
||||
|
||||
// Main loop, waiting for events and channels
|
||||
outer:
|
||||
for {
|
||||
//Reset()
|
||||
err := u.Draw()
|
||||
if err != nil {
|
||||
return fmt.Errorf("draw failed: %w", err)
|
||||
}
|
||||
var root *scan.Dir
|
||||
select {
|
||||
case root := <-rootChan:
|
||||
case root = <-rootChan:
|
||||
u.root = root
|
||||
u.setCurrentDir(root)
|
||||
case err := <-errChan:
|
||||
@@ -939,50 +926,39 @@ outer:
|
||||
u.listing = false
|
||||
case <-updated:
|
||||
// redraw
|
||||
// TODO: might want to limit updates per second
|
||||
// might want to limit updates per second
|
||||
u.sortCurrentDir()
|
||||
case ev := <-events:
|
||||
switch ev := ev.(type) {
|
||||
case *tcell.EventResize:
|
||||
if u.root != nil {
|
||||
u.sortCurrentDir() // redraw
|
||||
}
|
||||
u.s.Sync()
|
||||
case *tcell.EventKey:
|
||||
var c rune
|
||||
if k := ev.Key(); k == tcell.KeyRune {
|
||||
c = ev.Rune()
|
||||
} else {
|
||||
c = key(k)
|
||||
}
|
||||
switch c {
|
||||
case key(tcell.KeyEsc), key(tcell.KeyCtrlC), 'q':
|
||||
doneWithEvent <- true
|
||||
if ev.Type == termbox.EventKey {
|
||||
switch ev.Key + termbox.Key(ev.Ch) {
|
||||
case termbox.KeyEsc, termbox.KeyCtrlC, 'q':
|
||||
if u.showBox {
|
||||
u.showBox = false
|
||||
} else {
|
||||
break outer
|
||||
}
|
||||
case key(tcell.KeyDown), 'j':
|
||||
case termbox.KeyArrowDown, 'j':
|
||||
u.move(1)
|
||||
case key(tcell.KeyUp), 'k':
|
||||
case termbox.KeyArrowUp, 'k':
|
||||
u.move(-1)
|
||||
case key(tcell.KeyPgDn), '-', '_':
|
||||
case termbox.KeyPgdn, '-', '_':
|
||||
u.move(u.dirListHeight)
|
||||
case key(tcell.KeyPgUp), '=', '+':
|
||||
case termbox.KeyPgup, '=', '+':
|
||||
u.move(-u.dirListHeight)
|
||||
case key(tcell.KeyLeft), 'h':
|
||||
case termbox.KeyArrowLeft, 'h':
|
||||
if u.showBox {
|
||||
u.moveBox(-1)
|
||||
break
|
||||
}
|
||||
u.up()
|
||||
case key(tcell.KeyEnter):
|
||||
case termbox.KeyEnter:
|
||||
if len(u.boxMenu) > 0 {
|
||||
u.handleBoxOption()
|
||||
break
|
||||
}
|
||||
u.enter()
|
||||
case key(tcell.KeyRight), 'l':
|
||||
case termbox.KeyArrowRight, 'l':
|
||||
if u.showBox {
|
||||
u.moveBox(1)
|
||||
break
|
||||
@@ -1025,8 +1001,11 @@ outer:
|
||||
|
||||
// Refresh the screen. Not obvious what key to map
|
||||
// this onto, but ^L is a common choice.
|
||||
case key(tcell.KeyCtrlL):
|
||||
u.s.Sync()
|
||||
case termbox.KeyCtrlL:
|
||||
err := termbox.Sync()
|
||||
if err != nil {
|
||||
fs.Errorf(nil, "termbox sync returned error: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1034,8 +1013,3 @@ outer:
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// key returns a rune representing the key k. It is a negative value, to not collide with Unicode code-points.
|
||||
func key(k tcell.Key) rune {
|
||||
return rune(-k)
|
||||
}
|
||||
|
||||
@@ -42,9 +42,6 @@ obfuscating the hyphen itself.
|
||||
If you want to encrypt the config file then please use config file
|
||||
encryption - see [rclone config](/commands/rclone_config/) for more
|
||||
info.`,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.36",
|
||||
},
|
||||
RunE: func(command *cobra.Command, args []string) error {
|
||||
cmd.CheckArgs(1, 1, command, args)
|
||||
var password string
|
||||
|
||||
12
cmd/rc/rc.go
12
cmd/rc/rc.go
@@ -99,9 +99,6 @@ rclone rc server, e.g.:
|
||||
rclone rc --loopback operations/about fs=/
|
||||
|
||||
Use ` + "`rclone rc`" + ` to see a list of all possible commands.`,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.40",
|
||||
},
|
||||
Run: func(command *cobra.Command, args []string) {
|
||||
cmd.CheckArgs(0, 1e9, command, args)
|
||||
cmd.Run(false, false, command, func() error {
|
||||
@@ -156,15 +153,6 @@ func ParseOptions(options []string) (opt map[string]string) {
|
||||
func setAlternateFlag(flagName string, output *string) {
|
||||
if rcFlag := pflag.Lookup(flagName); rcFlag != nil && rcFlag.Changed {
|
||||
*output = rcFlag.Value.String()
|
||||
if sliceValue, ok := rcFlag.Value.(pflag.SliceValue); ok {
|
||||
stringSlice := sliceValue.GetSlice()
|
||||
for _, value := range stringSlice {
|
||||
if value != "" {
|
||||
*output = value
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,9 +56,6 @@ Note that the upload can also not be retried because the data is
|
||||
not kept around until the upload succeeds. If you need to transfer
|
||||
a lot of data, you're better off caching locally and then
|
||||
` + "`rclone move`" + ` it to the destination.`,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.38",
|
||||
},
|
||||
Run: func(command *cobra.Command, args []string) {
|
||||
cmd.CheckArgs(1, 1, command, args)
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"github.com/rclone/rclone/fs/rc/rcflags"
|
||||
"github.com/rclone/rclone/fs/rc/rcserver"
|
||||
"github.com/rclone/rclone/lib/atexit"
|
||||
libhttp "github.com/rclone/rclone/lib/http"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -32,10 +31,7 @@ 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.
|
||||
` + libhttp.Help + libhttp.TemplateHelp + libhttp.AuthHelp,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.45",
|
||||
},
|
||||
`,
|
||||
Run: func(command *cobra.Command, args []string) {
|
||||
cmd.CheckArgs(0, 1, command, args)
|
||||
if rcflags.Opt.Enabled {
|
||||
|
||||
@@ -16,9 +16,6 @@ func init() {
|
||||
var commandDefinition = &cobra.Command{
|
||||
Use: "reveal password",
|
||||
Short: `Reveal obscured password from rclone.conf`,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.43",
|
||||
},
|
||||
Run: func(command *cobra.Command, args []string) {
|
||||
cmd.CheckArgs(1, 1, command, args)
|
||||
cmd.Run(false, false, command, func() error {
|
||||
|
||||
@@ -38,9 +38,6 @@ used with option ` + "`--rmdirs`" + `).
|
||||
To delete a path and any objects in it, use [purge](/commands/rclone_purge/)
|
||||
command.
|
||||
`,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.35",
|
||||
},
|
||||
Run: func(command *cobra.Command, args []string) {
|
||||
cmd.CheckArgs(1, 1, command, args)
|
||||
fdst := cmd.NewFsDir(args)
|
||||
|
||||
@@ -64,9 +64,6 @@ var cmdSelfUpdate = &cobra.Command{
|
||||
Aliases: []string{"self-update"},
|
||||
Short: `Update the rclone binary.`,
|
||||
Long: strings.ReplaceAll(selfUpdateHelp, "|", "`"),
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.55",
|
||||
},
|
||||
Run: func(command *cobra.Command, args []string) {
|
||||
cmd.CheckArgs(0, 0, command, args)
|
||||
if Opt.Package == "" {
|
||||
|
||||
@@ -15,6 +15,8 @@ import (
|
||||
|
||||
"github.com/rclone/rclone/fs"
|
||||
"github.com/rclone/rclone/fstest/testy"
|
||||
"github.com/rclone/rclone/lib/file"
|
||||
"github.com/rclone/rclone/lib/random"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -44,6 +46,20 @@ func TestGetVersion(t *testing.T) {
|
||||
assert.Equal(t, "v1.52.3", resultVer)
|
||||
}
|
||||
|
||||
func makeTestDir() (testDir string, err error) {
|
||||
const maxAttempts = 5
|
||||
testDirBase := filepath.Join(os.TempDir(), "rclone-test-selfupdate.")
|
||||
|
||||
for attempt := 0; attempt < maxAttempts; attempt++ {
|
||||
testDir = testDirBase + random.String(4)
|
||||
err = file.MkdirAll(testDir, os.ModePerm)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func TestInstallOnLinux(t *testing.T) {
|
||||
testy.SkipUnreliable(t)
|
||||
if runtime.GOOS != "linux" {
|
||||
@@ -52,8 +68,13 @@ func TestInstallOnLinux(t *testing.T) {
|
||||
|
||||
// Prepare for test
|
||||
ctx := context.Background()
|
||||
testDir := t.TempDir()
|
||||
testDir, err := makeTestDir()
|
||||
assert.NoError(t, err)
|
||||
path := filepath.Join(testDir, "rclone")
|
||||
defer func() {
|
||||
_ = os.Chmod(path, 0644)
|
||||
_ = os.RemoveAll(testDir)
|
||||
}()
|
||||
|
||||
regexVer := regexp.MustCompile(`v[0-9]\S+`)
|
||||
|
||||
@@ -66,9 +87,6 @@ func TestInstallOnLinux(t *testing.T) {
|
||||
// Must fail on non-writable file
|
||||
assert.NoError(t, os.WriteFile(path, []byte("test"), 0644))
|
||||
assert.NoError(t, os.Chmod(path, 0000))
|
||||
defer func() {
|
||||
_ = os.Chmod(path, 0644)
|
||||
}()
|
||||
err = (InstallUpdate(ctx, &Options{Beta: true, Output: path}))
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "run self-update as root")
|
||||
@@ -104,7 +122,11 @@ func TestRenameOnWindows(t *testing.T) {
|
||||
// Prepare for test
|
||||
ctx := context.Background()
|
||||
|
||||
testDir := t.TempDir()
|
||||
testDir, err := makeTestDir()
|
||||
assert.NoError(t, err)
|
||||
defer func() {
|
||||
_ = os.RemoveAll(testDir)
|
||||
}()
|
||||
|
||||
path := filepath.Join(testDir, "rclone.exe")
|
||||
regexVer := regexp.MustCompile(`v[0-9]\S+`)
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
This directory contains code derived from https://github.com/anacrolix/dms
|
||||
which is under the following license.
|
||||
|
||||
Copyright (c) 2012, Matt Joiner <anacrolix@gmail.com>.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the <organization> nor the
|
||||
names of its contributors may be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
|
||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
"github.com/anacrolix/dms/soap"
|
||||
"github.com/anacrolix/dms/ssdp"
|
||||
"github.com/anacrolix/dms/upnp"
|
||||
"github.com/anacrolix/log"
|
||||
"github.com/rclone/rclone/cmd"
|
||||
"github.com/rclone/rclone/cmd/serve/dlna/data"
|
||||
"github.com/rclone/rclone/cmd/serve/dlna/dlnaflags"
|
||||
@@ -48,9 +47,6 @@ media transcoding support. This means that some players might show
|
||||
files that they are not able to play back correctly.
|
||||
|
||||
` + dlnaflags.Help + vfs.Help,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.46",
|
||||
},
|
||||
Run: func(command *cobra.Command, args []string) {
|
||||
cmd.CheckArgs(1, 1, command, args)
|
||||
f := cmd.NewFsSrc(args)
|
||||
@@ -405,7 +401,6 @@ func (s *server) ssdpInterface(intf net.Interface) {
|
||||
Server: serverField,
|
||||
UUID: s.RootDeviceUUID,
|
||||
NotifyInterval: s.AnnounceInterval,
|
||||
Logger: log.Default,
|
||||
}
|
||||
|
||||
// An interface with these flags should be valid for SSDP.
|
||||
|
||||
@@ -48,9 +48,7 @@ var Command = &cobra.Command{
|
||||
Use: "docker",
|
||||
Short: `Serve any remote on docker's volume plugin API.`,
|
||||
Long: strings.ReplaceAll(longHelp, "|", "`") + vfs.Help,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.56",
|
||||
},
|
||||
|
||||
Run: func(command *cobra.Command, args []string) {
|
||||
cmd.CheckArgs(0, 0, command, args)
|
||||
cmd.Run(false, false, command, func() error {
|
||||
|
||||
@@ -99,9 +99,6 @@ By default this will serve files without needing a login.
|
||||
|
||||
You can set a single username and password with the --user and --pass flags.
|
||||
` + vfs.Help + proxy.Help,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.44",
|
||||
},
|
||||
Run: func(command *cobra.Command, args []string) {
|
||||
var f fs.Fs
|
||||
if proxyflags.Opt.AuthProxy == "" {
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
@@ -13,11 +12,14 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/rclone/rclone/cmd"
|
||||
"github.com/rclone/rclone/cmd/serve/http/data"
|
||||
"github.com/rclone/rclone/fs"
|
||||
"github.com/rclone/rclone/fs/accounting"
|
||||
libhttp "github.com/rclone/rclone/lib/http"
|
||||
httplib "github.com/rclone/rclone/lib/http"
|
||||
"github.com/rclone/rclone/lib/http/auth"
|
||||
"github.com/rclone/rclone/lib/http/serve"
|
||||
"github.com/rclone/rclone/vfs"
|
||||
"github.com/rclone/rclone/vfs/vfsflags"
|
||||
@@ -26,27 +28,20 @@ import (
|
||||
|
||||
// Options required for http server
|
||||
type Options struct {
|
||||
Auth libhttp.AuthConfig
|
||||
HTTP libhttp.Config
|
||||
Template libhttp.TemplateConfig
|
||||
data.Options
|
||||
}
|
||||
|
||||
// DefaultOpt is the default values used for Options
|
||||
var DefaultOpt = Options{
|
||||
Auth: libhttp.DefaultAuthCfg(),
|
||||
HTTP: libhttp.DefaultCfg(),
|
||||
Template: libhttp.DefaultTemplateCfg(),
|
||||
}
|
||||
var DefaultOpt = Options{}
|
||||
|
||||
// Opt is options set by command line flags
|
||||
var Opt = DefaultOpt
|
||||
|
||||
func init() {
|
||||
flagSet := Command.Flags()
|
||||
libhttp.AddAuthFlagsPrefix(flagSet, "", &Opt.Auth)
|
||||
libhttp.AddHTTPFlagsPrefix(flagSet, "", &Opt.HTTP)
|
||||
libhttp.AddTemplateFlagsPrefix(flagSet, "", &Opt.Template)
|
||||
vfsflags.AddFlags(flagSet)
|
||||
data.AddFlags(Command.Flags(), "", &Opt.Options)
|
||||
httplib.AddFlags(Command.Flags())
|
||||
auth.AddFlags(Command.Flags())
|
||||
vfsflags.AddFlags(Command.Flags())
|
||||
}
|
||||
|
||||
// Command definition for cobra
|
||||
@@ -64,67 +59,57 @@ The server will log errors. Use ` + "`-v`" + ` to see access logs.
|
||||
|
||||
` + "`--bwlimit`" + ` will be respected for file transfers. Use ` + "`--stats`" + ` to
|
||||
control the stats printing.
|
||||
` + libhttp.Help + libhttp.TemplateHelp + libhttp.AuthHelp + vfs.Help,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.39",
|
||||
},
|
||||
` + httplib.Help + data.Help + auth.Help + vfs.Help,
|
||||
Run: func(command *cobra.Command, args []string) {
|
||||
cmd.CheckArgs(1, 1, command, args)
|
||||
f := cmd.NewFsSrc(args)
|
||||
|
||||
cmd.Run(false, true, command, func() error {
|
||||
ctx := context.Background()
|
||||
|
||||
s, err := run(ctx, f, Opt)
|
||||
s := newServer(f, Opt.Template)
|
||||
router, err := httplib.Router()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return err
|
||||
}
|
||||
|
||||
s.server.Wait()
|
||||
s.Bind(router)
|
||||
httplib.Wait()
|
||||
return nil
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
// server contains everything to run the server
|
||||
type serveCmd struct {
|
||||
f fs.Fs
|
||||
vfs *vfs.VFS
|
||||
server *libhttp.Server
|
||||
type server struct {
|
||||
f fs.Fs
|
||||
vfs *vfs.VFS
|
||||
HTMLTemplate *template.Template // HTML template for web interface
|
||||
}
|
||||
|
||||
func run(ctx context.Context, f fs.Fs, opt Options) (*serveCmd, error) {
|
||||
var err error
|
||||
|
||||
s := &serveCmd{
|
||||
f: f,
|
||||
vfs: vfs.New(f, &vfsflags.Opt),
|
||||
func newServer(f fs.Fs, templatePath string) *server {
|
||||
htmlTemplate, templateErr := data.GetTemplate(templatePath)
|
||||
if templateErr != nil {
|
||||
log.Fatalf(templateErr.Error())
|
||||
}
|
||||
|
||||
s.server, err = libhttp.NewServer(ctx,
|
||||
libhttp.WithConfig(opt.HTTP),
|
||||
libhttp.WithAuth(opt.Auth),
|
||||
libhttp.WithTemplate(opt.Template),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to init server: %w", err)
|
||||
s := &server{
|
||||
f: f,
|
||||
vfs: vfs.New(f, &vfsflags.Opt),
|
||||
HTMLTemplate: htmlTemplate,
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
router := s.server.Router()
|
||||
func (s *server) Bind(router chi.Router) {
|
||||
if m := auth.Auth(auth.Opt); m != nil {
|
||||
router.Use(m)
|
||||
}
|
||||
router.Use(
|
||||
middleware.SetHeader("Accept-Ranges", "bytes"),
|
||||
middleware.SetHeader("Server", "rclone/"+fs.Version),
|
||||
)
|
||||
router.Get("/*", s.handler)
|
||||
router.Head("/*", s.handler)
|
||||
|
||||
s.server.Serve()
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// handler reads incoming requests and dispatches them
|
||||
func (s *serveCmd) handler(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *server) handler(w http.ResponseWriter, r *http.Request) {
|
||||
isDir := strings.HasSuffix(r.URL.Path, "/")
|
||||
remote := strings.Trim(r.URL.Path, "/")
|
||||
if isDir {
|
||||
@@ -135,7 +120,7 @@ func (s *serveCmd) handler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// serveDir serves a directory index at dirRemote
|
||||
func (s *serveCmd) serveDir(w http.ResponseWriter, r *http.Request, dirRemote string) {
|
||||
func (s *server) serveDir(w http.ResponseWriter, r *http.Request, dirRemote string) {
|
||||
// List the directory
|
||||
node, err := s.vfs.Stat(dirRemote)
|
||||
if err == vfs.ENOENT {
|
||||
@@ -157,7 +142,7 @@ func (s *serveCmd) serveDir(w http.ResponseWriter, r *http.Request, dirRemote st
|
||||
}
|
||||
|
||||
// Make the entries for display
|
||||
directory := serve.NewDirectory(dirRemote, s.server.HTMLTemplate())
|
||||
directory := serve.NewDirectory(dirRemote, s.HTMLTemplate)
|
||||
for _, node := range dirEntries {
|
||||
if vfsflags.Opt.NoModTime {
|
||||
directory.AddHTMLEntry(node.Path(), node.IsDir(), node.Size(), time.Time{})
|
||||
@@ -177,7 +162,7 @@ func (s *serveCmd) serveDir(w http.ResponseWriter, r *http.Request, dirRemote st
|
||||
}
|
||||
|
||||
// serveFile serves a file object at remote
|
||||
func (s *serveCmd) serveFile(w http.ResponseWriter, r *http.Request, remote string) {
|
||||
func (s *server) serveFile(w http.ResponseWriter, r *http.Request, remote string) {
|
||||
node, err := s.vfs.Stat(remote)
|
||||
if err == vfs.ENOENT {
|
||||
fs.Infof(remote, "%s: File not found", r.RemoteAddr)
|
||||
|
||||
@@ -14,14 +14,14 @@ import (
|
||||
"github.com/rclone/rclone/fs"
|
||||
"github.com/rclone/rclone/fs/config/configfile"
|
||||
"github.com/rclone/rclone/fs/filter"
|
||||
libhttp "github.com/rclone/rclone/lib/http"
|
||||
httplib "github.com/rclone/rclone/lib/http"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var (
|
||||
updateGolden = flag.Bool("updategolden", false, "update golden files for regression test")
|
||||
sc *serveCmd
|
||||
httpServer *server
|
||||
testURL string
|
||||
)
|
||||
|
||||
@@ -30,25 +30,16 @@ const (
|
||||
testTemplate = "testdata/golden/testindex.html"
|
||||
)
|
||||
|
||||
func start(t *testing.T, f fs.Fs) {
|
||||
ctx := context.Background()
|
||||
|
||||
opts := Options{
|
||||
HTTP: libhttp.DefaultCfg(),
|
||||
Template: libhttp.TemplateConfig{
|
||||
Path: testTemplate,
|
||||
},
|
||||
func startServer(t *testing.T, f fs.Fs) {
|
||||
opt := httplib.DefaultOpt
|
||||
opt.ListenAddr = testBindAddress
|
||||
httpServer = newServer(f, testTemplate)
|
||||
router, err := httplib.Router()
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
opts.HTTP.ListenAddr = []string{testBindAddress}
|
||||
|
||||
s, err := run(ctx, f, opts)
|
||||
require.NoError(t, err, "failed to start server")
|
||||
sc = s
|
||||
|
||||
urls := s.server.URLs()
|
||||
require.Len(t, urls, 1, "expected one URL")
|
||||
|
||||
testURL = urls[0]
|
||||
httpServer.Bind(router)
|
||||
testURL = httplib.URL()
|
||||
|
||||
// try to connect to the test server
|
||||
pause := time.Millisecond
|
||||
@@ -63,6 +54,7 @@ func start(t *testing.T, f fs.Fs) {
|
||||
pause *= 2
|
||||
}
|
||||
t.Fatal("couldn't connect to server")
|
||||
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -92,7 +84,7 @@ func TestInit(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, obj.SetModTime(context.Background(), expectedTime))
|
||||
|
||||
start(t, f)
|
||||
startServer(t, f)
|
||||
}
|
||||
|
||||
// check body against the file, or re-write body if -updategolden is
|
||||
@@ -237,3 +229,7 @@ func TestGET(t *testing.T) {
|
||||
checkGolden(t, test.Golden, body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFinalise(t *testing.T) {
|
||||
_ = httplib.Shutdown()
|
||||
}
|
||||
|
||||
39
cmd/serve/httplib/httpflags/httpflags.go
Normal file
39
cmd/serve/httplib/httpflags/httpflags.go
Normal file
@@ -0,0 +1,39 @@
|
||||
// Package httpflags provides utility functionality to HTTP.
|
||||
package httpflags
|
||||
|
||||
import (
|
||||
"github.com/rclone/rclone/cmd/serve/httplib"
|
||||
"github.com/rclone/rclone/fs/config/flags"
|
||||
"github.com/rclone/rclone/fs/rc"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
// Options set by command line flags
|
||||
var (
|
||||
Opt = httplib.DefaultOpt
|
||||
)
|
||||
|
||||
// AddFlagsPrefix adds flags for the httplib
|
||||
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.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.IntVarP(flagSet, &Opt.MaxHeaderBytes, prefix+"max-header-bytes", "", Opt.MaxHeaderBytes, "Maximum size of request header")
|
||||
flags.StringVarP(flagSet, &Opt.SslCert, prefix+"cert", "", Opt.SslCert, "SSL PEM key (concatenation of certificate and CA certificate)")
|
||||
flags.StringVarP(flagSet, &Opt.SslKey, prefix+"key", "", Opt.SslKey, "SSL PEM Private key")
|
||||
flags.StringVarP(flagSet, &Opt.ClientCA, prefix+"client-ca", "", Opt.ClientCA, "Client certificate authority to verify clients with")
|
||||
flags.StringVarP(flagSet, &Opt.HtPasswd, prefix+"htpasswd", "", Opt.HtPasswd, "htpasswd file - if not provided no authentication is done")
|
||||
flags.StringVarP(flagSet, &Opt.Realm, prefix+"realm", "", Opt.Realm, "Realm for authentication")
|
||||
flags.StringVarP(flagSet, &Opt.BasicUser, prefix+"user", "", Opt.BasicUser, "User name for authentication")
|
||||
flags.StringVarP(flagSet, &Opt.BasicPass, prefix+"pass", "", Opt.BasicPass, "Password for authentication")
|
||||
flags.StringVarP(flagSet, &Opt.BaseURL, prefix+"baseurl", "", Opt.BaseURL, "Prefix for URLs - leave blank for root")
|
||||
flags.StringVarP(flagSet, &Opt.Template, prefix+"template", "", Opt.Template, "User-specified template")
|
||||
flags.StringVarP(flagSet, &Opt.MinTLSVersion, prefix+"min-tls-version", "", Opt.MinTLSVersion, "Minimum TLS version that is acceptable")
|
||||
|
||||
}
|
||||
|
||||
// AddFlags adds flags for the httplib
|
||||
func AddFlags(flagSet *pflag.FlagSet) {
|
||||
AddFlagsPrefix(flagSet, "", &Opt)
|
||||
}
|
||||
438
cmd/serve/httplib/httplib.go
Normal file
438
cmd/serve/httplib/httplib.go
Normal file
@@ -0,0 +1,438 @@
|
||||
// Package httplib provides common functionality for http servers
|
||||
//
|
||||
// Deprecated: httplib has been replaced with lib/http
|
||||
package httplib
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
auth "github.com/abbot/go-http-auth"
|
||||
"github.com/rclone/rclone/cmd/serve/http/data"
|
||||
"github.com/rclone/rclone/fs"
|
||||
)
|
||||
|
||||
// Globals
|
||||
var ()
|
||||
|
||||
// Help contains text describing the http server to add to the command
|
||||
// help.
|
||||
var Help = `
|
||||
### Server options
|
||||
|
||||
Use ` + "`--addr`" + ` to specify which IP address and port the server should
|
||||
listen on, e.g. ` + "`--addr 1.2.3.4:8000` or `--addr :8080`" + ` to
|
||||
listen to all IPs. By default it only listens on localhost. You can use port
|
||||
:0 to let the OS choose an available port.
|
||||
|
||||
If you set ` + "`--addr`" + ` to listen on a public or LAN accessible IP address
|
||||
then using Authentication is advised - see the next section for info.
|
||||
|
||||
` + "`--server-read-timeout` and `--server-write-timeout`" + ` can be used to
|
||||
control the timeouts on the server. Note that this is the total time
|
||||
for a transfer.
|
||||
|
||||
` + "`--max-header-bytes`" + ` controls the maximum number of bytes the server will
|
||||
accept in the HTTP header.
|
||||
|
||||
` + "`--baseurl`" + ` controls the URL prefix that rclone serves from. By default
|
||||
rclone will serve from the root. If you used ` + "`--baseurl \"/rclone\"`" + ` then
|
||||
rclone would serve from a URL starting with "/rclone/". This is
|
||||
useful if you wish to proxy rclone serve. Rclone automatically
|
||||
inserts leading and trailing "/" on ` + "`--baseurl`" + `, so ` + "`--baseurl \"rclone\"`" + `,
|
||||
` + "`--baseurl \"/rclone\"` and `--baseurl \"/rclone/\"`" + ` are all treated
|
||||
identically.
|
||||
|
||||
` + "`--template`" + ` allows a user to specify a custom markup template for HTTP
|
||||
and WebDAV serve functions. The server exports the following markup
|
||||
to be used within the template to server pages:
|
||||
|
||||
| Parameter | Description |
|
||||
| :---------- | :---------- |
|
||||
| .Name | The full path of a file/directory. |
|
||||
| .Title | Directory listing of .Name |
|
||||
| .Sort | The current sort used. This is changeable via ?sort= parameter |
|
||||
| | Sort Options: namedirfirst,name,size,time (default namedirfirst) |
|
||||
| .Order | The current ordering used. This is changeable via ?order= parameter |
|
||||
| | Order Options: asc,desc (default asc) |
|
||||
| .Query | Currently unused. |
|
||||
| .Breadcrumb | Allows for creating a relative navigation |
|
||||
|-- .Link | The relative to the root link of the Text. |
|
||||
|-- .Text | The Name of the directory. |
|
||||
| .Entries | Information about a specific file/directory. |
|
||||
|-- .URL | The 'url' of an entry. |
|
||||
|-- .Leaf | Currently same as 'URL' but intended to be 'just' the name. |
|
||||
|-- .IsDir | Boolean for if an entry is a directory or not. |
|
||||
|-- .Size | Size in Bytes of the entry. |
|
||||
|-- .ModTime | The UTC timestamp of an entry. |
|
||||
|
||||
#### Authentication
|
||||
|
||||
By default this will serve files without needing a login.
|
||||
|
||||
You can either use an htpasswd file which can take lots of users, or
|
||||
set a single username and password with the ` + "`--user` and `--pass`" + ` flags.
|
||||
|
||||
Use ` + "`--htpasswd /path/to/htpasswd`" + ` to provide an htpasswd file. This is
|
||||
in standard apache format and supports MD5, SHA1 and BCrypt for basic
|
||||
authentication. Bcrypt is recommended.
|
||||
|
||||
To create an htpasswd file:
|
||||
|
||||
touch htpasswd
|
||||
htpasswd -B htpasswd user
|
||||
htpasswd -B htpasswd anotherUser
|
||||
|
||||
The password file can be updated while rclone is running.
|
||||
|
||||
Use ` + "`--realm`" + ` to set the authentication realm.
|
||||
|
||||
#### SSL/TLS
|
||||
|
||||
By default this will serve over HTTP. If you want you can serve over
|
||||
HTTPS. You will need to supply the ` + "`--cert` and `--key`" + ` flags.
|
||||
If you wish to do client side certificate validation then you will need to
|
||||
supply ` + "`--client-ca`" + ` also.
|
||||
|
||||
` + "`--cert`" + ` should be either a PEM encoded certificate or a concatenation
|
||||
of that with the CA certificate. ` + "`--key`" + ` should be the PEM encoded
|
||||
private key and ` + "`--client-ca`" + ` should be the PEM encoded client
|
||||
certificate authority certificate.
|
||||
|
||||
--min-tls-version is minimum TLS version that is acceptable. Valid
|
||||
values are "tls1.0", "tls1.1", "tls1.2" and "tls1.3" (default
|
||||
"tls1.0").
|
||||
`
|
||||
|
||||
// Options contains options for the http Server
|
||||
type Options struct {
|
||||
ListenAddr string // Port to listen on
|
||||
BaseURL string // prefix to strip from URLs
|
||||
ServerReadTimeout time.Duration // Timeout for server reading data
|
||||
ServerWriteTimeout time.Duration // Timeout for server writing data
|
||||
MaxHeaderBytes int // Maximum size of request header
|
||||
SslCert string // SSL PEM key (concatenation of certificate and CA certificate)
|
||||
SslKey string // SSL PEM Private key
|
||||
ClientCA string // Client certificate authority to verify clients with
|
||||
HtPasswd string // htpasswd file - if not provided no authentication is done
|
||||
Realm string // realm for authentication
|
||||
BasicUser string // single username for basic auth if not using Htpasswd
|
||||
BasicPass string // password for BasicUser
|
||||
Auth AuthFn `json:"-"` // custom Auth (not set by command line flags)
|
||||
Template string // User specified template
|
||||
MinTLSVersion string // MinTLSVersion contains the minimum TLS version that is acceptable
|
||||
}
|
||||
|
||||
// AuthFn if used will be used to authenticate user, pass. If an error
|
||||
// is returned then the user is not authenticated.
|
||||
//
|
||||
// If a non nil value is returned then it is added to the context under the key
|
||||
type AuthFn func(user, pass string) (value interface{}, err error)
|
||||
|
||||
// DefaultOpt is the default values used for Options
|
||||
var DefaultOpt = Options{
|
||||
ListenAddr: "localhost:8080",
|
||||
Realm: "rclone",
|
||||
ServerReadTimeout: 1 * time.Hour,
|
||||
ServerWriteTimeout: 1 * time.Hour,
|
||||
MaxHeaderBytes: 4096,
|
||||
MinTLSVersion: "tls1.0",
|
||||
}
|
||||
|
||||
// Server contains info about the running http server
|
||||
type Server struct {
|
||||
Opt Options
|
||||
handler http.Handler // original handler
|
||||
listener net.Listener
|
||||
waitChan chan struct{} // for waiting on the listener to close
|
||||
httpServer *http.Server
|
||||
basicPassHashed string
|
||||
useSSL bool // if server is configured for SSL/TLS
|
||||
usingAuth bool // set if authentication is configured
|
||||
HTMLTemplate *template.Template // HTML template for web interface
|
||||
}
|
||||
|
||||
type contextUserType struct{}
|
||||
|
||||
// ContextUserKey is a simple context key for storing the username of the request
|
||||
var ContextUserKey = &contextUserType{}
|
||||
|
||||
type contextAuthType struct{}
|
||||
|
||||
// ContextAuthKey is a simple context key for storing info returned by AuthFn
|
||||
var ContextAuthKey = &contextAuthType{}
|
||||
|
||||
// singleUserProvider provides the encrypted password for a single user
|
||||
func (s *Server) singleUserProvider(user, realm string) string {
|
||||
if user == s.Opt.BasicUser {
|
||||
return s.basicPassHashed
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// parseAuthorization parses the Authorization header into user, pass
|
||||
// it returns a boolean as to whether the parse was successful
|
||||
func parseAuthorization(r *http.Request) (user, pass string, ok bool) {
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader != "" {
|
||||
s := strings.SplitN(authHeader, " ", 2)
|
||||
if len(s) == 2 && s[0] == "Basic" {
|
||||
b, err := base64.StdEncoding.DecodeString(s[1])
|
||||
if err == nil {
|
||||
parts := strings.SplitN(string(b), ":", 2)
|
||||
user = parts[0]
|
||||
if len(parts) > 1 {
|
||||
pass = parts[1]
|
||||
ok = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// NewServer creates an http server. The opt can be nil in which case
|
||||
// the default options will be used.
|
||||
func NewServer(handler http.Handler, opt *Options) *Server {
|
||||
s := &Server{
|
||||
handler: handler,
|
||||
}
|
||||
|
||||
// Make a copy of the options
|
||||
if opt != nil {
|
||||
s.Opt = *opt
|
||||
} else {
|
||||
s.Opt = DefaultOpt
|
||||
}
|
||||
|
||||
// Use htpasswd if required on everything
|
||||
if s.Opt.HtPasswd != "" || s.Opt.BasicUser != "" || s.Opt.Auth != nil {
|
||||
var authenticator *auth.BasicAuth
|
||||
if s.Opt.Auth == nil {
|
||||
var secretProvider auth.SecretProvider
|
||||
if s.Opt.HtPasswd != "" {
|
||||
fs.Infof(nil, "Using %q as htpasswd storage", s.Opt.HtPasswd)
|
||||
secretProvider = auth.HtpasswdFileProvider(s.Opt.HtPasswd)
|
||||
} else {
|
||||
fs.Infof(nil, "Using --user %s --pass XXXX as authenticated user", s.Opt.BasicUser)
|
||||
s.basicPassHashed = string(auth.MD5Crypt([]byte(s.Opt.BasicPass), []byte("dlPL2MqE"), []byte("$1$")))
|
||||
secretProvider = s.singleUserProvider
|
||||
}
|
||||
authenticator = auth.NewBasicAuthenticator(s.Opt.Realm, secretProvider)
|
||||
}
|
||||
oldHandler := handler
|
||||
handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// No auth wanted for OPTIONS method
|
||||
if r.Method == "OPTIONS" {
|
||||
oldHandler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
unauthorized := func() {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="`+s.Opt.Realm+`"`)
|
||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
}
|
||||
user, pass, authValid := parseAuthorization(r)
|
||||
if !authValid {
|
||||
unauthorized()
|
||||
return
|
||||
}
|
||||
if s.Opt.Auth == nil {
|
||||
if username := authenticator.CheckAuth(r); username == "" {
|
||||
fs.Infof(r.URL.Path, "%s: Unauthorized request from %s", r.RemoteAddr, user)
|
||||
unauthorized()
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// Custom Auth
|
||||
value, err := s.Opt.Auth(user, pass)
|
||||
if err != nil {
|
||||
fs.Infof(r.URL.Path, "%s: Auth failed from %s: %v", r.RemoteAddr, user, err)
|
||||
unauthorized()
|
||||
return
|
||||
}
|
||||
if value != nil {
|
||||
r = r.WithContext(context.WithValue(r.Context(), ContextAuthKey, value))
|
||||
}
|
||||
}
|
||||
r = r.WithContext(context.WithValue(r.Context(), ContextUserKey, user))
|
||||
oldHandler.ServeHTTP(w, r)
|
||||
})
|
||||
s.usingAuth = true
|
||||
}
|
||||
|
||||
s.useSSL = s.Opt.SslKey != ""
|
||||
if (s.Opt.SslCert != "") != s.useSSL {
|
||||
log.Fatalf("Need both -cert and -key to use SSL")
|
||||
}
|
||||
|
||||
// If a Base URL is set then serve from there
|
||||
s.Opt.BaseURL = strings.Trim(s.Opt.BaseURL, "/")
|
||||
if s.Opt.BaseURL != "" {
|
||||
s.Opt.BaseURL = "/" + s.Opt.BaseURL
|
||||
}
|
||||
|
||||
var minTLSVersion uint16
|
||||
switch opt.MinTLSVersion {
|
||||
case "tls1.0":
|
||||
minTLSVersion = tls.VersionTLS10
|
||||
case "tls1.1":
|
||||
minTLSVersion = tls.VersionTLS11
|
||||
case "tls1.2":
|
||||
minTLSVersion = tls.VersionTLS12
|
||||
case "tls1.3":
|
||||
minTLSVersion = tls.VersionTLS13
|
||||
default:
|
||||
log.Fatalf("Invalid value for --min-tls-version")
|
||||
}
|
||||
|
||||
// FIXME make a transport?
|
||||
s.httpServer = &http.Server{
|
||||
Addr: s.Opt.ListenAddr,
|
||||
Handler: handler,
|
||||
ReadTimeout: s.Opt.ServerReadTimeout,
|
||||
WriteTimeout: s.Opt.ServerWriteTimeout,
|
||||
MaxHeaderBytes: s.Opt.MaxHeaderBytes,
|
||||
ReadHeaderTimeout: 10 * time.Second, // time to send the headers
|
||||
IdleTimeout: 60 * time.Second, // time to keep idle connections open
|
||||
TLSConfig: &tls.Config{
|
||||
MinVersion: minTLSVersion,
|
||||
},
|
||||
}
|
||||
|
||||
if s.Opt.ClientCA != "" {
|
||||
if !s.useSSL {
|
||||
log.Fatalf("Can't use --client-ca without --cert and --key")
|
||||
}
|
||||
certpool := x509.NewCertPool()
|
||||
pem, err := os.ReadFile(s.Opt.ClientCA)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to read client certificate authority: %v", err)
|
||||
}
|
||||
if !certpool.AppendCertsFromPEM(pem) {
|
||||
log.Fatalf("Can't parse client certificate authority")
|
||||
}
|
||||
s.httpServer.TLSConfig.ClientCAs = certpool
|
||||
s.httpServer.TLSConfig.ClientAuth = tls.RequireAndVerifyClientCert
|
||||
}
|
||||
|
||||
htmlTemplate, templateErr := data.GetTemplate(s.Opt.Template)
|
||||
if templateErr != nil {
|
||||
log.Fatalf(templateErr.Error())
|
||||
}
|
||||
s.HTMLTemplate = htmlTemplate
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// Serve runs the server - returns an error only if
|
||||
// the listener was not started; does not block, so
|
||||
// use s.Wait() to block on the listener indefinitely.
|
||||
func (s *Server) Serve() error {
|
||||
ln, err := net.Listen("tcp", s.httpServer.Addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("start server failed: %w", err)
|
||||
}
|
||||
s.listener = ln
|
||||
s.waitChan = make(chan struct{})
|
||||
go func() {
|
||||
var err error
|
||||
if s.useSSL {
|
||||
// hacky hack to get this to work with old Go versions, which
|
||||
// don't have ServeTLS on http.Server; see PR #2194.
|
||||
type tlsServer interface {
|
||||
ServeTLS(ln net.Listener, cert, key string) error
|
||||
}
|
||||
srvIface := interface{}(s.httpServer)
|
||||
if tlsSrv, ok := srvIface.(tlsServer); ok {
|
||||
// yay -- we get easy TLS support with HTTP/2
|
||||
err = tlsSrv.ServeTLS(s.listener, s.Opt.SslCert, s.Opt.SslKey)
|
||||
} else {
|
||||
// oh well -- we can still do TLS but might not have HTTP/2
|
||||
tlsConfig := new(tls.Config)
|
||||
tlsConfig.Certificates = make([]tls.Certificate, 1)
|
||||
tlsConfig.Certificates[0], err = tls.LoadX509KeyPair(s.Opt.SslCert, s.Opt.SslKey)
|
||||
if err != nil {
|
||||
log.Printf("Error loading key pair: %v", err)
|
||||
}
|
||||
tlsLn := tls.NewListener(s.listener, tlsConfig)
|
||||
err = s.httpServer.Serve(tlsLn)
|
||||
}
|
||||
} else {
|
||||
err = s.httpServer.Serve(s.listener)
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("Error on serving HTTP server: %v", err)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Wait blocks while the listener is open.
|
||||
func (s *Server) Wait() {
|
||||
<-s.waitChan
|
||||
}
|
||||
|
||||
// Close shuts the running server down
|
||||
func (s *Server) Close() {
|
||||
err := s.httpServer.Close()
|
||||
if err != nil {
|
||||
log.Printf("Error on closing HTTP server: %v", err)
|
||||
return
|
||||
}
|
||||
close(s.waitChan)
|
||||
}
|
||||
|
||||
// URL returns the serving address of this server
|
||||
func (s *Server) URL() string {
|
||||
proto := "http"
|
||||
if s.useSSL {
|
||||
proto = "https"
|
||||
}
|
||||
addr := s.Opt.ListenAddr
|
||||
// prefer actual listener address if using ":port" or "addr:0"
|
||||
useActualAddress := addr == "" || addr[0] == ':' || addr[len(addr)-1] == ':' || strings.HasSuffix(addr, ":0")
|
||||
if s.listener != nil && useActualAddress {
|
||||
// use actual listener address; required if using 0-port
|
||||
// (i.e. port assigned by operating system)
|
||||
addr = s.listener.Addr().String()
|
||||
}
|
||||
return fmt.Sprintf("%s://%s%s/", proto, addr, s.Opt.BaseURL)
|
||||
}
|
||||
|
||||
// UsingAuth returns true if authentication is required
|
||||
func (s *Server) UsingAuth() bool {
|
||||
return s.usingAuth
|
||||
}
|
||||
|
||||
// Path returns the current path with the Prefix stripped
|
||||
//
|
||||
// If it returns false, then the path was invalid and the handler
|
||||
// should exit as the error response has already been sent
|
||||
func (s *Server) Path(w http.ResponseWriter, r *http.Request) (Path string, ok bool) {
|
||||
Path = r.URL.Path
|
||||
if s.Opt.BaseURL == "" {
|
||||
return Path, true
|
||||
}
|
||||
if !strings.HasPrefix(Path, s.Opt.BaseURL+"/") {
|
||||
// Send a redirect if the BaseURL was requested without a /
|
||||
if Path == s.Opt.BaseURL {
|
||||
http.Redirect(w, r, s.Opt.BaseURL+"/", http.StatusPermanentRedirect)
|
||||
return Path, false
|
||||
}
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
return Path, false
|
||||
}
|
||||
Path = Path[len(s.Opt.BaseURL):]
|
||||
return Path, true
|
||||
}
|
||||
@@ -9,22 +9,20 @@ import (
|
||||
|
||||
// cache implements a simple object cache
|
||||
type cache struct {
|
||||
mu sync.RWMutex // protects the cache
|
||||
items map[string]fs.Object // cache of objects
|
||||
cacheObjects bool // whether we are actually caching
|
||||
mu sync.RWMutex // protects the cache
|
||||
items map[string]fs.Object // cache of objects
|
||||
}
|
||||
|
||||
// create a new cache
|
||||
func newCache(cacheObjects bool) *cache {
|
||||
func newCache() *cache {
|
||||
return &cache{
|
||||
items: map[string]fs.Object{},
|
||||
cacheObjects: cacheObjects,
|
||||
items: map[string]fs.Object{},
|
||||
}
|
||||
}
|
||||
|
||||
// find the object at remote or return nil
|
||||
func (c *cache) find(remote string) fs.Object {
|
||||
if !c.cacheObjects {
|
||||
if !cacheObjects {
|
||||
return nil
|
||||
}
|
||||
c.mu.RLock()
|
||||
@@ -35,7 +33,7 @@ func (c *cache) find(remote string) fs.Object {
|
||||
|
||||
// add the object to the cache
|
||||
func (c *cache) add(remote string, o fs.Object) {
|
||||
if !c.cacheObjects {
|
||||
if !cacheObjects {
|
||||
return
|
||||
}
|
||||
c.mu.Lock()
|
||||
@@ -45,7 +43,7 @@ func (c *cache) add(remote string, o fs.Object) {
|
||||
|
||||
// remove the object from the cache
|
||||
func (c *cache) remove(remote string) {
|
||||
if !c.cacheObjects {
|
||||
if !cacheObjects {
|
||||
return
|
||||
}
|
||||
c.mu.Lock()
|
||||
@@ -55,7 +53,7 @@ func (c *cache) remove(remote string) {
|
||||
|
||||
// remove all the items with prefix from the cache
|
||||
func (c *cache) removePrefix(prefix string) {
|
||||
if !c.cacheObjects {
|
||||
if !cacheObjects {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ func (c *cache) String() string {
|
||||
}
|
||||
|
||||
func TestCacheCRUD(t *testing.T) {
|
||||
c := newCache(true)
|
||||
c := newCache()
|
||||
assert.Equal(t, "", c.String())
|
||||
assert.Nil(t, c.find("potato"))
|
||||
o := mockobject.New("potato")
|
||||
@@ -35,7 +35,7 @@ func TestCacheCRUD(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCacheRemovePrefix(t *testing.T) {
|
||||
c := newCache(true)
|
||||
c := newCache()
|
||||
for _, remote := range []string{
|
||||
"a",
|
||||
"b",
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
@@ -13,55 +12,41 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/rclone/rclone/cmd"
|
||||
"github.com/rclone/rclone/cmd/serve/httplib"
|
||||
"github.com/rclone/rclone/cmd/serve/httplib/httpflags"
|
||||
"github.com/rclone/rclone/fs"
|
||||
"github.com/rclone/rclone/fs/accounting"
|
||||
"github.com/rclone/rclone/fs/config/flags"
|
||||
"github.com/rclone/rclone/fs/operations"
|
||||
"github.com/rclone/rclone/fs/walk"
|
||||
libhttp "github.com/rclone/rclone/lib/http"
|
||||
"github.com/rclone/rclone/lib/http/serve"
|
||||
"github.com/rclone/rclone/lib/terminal"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
// Options required for http server
|
||||
type Options struct {
|
||||
Auth libhttp.AuthConfig
|
||||
HTTP libhttp.Config
|
||||
Stdio bool
|
||||
AppendOnly bool
|
||||
PrivateRepos bool
|
||||
CacheObjects bool
|
||||
}
|
||||
|
||||
// DefaultOpt is the default values used for Options
|
||||
var DefaultOpt = Options{
|
||||
Auth: libhttp.DefaultAuthCfg(),
|
||||
HTTP: libhttp.DefaultCfg(),
|
||||
}
|
||||
|
||||
// Opt is options set by command line flags
|
||||
var Opt = DefaultOpt
|
||||
var (
|
||||
stdio bool
|
||||
appendOnly bool
|
||||
privateRepos bool
|
||||
cacheObjects bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
httpflags.AddFlags(Command.Flags())
|
||||
flagSet := Command.Flags()
|
||||
libhttp.AddAuthFlagsPrefix(flagSet, "", &Opt.Auth)
|
||||
libhttp.AddHTTPFlagsPrefix(flagSet, "", &Opt.HTTP)
|
||||
flags.BoolVarP(flagSet, &Opt.Stdio, "stdio", "", false, "Run an HTTP2 server on stdin/stdout")
|
||||
flags.BoolVarP(flagSet, &Opt.AppendOnly, "append-only", "", false, "Disallow deletion of repository data")
|
||||
flags.BoolVarP(flagSet, &Opt.PrivateRepos, "private-repos", "", false, "Users can only access their private repo")
|
||||
flags.BoolVarP(flagSet, &Opt.CacheObjects, "cache-objects", "", true, "Cache listed objects")
|
||||
flags.BoolVarP(flagSet, &stdio, "stdio", "", false, "Run an HTTP2 server on stdin/stdout")
|
||||
flags.BoolVarP(flagSet, &appendOnly, "append-only", "", false, "Disallow deletion of repository data")
|
||||
flags.BoolVarP(flagSet, &privateRepos, "private-repos", "", false, "Users can only access their private repo")
|
||||
flags.BoolVarP(flagSet, &cacheObjects, "cache-objects", "", true, "Cache listed objects")
|
||||
}
|
||||
|
||||
// Command definition for cobra
|
||||
var Command = &cobra.Command{
|
||||
Use: "restic remote:path",
|
||||
Short: `Serve the remote for restic's REST API.`,
|
||||
Long: `Run a basic web server to serve a remote over restic's REST backend
|
||||
Long: `Run a basic web server to serve a remove over restic's REST backend
|
||||
API over HTTP. This allows restic to use rclone as a data storage
|
||||
mechanism for cloud providers that restic does not support directly.
|
||||
|
||||
@@ -142,20 +127,13 @@ these **must** end with /. Eg
|
||||
|
||||
The` + "`--private-repos`" + ` flag can be used to limit users to repositories starting
|
||||
with a path of ` + "`/<username>/`" + `.
|
||||
` + libhttp.Help + libhttp.AuthHelp,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.40",
|
||||
},
|
||||
` + httplib.Help,
|
||||
Run: func(command *cobra.Command, args []string) {
|
||||
ctx := context.Background()
|
||||
cmd.CheckArgs(1, 1, command, args)
|
||||
f := cmd.NewFsSrc(args)
|
||||
cmd.Run(false, true, command, func() error {
|
||||
s, err := newServer(ctx, f, &Opt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if s.opt.Stdio {
|
||||
s := NewServer(f, &httpflags.Opt)
|
||||
if stdio {
|
||||
if terminal.IsTerminal(int(os.Stdout.Fd())) {
|
||||
return errors.New("refusing to run HTTP2 server directly on a terminal, please let restic start rclone")
|
||||
}
|
||||
@@ -167,12 +145,15 @@ with a path of ` + "`/<username>/`" + `.
|
||||
|
||||
httpSrv := &http2.Server{}
|
||||
opts := &http2.ServeConnOpts{
|
||||
Handler: s.Server.Router(),
|
||||
Handler: s,
|
||||
}
|
||||
httpSrv.ServeConn(conn, opts)
|
||||
return nil
|
||||
}
|
||||
fs.Logf(s.f, "Serving restic REST API on %s", s.URLs())
|
||||
err := s.Serve()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.Wait()
|
||||
return nil
|
||||
})
|
||||
@@ -183,134 +164,101 @@ const (
|
||||
resticAPIV2 = "application/vnd.x.restic.rest.v2"
|
||||
)
|
||||
|
||||
type contextRemoteType struct{}
|
||||
|
||||
// ContextRemoteKey is a simple context key for storing the username of the request
|
||||
var ContextRemoteKey = &contextRemoteType{}
|
||||
|
||||
// WithRemote makes a remote from a URL path. This implements the backend layout
|
||||
// required by restic.
|
||||
func WithRemote(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var urlpath string
|
||||
rctx := chi.RouteContext(r.Context())
|
||||
if rctx != nil && rctx.RoutePath != "" {
|
||||
urlpath = rctx.RoutePath
|
||||
} else {
|
||||
urlpath = r.URL.Path
|
||||
}
|
||||
urlpath = strings.Trim(urlpath, "/")
|
||||
parts := matchData.FindStringSubmatch(urlpath)
|
||||
// if no data directory, layout is flat
|
||||
if parts != nil {
|
||||
// otherwise map
|
||||
// data/2159dd48 to
|
||||
// data/21/2159dd48
|
||||
fileName := parts[1]
|
||||
prefix := urlpath[:len(urlpath)-len(fileName)]
|
||||
urlpath = prefix + fileName[:2] + "/" + fileName
|
||||
}
|
||||
ctx := context.WithValue(r.Context(), ContextRemoteKey, urlpath)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
// Middleware to ensure authenticated user is accessing their own private folder
|
||||
func checkPrivate(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
user := chi.URLParam(r, "userID")
|
||||
userID, ok := libhttp.CtxGetUser(r.Context())
|
||||
if ok && user != "" && user == userID {
|
||||
next.ServeHTTP(w, r)
|
||||
} else {
|
||||
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// server contains everything to run the server
|
||||
type server struct {
|
||||
*libhttp.Server
|
||||
// Server contains everything to run the Server
|
||||
type Server struct {
|
||||
*httplib.Server
|
||||
f fs.Fs
|
||||
cache *cache
|
||||
opt Options
|
||||
}
|
||||
|
||||
func newServer(ctx context.Context, f fs.Fs, opt *Options) (s *server, err error) {
|
||||
s = &server{
|
||||
f: f,
|
||||
cache: newCache(opt.CacheObjects),
|
||||
opt: *opt,
|
||||
// NewServer returns an HTTP server that speaks the rest protocol
|
||||
func NewServer(f fs.Fs, opt *httplib.Options) *Server {
|
||||
mux := http.NewServeMux()
|
||||
s := &Server{
|
||||
Server: httplib.NewServer(mux, opt),
|
||||
f: f,
|
||||
cache: newCache(),
|
||||
}
|
||||
// Don't bind any HTTP listeners if running with --stdio
|
||||
if opt.Stdio {
|
||||
opt.HTTP.ListenAddr = nil
|
||||
}
|
||||
s.Server, err = libhttp.NewServer(ctx,
|
||||
libhttp.WithConfig(opt.HTTP),
|
||||
libhttp.WithAuth(opt.Auth),
|
||||
)
|
||||
mux.HandleFunc(s.Opt.BaseURL+"/", s.ServeHTTP)
|
||||
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 nil, fmt.Errorf("failed to init server: %w", err)
|
||||
}
|
||||
router := s.Router()
|
||||
s.Bind(router)
|
||||
s.Server.Serve()
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// bind helper for main Bind method
|
||||
func (s *server) bind(router chi.Router) {
|
||||
router.MethodFunc("GET", "/*", func(w http.ResponseWriter, r *http.Request) {
|
||||
urlpath := chi.URLParam(r, "*")
|
||||
if urlpath == "" || strings.HasSuffix(urlpath, "/") {
|
||||
s.listObjects(w, r)
|
||||
} else {
|
||||
s.serveObject(w, r)
|
||||
}
|
||||
})
|
||||
router.MethodFunc("POST", "/*", func(w http.ResponseWriter, r *http.Request) {
|
||||
urlpath := chi.URLParam(r, "*")
|
||||
if urlpath == "" || strings.HasSuffix(urlpath, "/") {
|
||||
s.createRepo(w, r)
|
||||
} else {
|
||||
s.postObject(w, r)
|
||||
}
|
||||
})
|
||||
router.MethodFunc("HEAD", "/*", s.serveObject)
|
||||
router.MethodFunc("DELETE", "/*", s.deleteObject)
|
||||
}
|
||||
|
||||
// Bind restic server routes to passed router
|
||||
func (s *server) Bind(router chi.Router) {
|
||||
// FIXME
|
||||
// if m := authX.Auth(authX.Opt); m != nil {
|
||||
// router.Use(m)
|
||||
// }
|
||||
router.Use(
|
||||
middleware.SetHeader("Accept-Ranges", "bytes"),
|
||||
middleware.SetHeader("Server", "rclone/"+fs.Version),
|
||||
WithRemote,
|
||||
)
|
||||
|
||||
if s.opt.PrivateRepos {
|
||||
router.Route("/{userID}", func(r chi.Router) {
|
||||
r.Use(checkPrivate)
|
||||
s.bind(r)
|
||||
})
|
||||
router.NotFound(func(w http.ResponseWriter, _ *http.Request) {
|
||||
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
|
||||
})
|
||||
} else {
|
||||
s.bind(router)
|
||||
return err
|
||||
}
|
||||
fs.Logf(s.f, "Serving restic REST API on %s", s.URL())
|
||||
return nil
|
||||
}
|
||||
|
||||
var matchData = regexp.MustCompile("(?:^|/)data/([^/]{2,})$")
|
||||
|
||||
// Makes a remote from a URL path. This implements the backend layout
|
||||
// required by restic.
|
||||
func makeRemote(path string) string {
|
||||
path = strings.Trim(path, "/")
|
||||
parts := matchData.FindStringSubmatch(path)
|
||||
// if no data directory, layout is flat
|
||||
if parts == nil {
|
||||
return path
|
||||
}
|
||||
// otherwise map
|
||||
// data/2159dd48 to
|
||||
// data/21/2159dd48
|
||||
fileName := parts[1]
|
||||
prefix := path[:len(path)-len(fileName)]
|
||||
return prefix + fileName[:2] + "/" + fileName
|
||||
}
|
||||
|
||||
// ServeHTTP reads incoming requests and dispatches them
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Accept-Ranges", "bytes")
|
||||
w.Header().Set("Server", "rclone/"+fs.Version)
|
||||
|
||||
path, ok := s.Path(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
remote := makeRemote(path)
|
||||
fs.Debugf(s.f, "%s %s", r.Method, path)
|
||||
|
||||
v := r.Context().Value(httplib.ContextUserKey)
|
||||
if privateRepos && (v == nil || !strings.HasPrefix(path, "/"+v.(string)+"/")) {
|
||||
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// Dispatch on path then method
|
||||
if strings.HasSuffix(path, "/") {
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
s.listObjects(w, r, remote)
|
||||
case "POST":
|
||||
s.createRepo(w, r, remote)
|
||||
default:
|
||||
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||
}
|
||||
} else {
|
||||
switch r.Method {
|
||||
case "GET", "HEAD":
|
||||
s.serveObject(w, r, remote)
|
||||
case "POST":
|
||||
s.postObject(w, r, remote)
|
||||
case "DELETE":
|
||||
s.deleteObject(w, r, remote)
|
||||
default:
|
||||
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// newObject returns an object with the remote given either from the
|
||||
// cache or directly
|
||||
func (s *server) newObject(ctx context.Context, remote string) (fs.Object, error) {
|
||||
func (s *Server) newObject(ctx context.Context, remote string) (fs.Object, error) {
|
||||
o := s.cache.find(remote)
|
||||
if o != nil {
|
||||
return o, nil
|
||||
@@ -324,12 +272,7 @@ func (s *server) newObject(ctx context.Context, remote string) (fs.Object, error
|
||||
}
|
||||
|
||||
// get the remote
|
||||
func (s *server) serveObject(w http.ResponseWriter, r *http.Request) {
|
||||
remote, ok := r.Context().Value(ContextRemoteKey).(string)
|
||||
if !ok {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
func (s *Server) serveObject(w http.ResponseWriter, r *http.Request, remote string) {
|
||||
o, err := s.newObject(r.Context(), remote)
|
||||
if err != nil {
|
||||
fs.Debugf(remote, "%s request error: %v", r.Method, err)
|
||||
@@ -340,13 +283,8 @@ func (s *server) serveObject(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// postObject posts an object to the repository
|
||||
func (s *server) postObject(w http.ResponseWriter, r *http.Request) {
|
||||
remote, ok := r.Context().Value(ContextRemoteKey).(string)
|
||||
if !ok {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if s.opt.AppendOnly {
|
||||
func (s *Server) postObject(w http.ResponseWriter, r *http.Request, remote string) {
|
||||
if appendOnly {
|
||||
// make sure the file does not exist yet
|
||||
_, err := s.newObject(r.Context(), remote)
|
||||
if err == nil {
|
||||
@@ -371,13 +309,8 @@ func (s *server) postObject(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// delete the remote
|
||||
func (s *server) deleteObject(w http.ResponseWriter, r *http.Request) {
|
||||
remote, ok := r.Context().Value(ContextRemoteKey).(string)
|
||||
if !ok {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if s.opt.AppendOnly {
|
||||
func (s *Server) deleteObject(w http.ResponseWriter, r *http.Request, remote string) {
|
||||
if appendOnly {
|
||||
parts := strings.Split(r.URL.Path, "/")
|
||||
|
||||
// if path doesn't end in "/locks/:name", disallow the operation
|
||||
@@ -426,19 +359,15 @@ func (ls *listItems) add(o fs.Object) {
|
||||
}
|
||||
|
||||
// listObjects lists all Objects of a given type in an arbitrary order.
|
||||
func (s *server) listObjects(w http.ResponseWriter, r *http.Request) {
|
||||
remote, ok := r.Context().Value(ContextRemoteKey).(string)
|
||||
if !ok {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if r.Header.Get("Accept") != resticAPIV2 {
|
||||
fs.Errorf(remote, "Restic v2 API required for List Objects")
|
||||
http.Error(w, "Restic v2 API required for List Objects", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
func (s *Server) listObjects(w http.ResponseWriter, r *http.Request, remote string) {
|
||||
fs.Debugf(remote, "list request")
|
||||
|
||||
if r.Header.Get("Accept") != resticAPIV2 {
|
||||
fs.Errorf(remote, "Restic v2 API required")
|
||||
http.Error(w, "Restic v2 API required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// make sure an empty list is returned, and not a 'nil' value
|
||||
ls := listItems{}
|
||||
|
||||
@@ -476,12 +405,7 @@ func (s *server) listObjects(w http.ResponseWriter, r *http.Request) {
|
||||
// createRepo creates repository directories.
|
||||
//
|
||||
// We don't bother creating the data dirs as rclone will create them on the fly
|
||||
func (s *server) createRepo(w http.ResponseWriter, r *http.Request) {
|
||||
remote, ok := r.Context().Value(ContextRemoteKey).(string)
|
||||
if !ok {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
func (s *Server) createRepo(w http.ResponseWriter, r *http.Request, remote string) {
|
||||
fs.Infof(remote, "Creating repository")
|
||||
|
||||
if r.URL.Query().Get("create") != "true" {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package restic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
@@ -10,6 +9,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/rclone/rclone/cmd"
|
||||
"github.com/rclone/rclone/cmd/serve/httplib/httpflags"
|
||||
"github.com/rclone/rclone/fs/config/configfile"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -62,7 +62,6 @@ func createOverwriteDeleteSeq(t testing.TB, path string) []TestRequest {
|
||||
|
||||
// TestResticHandler runs tests on the restic handler code, especially in append-only mode.
|
||||
func TestResticHandler(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
configfile.Install()
|
||||
buf := make([]byte, 32)
|
||||
_, err := io.ReadFull(rand.Reader, buf)
|
||||
@@ -111,18 +110,19 @@ func TestResticHandler(t *testing.T) {
|
||||
// setup rclone with a local backend in a temporary directory
|
||||
tempdir := t.TempDir()
|
||||
|
||||
// set append-only mode
|
||||
opt := newOpt()
|
||||
opt.AppendOnly = true
|
||||
// globally set append-only mode
|
||||
prev := appendOnly
|
||||
appendOnly = true
|
||||
defer func() {
|
||||
appendOnly = prev // reset when done
|
||||
}()
|
||||
|
||||
// make a new file system in the temp dir
|
||||
f := cmd.NewFsSrc([]string{tempdir})
|
||||
s, err := newServer(ctx, f, &opt)
|
||||
require.NoError(t, err)
|
||||
router := s.Server.Router()
|
||||
srv := NewServer(f, &httpflags.Opt)
|
||||
|
||||
// create the repo
|
||||
checkRequest(t, router.ServeHTTP,
|
||||
checkRequest(t, srv.ServeHTTP,
|
||||
newRequest(t, "POST", "/?create=true", nil),
|
||||
[]wantFunc{wantCode(http.StatusOK)})
|
||||
|
||||
@@ -130,7 +130,7 @@ func TestResticHandler(t *testing.T) {
|
||||
t.Run("", func(t *testing.T) {
|
||||
for i, seq := range test.seq {
|
||||
t.Logf("request %v: %v %v", i, seq.req.Method, seq.req.URL.Path)
|
||||
checkRequest(t, router.ServeHTTP, seq.req, seq.want)
|
||||
checkRequest(t, srv.ServeHTTP, seq.req, seq.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,21 +8,23 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/rclone/rclone/cmd/serve/httplib"
|
||||
|
||||
"github.com/rclone/rclone/cmd"
|
||||
"github.com/rclone/rclone/cmd/serve/httplib/httpflags"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// newAuthenticatedRequest returns a new HTTP request with the given params.
|
||||
func newAuthenticatedRequest(t testing.TB, method, path string, body io.Reader, user, pass string) *http.Request {
|
||||
func newAuthenticatedRequest(t testing.TB, method, path string, body io.Reader) *http.Request {
|
||||
req := newRequest(t, method, path, body)
|
||||
req.SetBasicAuth(user, pass)
|
||||
req = req.WithContext(context.WithValue(req.Context(), httplib.ContextUserKey, "test"))
|
||||
req.Header.Add("Accept", resticAPIV2)
|
||||
return req
|
||||
}
|
||||
|
||||
// TestResticPrivateRepositories runs tests on the restic handler code for private repositories
|
||||
func TestResticPrivateRepositories(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
buf := make([]byte, 32)
|
||||
_, err := io.ReadFull(rand.Reader, buf)
|
||||
require.NoError(t, err)
|
||||
@@ -30,49 +32,42 @@ func TestResticPrivateRepositories(t *testing.T) {
|
||||
// setup rclone with a local backend in a temporary directory
|
||||
tempdir := t.TempDir()
|
||||
|
||||
opt := newOpt()
|
||||
|
||||
// set private-repos mode & test user
|
||||
opt.PrivateRepos = true
|
||||
opt.Auth.BasicUser = "test"
|
||||
opt.Auth.BasicPass = "password"
|
||||
// globally set private-repos mode & test user
|
||||
prev := privateRepos
|
||||
prevUser := httpflags.Opt.BasicUser
|
||||
prevPassword := httpflags.Opt.BasicPass
|
||||
privateRepos = true
|
||||
httpflags.Opt.BasicUser = "test"
|
||||
httpflags.Opt.BasicPass = "password"
|
||||
// reset when done
|
||||
defer func() {
|
||||
privateRepos = prev
|
||||
httpflags.Opt.BasicUser = prevUser
|
||||
httpflags.Opt.BasicPass = prevPassword
|
||||
}()
|
||||
|
||||
// make a new file system in the temp dir
|
||||
f := cmd.NewFsSrc([]string{tempdir})
|
||||
s, err := newServer(ctx, f, &opt)
|
||||
require.NoError(t, err)
|
||||
router := s.Server.Router()
|
||||
srv := NewServer(f, &httpflags.Opt)
|
||||
|
||||
// Requesting /test/ should allow access
|
||||
reqs := []*http.Request{
|
||||
newAuthenticatedRequest(t, "POST", "/test/?create=true", nil, opt.Auth.BasicUser, opt.Auth.BasicPass),
|
||||
newAuthenticatedRequest(t, "POST", "/test/config", strings.NewReader("foobar test config"), opt.Auth.BasicUser, opt.Auth.BasicPass),
|
||||
newAuthenticatedRequest(t, "GET", "/test/config", nil, opt.Auth.BasicUser, opt.Auth.BasicPass),
|
||||
newAuthenticatedRequest(t, "POST", "/test/?create=true", nil),
|
||||
newAuthenticatedRequest(t, "POST", "/test/config", strings.NewReader("foobar test config")),
|
||||
newAuthenticatedRequest(t, "GET", "/test/config", nil),
|
||||
}
|
||||
for _, req := range reqs {
|
||||
checkRequest(t, router.ServeHTTP, req, []wantFunc{wantCode(http.StatusOK)})
|
||||
}
|
||||
|
||||
// Requesting with bad credentials should raise unauthorised errors
|
||||
reqs = []*http.Request{
|
||||
newRequest(t, "GET", "/test/config", nil),
|
||||
newAuthenticatedRequest(t, "GET", "/test/config", nil, opt.Auth.BasicUser, ""),
|
||||
newAuthenticatedRequest(t, "GET", "/test/config", nil, "", opt.Auth.BasicPass),
|
||||
newAuthenticatedRequest(t, "GET", "/test/config", nil, opt.Auth.BasicUser+"x", opt.Auth.BasicPass),
|
||||
newAuthenticatedRequest(t, "GET", "/test/config", nil, opt.Auth.BasicUser, opt.Auth.BasicPass+"x"),
|
||||
}
|
||||
for _, req := range reqs {
|
||||
checkRequest(t, router.ServeHTTP, req, []wantFunc{wantCode(http.StatusUnauthorized)})
|
||||
checkRequest(t, srv.ServeHTTP, req, []wantFunc{wantCode(http.StatusOK)})
|
||||
}
|
||||
|
||||
// Requesting everything else should raise forbidden errors
|
||||
reqs = []*http.Request{
|
||||
newAuthenticatedRequest(t, "GET", "/", nil, opt.Auth.BasicUser, opt.Auth.BasicPass),
|
||||
newAuthenticatedRequest(t, "POST", "/other_user", nil, opt.Auth.BasicUser, opt.Auth.BasicPass),
|
||||
newAuthenticatedRequest(t, "GET", "/other_user/config", nil, opt.Auth.BasicUser, opt.Auth.BasicPass),
|
||||
newAuthenticatedRequest(t, "GET", "/", nil),
|
||||
newAuthenticatedRequest(t, "POST", "/other_user", nil),
|
||||
newAuthenticatedRequest(t, "GET", "/other_user/config", nil),
|
||||
}
|
||||
for _, req := range reqs {
|
||||
checkRequest(t, router.ServeHTTP, req, []wantFunc{wantCode(http.StatusForbidden)})
|
||||
checkRequest(t, srv.ServeHTTP, req, []wantFunc{wantCode(http.StatusForbidden)})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,16 +5,14 @@ package restic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
_ "github.com/rclone/rclone/backend/all"
|
||||
"github.com/rclone/rclone/cmd/serve/httplib"
|
||||
"github.com/rclone/rclone/fstest"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -22,24 +20,16 @@ const (
|
||||
resticSource = "../../../../../restic/restic"
|
||||
)
|
||||
|
||||
func newOpt() Options {
|
||||
opt := DefaultOpt
|
||||
opt.HTTP.ListenAddr = []string{testBindAddress}
|
||||
return opt
|
||||
}
|
||||
|
||||
// TestRestic runs the restic server then runs the unit tests for the
|
||||
// restic remote against it.
|
||||
//
|
||||
// Requires the restic source code in the location indicated by resticSource.
|
||||
func TestResticIntegration(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
func TestRestic(t *testing.T) {
|
||||
_, err := os.Stat(resticSource)
|
||||
if err != nil {
|
||||
t.Skipf("Skipping test as restic source not found: %v", err)
|
||||
}
|
||||
|
||||
opt := newOpt()
|
||||
opt := httplib.DefaultOpt
|
||||
opt.ListenAddr = testBindAddress
|
||||
|
||||
fstest.Initialise()
|
||||
|
||||
@@ -51,16 +41,16 @@ func TestResticIntegration(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Start the server
|
||||
s, err := newServer(ctx, fremote, &opt)
|
||||
require.NoError(t, err)
|
||||
testURL := s.Server.URLs()[0]
|
||||
w := NewServer(fremote, &opt)
|
||||
assert.NoError(t, w.Serve())
|
||||
defer func() {
|
||||
_ = s.Shutdown()
|
||||
w.Close()
|
||||
w.Wait()
|
||||
}()
|
||||
|
||||
// Change directory to run the tests
|
||||
err = os.Chdir(resticSource)
|
||||
require.NoError(t, err, "failed to cd to restic source code")
|
||||
assert.NoError(t, err, "failed to cd to restic source code")
|
||||
|
||||
// Run the restic tests
|
||||
runTests := func(path string) {
|
||||
@@ -70,7 +60,7 @@ func TestResticIntegration(t *testing.T) {
|
||||
}
|
||||
cmd := exec.Command("go", args...)
|
||||
cmd.Env = append(os.Environ(),
|
||||
"RESTIC_TEST_REST_REPOSITORY=rest:"+testURL+path,
|
||||
"RESTIC_TEST_REST_REPOSITORY=rest:"+w.Server.URL()+path,
|
||||
"GO111MODULE=on",
|
||||
)
|
||||
out, err := cmd.CombinedOutput()
|
||||
@@ -91,6 +81,7 @@ func TestMakeRemote(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
in, want string
|
||||
}{
|
||||
{"", ""},
|
||||
{"/", ""},
|
||||
{"/data", "data"},
|
||||
{"/data/", "data"},
|
||||
@@ -103,14 +94,7 @@ func TestMakeRemote(t *testing.T) {
|
||||
{"/keys/12", "keys/12"},
|
||||
{"/keys/123", "keys/123"},
|
||||
} {
|
||||
r := httptest.NewRequest("GET", test.in, nil)
|
||||
w := httptest.NewRecorder()
|
||||
next := http.HandlerFunc(func(_ http.ResponseWriter, request *http.Request) {
|
||||
remote, ok := request.Context().Value(ContextRemoteKey).(string)
|
||||
assert.True(t, ok, "Failed to get remote from context")
|
||||
assert.Equal(t, test.want, remote, test.in)
|
||||
})
|
||||
got := WithRemote(next)
|
||||
got.ServeHTTP(w, r)
|
||||
got := makeRemote(test.in)
|
||||
assert.Equal(t, test.want, got, test.in)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// declare a few helper functions
|
||||
@@ -14,10 +15,11 @@ import (
|
||||
// wantFunc tests the HTTP response in res and marks the test as errored if something is incorrect.
|
||||
type wantFunc func(t testing.TB, res *httptest.ResponseRecorder)
|
||||
|
||||
// newRequest returns a new HTTP request with the given params
|
||||
// newRequest returns a new HTTP request with the given params. On error, the
|
||||
// test is marked as failed.
|
||||
func newRequest(t testing.TB, method, path string, body io.Reader) *http.Request {
|
||||
req := httptest.NewRequest(method, path, body)
|
||||
req.Header.Add("Accept", resticAPIV2)
|
||||
req, err := http.NewRequest(method, path, body)
|
||||
require.NoError(t, err)
|
||||
return req
|
||||
}
|
||||
|
||||
|
||||
@@ -49,9 +49,6 @@ subcommand to specify the protocol, e.g.
|
||||
|
||||
Each subcommand has its own options which you can see in their help.
|
||||
`,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.39",
|
||||
},
|
||||
RunE: func(command *cobra.Command, args []string) error {
|
||||
if len(args) == 0 {
|
||||
return errors.New("serve requires a protocol, e.g. 'rclone serve http remote:'")
|
||||
|
||||
@@ -114,9 +114,6 @@ checksumming is possible but less secure and you could use the SFTP server
|
||||
provided by OpenSSH in this case.
|
||||
|
||||
` + vfs.Help + proxy.Help,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.48",
|
||||
},
|
||||
Run: func(command *cobra.Command, args []string) {
|
||||
var f fs.Fs
|
||||
if proxyflags.Opt.AuthProxy == "" {
|
||||
|
||||
@@ -10,15 +10,14 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
chi "github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/rclone/rclone/cmd"
|
||||
"github.com/rclone/rclone/cmd/serve/httplib"
|
||||
"github.com/rclone/rclone/cmd/serve/httplib/httpflags"
|
||||
"github.com/rclone/rclone/cmd/serve/proxy"
|
||||
"github.com/rclone/rclone/cmd/serve/proxy/proxyflags"
|
||||
"github.com/rclone/rclone/fs"
|
||||
"github.com/rclone/rclone/fs/config/flags"
|
||||
"github.com/rclone/rclone/fs/hash"
|
||||
libhttp "github.com/rclone/rclone/lib/http"
|
||||
"github.com/rclone/rclone/lib/http/serve"
|
||||
"github.com/rclone/rclone/vfs"
|
||||
"github.com/rclone/rclone/vfs/vfsflags"
|
||||
@@ -26,37 +25,19 @@ import (
|
||||
"golang.org/x/net/webdav"
|
||||
)
|
||||
|
||||
// Options required for http server
|
||||
type Options struct {
|
||||
Auth libhttp.AuthConfig
|
||||
HTTP libhttp.Config
|
||||
Template libhttp.TemplateConfig
|
||||
HashName string
|
||||
HashType hash.Type
|
||||
DisableGETDir bool
|
||||
}
|
||||
|
||||
// DefaultOpt is the default values used for Options
|
||||
var DefaultOpt = Options{
|
||||
Auth: libhttp.DefaultAuthCfg(),
|
||||
HTTP: libhttp.DefaultCfg(),
|
||||
Template: libhttp.DefaultTemplateCfg(),
|
||||
HashType: hash.None,
|
||||
DisableGETDir: false,
|
||||
}
|
||||
|
||||
// Opt is options set by command line flags
|
||||
var Opt = DefaultOpt
|
||||
var (
|
||||
hashName string
|
||||
hashType = hash.None
|
||||
disableGETDir = false
|
||||
)
|
||||
|
||||
func init() {
|
||||
flagSet := Command.Flags()
|
||||
libhttp.AddAuthFlagsPrefix(flagSet, "", &Opt.Auth)
|
||||
libhttp.AddHTTPFlagsPrefix(flagSet, "", &Opt.HTTP)
|
||||
libhttp.AddTemplateFlagsPrefix(flagSet, "", &Opt.Template)
|
||||
httpflags.AddFlags(flagSet)
|
||||
vfsflags.AddFlags(flagSet)
|
||||
proxyflags.AddFlags(flagSet)
|
||||
flags.StringVarP(flagSet, &Opt.HashName, "etag-hash", "", "", "Which hash to use for the ETag, or auto or blank for off")
|
||||
flags.BoolVarP(flagSet, &Opt.DisableGETDir, "disable-dir-list", "", false, "Disable HTML directory list on GET request for a directory")
|
||||
flags.StringVarP(flagSet, &hashName, "etag-hash", "", "", "Which hash to use for the ETag, or auto or blank for off")
|
||||
flags.BoolVarP(flagSet, &disableGETDir, "disable-dir-list", "", false, "Disable HTML directory list on GET request for a directory")
|
||||
}
|
||||
|
||||
// Command definition for cobra
|
||||
@@ -79,34 +60,7 @@ supported hash on the backend or you can use a named hash such as
|
||||
"MD5" or "SHA-1". Use the [hashsum](/commands/rclone_hashsum/) command
|
||||
to see the full list.
|
||||
|
||||
### Access WebDAV on Windows
|
||||
WebDAV shared folder can be mapped as a drive on Windows, however the default settings prevent it.
|
||||
Windows will fail to connect to the server using insecure Basic authentication.
|
||||
It will not even display any login dialog. Windows requires SSL / HTTPS connection to be used with Basic.
|
||||
If you try to connect via Add Network Location Wizard you will get the following error:
|
||||
"The folder you entered does not appear to be valid. Please choose another".
|
||||
However, you still can connect if you set the following registry key on a client machine:
|
||||
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\WebClient\Parameters\BasicAuthLevel to 2.
|
||||
The BasicAuthLevel can be set to the following values:
|
||||
0 - Basic authentication disabled
|
||||
1 - Basic authentication enabled for SSL connections only
|
||||
2 - Basic authentication enabled for SSL connections and for non-SSL connections
|
||||
If required, increase the FileSizeLimitInBytes to a higher value.
|
||||
Navigate to the Services interface, then restart the WebClient service.
|
||||
|
||||
### Access Office applications on WebDAV
|
||||
Navigate to following registry HKEY_CURRENT_USER\Software\Microsoft\Office\[14.0/15.0/16.0]\Common\Internet
|
||||
Create a new DWORD BasicAuthLevel with value 2.
|
||||
0 - Basic authentication disabled
|
||||
1 - Basic authentication enabled for SSL connections only
|
||||
2 - Basic authentication enabled for SSL and for non-SSL connections
|
||||
|
||||
https://learn.microsoft.com/en-us/office/troubleshoot/powerpoint/office-opens-blank-from-sharepoint
|
||||
|
||||
` + libhttp.Help + libhttp.TemplateHelp + libhttp.AuthHelp + vfs.Help + proxy.Help,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.39",
|
||||
},
|
||||
` + httplib.Help + vfs.Help + proxy.Help,
|
||||
RunE: func(command *cobra.Command, args []string) error {
|
||||
var f fs.Fs
|
||||
if proxyflags.Opt.AuthProxy == "" {
|
||||
@@ -115,24 +69,21 @@ https://learn.microsoft.com/en-us/office/troubleshoot/powerpoint/office-opens-bl
|
||||
} else {
|
||||
cmd.CheckArgs(0, 0, command, args)
|
||||
}
|
||||
Opt.HashType = hash.None
|
||||
if Opt.HashName == "auto" {
|
||||
Opt.HashType = f.Hashes().GetOne()
|
||||
} else if Opt.HashName != "" {
|
||||
err := Opt.HashType.Set(Opt.HashName)
|
||||
hashType = hash.None
|
||||
if hashName == "auto" {
|
||||
hashType = f.Hashes().GetOne()
|
||||
} else if hashName != "" {
|
||||
err := hashType.Set(hashName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if Opt.HashType != hash.None {
|
||||
fs.Debugf(f, "Using hash %v for ETag", Opt.HashType)
|
||||
if hashType != hash.None {
|
||||
fs.Debugf(f, "Using hash %v for ETag", hashType)
|
||||
}
|
||||
cmd.Run(false, false, command, func() error {
|
||||
s, err := newWebDAV(context.Background(), f, &Opt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.serve()
|
||||
s := newWebDAV(context.Background(), f, &httpflags.Opt)
|
||||
err := s.serve()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -156,8 +107,7 @@ https://learn.microsoft.com/en-us/office/troubleshoot/powerpoint/office-opens-bl
|
||||
// might apply". In particular, whether or not renaming a file or directory
|
||||
// overwriting another existing file or directory is an error is OS-dependent.
|
||||
type WebDAV struct {
|
||||
*libhttp.Server
|
||||
opt Options
|
||||
*httplib.Server
|
||||
f fs.Fs
|
||||
_vfs *vfs.VFS // don't use directly, use getVFS
|
||||
webdavhandler *webdav.Handler
|
||||
@@ -169,61 +119,29 @@ type WebDAV struct {
|
||||
var _ webdav.FileSystem = (*WebDAV)(nil)
|
||||
|
||||
// Make a new WebDAV to serve the remote
|
||||
func newWebDAV(ctx context.Context, f fs.Fs, opt *Options) (w *WebDAV, err error) {
|
||||
w = &WebDAV{
|
||||
func newWebDAV(ctx context.Context, f fs.Fs, opt *httplib.Options) *WebDAV {
|
||||
w := &WebDAV{
|
||||
f: f,
|
||||
ctx: ctx,
|
||||
opt: *opt,
|
||||
}
|
||||
if proxyflags.Opt.AuthProxy != "" {
|
||||
w.proxy = proxy.New(ctx, &proxyflags.Opt)
|
||||
// override auth
|
||||
w.opt.Auth.CustomAuthFn = w.auth
|
||||
copyOpt := *opt
|
||||
copyOpt.Auth = w.auth
|
||||
opt = ©Opt
|
||||
} else {
|
||||
w._vfs = vfs.New(f, &vfsflags.Opt)
|
||||
}
|
||||
|
||||
w.Server, err = libhttp.NewServer(ctx,
|
||||
libhttp.WithConfig(w.opt.HTTP),
|
||||
libhttp.WithAuth(w.opt.Auth),
|
||||
libhttp.WithTemplate(w.opt.Template),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to init server: %w", err)
|
||||
}
|
||||
|
||||
w.Server = httplib.NewServer(http.HandlerFunc(w.handler), opt)
|
||||
webdavHandler := &webdav.Handler{
|
||||
Prefix: w.opt.HTTP.BaseURL,
|
||||
Prefix: w.Server.Opt.BaseURL,
|
||||
FileSystem: w,
|
||||
LockSystem: webdav.NewMemLS(),
|
||||
Logger: w.logRequest, // FIXME
|
||||
}
|
||||
w.webdavhandler = webdavHandler
|
||||
|
||||
router := w.Server.Router()
|
||||
router.Use(
|
||||
middleware.SetHeader("Accept-Ranges", "bytes"),
|
||||
middleware.SetHeader("Server", "rclone/"+fs.Version),
|
||||
)
|
||||
|
||||
router.Handle("/*", w)
|
||||
|
||||
// Webdav only methods not defined in chi
|
||||
methods := []string{
|
||||
"COPY", // Copies the resource.
|
||||
"LOCK", // Locks the resource.
|
||||
"MKCOL", // Creates the collection specified.
|
||||
"MOVE", // Moves the resource.
|
||||
"PROPFIND", // Performs a property find on the server.
|
||||
"PROPPATCH", // Sets or removes properties on the server.
|
||||
"UNLOCK", // Unlocks the resource.
|
||||
}
|
||||
for _, method := range methods {
|
||||
chi.RegisterMethod(method)
|
||||
router.Method(method, "/*", w)
|
||||
}
|
||||
|
||||
return w, nil
|
||||
return w
|
||||
}
|
||||
|
||||
// Gets the VFS in use for this request
|
||||
@@ -231,7 +149,7 @@ func (w *WebDAV) getVFS(ctx context.Context) (VFS *vfs.VFS, err error) {
|
||||
if w._vfs != nil {
|
||||
return w._vfs, nil
|
||||
}
|
||||
value := libhttp.CtxGetAuth(ctx)
|
||||
value := ctx.Value(httplib.ContextAuthKey)
|
||||
if value == nil {
|
||||
return nil, errors.New("no VFS found in context")
|
||||
}
|
||||
@@ -251,17 +169,17 @@ func (w *WebDAV) auth(user, pass string) (value interface{}, err error) {
|
||||
return VFS, err
|
||||
}
|
||||
|
||||
func (w *WebDAV) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
||||
urlPath := r.URL.Path
|
||||
func (w *WebDAV) handler(rw http.ResponseWriter, r *http.Request) {
|
||||
urlPath, ok := w.Path(rw, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
isDir := strings.HasSuffix(urlPath, "/")
|
||||
remote := strings.Trim(urlPath, "/")
|
||||
if !w.opt.DisableGETDir && (r.Method == "GET" || r.Method == "HEAD") && isDir {
|
||||
if !disableGETDir && (r.Method == "GET" || r.Method == "HEAD") && isDir {
|
||||
w.serveDir(rw, r, remote)
|
||||
return
|
||||
}
|
||||
// Add URL Prefix back to path since webdavhandler needs to
|
||||
// return absolute references.
|
||||
r.URL.Path = w.opt.HTTP.BaseURL + r.URL.Path
|
||||
w.webdavhandler.ServeHTTP(rw, r)
|
||||
}
|
||||
|
||||
@@ -296,7 +214,7 @@ func (w *WebDAV) serveDir(rw http.ResponseWriter, r *http.Request, dirRemote str
|
||||
}
|
||||
|
||||
// Make the entries for display
|
||||
directory := serve.NewDirectory(dirRemote, w.Server.HTMLTemplate())
|
||||
directory := serve.NewDirectory(dirRemote, w.HTMLTemplate)
|
||||
for _, node := range dirEntries {
|
||||
if vfsflags.Opt.NoModTime {
|
||||
directory.AddHTMLEntry(node.Path(), node.IsDir(), node.Size(), time.Time{})
|
||||
@@ -316,8 +234,11 @@ func (w *WebDAV) serveDir(rw http.ResponseWriter, r *http.Request, dirRemote str
|
||||
//
|
||||
// Use s.Close() and s.Wait() to shutdown server
|
||||
func (w *WebDAV) serve() error {
|
||||
w.Serve()
|
||||
fs.Logf(w.f, "WebDav Server started on %s", w.URLs())
|
||||
err := w.Serve()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fs.Logf(w.f, "WebDav Server started on %s", w.URL())
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -352,7 +273,7 @@ func (w *WebDAV) OpenFile(ctx context.Context, name string, flags int, perm os.F
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return Handle{Handle: f, w: w}, nil
|
||||
return Handle{f}, nil
|
||||
}
|
||||
|
||||
// RemoveAll removes a file or a directory and its contents
|
||||
@@ -394,13 +315,12 @@ func (w *WebDAV) Stat(ctx context.Context, name string) (fi os.FileInfo, err err
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return FileInfo{FileInfo: fi, w: w}, nil
|
||||
return FileInfo{fi}, nil
|
||||
}
|
||||
|
||||
// Handle represents an open file
|
||||
type Handle struct {
|
||||
vfs.Handle
|
||||
w *WebDAV
|
||||
}
|
||||
|
||||
// Readdir reads directory entries from the handle
|
||||
@@ -411,7 +331,7 @@ func (h Handle) Readdir(count int) (fis []os.FileInfo, err error) {
|
||||
}
|
||||
// Wrap each FileInfo
|
||||
for i := range fis {
|
||||
fis[i] = FileInfo{FileInfo: fis[i], w: h.w}
|
||||
fis[i] = FileInfo{fis[i]}
|
||||
}
|
||||
return fis, nil
|
||||
}
|
||||
@@ -422,20 +342,19 @@ func (h Handle) Stat() (fi os.FileInfo, err error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return FileInfo{FileInfo: fi, w: h.w}, nil
|
||||
return FileInfo{fi}, nil
|
||||
}
|
||||
|
||||
// FileInfo represents info about a file satisfying os.FileInfo and
|
||||
// also some additional interfaces for webdav for ETag and ContentType
|
||||
type FileInfo struct {
|
||||
os.FileInfo
|
||||
w *WebDAV
|
||||
}
|
||||
|
||||
// ETag returns an ETag for the FileInfo
|
||||
func (fi FileInfo) ETag(ctx context.Context) (etag string, err error) {
|
||||
// defer log.Trace(fi, "")("etag=%q, err=%v", &etag, &err)
|
||||
if fi.w.opt.HashType == hash.None {
|
||||
if hashType == hash.None {
|
||||
return "", webdav.ErrNotImplemented
|
||||
}
|
||||
node, ok := (fi.FileInfo).(vfs.Node)
|
||||
@@ -448,7 +367,7 @@ func (fi FileInfo) ETag(ctx context.Context) (etag string, err error) {
|
||||
if !ok {
|
||||
return "", webdav.ErrNotImplemented
|
||||
}
|
||||
hash, err := o.Hash(ctx, fi.w.opt.HashType)
|
||||
hash, err := o.Hash(ctx, hashType)
|
||||
if err != nil || hash == "" {
|
||||
return "", webdav.ErrNotImplemented
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"time"
|
||||
|
||||
_ "github.com/rclone/rclone/backend/local"
|
||||
"github.com/rclone/rclone/cmd/serve/httplib"
|
||||
"github.com/rclone/rclone/cmd/serve/servetest"
|
||||
"github.com/rclone/rclone/fs"
|
||||
"github.com/rclone/rclone/fs/config/configmap"
|
||||
@@ -39,9 +40,9 @@ const (
|
||||
|
||||
// check interfaces
|
||||
var (
|
||||
_ os.FileInfo = FileInfo{nil, nil}
|
||||
_ webdav.ETager = FileInfo{nil, nil}
|
||||
_ webdav.ContentTyper = FileInfo{nil, nil}
|
||||
_ os.FileInfo = FileInfo{nil}
|
||||
_ webdav.ETager = FileInfo{nil}
|
||||
_ webdav.ContentTyper = FileInfo{nil}
|
||||
)
|
||||
|
||||
// TestWebDav runs the webdav server then runs the unit tests for the
|
||||
@@ -49,30 +50,28 @@ var (
|
||||
func TestWebDav(t *testing.T) {
|
||||
// Configure and start the server
|
||||
start := func(f fs.Fs) (configmap.Simple, func()) {
|
||||
opt := DefaultOpt
|
||||
opt.HTTP.ListenAddr = []string{testBindAddress}
|
||||
opt.HTTP.BaseURL = "/prefix"
|
||||
opt.Auth.BasicUser = testUser
|
||||
opt.Auth.BasicPass = testPass
|
||||
opt.Template.Path = testTemplate
|
||||
opt.HashType = hash.MD5
|
||||
opt := httplib.DefaultOpt
|
||||
opt.ListenAddr = testBindAddress
|
||||
opt.BasicUser = testUser
|
||||
opt.BasicPass = testPass
|
||||
opt.Template = testTemplate
|
||||
hashType = hash.MD5
|
||||
|
||||
// Start the server
|
||||
w, err := newWebDAV(context.Background(), f, &opt)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, w.serve())
|
||||
w := newWebDAV(context.Background(), f, &opt)
|
||||
assert.NoError(t, w.serve())
|
||||
|
||||
// Config for the backend we'll use to connect to the server
|
||||
config := configmap.Simple{
|
||||
"type": "webdav",
|
||||
"vendor": "other",
|
||||
"url": w.Server.URLs()[0],
|
||||
"url": w.Server.URL(),
|
||||
"user": testUser,
|
||||
"pass": obscure.MustObscure(testPass),
|
||||
}
|
||||
|
||||
return config, func() {
|
||||
assert.NoError(t, w.Shutdown())
|
||||
w.Close()
|
||||
w.Wait()
|
||||
}
|
||||
}
|
||||
@@ -99,19 +98,18 @@ func TestHTTPFunction(t *testing.T) {
|
||||
f, err := fs.NewFs(context.Background(), "../http/testdata/files")
|
||||
assert.NoError(t, err)
|
||||
|
||||
opt := DefaultOpt
|
||||
opt.HTTP.ListenAddr = []string{testBindAddress}
|
||||
opt.Template.Path = testTemplate
|
||||
opt := httplib.DefaultOpt
|
||||
opt.ListenAddr = testBindAddress
|
||||
opt.Template = testTemplate
|
||||
|
||||
// Start the server
|
||||
w, err := newWebDAV(context.Background(), f, &opt)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, w.serve())
|
||||
w := newWebDAV(context.Background(), f, &opt)
|
||||
assert.NoError(t, w.serve())
|
||||
defer func() {
|
||||
assert.NoError(t, w.Shutdown())
|
||||
w.Close()
|
||||
w.Wait()
|
||||
}()
|
||||
testURL := w.Server.URLs()[0]
|
||||
testURL := w.Server.URL()
|
||||
pause := time.Millisecond
|
||||
i := 0
|
||||
for ; i < 10; i++ {
|
||||
|
||||
@@ -40,9 +40,6 @@ Or just provide remote directory and all files in directory will be tiered
|
||||
|
||||
rclone settier tier remote:path/dir
|
||||
`,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.44",
|
||||
},
|
||||
Run: func(command *cobra.Command, args []string) {
|
||||
cmd.CheckArgs(2, 2, command, args)
|
||||
tier := args[0]
|
||||
|
||||
@@ -41,9 +41,6 @@ as a relative path).
|
||||
This command can also hash data received on STDIN, if not passing
|
||||
a remote:path.
|
||||
`,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.27",
|
||||
},
|
||||
RunE: func(command *cobra.Command, args []string) error {
|
||||
cmd.CheckArgs(0, 1, command, args)
|
||||
if found, err := hashsum.CreateFromStdinArg(hash.SHA1, args, 0); found {
|
||||
|
||||
@@ -44,9 +44,6 @@ Rclone will then show a notice in the log indicating how many such
|
||||
files were encountered, and count them in as empty files in the output
|
||||
of the size command.
|
||||
`,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.23",
|
||||
},
|
||||
Run: func(command *cobra.Command, args []string) {
|
||||
cmd.CheckArgs(1, 1, command, args)
|
||||
fsrc := cmd.NewFsSrc(args)
|
||||
|
||||
@@ -26,9 +26,6 @@ func init() {
|
||||
var commandDefinition = &cobra.Command{
|
||||
Use: "changenotify remote:",
|
||||
Short: `Log any change notify requests for the remote passed in.`,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.56",
|
||||
},
|
||||
RunE: func(command *cobra.Command, args []string) error {
|
||||
cmd.CheckArgs(1, 1, command, args)
|
||||
f := cmd.NewFsSrc(args)
|
||||
|
||||
@@ -28,9 +28,6 @@ in filenames in the remote:path specified.
|
||||
The data doesn't contain any identifying information but is useful for
|
||||
the rclone developers when developing filename compression.
|
||||
`,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.55",
|
||||
},
|
||||
Run: func(command *cobra.Command, args []string) {
|
||||
cmd.CheckArgs(1, 1, command, args)
|
||||
f := cmd.NewFsDir(args)
|
||||
|
||||
@@ -66,9 +66,6 @@ a bit of go code for each one.
|
||||
|
||||
**NB** this can create undeletable files and other hazards - use with care
|
||||
`,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.55",
|
||||
},
|
||||
Run: func(command *cobra.Command, args []string) {
|
||||
cmd.CheckArgs(1, 1e6, command, args)
|
||||
if !checkNormalization && !checkControl && !checkLength && !checkStreaming && !all {
|
||||
|
||||
@@ -74,9 +74,6 @@ func init() {
|
||||
var makefilesCmd = &cobra.Command{
|
||||
Use: "makefiles <dir>",
|
||||
Short: `Make a random file hierarchy in a directory`,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.55",
|
||||
},
|
||||
Run: func(command *cobra.Command, args []string) {
|
||||
cmd.CheckArgs(1, 1, command, args)
|
||||
commonInit()
|
||||
@@ -108,9 +105,6 @@ var makefilesCmd = &cobra.Command{
|
||||
var makefileCmd = &cobra.Command{
|
||||
Use: "makefile <size> [<file>]+ [flags]",
|
||||
Short: `Make files with random contents of the size given`,
|
||||
Annotations: map[string]string{
|
||||
"versionIntroduced": "v1.59",
|
||||
},
|
||||
Run: func(command *cobra.Command, args []string) {
|
||||
cmd.CheckArgs(1, 1e6, command, args)
|
||||
commonInit()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user