1
0
mirror of https://github.com/rclone/rclone.git synced 2025-12-06 00:03:32 +00:00

Compare commits

...

72 Commits

Author SHA1 Message Date
Nick Craig-Wood
69fdcd4300 build: disable cmount tests under macOS and the CI since they are locking up
This fixes #5951 and allows the macOS builds to run again

See #5960 for more info.
2022-01-27 16:54:52 +00:00
Nick Craig-Wood
c0331c0c83 build: don't specify macos SDK any more as default is good enough #5951
This fixes the build, in particular the error:

    Failed to run ["xcrun" "--sdk" "macosx11.1" "--show-sdk-path"]: exit status 1
2022-01-27 09:42:04 +00:00
Nick Craig-Wood
533377a955 build: upgrade to macos-11 to attempt to fix macOS build problems #5951 2022-01-26 17:49:21 +00:00
Nick Craig-Wood
f6f7bb35d5 build: cut down builds to macOS only FIXME DO NOT MERGE 2022-01-26 17:49:21 +00:00
albertony
a667e03fc9 http: improved recognition of url pointing to a single file - fixes #5929 2022-01-26 11:41:01 +01:00
albertony
1045344943 http: status string already includes the status code 2022-01-26 11:41:01 +01:00
albertony
5e469db420 docs/http: fix list layout in --http-no-head help
Existing help text ended with a list, but then auto-generated list items
Config, Env Var, Type and Default would be included in the same list.
2022-01-26 11:41:01 +01:00
albertony
946e84d194 http: use string contains instead of index 2022-01-26 11:41:01 +01:00
albertony
162aba60eb http: error strings should not be capitalized 2022-01-26 11:41:01 +01:00
albertony
d8a874c32b Make http tests line ending agnostic 2022-01-26 11:41:01 +01:00
albertony
9c451d9ac6 Fix linting errors 2022-01-26 00:02:17 +01:00
albertony
8f3f24672c docs/serve: move help for template option into separate section 2022-01-25 18:19:21 +01:00
Paulo Martins
0eb7b716d9 s3: document Content-MD5 workaround for object-lock enabled buckets - Fixes #5765 2022-01-25 16:10:57 +00:00
Gourav T
ee9684e60f fichier: implemented about functionality 2022-01-25 15:53:58 +00:00
negative0
e0cbe413e1 rc: Allow user to disable authentication for web gui 2022-01-25 15:52:30 +00:00
albertony
2523dd6220 version: report correct friendly-name for windows 10/11 versions after 2004
Until Windows 10 version 2004 (May 2020) this can be found from registry entry
ReleaseID, after that we must use entry DisplayVersion (ReleaseId is stuck at 2009).
Source: https://ss64.com/nt/ver.html
2022-01-24 21:27:42 +01:00
albertony
c504d97017 config: fix display of config choices with empty help text 2022-01-18 20:17:57 +01:00
albertony
b783f09fc6 config: show default and example values in correct input syntax instead of quoted and escaped golang string syntax
See #5551
2022-01-16 14:57:38 +01:00
albertony
a301478a13 config: improved punctuation in initial config prompt 2022-01-16 14:57:38 +01:00
albertony
63b450a2a5 config: minor improvement of help text for encoding option
See #5551
2022-01-16 14:57:38 +01:00
albertony
843b77aaaa docs/ftp: improved default value description of port and username options
See #5551
2022-01-16 14:57:38 +01:00
albertony
3641727edb config: fix issue where required password options had to be re-entered when editing existing remote
See #5551
2022-01-16 14:57:38 +01:00
albertony
38e2f835ed config: fix handling of default, exclusive and required properties of multiple-choice options
Previously an empty input (just pressing enter) was only allowed for multiple-choice
options that did not have the Exclusive property set. With this change the existing
Required property is introduced into the multiple choice handling, so that one can have
Exclusive and Required options where only a value from the list is allowed, and one can
have Exclusive but not Required options where an empty value is accepted but any
non-empty value must still be matching an item from the list.

Fixes #5549

See #5551
2022-01-16 14:57:38 +01:00
albertony
bd4bbed592 config: remove explicit setting of required property to true for options with a default value
See #5551
2022-01-16 14:57:38 +01:00
albertony
994b501188 config: remove explicit setting of required property to its default value false
See #5551
2022-01-16 14:57:38 +01:00
albertony
dfa9381814 docs/jottacloud: correct reference to temp-dir 2022-01-16 14:34:15 +01:00
albertony
2a85feda4b docs/jottacloud: add note about upload only being supported on jotta device 2022-01-16 14:34:15 +01:00
albertony
ad46af9168 docs/librclone: note that adding -ldflags -s to the build command will reduce size of library file 2022-01-16 14:32:01 +01:00
albertony
2fed02211c docs/librclone: document use from C/C++ on Windows 2022-01-16 14:11:56 +01:00
albertony
237daa8aaf dedupe: add quit as a choice in interactive mode
Fixes #5881
2022-01-14 19:57:48 +01:00
albertony
8aeca6c033 docs: align menu items when icons have different sizes 2022-01-14 17:39:27 +00:00
albertony
fd82876086 librclone: allow empty string or null input instead of empty json object 2022-01-14 17:37:13 +00:00
Isaac Levy
be1a668e95 onedrive: minor optimization of quickxorhash
This patch avoids creating a new slice header in favour of a for loop.

This saves a few instructions!
2022-01-14 17:30:56 +00:00
Vanessasaurus
9d4eab32d8 cmd: fix broken example link in help.go
This link appears to be broken, so here is another reference to (I think) the same file that provides a good example of coba. We could also do the current commit 8312004f41/cli/cobra.go although it might be better to maintain an up to date example.
2022-01-13 16:26:19 +00:00
Alain Nussbaumer
b4ba7b69b8 dlna: change icons to the newest ones. 2022-01-13 16:23:24 +00:00
albertony
deef659aef Add Bumsu Hyeon to contributors 2022-01-13 13:25:20 +01:00
Bumsu Hyeon
4b99e84242 vfs/cache: fix handling of special characters in file names (#5875) 2022-01-13 13:23:25 +01:00
albertony
06bdf7c64c Add Lu Wang to contributors 2022-01-12 21:33:35 +01:00
Lu Wang
e1225b5729 docs/s3: fixed max-age example 2022-01-12 21:31:54 +01:00
albertony
871cc2f62d docs: fix links to rc sections 2022-01-12 19:51:26 +01:00
Charlie Jiang
bc23bf11db onedrive: add config option for oauth scope Sites.Read.All (#5883) 2022-01-10 21:28:19 +08:00
albertony
b55575e622 docs: fix typo 2022-01-03 18:46:40 +01:00
albertony
328f0e7135 docs: fix links to rc debug commands 2021-12-30 21:52:34 +01:00
albertony
a52814eed9 docs: fix links to rc data types section 2021-12-30 20:46:39 +01:00
albertony
071a9e882d docs: capitalization of flag usage strings 2021-12-30 14:07:24 +01:00
albertony
4e2ca3330c tree: remove obsolete --human replaced by global --human-readable - fixes #5868 2021-12-21 20:17:00 +01:00
Yunhai Luo
408d9f3e7a s3: Add GLACIER_IR storage class 2021-12-03 14:46:45 +00:00
Koopa
0681a5c86a lib/rest: process HTML entities within XML
MEGAcmd currently includes escaped HTML4 entites in its XML messages.
This behavior deviates from the XML standard, but currently it prevents
rclone from being able to use the remote.
2021-12-01 16:31:43 +00:00
Niels van de Weem
df09c3f555 pcloud: add support for recursive list 2021-12-01 15:58:44 +00:00
Kim
c41814fd2d backend:jottacloud change api used by ListR ( --fast-list ) 2021-12-01 14:21:37 +01:00
Nick Craig-Wood
c2557cc432 azureblob: fix crash with SAS URL and no container - fixes #5820
Before this change attempting NewObject on a SAS URL's root would
crash the Azure SDK.

This change detects that using the code from this previous fix

f7404f52e7 azureblob: fix crash when listing outside a SAS URL's root - fixes #4851

And returns not object not found instead.

It also prevents things being uploaded to the root of the SAS URL
which also crashes the Azure SDK.
2021-11-27 16:18:18 +00:00
Nick Craig-Wood
3425726c50 oauthutil: fix crash when webrowser requests /robots.txt - fixes #5836
Before this change the oauth webserver would crash if it received a
request to /robots.txt.

This patch makes it ignore (with 404 error) any paths it isn't
expecting.
2021-11-25 12:12:14 +00:00
Nick Craig-Wood
46175a22d8 Add Logeshwaran Murugesan to contributors 2021-11-25 12:11:47 +00:00
Logeshwaran Murugesan
bcf0e15ad7 Simplify content length processing in s3 with download url 2021-11-25 12:03:14 +00:00
Nick Craig-Wood
b91c349cd5 local: fix hash invalidation which caused errors with local crypt mount
Before this fix if a file was updated, but to the same length and
timestamp then the local backend would return the wrong (cached)
hashes for the object.

This happens regularly on a crypted local disk mount when the VFS
thinks files have been changed but actually their contents are
identical to that written previously. This is because when files are
uploaded their nonce changes so the contents of the file changes but
the timestamp and size remain the same because the file didn't
actually change.

This causes errors like this:

    ERROR: file: Failed to copy: corrupted on transfer: md5 crypted
    hash differ "X" vs "Y"

This turned out to be because the local backend wasn't clearing its
cache of hashes when the file was updated.

This fix clears the hash cache for Update and Remove.

It also puts a src and destination in the crypt message to make future
debugging easier.

Fixes #4031
2021-11-24 12:09:34 +00:00
Nick Craig-Wood
d252816706 vfs: add vfs/stats remote control to show statistics - fixes #5816 2021-11-23 18:00:21 +00:00
Nick Craig-Wood
729117af68 Add GGG KILLER to contributors 2021-11-23 18:00:21 +00:00
GGG KILLER
cd4d8d55ec docs: add a note about the B2 download_url format
Currently the B2 docs don't specify which format the download_url
setting should have, and if you input it wrong, there is nothing
in the verbose logs or anywhere else that can let you know that.
2021-11-23 17:57:34 +00:00
Nick Craig-Wood
f26abc89a6 union: fix treatment of remotes with // in
See: https://forum.rclone.org/t/connection-string-with-union-backend-and-a-lot-of-quotes/27577
2021-11-23 17:41:12 +00:00
lindwurm
b5abbe819f s3: Add Wasabi AP Northeast 2 endpoint info
* Wasabi starts to provide AP Northeast 2 (Osaka) endpoint, so add it to the list
* Rename ap-northeast-1 as "AP Northeast 1 (Tokyo)" from "AP Northeast"

Signed-off-by: lindwurm <lindwurm.q@gmail.com>
2021-11-22 18:02:57 +00:00
Nick Craig-Wood
a351484997 sftp: fix timeout on hashing large files by sending keepalives
Before this fix the SFTP sessions could timeout when doing hashes if
they took longer than the --timeout parameter.

This patch sends keepalive packets every minute while a shell command
is running to keep the connection open.

See: https://forum.rclone.org/t/rclone-check-over-sftp-failure-to-calculate-md5-hash-for-large-files/27487
2021-11-22 15:26:29 +00:00
Nick Craig-Wood
099eff8891 sftp: refactor so we only have one way of running remote commands
This also returns errors from running ssh Hash commands which we
didn't do before.
2021-11-22 15:26:29 +00:00
albertony
c4cb167d4a Add rsapkf and Will Holtz to contributors 2021-11-21 19:26:05 +01:00
Will Holtz
38e100ab19 docs/config: more explicit doc for config create --all with params 2021-11-21 19:22:19 +01:00
rsapkf
db95a0d6c3 docs/pcloud: fix typo 2021-11-21 19:16:19 +01:00
Nick Craig-Wood
df07964db3 azureblob: raise --azureblob-upload-concurrency to 16 by default
After speed testing it was discovered that upload speed goes up pretty
much linearly with upload concurrency. This patch changes the default
from 4 to 16 which means that rclone will use 16 * 4M = 64M per
transfer which is OK even for low memory devices.

This adds a note that performance may be increased by increasing
upload concurrency.

See: https://forum.rclone.org/t/performance-of-rclone-vs-azcopy/27437/9
2021-11-18 16:09:02 +00:00
Nick Craig-Wood
fbc4c4ad9a azureblob: remove 100MB upper limit on chunk_size as it is no longer needed 2021-11-18 16:09:02 +00:00
Nick Craig-Wood
4454b3e1ae azureblob: implement --azureblob-upload-concurrency parameter to speed uploads
See: https://forum.rclone.org/t/performance-of-rclone-vs-azcopy/27437
2021-11-18 16:08:57 +00:00
Nick Craig-Wood
f9321fccbb Add deinferno to contributors 2021-11-18 15:51:45 +00:00
Ole Frost
3c2252b7c0 fs/operations: add server-side moves to stats
Fixes #5430
2021-11-18 12:20:56 +00:00
Cnly
51c952654c fstests: treat accountUpgradeRequired as success for OneDrive PublicLink 2021-11-17 17:35:17 +00:00
deinferno
80e47be65f yandex: add permanent deletion support 2021-11-17 16:57:41 +00:00
98 changed files with 1796 additions and 554 deletions

View File

@@ -25,22 +25,22 @@ jobs:
strategy:
fail-fast: false
matrix:
job_name: ['linux', 'mac_amd64', 'mac_arm64', 'windows_amd64', 'windows_386', 'other_os', 'go1.15', 'go1.16']
job_name: ['mac_amd64', 'mac_arm64']
include:
- job_name: linux
os: ubuntu-latest
go: '1.17.x'
gotags: cmount
build_flags: '-include "^linux/"'
check: true
quicktest: true
racequicktest: true
librclonetest: true
deploy: true
# - job_name: linux
# os: ubuntu-latest
# go: '1.17.x'
# gotags: cmount
# build_flags: '-include "^linux/"'
# check: true
# quicktest: true
# racequicktest: true
# librclonetest: true
# deploy: true
- job_name: mac_amd64
os: macOS-latest
os: macos-11
go: '1.17.x'
gotags: 'cmount'
build_flags: '-include "^darwin/amd64" -cgo'
@@ -49,51 +49,51 @@ jobs:
deploy: true
- job_name: mac_arm64
os: macOS-latest
os: macos-11
go: '1.17.x'
gotags: 'cmount'
build_flags: '-include "^darwin/arm64" -cgo -macos-arch arm64 -macos-sdk macosx11.1 -cgo-cflags=-I/usr/local/include -cgo-ldflags=-L/usr/local/lib'
build_flags: '-include "^darwin/arm64" -cgo -macos-arch arm64 -cgo-cflags=-I/usr/local/include -cgo-ldflags=-L/usr/local/lib'
deploy: true
- job_name: windows_amd64
os: windows-latest
go: '1.17.x'
gotags: cmount
build_flags: '-include "^windows/amd64" -cgo'
build_args: '-buildmode exe'
quicktest: true
racequicktest: true
deploy: true
# - job_name: windows_amd64
# os: windows-latest
# go: '1.17.x'
# gotags: cmount
# build_flags: '-include "^windows/amd64" -cgo'
# build_args: '-buildmode exe'
# quicktest: true
# racequicktest: true
# deploy: true
- job_name: windows_386
os: windows-latest
go: '1.17.x'
gotags: cmount
goarch: '386'
cgo: '1'
build_flags: '-include "^windows/386" -cgo'
build_args: '-buildmode exe'
quicktest: true
deploy: true
# - job_name: windows_386
# os: windows-latest
# go: '1.17.x'
# gotags: cmount
# goarch: '386'
# cgo: '1'
# build_flags: '-include "^windows/386" -cgo'
# build_args: '-buildmode exe'
# quicktest: true
# deploy: true
- job_name: other_os
os: ubuntu-latest
go: '1.17.x'
build_flags: '-exclude "^(windows/|darwin/|linux/)"'
compile_all: true
deploy: true
# - job_name: other_os
# os: ubuntu-latest
# go: '1.17.x'
# build_flags: '-exclude "^(windows/|darwin/|linux/)"'
# compile_all: true
# deploy: true
- job_name: go1.15
os: ubuntu-latest
go: '1.15.x'
quicktest: true
racequicktest: true
# - job_name: go1.15
# os: ubuntu-latest
# go: '1.15.x'
# quicktest: true
# racequicktest: true
- job_name: go1.16
os: ubuntu-latest
go: '1.16.x'
quicktest: true
racequicktest: true
# - job_name: go1.16
# os: ubuntu-latest
# go: '1.16.x'
# quicktest: true
# racequicktest: true
name: ${{ matrix.job_name }}
@@ -134,7 +134,7 @@ jobs:
run: |
brew update
brew install --cask macfuse
if: matrix.os == 'macOS-latest'
if: matrix.os == 'macos-11'
- name: Install Libraries on Windows
shell: powershell

View File

@@ -50,8 +50,6 @@ const (
timeFormatOut = "2006-01-02T15:04:05.000000000Z07:00"
storageDefaultBaseURL = "blob.core.windows.net"
defaultChunkSize = 4 * fs.Mebi
maxChunkSize = 100 * fs.Mebi
uploadConcurrency = 4
defaultAccessTier = azblob.AccessTierNone
maxTryTimeout = time.Hour * 24 * 365 //max time of an azure web request response window (whether or not data is flowing)
// Default storage account, key and blob endpoint for emulator support,
@@ -134,12 +132,33 @@ msi_client_id, or msi_mi_res_id parameters.`,
Advanced: true,
}, {
Name: "chunk_size",
Help: `Upload chunk size (<= 100 MiB).
Help: `Upload chunk size.
Note that this is stored in memory and there may be up to
"--transfers" chunks stored at once in memory.`,
"--transfers" * "--azureblob-upload-concurrency" chunks stored at once
in memory.`,
Default: defaultChunkSize,
Advanced: true,
}, {
Name: "upload_concurrency",
Help: `Concurrency for multipart uploads.
This is the number of chunks of the same file that are uploaded
concurrently.
If you are uploading small numbers of large files over high-speed
links and these uploads do not fully utilize your bandwidth, then
increasing this may help to speed up the transfers.
In tests, upload speed increases almost linearly with upload
concurrency. For example to fill a gigabit pipe it may be necessary to
raise this to 64. Note that this will use more memory.
Note that chunks are stored in memory and there may be up to
"--transfers" * "--azureblob-upload-concurrency" chunks stored at once
in memory.`,
Default: 16,
Advanced: true,
}, {
Name: "list_chunk",
Help: `Size of blob list.
@@ -257,6 +276,7 @@ type Options struct {
Endpoint string `config:"endpoint"`
SASURL string `config:"sas_url"`
ChunkSize fs.SizeSuffix `config:"chunk_size"`
UploadConcurrency int `config:"upload_concurrency"`
ListChunkSize uint `config:"list_chunk"`
AccessTier string `config:"access_tier"`
ArchiveTierDelete bool `config:"archive_tier_delete"`
@@ -416,9 +436,6 @@ func checkUploadChunkSize(cs fs.SizeSuffix) error {
if cs < minChunkSize {
return fmt.Errorf("%s is less than %s", cs, minChunkSize)
}
if cs > maxChunkSize {
return fmt.Errorf("%s is greater than %s", cs, maxChunkSize)
}
return nil
}
@@ -1444,6 +1461,10 @@ func (o *Object) clearMetaData() {
// o.size
// o.md5
func (o *Object) readMetaData() (err error) {
container, _ := o.split()
if !o.fs.containerOK(container) {
return fs.ErrorObjectNotFound
}
if !o.modTime.IsZero() {
return nil
}
@@ -1636,7 +1657,10 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
return errCantUpdateArchiveTierBlobs
}
}
container, _ := o.split()
container, containerPath := o.split()
if container == "" || containerPath == "" {
return fmt.Errorf("can't upload to root - need a container")
}
err = o.fs.makeContainer(ctx, container)
if err != nil {
return err
@@ -1667,10 +1691,10 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
putBlobOptions := azblob.UploadStreamToBlockBlobOptions{
BufferSize: int(o.fs.opt.ChunkSize),
MaxBuffers: uploadConcurrency,
MaxBuffers: o.fs.opt.UploadConcurrency,
Metadata: o.meta,
BlobHTTPHeaders: httpHeaders,
TransferManager: o.fs.newPoolWrapper(uploadConcurrency),
TransferManager: o.fs.newPoolWrapper(o.fs.opt.UploadConcurrency),
}
// Don't retry, return a retry error instead

View File

@@ -17,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{
MaxChunkSize: maxChunkSize,
},
RemoteName: "TestAzureBlob:",
NilObject: (*Object)(nil),
TiersToTest: []string{"Hot", "Cool"},
ChunkedUpload: fstests.ChunkedUploadConfig{},
})
}

View File

@@ -160,7 +160,15 @@ free egress for data downloaded through the Cloudflare network.
Rclone works with private buckets by sending an "Authorization" header.
If the custom endpoint rewrites the requests for authentication,
e.g., in Cloudflare Workers, this header needs to be handled properly.
Leave blank if you want to use the endpoint provided by Backblaze.`,
Leave blank if you want to use the endpoint provided by Backblaze.
The URL provided here SHOULD have the protocol and SHOULD NOT have
a trailing slash or specify the /file/bucket subpath as rclone will
request files with "{download_url}/file/{bucket_name}/{path}".
Example:
> https://mysubdomain.mydomain.tld
(No trailing "/", "file" or "bucket")`,
Advanced: true,
}, {
Name: "download_auth_duration",

View File

@@ -443,7 +443,7 @@ func (f *Fs) put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options [
if err != nil {
fs.Errorf(o, "Failed to remove corrupted object: %v", err)
}
return nil, fmt.Errorf("corrupted on transfer: %v crypted hash differ %q vs %q", ht, srcHash, dstHash)
return nil, fmt.Errorf("corrupted on transfer: %v crypted hash differ src %q vs dst %q", ht, srcHash, dstHash)
}
fs.Debugf(src, "%v = %s OK", ht, srcHash)
}

View File

@@ -42,18 +42,15 @@ func init() {
}, {
Help: "If you want to download a shared folder, add this parameter.",
Name: "shared_folder",
Required: false,
Advanced: true,
}, {
Help: "If you want to download a shared file that is password protected, add this parameter.",
Name: "file_password",
Required: false,
Advanced: true,
IsPassword: true,
}, {
Help: "If you want to list the files in a shared folder that is password protected, add this parameter.",
Name: "folder_password",
Required: false,
Advanced: true,
IsPassword: true,
}, {
@@ -517,6 +514,32 @@ func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object,
return dstObj, nil
}
// About gets quota information
func (f *Fs) About(ctx context.Context) (usage *fs.Usage, err error) {
opts := rest.Opts{
Method: "POST",
Path: "/user/info.cgi",
ContentType: "application/json",
}
var accountInfo AccountInfo
var resp *http.Response
err = f.pacer.Call(func() (bool, error) {
resp, err = f.rest.CallJSON(ctx, &opts, nil, &accountInfo)
return shouldRetry(ctx, resp, err)
})
if err != nil {
return nil, fmt.Errorf("failed to read user info: %w", err)
}
// FIXME max upload size would be useful to use in Update
usage = &fs.Usage{
Used: fs.NewUsageValue(accountInfo.ColdStorage), // bytes in use
Total: fs.NewUsageValue(accountInfo.AvailableColdStorage), // bytes total
Free: fs.NewUsageValue(accountInfo.AvailableColdStorage - accountInfo.ColdStorage), // bytes free
}
return usage, nil
}
// PublicLink adds a "readable by anyone with link" permission on the given file or folder.
func (f *Fs) PublicLink(ctx context.Context, remote string, expire fs.Duration, unlink bool) (string, error) {
o, err := f.NewObject(ctx, remote)

View File

@@ -182,3 +182,34 @@ type FoldersList struct {
Status string `json:"Status"`
SubFolders []Folder `json:"sub_folders"`
}
// AccountInfo is the structure how 1Fichier returns user info
type AccountInfo struct {
StatsDate string `json:"stats_date"`
MailRM string `json:"mail_rm"`
DefaultQuota int64 `json:"default_quota"`
UploadForbidden string `json:"upload_forbidden"`
PageLimit int `json:"page_limit"`
ColdStorage int64 `json:"cold_storage"`
Status string `json:"status"`
UseCDN string `json:"use_cdn"`
AvailableColdStorage int64 `json:"available_cold_storage"`
DefaultPort string `json:"default_port"`
DefaultDomain int `json:"default_domain"`
Email string `json:"email"`
DownloadMenu string `json:"download_menu"`
FTPDID int `json:"ftp_did"`
DefaultPortFiles string `json:"default_port_files"`
FTPReport string `json:"ftp_report"`
OverQuota int64 `json:"overquota"`
AvailableStorage int64 `json:"available_storage"`
CDN string `json:"cdn"`
Offer string `json:"offer"`
SubscriptionEnd string `json:"subscription_end"`
TFA string `json:"2fa"`
AllowedColdStorage int64 `json:"allowed_cold_storage"`
HotStorage int64 `json:"hot_storage"`
DefaultColdStorageQuota int64 `json:"default_cold_storage_quota"`
FTPMode string `json:"ftp_mode"`
RUReport string `json:"ru_report"`
}

View File

@@ -52,11 +52,13 @@ func init() {
Help: "FTP host to connect to.\n\nE.g. \"ftp.example.com\".",
Required: true,
}, {
Name: "user",
Help: "FTP username, leave blank for current username, " + currentUser + ".",
Name: "user",
Help: "FTP username.",
Default: currentUser,
}, {
Name: "port",
Help: "FTP port, leave blank to use default (21).",
Name: "port",
Help: "FTP port number.",
Default: 21,
}, {
Name: "pass",
Help: "FTP password.",

View File

@@ -22,9 +22,8 @@ func init() {
Help: "Hadoop name node and port.\n\nE.g. \"namenode:8020\" to connect to host namenode at port 8020.",
Required: true,
}, {
Name: "username",
Help: "Hadoop user name.",
Required: false,
Name: "username",
Help: "Hadoop user name.",
Examples: []fs.OptionExample{{
Value: "root",
Help: "Connect to hdfs as root.",
@@ -36,7 +35,6 @@ func init() {
Enables KERBEROS authentication. Specifies the Service Principal Name
(SERVICE/FQDN) for the namenode. E.g. \"hdfs/namenode.hadoop.docker\"
for namenode running as service 'hdfs' with FQDN 'namenode.hadoop.docker'.`,
Required: false,
Advanced: true,
}, {
Name: "data_transfer_protection",
@@ -46,7 +44,6 @@ Specifies whether or not authentication, data signature integrity
checks, and wire encryption is required when communicating the the
datanodes. Possible values are 'authentication', 'integrity' and
'privacy'. Used only with KERBEROS enabled.`,
Required: false,
Examples: []fs.OptionExample{{
Value: "privacy",
Help: "Ensure authentication, integrity and encryption enabled.",

View File

@@ -52,8 +52,7 @@ The input format is comma separated list of key,value pairs. Standard
For example, to set a Cookie use 'Cookie,name=value', or '"Cookie","name=value"'.
You can set multiple headers, e.g. '"Cookie","name=value","Authorization","xxx"'.
`,
You can set multiple headers, e.g. '"Cookie","name=value","Authorization","xxx"'.`,
Default: fs.CommaSepList{},
Advanced: true,
}, {
@@ -74,8 +73,9 @@ directories.`,
Advanced: true,
}, {
Name: "no_head",
Help: `Don't use HEAD requests to find file sizes in dir listing.
Help: `Don't use HEAD requests.
HEAD requests are mainly used to find file sizes in dir listing.
If your site is being very slow to load then you can try this option.
Normally rclone does a HEAD request for each potential file in a
directory listing to:
@@ -84,12 +84,9 @@ directory listing to:
- check it really exists
- check to see if it is a directory
If you set this option, rclone will not do the HEAD request. This will mean
- directory listings are much quicker
- rclone won't have the times or sizes of any files
- some files that don't exist may be in the listing
`,
If you set this option, rclone will not do the HEAD request. This will mean
that directory listings are much quicker, but rclone won't have the times or
sizes of any files, and some files that don't exist may be in the listing.`,
Default: false,
Advanced: true,
}},
@@ -133,11 +130,87 @@ func statusError(res *http.Response, err error) error {
}
if res.StatusCode < 200 || res.StatusCode > 299 {
_ = res.Body.Close()
return fmt.Errorf("HTTP Error %d: %s", res.StatusCode, res.Status)
return fmt.Errorf("HTTP Error: %s", res.Status)
}
return nil
}
// getFsEndpoint decides if url is to be considered a file or directory,
// and returns a proper endpoint url to use for the fs.
func getFsEndpoint(ctx context.Context, client *http.Client, url string, opt *Options) (string, bool) {
// If url ends with '/' it is already a proper url always assumed to be a directory.
if url[len(url)-1] == '/' {
return url, false
}
// If url does not end with '/' we send a HEAD request to decide
// if it is directory or file, and if directory appends the missing
// '/', or if file returns the directory url to parent instead.
createFileResult := func() (string, bool) {
fs.Debugf(nil, "If path is a directory you must add a trailing '/'")
parent, _ := path.Split(url)
return parent, true
}
createDirResult := func() (string, bool) {
fs.Debugf(nil, "To avoid the initial HEAD request add a trailing '/' to the path")
return url + "/", false
}
// If HEAD requests are not allowed we just have to assume it is a file.
if opt.NoHead {
fs.Debugf(nil, "Assuming path is a file as --http-no-head is set")
return createFileResult()
}
// Use a client which doesn't follow redirects so the server
// doesn't redirect http://host/dir to http://host/dir/
noRedir := *client
noRedir.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
req, err := http.NewRequestWithContext(ctx, "HEAD", url, nil)
if err != nil {
fs.Debugf(nil, "Assuming path is a file as HEAD request could not be created: %v", err)
return createFileResult()
}
addHeaders(req, opt)
res, err := noRedir.Do(req)
if err != nil {
fs.Debugf(nil, "Assuming path is a file as HEAD request could not be sent: %v", err)
return createFileResult()
}
if res.StatusCode == http.StatusNotFound {
fs.Debugf(nil, "Assuming path is a directory as HEAD response is it does not exist as a file (%s)", res.Status)
return createDirResult()
}
if res.StatusCode == http.StatusMovedPermanently ||
res.StatusCode == http.StatusFound ||
res.StatusCode == http.StatusSeeOther ||
res.StatusCode == http.StatusTemporaryRedirect ||
res.StatusCode == http.StatusPermanentRedirect {
redir := res.Header.Get("Location")
if redir != "" {
if redir[len(redir)-1] == '/' {
fs.Debugf(nil, "Assuming path is a directory as HEAD response is redirect (%s) to a path that ends with '/': %s", res.Status, redir)
return createDirResult()
}
fs.Debugf(nil, "Assuming path is a file as HEAD response is redirect (%s) to a path that does not end with '/': %s", res.Status, redir)
return createFileResult()
}
fs.Debugf(nil, "Assuming path is a file as HEAD response is redirect (%s) but no location header", res.Status)
return createFileResult()
}
if res.StatusCode < 200 || res.StatusCode > 299 {
// Example is 403 (http.StatusForbidden) for servers not allowing HEAD requests.
fs.Debugf(nil, "Assuming path is a file as HEAD response is an error (%s)", res.Status)
return createFileResult()
}
fs.Debugf(nil, "Assuming path is a file as HEAD response is success (%s)", res.Status)
return createFileResult()
}
// NewFs creates a new Fs object from the name and root. It connects to
// the host specified in the config file.
func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, error) {
@@ -168,37 +241,9 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
client := fshttp.NewClient(ctx)
var isFile = false
if !strings.HasSuffix(u.String(), "/") {
// Make a client which doesn't follow redirects so the server
// doesn't redirect http://host/dir to http://host/dir/
noRedir := *client
noRedir.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
// check to see if points to a file
req, err := http.NewRequestWithContext(ctx, "HEAD", u.String(), nil)
if err == nil {
addHeaders(req, opt)
res, err := noRedir.Do(req)
err = statusError(res, err)
if err == nil {
isFile = true
}
}
}
newRoot := u.String()
if isFile {
// Point to the parent if this is a file
newRoot, _ = path.Split(u.String())
} else {
if !strings.HasSuffix(newRoot, "/") {
newRoot += "/"
}
}
u, err = url.Parse(newRoot)
endpoint, isFile := getFsEndpoint(ctx, client, u.String(), opt)
fs.Debugf(nil, "Root: %s", endpoint)
u, err = url.Parse(endpoint)
if err != nil {
return nil, err
}
@@ -216,12 +261,16 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
f.features = (&fs.Features{
CanHaveEmptyDirectories: true,
}).Fill(ctx, f)
if isFile {
// return an error with an fs which points to the parent
return f, fs.ErrorIsFile
}
if !strings.HasSuffix(f.endpointURL, "/") {
return nil, errors.New("internal error: url doesn't end with /")
}
return f, nil
}
@@ -297,7 +346,7 @@ func parseName(base *url.URL, name string) (string, error) {
}
// check it doesn't have URL parameters
uStr := u.String()
if strings.Index(uStr, "?") >= 0 {
if strings.Contains(uStr, "?") {
return "", errFoundQuestionMark
}
// check that this is going back to the same host and scheme
@@ -409,7 +458,7 @@ func (f *Fs) readDir(ctx context.Context, dir string) (names []string, err error
return nil, fmt.Errorf("readDir: %w", err)
}
default:
return nil, fmt.Errorf("Can't parse content type %q", contentType)
return nil, fmt.Errorf("can't parse content type %q", contentType)
}
return names, nil
}

View File

@@ -8,8 +8,10 @@ import (
"net/http/httptest"
"net/url"
"os"
"path"
"path/filepath"
"sort"
"strconv"
"strings"
"testing"
"time"
@@ -24,10 +26,11 @@ import (
)
var (
remoteName = "TestHTTP"
testPath = "test"
filesPath = filepath.Join(testPath, "files")
headers = []string{"X-Potato", "sausage", "X-Rhubarb", "cucumber"}
remoteName = "TestHTTP"
testPath = "test"
filesPath = filepath.Join(testPath, "files")
headers = []string{"X-Potato", "sausage", "X-Rhubarb", "cucumber"}
lineEndSize = 1
)
// prepareServer the test server and return a function to tidy it up afterwards
@@ -35,6 +38,22 @@ func prepareServer(t *testing.T) (configmap.Simple, func()) {
// file server for test/files
fileServer := http.FileServer(http.Dir(filesPath))
// verify the file path is correct, and also check which line endings
// are used to get sizes right ("\n" except on Windows, but even there
// we may have "\n" or "\r\n" depending on git crlf setting)
fileList, err := ioutil.ReadDir(filesPath)
require.NoError(t, err)
require.Greater(t, len(fileList), 0)
for _, file := range fileList {
if !file.IsDir() {
data, _ := ioutil.ReadFile(filepath.Join(filesPath, file.Name()))
if strings.HasSuffix(string(data), "\r\n") {
lineEndSize = 2
}
break
}
}
// test the headers are there then pass on to fileServer
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
what := fmt.Sprintf("%s %s: Header ", r.Method, r.URL.Path)
@@ -91,7 +110,7 @@ func testListRoot(t *testing.T, f fs.Fs, noSlash bool) {
e = entries[1]
assert.Equal(t, "one%.txt", e.Remote())
assert.Equal(t, int64(6), e.Size())
assert.Equal(t, int64(5+lineEndSize), e.Size())
_, ok = e.(*Object)
assert.True(t, ok)
@@ -108,7 +127,7 @@ func testListRoot(t *testing.T, f fs.Fs, noSlash bool) {
_, ok = e.(fs.Directory)
assert.True(t, ok)
} else {
assert.Equal(t, int64(41), e.Size())
assert.Equal(t, int64(40+lineEndSize), e.Size())
_, ok = e.(*Object)
assert.True(t, ok)
}
@@ -141,7 +160,7 @@ func TestListSubDir(t *testing.T) {
e := entries[0]
assert.Equal(t, "three/underthree.txt", e.Remote())
assert.Equal(t, int64(9), e.Size())
assert.Equal(t, int64(8+lineEndSize), e.Size())
_, ok := e.(*Object)
assert.True(t, ok)
}
@@ -154,7 +173,7 @@ func TestNewObject(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "four/under four.txt", o.Remote())
assert.Equal(t, int64(9), o.Size())
assert.Equal(t, int64(8+lineEndSize), o.Size())
_, ok := o.(*Object)
assert.True(t, ok)
@@ -187,7 +206,11 @@ func TestOpen(t *testing.T) {
data, err := ioutil.ReadAll(fd)
require.NoError(t, err)
require.NoError(t, fd.Close())
assert.Equal(t, "beetroot\n", string(data))
if lineEndSize == 2 {
assert.Equal(t, "beetroot\r\n", string(data))
} else {
assert.Equal(t, "beetroot\n", string(data))
}
// Test with range request
fd, err = o.Open(context.Background(), &fs.RangeOption{Start: 1, End: 5})
@@ -236,7 +259,7 @@ func TestIsAFileSubDir(t *testing.T) {
e := entries[0]
assert.Equal(t, "underthree.txt", e.Remote())
assert.Equal(t, int64(9), e.Size())
assert.Equal(t, int64(8+lineEndSize), e.Size())
_, ok := e.(*Object)
assert.True(t, ok)
}
@@ -353,3 +376,106 @@ func TestParseCaddy(t *testing.T) {
"v1.36-22-g06ea13a-ssh-agentβ/",
})
}
func TestFsNoSlashRoots(t *testing.T) {
// Test Fs with roots that does not end with '/', the logic that
// decides if url is to be considered a file or directory, based
// on result from a HEAD request.
// Handler for faking HEAD responses with different status codes
headCount := 0
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == "HEAD" {
headCount++
responseCode, err := strconv.Atoi(path.Base(r.URL.String()))
require.NoError(t, err)
if strings.HasPrefix(r.URL.String(), "/redirect/") {
var redir string
if strings.HasPrefix(r.URL.String(), "/redirect/file/") {
redir = "/redirected"
} else if strings.HasPrefix(r.URL.String(), "/redirect/dir/") {
redir = "/redirected/"
} else {
require.Fail(t, "Redirect test requests must start with '/redirect/file/' or '/redirect/dir/'")
}
http.Redirect(w, r, redir, responseCode)
} else {
http.Error(w, http.StatusText(responseCode), responseCode)
}
}
})
// Make the test server
ts := httptest.NewServer(handler)
defer ts.Close()
// Configure the remote
configfile.Install()
m := configmap.Simple{
"type": "http",
"url": ts.URL,
}
// Test
for i, test := range []struct {
root string
isFile bool
}{
// 2xx success
{"parent/200", true},
{"parent/204", true},
// 3xx redirection Redirect status 301, 302, 303, 307, 308
{"redirect/file/301", true}, // Request is redirected to "/redirected"
{"redirect/dir/301", false}, // Request is redirected to "/redirected/"
{"redirect/file/302", true}, // Request is redirected to "/redirected"
{"redirect/dir/302", false}, // Request is redirected to "/redirected/"
{"redirect/file/303", true}, // Request is redirected to "/redirected"
{"redirect/dir/303", false}, // Request is redirected to "/redirected/"
{"redirect/file/304", true}, // Not really a redirect, handled like 4xx errors (below)
{"redirect/file/305", true}, // Not really a redirect, handled like 4xx errors (below)
{"redirect/file/306", true}, // Not really a redirect, handled like 4xx errors (below)
{"redirect/file/307", true}, // Request is redirected to "/redirected"
{"redirect/dir/307", false}, // Request is redirected to "/redirected/"
{"redirect/file/308", true}, // Request is redirected to "/redirected"
{"redirect/dir/308", false}, // Request is redirected to "/redirected/"
// 4xx client errors
{"parent/403", true}, // Forbidden status (head request blocked)
{"parent/404", false}, // Not found status
} {
for _, noHead := range []bool{false, true} {
var isFile bool
if noHead {
m.Set("no_head", "true")
isFile = true
} else {
m.Set("no_head", "false")
isFile = test.isFile
}
headCount = 0
f, err := NewFs(context.Background(), remoteName, test.root, m)
if noHead {
assert.Equal(t, 0, headCount)
} else {
assert.Equal(t, 1, headCount)
}
if isFile {
assert.ErrorIs(t, err, fs.ErrorIsFile)
} else {
assert.NoError(t, err)
}
var endpoint string
if isFile {
parent, _ := path.Split(test.root)
endpoint = "/" + parent
} else {
endpoint = "/" + test.root + "/"
}
what := fmt.Sprintf("i=%d, root=%q, isFile=%v, noHead=%v", i, test.root, isFile, noHead)
assert.Equal(t, ts.URL+endpoint, f.String(), what)
}
}
}

View File

@@ -7,6 +7,7 @@ import (
"encoding/base64"
"encoding/hex"
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"io"
@@ -931,49 +932,121 @@ func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err e
return entries, nil
}
// listFileDirFn is called from listFileDir to handle an object.
type listFileDirFn func(fs.DirEntry) error
type listStreamTime time.Time
// List the objects and directories into entries, from a
// special kind of JottaFolder representing a FileDirLis
func (f *Fs) listFileDir(ctx context.Context, remoteStartPath string, startFolder *api.JottaFolder, fn listFileDirFn) error {
pathPrefix := "/" + f.filePathRaw("") // Non-escaped prefix of API paths to be cut off, to be left with the remote path including the remoteStartPath
pathPrefixLength := len(pathPrefix)
startPath := path.Join(pathPrefix, remoteStartPath) // Non-escaped API path up to and including remoteStartPath, to decide if it should be created as a new dir object
startPathLength := len(startPath)
for i := range startFolder.Folders {
folder := &startFolder.Folders[i]
if !f.validFolder(folder) {
return nil
func (c *listStreamTime) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
var v string
if err := d.DecodeElement(&v, &start); err != nil {
return err
}
t, err := time.Parse(time.RFC3339, v)
if err != nil {
return err
}
*c = listStreamTime(t)
return nil
}
func (c listStreamTime) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf("\"%s\"", time.Time(c).Format(time.RFC3339))), nil
}
func parseListRStream(ctx context.Context, r io.Reader, trimPrefix string, filesystem *Fs, callback func(fs.DirEntry) error) error {
type stats struct {
Folders int `xml:"folders"`
Files int `xml:"files"`
}
var expected, actual stats
type xmlFile struct {
Path string `xml:"path"`
Name string `xml:"filename"`
Checksum string `xml:"md5"`
Size int64 `xml:"size"`
Modified listStreamTime `xml:"modified"`
Created listStreamTime `xml:"created"`
}
type xmlFolder struct {
Path string `xml:"path"`
}
addFolder := func(path string) error {
return callback(fs.NewDir(filesystem.opt.Enc.ToStandardPath(path), time.Time{}))
}
addFile := func(f *xmlFile) error {
return callback(&Object{
hasMetaData: true,
fs: filesystem,
remote: filesystem.opt.Enc.ToStandardPath(path.Join(f.Path, f.Name)),
size: f.Size,
md5: f.Checksum,
modTime: time.Time(f.Modified),
})
}
trimPathPrefix := func(p string) string {
p = strings.TrimPrefix(p, trimPrefix)
p = strings.TrimPrefix(p, "/")
return p
}
uniqueFolders := map[string]bool{}
decoder := xml.NewDecoder(r)
for {
t, err := decoder.Token()
if err != nil {
if err != io.EOF {
return err
}
break
}
folderPath := f.opt.Enc.ToStandardPath(path.Join(folder.Path, folder.Name))
folderPathLength := len(folderPath)
var remoteDir string
if folderPathLength > pathPrefixLength {
remoteDir = folderPath[pathPrefixLength+1:]
if folderPathLength > startPathLength {
d := fs.NewDir(remoteDir, time.Time(folder.ModifiedAt))
err := fn(d)
if err != nil {
return err
}
}
}
for i := range folder.Files {
file := &folder.Files[i]
if f.validFile(file) {
remoteFile := path.Join(remoteDir, f.opt.Enc.ToStandardName(file.Name))
o, err := f.newObjectWithInfo(ctx, remoteFile, file)
if err != nil {
return err
}
err = fn(o)
if err != nil {
switch se := t.(type) {
case xml.StartElement:
switch se.Name.Local {
case "file":
var f xmlFile
if err := decoder.DecodeElement(&f, &se); err != nil {
return err
}
f.Path = trimPathPrefix(f.Path)
actual.Files++
if !uniqueFolders[f.Path] {
uniqueFolders[f.Path] = true
actual.Folders++
if err := addFolder(f.Path); err != nil {
return err
}
}
if err := addFile(&f); err != nil {
return err
}
case "folder":
var f xmlFolder
if err := decoder.DecodeElement(&f, &se); err != nil {
return err
}
f.Path = trimPathPrefix(f.Path)
uniqueFolders[f.Path] = true
actual.Folders++
if err := addFolder(f.Path); err != nil {
return err
}
case "stats":
if err := decoder.DecodeElement(&expected, &se); err != nil {
return err
}
}
}
}
if expected.Folders != actual.Folders ||
expected.Files != actual.Files {
return fmt.Errorf("Invalid result from listStream: expected[%#v] != actual[%#v]", expected, actual)
}
return nil
}
@@ -988,12 +1061,27 @@ func (f *Fs) ListR(ctx context.Context, dir string, callback fs.ListRCallback) (
Path: f.filePath(dir),
Parameters: url.Values{},
}
opts.Parameters.Set("mode", "list")
opts.Parameters.Set("mode", "liststream")
list := walk.NewListRHelper(callback)
var resp *http.Response
var result api.JottaFolder // Could be JottaFileDirList, but JottaFolder is close enough
err = f.pacer.Call(func() (bool, error) {
resp, err = f.srv.CallXML(ctx, &opts, nil, &result)
resp, err = f.srv.Call(ctx, &opts)
if err != nil {
return shouldRetry(ctx, resp, err)
}
// liststream paths are /mountpoint/root/path
// so the returned paths should have /mountpoint/root/ trimmed
// as the caller is expecting path.
trimPrefix := path.Join("/", f.opt.Mountpoint, f.root)
err = parseListRStream(ctx, resp.Body, trimPrefix, f, func(d fs.DirEntry) error {
if d.Remote() == dir {
return nil
}
return list.Add(d)
})
_ = resp.Body.Close()
return shouldRetry(ctx, resp, err)
})
if err != nil {
@@ -1005,10 +1093,6 @@ func (f *Fs) ListR(ctx context.Context, dir string, callback fs.ListRCallback) (
}
return fmt.Errorf("couldn't list files: %w", err)
}
list := walk.NewListRHelper(callback)
err = f.listFileDir(ctx, dir, &result, func(entry fs.DirEntry) error {
return list.Add(entry)
})
if err != nil {
return err
}

View File

@@ -34,19 +34,15 @@ func init() {
Name: "endpoint",
Help: "The Koofr API endpoint to use.",
Default: "https://app.koofr.net",
Required: true,
Advanced: true,
}, {
Name: "mountid",
Help: "Mount ID of the mount to use.\n\nIf omitted, the primary mount is used.",
Required: false,
Default: "",
Advanced: true,
}, {
Name: "setmtime",
Help: "Does the backend support setting modification time.\n\nSet this to false if you use a mount ID that points to a Dropbox or Amazon Drive backend.",
Default: true,
Required: true,
Advanced: true,
}, {
Name: "user",

View File

@@ -1133,6 +1133,9 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
return err
}
// Wipe hashes before update
o.clearHashCache()
var symlinkData bytes.Buffer
// If the object is a regular file, create it.
// If it is a translated link, just read in the contents, and
@@ -1295,6 +1298,13 @@ func (o *Object) setMetadata(info os.FileInfo) {
}
}
// clearHashCache wipes any cached hashes for the object
func (o *Object) clearHashCache() {
o.fs.objectMetaMu.Lock()
o.hashes = nil
o.fs.objectMetaMu.Unlock()
}
// Stat an Object into info
func (o *Object) lstat() error {
info, err := o.fs.lstat(o.path)
@@ -1306,6 +1316,7 @@ func (o *Object) lstat() error {
// Remove an object
func (o *Object) Remove(ctx context.Context) error {
o.clearHashCache()
return remove(o.path)
}

View File

@@ -1,6 +1,7 @@
package local
import (
"bytes"
"context"
"io/ioutil"
"os"
@@ -12,6 +13,7 @@ import (
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/config/configmap"
"github.com/rclone/rclone/fs/hash"
"github.com/rclone/rclone/fs/object"
"github.com/rclone/rclone/fstest"
"github.com/rclone/rclone/lib/file"
"github.com/rclone/rclone/lib/readers"
@@ -166,3 +168,64 @@ func TestSymlinkError(t *testing.T) {
_, err := NewFs(context.Background(), "local", "/", m)
assert.Equal(t, errLinksAndCopyLinks, err)
}
// Test hashes on updating an object
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)
f := r.Flocal.(*Fs)
// Get the object
o, err := f.NewObject(ctx, filePath)
require.NoError(t, err)
// Test the hash is as we expect
md5, err := o.Hash(ctx, hash.MD5)
require.NoError(t, err)
assert.Equal(t, "9a0364b9e99bb480dd25e1f0284c8555", md5)
// Reupload it with diferent contents but same size and timestamp
var b = bytes.NewBufferString("CONTENT")
src := object.NewStaticObjectInfo(filePath, when, int64(b.Len()), true, nil, f)
err = o.Update(ctx, b, src)
require.NoError(t, err)
// Check the hash is as expected
md5, err = o.Hash(ctx, hash.MD5)
require.NoError(t, err)
assert.Equal(t, "45685e95985e20822fb2538a522a5ccf", md5)
}
// Test hashes on deleting an object
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)
f := r.Flocal.(*Fs)
// Get the object
o, err := f.NewObject(ctx, filePath)
require.NoError(t, err)
// Test the hash is as we expect
md5, err := o.Hash(ctx, hash.MD5)
require.NoError(t, err)
assert.Equal(t, "9a0364b9e99bb480dd25e1f0284c8555", md5)
// Delete the object
require.NoError(t, o.Remove(ctx))
// Test the hash cache is empty
require.Nil(t, o.(*Object).hashes)
// Test the hash returns an error
_, err = o.Hash(ctx, hash.MD5)
require.Error(t, err)
}

View File

@@ -65,9 +65,12 @@ var (
authPath = "/common/oauth2/v2.0/authorize"
tokenPath = "/common/oauth2/v2.0/token"
scopesWithSitePermission = []string{"Files.Read", "Files.ReadWrite", "Files.Read.All", "Files.ReadWrite.All", "offline_access", "Sites.Read.All"}
scopesWithoutSitePermission = []string{"Files.Read", "Files.ReadWrite", "Files.Read.All", "Files.ReadWrite.All", "offline_access"}
// Description of how to auth for this app for a business account
oauthConfig = &oauth2.Config{
Scopes: []string{"Files.Read", "Files.ReadWrite", "Files.Read.All", "Files.ReadWrite.All", "offline_access", "Sites.Read.All"},
Scopes: scopesWithSitePermission,
ClientID: rcloneClientID,
ClientSecret: obscure.MustReveal(rcloneEncryptedClientSecret),
RedirectURL: oauthutil.RedirectLocalhostURL,
@@ -137,6 +140,17 @@ Note that the chunks will be buffered into memory.`,
Help: "The type of the drive (" + driveTypePersonal + " | " + driveTypeBusiness + " | " + driveTypeSharepoint + ").",
Default: "",
Advanced: true,
}, {
Name: "disable_site_permission",
Help: `Disable the request for Sites.Read.All permission.
If set to true, you will no longer be able to search for a SharePoint site when
configuring drive ID, because rclone will not request Sites.Read.All permission.
Set it to true if your organization didn't assign Sites.Read.All permission to the
application, and your organization disallows users to consent app permission
request on their own.`,
Default: false,
Advanced: true,
}, {
Name: "expose_onenote_files",
Help: `Set to make OneNote files show up in directory listings.
@@ -374,6 +388,12 @@ func Config(ctx context.Context, name string, m configmap.Mapper, config fs.Conf
region, graphURL := getRegionURL(m)
if config.State == "" {
disableSitePermission, _ := m.Get("disable_site_permission")
if disableSitePermission == "true" {
oauthConfig.Scopes = scopesWithoutSitePermission
} else {
oauthConfig.Scopes = scopesWithSitePermission
}
oauthConfig.Endpoint = oauth2.Endpoint{
AuthURL: authEndpoint[region] + authPath,
TokenURL: authEndpoint[region] + tokenPath,
@@ -527,6 +547,7 @@ type Options struct {
ChunkSize fs.SizeSuffix `config:"chunk_size"`
DriveID string `config:"drive_id"`
DriveType string `config:"drive_type"`
DisableSitePermission bool `config:"disable_site_permission"`
ExposeOneNoteFiles bool `config:"expose_onenote_files"`
ServerSideAcrossConfigs bool `config:"server_side_across_configs"`
ListChunk int64 `config:"list_chunk"`
@@ -789,6 +810,11 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
}
rootURL := graphAPIEndpoint[opt.Region] + "/v1.0" + "/drives/" + opt.DriveID
if opt.DisableSitePermission {
oauthConfig.Scopes = scopesWithoutSitePermission
} else {
oauthConfig.Scopes = scopesWithSitePermission
}
oauthConfig.Endpoint = oauth2.Endpoint{
AuthURL: authEndpoint[opt.Region] + authPath,
TokenURL: authEndpoint[opt.Region] + tokenPath,

View File

@@ -136,7 +136,8 @@ func (q *quickXorHash) Write(p []byte) (n int, err error) {
func (q *quickXorHash) checkSum() (h [Size]byte) {
// Output the data as little endian bytes
ph := 0
for _, d := range q.data[:len(q.data)-1] {
for i := 0; i < len(q.data)-1; i++ {
d := q.data[i]
_ = h[ph+7] // bounds check
h[ph+0] = byte(d >> (8 * 0))
h[ph+1] = byte(d >> (8 * 1))

View File

@@ -2,8 +2,6 @@
// object storage system.
package pcloud
// FIXME implement ListR? /listfolder can do recursive lists
// FIXME cleanup returns login required?
// FIXME mime type? Fix overview if implement.
@@ -27,6 +25,7 @@ import (
"github.com/rclone/rclone/fs/config/obscure"
"github.com/rclone/rclone/fs/fserrors"
"github.com/rclone/rclone/fs/hash"
"github.com/rclone/rclone/fs/walk"
"github.com/rclone/rclone/lib/dircache"
"github.com/rclone/rclone/lib/encoder"
"github.com/rclone/rclone/lib/oauthutil"
@@ -246,7 +245,7 @@ func (f *Fs) readMetaDataForPath(ctx context.Context, path string) (info *api.It
return nil, err
}
found, err := f.listAll(ctx, directoryID, false, true, func(item *api.Item) bool {
found, err := f.listAll(ctx, directoryID, false, true, false, func(item *api.Item) bool {
if item.Name == leaf {
info = item
return true
@@ -380,7 +379,7 @@ func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) {
// FindLeaf finds a directory of name leaf in the folder with ID pathID
func (f *Fs) FindLeaf(ctx context.Context, pathID, leaf string) (pathIDOut string, found bool, err error) {
// Find the leaf in pathID
found, err = f.listAll(ctx, pathID, true, false, func(item *api.Item) bool {
found, err = f.listAll(ctx, pathID, true, false, false, func(item *api.Item) bool {
if item.Name == leaf {
pathIDOut = item.ID
return true
@@ -446,14 +445,16 @@ type listAllFn func(*api.Item) bool
// Lists the directory required calling the user function on each item found
//
// If the user fn ever returns true then it early exits with found = true
func (f *Fs) listAll(ctx context.Context, dirID string, directoriesOnly bool, filesOnly bool, fn listAllFn) (found bool, err error) {
func (f *Fs) listAll(ctx context.Context, dirID string, directoriesOnly bool, filesOnly bool, recursive bool, fn listAllFn) (found bool, err error) {
opts := rest.Opts{
Method: "GET",
Path: "/listfolder",
Parameters: url.Values{},
}
if recursive {
opts.Parameters.Set("recursive", "1")
}
opts.Parameters.Set("folderid", dirIDtoNumber(dirID))
// FIXME can do recursive
var result api.ItemResult
var resp *http.Response
@@ -465,26 +466,71 @@ func (f *Fs) listAll(ctx context.Context, dirID string, directoriesOnly bool, fi
if err != nil {
return found, fmt.Errorf("couldn't list files: %w", err)
}
for i := range result.Metadata.Contents {
item := &result.Metadata.Contents[i]
if item.IsFolder {
if filesOnly {
continue
var recursiveContents func(is []api.Item, path string)
recursiveContents = func(is []api.Item, path string) {
for i := range is {
item := &is[i]
if item.IsFolder {
if filesOnly {
continue
}
} else {
if directoriesOnly {
continue
}
}
} else {
if directoriesOnly {
continue
item.Name = path + f.opt.Enc.ToStandardName(item.Name)
if fn(item) {
found = true
break
}
if recursive {
recursiveContents(item.Contents, item.Name+"/")
}
}
item.Name = f.opt.Enc.ToStandardName(item.Name)
if fn(item) {
found = true
break
}
}
recursiveContents(result.Metadata.Contents, "")
return
}
// listHelper iterates over all items from the directory
// and calls the callback for each element.
func (f *Fs) listHelper(ctx context.Context, dir string, recursive bool, callback func(entries fs.DirEntry) error) (err error) {
directoryID, err := f.dirCache.FindDir(ctx, dir, false)
if err != nil {
return err
}
var iErr error
_, err = f.listAll(ctx, directoryID, false, false, recursive, func(info *api.Item) bool {
remote := path.Join(dir, info.Name)
if info.IsFolder {
// cache the directory ID for later lookups
f.dirCache.Put(remote, info.ID)
d := fs.NewDir(remote, info.ModTime()).SetID(info.ID)
// FIXME more info from dir?
iErr = callback(d)
} else {
o, err := f.newObjectWithInfo(ctx, remote, info)
if err != nil {
iErr = err
return true
}
iErr = callback(o)
}
if iErr != nil {
return true
}
return false
})
if err != nil {
return err
}
if iErr != nil {
return iErr
}
return nil
}
// List the objects and directories in dir into entries. The
// entries can be returned in any order but should be for a
// complete directory.
@@ -495,36 +541,24 @@ func (f *Fs) listAll(ctx context.Context, dirID string, directoriesOnly bool, fi
// This should return ErrDirNotFound if the directory isn't
// found.
func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) {
directoryID, err := f.dirCache.FindDir(ctx, dir, false)
if err != nil {
return nil, err
}
var iErr error
_, err = f.listAll(ctx, directoryID, false, false, func(info *api.Item) bool {
remote := path.Join(dir, info.Name)
if info.IsFolder {
// cache the directory ID for later lookups
f.dirCache.Put(remote, info.ID)
d := fs.NewDir(remote, info.ModTime()).SetID(info.ID)
// FIXME more info from dir?
entries = append(entries, d)
} else {
o, err := f.newObjectWithInfo(ctx, remote, info)
if err != nil {
iErr = err
return true
}
entries = append(entries, o)
}
return false
err = f.listHelper(ctx, dir, false, func(o fs.DirEntry) error {
entries = append(entries, o)
return nil
})
return entries, err
}
// ListR lists the objects and directories of the Fs starting
// from dir recursively into out.
func (f *Fs) ListR(ctx context.Context, dir string, callback fs.ListRCallback) (err error) {
list := walk.NewListRHelper(callback)
err = f.listHelper(ctx, dir, true, func(o fs.DirEntry) error {
return list.Add(o)
})
if err != nil {
return nil, err
return err
}
if iErr != nil {
return nil, iErr
}
return entries, nil
return list.Flush()
}
// Creates from the parameters passed in a half finished Object which

View File

@@ -761,7 +761,11 @@ func init() {
Provider: "Wasabi",
}, {
Value: "s3.ap-northeast-1.wasabisys.com",
Help: "Wasabi AP Northeast endpoint",
Help: "Wasabi AP Northeast 1 (Tokyo) endpoint",
Provider: "Wasabi",
}, {
Value: "s3.ap-northeast-2.wasabisys.com",
Help: "Wasabi AP Northeast 2 (Osaka) endpoint",
Provider: "Wasabi",
}},
}, {
@@ -1180,6 +1184,9 @@ If you leave it blank, this is calculated automatically from the sse_customer_ke
}, {
Value: "INTELLIGENT_TIERING",
Help: "Intelligent-Tiering storage class",
}, {
Value: "GLACIER_IR",
Help: "Glacier Instant Retrieval storage class",
}},
}, {
// Mapping from here: https://www.alibabacloud.com/help/doc-detail/64919.htm
@@ -3321,11 +3328,7 @@ func (o *Object) downloadFromURL(ctx context.Context, bucketPath string, options
return nil, err
}
size, err := strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64)
if err != nil {
fs.Debugf(o, "Failed to parse content length from string %s, %v", resp.Header.Get("Content-Length"), err)
}
contentLength := &size
contentLength := &resp.ContentLength
if resp.Header.Get("Content-Range") != "" {
var contentRange = resp.Header.Get("Content-Range")
slash := strings.IndexRune(contentRange, '/')

View File

@@ -42,7 +42,8 @@ const (
hashCommandNotSupported = "none"
minSleep = 100 * time.Millisecond
maxSleep = 2 * time.Second
decayConstant = 2 // bigger for slower decay, exponential
decayConstant = 2 // bigger for slower decay, exponential
keepAliveInterval = time.Minute // send keepalives every this long while running commands
)
var (
@@ -59,11 +60,13 @@ func init() {
Help: "SSH host to connect to.\n\nE.g. \"example.com\".",
Required: true,
}, {
Name: "user",
Help: "SSH username, leave blank for current username, " + currentUser + ".",
Name: "user",
Help: "SSH username.",
Default: currentUser,
}, {
Name: "port",
Help: "SSH port, leave blank to use default (22).",
Name: "port",
Help: "SSH port number.",
Default: 22,
}, {
Name: "pass",
Help: "SSH password, leave blank to use ssh-agent.",
@@ -339,6 +342,32 @@ func (c *conn) wait() {
c.err <- c.sshClient.Conn.Wait()
}
// Send a keepalive over the ssh connection
func (c *conn) sendKeepAlive() {
_, _, err := c.sshClient.SendRequest("keepalive@openssh.com", true, nil)
if err != nil {
fs.Debugf(nil, "Failed to send keep alive: %v", err)
}
}
// Send keepalives every interval over the ssh connection until done is closed
func (c *conn) sendKeepAlives(interval time.Duration) (done chan struct{}) {
done = make(chan struct{})
go func() {
t := time.NewTicker(interval)
defer t.Stop()
for {
select {
case <-t.C:
c.sendKeepAlive()
case <-done:
return
}
}
}()
return done
}
// Closes the connection
func (c *conn) close() error {
sftpErr := c.sftpClient.Close()
@@ -1098,6 +1127,9 @@ func (f *Fs) run(ctx context.Context, cmd string) ([]byte, error) {
}
defer f.putSftpConnection(&c, err)
// Send keepalives while the connection is open
defer close(c.sendKeepAlives(keepAliveInterval))
session, err := c.sshClient.NewSession()
if err != nil {
return nil, fmt.Errorf("run: get SFTP session: %w", err)
@@ -1110,10 +1142,12 @@ func (f *Fs) run(ctx context.Context, cmd string) ([]byte, error) {
session.Stdout = &stdout
session.Stderr = &stderr
fs.Debugf(f, "Running remote command: %s", cmd)
err = session.Run(cmd)
if err != nil {
return nil, fmt.Errorf("failed to run %q: %s: %w", cmd, stderr.Bytes(), err)
return nil, fmt.Errorf("failed to run %q: %s: %w", cmd, bytes.TrimSpace(stderr.Bytes()), err)
}
fs.Debugf(f, "Remote command result: %s", bytes.TrimSpace(stdout.Bytes()))
return stdout.Bytes(), nil
}
@@ -1230,8 +1264,6 @@ func (o *Object) Remote() string {
// Hash returns the selected checksum of the file
// If no checksum is available it returns ""
func (o *Object) Hash(ctx context.Context, r hash.Type) (string, error) {
o.fs.addSession() // Show session in use
defer o.fs.removeSession()
if o.fs.opt.DisableHashCheck {
return "", nil
}
@@ -1255,36 +1287,16 @@ func (o *Object) Hash(ctx context.Context, r hash.Type) (string, error) {
return "", hash.ErrUnsupported
}
c, err := o.fs.getSftpConnection(ctx)
if err != nil {
return "", fmt.Errorf("Hash get SFTP connection: %w", err)
}
session, err := c.sshClient.NewSession()
o.fs.putSftpConnection(&c, err)
if err != nil {
return "", fmt.Errorf("Hash put SFTP connection: %w", err)
}
var stdout, stderr bytes.Buffer
session.Stdout = &stdout
session.Stderr = &stderr
escapedPath := shellEscape(o.path())
if o.fs.opt.PathOverride != "" {
escapedPath = shellEscape(path.Join(o.fs.opt.PathOverride, o.remote))
}
err = session.Run(hashCmd + " " + escapedPath)
fs.Debugf(nil, "sftp cmd = %s", escapedPath)
b, err := o.fs.run(ctx, hashCmd+" "+escapedPath)
if err != nil {
_ = session.Close()
fs.Debugf(o, "Failed to calculate %v hash: %v (%s)", r, err, bytes.TrimSpace(stderr.Bytes()))
return "", nil
return "", fmt.Errorf("failed to calculate %v hash: %w", r, err)
}
_ = session.Close()
b := stdout.Bytes()
fs.Debugf(nil, "sftp output = %q", b)
str := parseHash(b)
fs.Debugf(nil, "sftp hash = %q", str)
if r == hash.MD5 {
o.md5sum = &str
} else if r == hash.SHA1 {

View File

@@ -84,10 +84,9 @@ func init() {
},
Options: []fs.Option{
{
Name: fs.ConfigProvider,
Help: "Choose an authentication method.",
Required: true,
Default: existingProvider,
Name: fs.ConfigProvider,
Help: "Choose an authentication method.",
Default: existingProvider,
Examples: []fs.OptionExample{{
Value: "existing",
Help: "Use an existing access grant.",
@@ -99,13 +98,11 @@ func init() {
{
Name: "access_grant",
Help: "Access grant.",
Required: false,
Provider: "existing",
},
{
Name: "satellite_address",
Help: "Satellite address.\n\nCustom satellite address should match the format: `<nodeid>@<address>:<port>`.",
Required: false,
Provider: newProvider,
Default: "us-central-1.tardigrade.io",
Examples: []fs.OptionExample{{
@@ -123,13 +120,11 @@ func init() {
{
Name: "api_key",
Help: "API key.",
Required: false,
Provider: newProvider,
},
{
Name: "passphrase",
Help: "Encryption passphrase.\n\nTo access existing objects enter passphrase used for uploading.",
Required: false,
Provider: newProvider,
},
},

View File

@@ -33,25 +33,21 @@ func init() {
Help: "List of space separated upstreams.\n\nCan be 'upstreama:test/dir upstreamb:', '\"upstreama:test/space:ro dir\" upstreamb:', etc.",
Required: true,
}, {
Name: "action_policy",
Help: "Policy to choose upstream on ACTION category.",
Required: true,
Default: "epall",
Name: "action_policy",
Help: "Policy to choose upstream on ACTION category.",
Default: "epall",
}, {
Name: "create_policy",
Help: "Policy to choose upstream on CREATE category.",
Required: true,
Default: "epmfs",
Name: "create_policy",
Help: "Policy to choose upstream on CREATE category.",
Default: "epmfs",
}, {
Name: "search_policy",
Help: "Policy to choose upstream on SEARCH category.",
Required: true,
Default: "ff",
Name: "search_policy",
Help: "Policy to choose upstream on SEARCH category.",
Default: "ff",
}, {
Name: "cache_time",
Help: "Cache time of usage and free space (in seconds).\n\nThis option is only useful when a path preserving policy is used.",
Required: true,
Default: 120,
Name: "cache_time",
Help: "Cache time of usage and free space (in seconds).\n\nThis option is only useful when a path preserving policy is used.",
Default: 120,
}},
}
fs.Register(fsi)

View File

@@ -6,8 +6,6 @@ import (
"fmt"
"io"
"math"
"path"
"path/filepath"
"strings"
"sync"
"sync/atomic"
@@ -91,7 +89,7 @@ func New(ctx context.Context, remote, root string, cacheTime time.Duration) (*Fs
return nil, err
}
f.RootFs = rFs
rootString := path.Join(remote, filepath.ToSlash(root))
rootString := fspath.JoinRootPath(remote, root)
myFs, err := cache.Get(ctx, rootString)
if err != nil && err != fs.ErrorIsFile {
return nil, err

View File

@@ -66,6 +66,11 @@ func init() {
})
},
Options: append(oauthutil.SharedOptions, []fs.Option{{
Name: "hard_delete",
Help: "Delete files permanently rather than putting them into the trash.",
Default: false,
Advanced: true,
}, {
Name: config.ConfigEncoding,
Help: config.ConfigEncodingHelp,
Advanced: true,
@@ -79,8 +84,9 @@ func init() {
// Options defines the configuration for this backend
type Options struct {
Token string `config:"token"`
Enc encoder.MultiEncoder `config:"encoding"`
Token string `config:"token"`
HardDelete bool `config:"hard_delete"`
Enc encoder.MultiEncoder `config:"encoding"`
}
// Fs represents a remote yandex
@@ -630,7 +636,7 @@ func (f *Fs) purgeCheck(ctx context.Context, dir string, check bool) error {
}
}
//delete directory
return f.delete(ctx, root, false)
return f.delete(ctx, root, f.opt.HardDelete)
}
// Rmdir deletes the container
@@ -1141,7 +1147,7 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
// Remove an object
func (o *Object) Remove(ctx context.Context) error {
return o.fs.delete(ctx, o.filePath(), false)
return o.fs.delete(ctx, o.filePath(), o.fs.opt.HardDelete)
}
// MimeType of an Object if known, "" otherwise

View File

@@ -41,7 +41,7 @@ You can discover what commands a backend implements by using
rclone backend help <backendname>
You can also discover information about the backend using (see
[operations/fsinfo](/rc/#operations/fsinfo) in the remote control docs
[operations/fsinfo](/rc/#operations-fsinfo) in the remote control docs
for more info).
rclone backend features remote:
@@ -55,7 +55,7 @@ Pass arguments to the backend by placing them on the end of the line
rclone backend cleanup remote:path file1 file2 file3
Note to run these commands on a running backend then see
[backend/command](/rc/#backend/command) in the rc docs.
[backend/command](/rc/#backend-command) in the rc docs.
`,
RunE: func(command *cobra.Command, args []string) error {
cmd.CheckArgs(2, 1e6, command, args)
@@ -149,7 +149,7 @@ See [the "rclone backend" command](/commands/rclone_backend/) for more
info on how to pass options and arguments.
These can be run on a running backend using the rc command
[backend/command](/rc/#backend/command).
[backend/command](/rc/#backend-command).
`, name)
for _, cmd := range cmds {

View File

@@ -10,11 +10,17 @@
package cmount
import (
"runtime"
"testing"
"github.com/rclone/rclone/fstest/testy"
"github.com/rclone/rclone/vfs/vfstest"
)
func TestMount(t *testing.T) {
// Disable tests under macOS and the CI since they are locking up
if runtime.GOOS == "darwin" {
testy.SkipUnreliable(t)
}
vfstest.RunTests(t, false, mount)
}

View File

@@ -165,7 +165,7 @@ func runRoot(cmd *cobra.Command, args []string) {
// setupRootCommand sets default usage, help, and error handling for
// the root command.
//
// Helpful example: http://rtfcode.com/xref/moby-17.03.2-ce/cli/cobra.go
// Helpful example: https://github.com/moby/moby/blob/master/cli/cobra.go
func setupRootCommand(rootCmd *cobra.Command) {
ci := fs.GetConfig(context.Background())
// Add global flags

View File

@@ -16,11 +16,16 @@ import (
"github.com/rclone/rclone/cmd/mountlib"
"github.com/rclone/rclone/fs/config/configfile"
"github.com/rclone/rclone/fs/rc"
"github.com/rclone/rclone/fstest/testy"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRc(t *testing.T) {
// Disable tests under macOS and the CI since they are locking up
if runtime.GOOS == "darwin" {
testy.SkipUnreliable(t)
}
ctx := context.Background()
configfile.Install()
mount := rc.Calls.Get("mount/mount")

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 724 B

View File

@@ -23,6 +23,7 @@ import (
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/config"
"github.com/rclone/rclone/fstest"
"github.com/rclone/rclone/fstest/testy"
"github.com/rclone/rclone/lib/file"
"github.com/stretchr/testify/assert"
@@ -303,6 +304,10 @@ func (a *APIClient) request(path string, in, out interface{}, wantErr bool) {
}
func testMountAPI(t *testing.T, sockAddr string) {
// Disable tests under macOS and the CI since they are locking up
if runtime.GOOS == "darwin" {
testy.SkipUnreliable(t)
}
if _, mountFn := mountlib.ResolveMountMethod(""); mountFn == nil {
t.Skip("Test requires working mount command")
}

View File

@@ -16,7 +16,10 @@ import (
)
// Help describes the options for the serve package
var Help = `--template allows a user to specify a custom markup template for http
var Help = `
#### Template
--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:

View File

@@ -23,7 +23,7 @@ func AddFlagsPrefix(flagSet *pflag.FlagSet, prefix string, Opt *httplib.Options)
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.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")

View File

@@ -43,7 +43,6 @@ func init() {
flags.StringVarP(cmdFlags, &outFileName, "output", "o", "", "Output to file instead of stdout")
// Files
flags.BoolVarP(cmdFlags, &opts.ByteSize, "size", "s", false, "Print the size in bytes of each file.")
flags.BoolVarP(cmdFlags, &opts.UnitSize, "human", "", false, "Print the size in a more human readable way.")
flags.BoolVarP(cmdFlags, &opts.FileMode, "protections", "p", false, "Print the protections for each file.")
// flags.BoolVarP(cmdFlags, &opts.ShowUid, "uid", "", false, "Displays file owner or UID number.")
// flags.BoolVarP(cmdFlags, &opts.ShowGid, "gid", "", false, "Displays file group owner or GID number.")

View File

@@ -33,7 +33,7 @@ First run:
This will guide you through an interactive setup process:
```
No remotes found - make a new one
No remotes found, make a new one?
n) New remote
s) Set configuration password
q) Quit config

View File

@@ -53,7 +53,7 @@ Here is an example of how to make a remote called `remote`. First run:
This will guide you through an interactive setup process:
```
No remotes found - make a new one
No remotes found, make a new one?
n) New remote
r) Rename remote
c) Copy remote

View File

@@ -550,3 +550,10 @@ put them back in again.` >}}
* Fredric Arklid <fredric.arklid@consid.se>
* Andy Jackson <Andrew.Jackson@bl.uk>
* Sinan Tan <i@tinytangent.com>
* deinferno <14363193+deinferno@users.noreply.github.com>
* rsapkf <rsapkfff@pm.me>
* Will Holtz <wholtz@gmail.com>
* GGG KILLER <gggkiller2@gmail.com>
* Logeshwaran Murugesan <logeshwaran@testpress.in>
* Lu Wang <coolwanglu@gmail.com>
* Bumsu Hyeon <ksitht@gmail.com>

View File

@@ -19,7 +19,7 @@ configuration. For a remote called `remote`. First run:
This will guide you through an interactive setup process:
```
No remotes found - make a new one
No remotes found, make a new one?
n) New remote
s) Set configuration password
q) Quit config
@@ -81,6 +81,14 @@ key. It is stored using RFC3339 Format time with nanosecond
precision. The metadata is supplied during directory listings so
there is no overhead to using it.
### Performance
When uploading large files, increasing the value of
`--azureblob-upload-concurrency` will increase performance at the cost
of using more memory. The default of 16 is set quite conservatively to
use less memory. It maybe be necessary raise it to 64 or higher to
fully utilize a 1 GBit/s link with a single file transfer.
### Restricted filename characters
In addition to the [default restricted characters set](/overview/#restricted-characters)

View File

@@ -23,7 +23,7 @@ recommended method. See below for further details on generating and using
an Application Key.
```
No remotes found - make a new one
No remotes found, make a new one?
n) New remote
q) Quit config
n/q> n

View File

@@ -22,7 +22,7 @@ Here is an example of how to make a remote called `remote`. First run:
This will guide you through an interactive setup process:
```
No remotes found - make a new one
No remotes found, make a new one?
n) New remote
s) Set configuration password
q) Quit config

View File

@@ -34,7 +34,7 @@ Here is an example of how to make a remote called `test-cache`. First run:
This will guide you through an interactive setup process:
```
No remotes found - make a new one
No remotes found, make a new one?
n) New remote
r) Rename remote
c) Copy remote

View File

@@ -25,7 +25,7 @@ Now configure `chunker` using `rclone config`. We will call this one `overlay`
to separate it from the `remote` itself.
```
No remotes found - make a new one
No remotes found, make a new one?
n) New remote
s) Set configuration password
q) Quit config

View File

@@ -107,8 +107,9 @@ At the end of the non interactive process, rclone will return a result
with `State` as empty string.
If `--all` is passed then rclone will ask all the config questions,
not just the post config questions. Any parameters are used as
defaults for questions as usual.
not just the post config questions. Parameters that are supplied on
the command line or from environment variables are used as defaults
for questions as usual.
Note that `bin/config.py` in the rclone source implements this protocol
as a readable demonstration.

View File

@@ -74,7 +74,8 @@ Now the `dedupe` session
s) Skip and do nothing
k) Keep just one (choose which in next step)
r) Rename all to be different (by changing file.jpg to file-1.jpg)
s/k/r> k
q) Quit
s/k/r/q> k
Enter the number of the file to keep> 1
one.txt: Deleted 1 extra copies
two.txt: Found 3 files with duplicate names
@@ -85,7 +86,8 @@ Now the `dedupe` session
s) Skip and do nothing
k) Keep just one (choose which in next step)
r) Rename all to be different (by changing file.jpg to file-1.jpg)
s/k/r> r
q) Quit
s/k/r/q> r
two-1.txt: renamed from: two.txt
two-2.txt: renamed from: two.txt
two-3.txt: renamed from: two.txt

View File

@@ -86,7 +86,7 @@ configure a dedicated path for encrypted content, and access it
exclusively through a crypt remote.
```
No remotes found - make a new one
No remotes found, make a new one?
n) New remote
s) Set configuration password
q) Quit config
@@ -369,7 +369,7 @@ Obfuscation cannot be relied upon for strong protection.
Cloud storage systems have limits on file name length and
total path length which rclone is more likely to breach using
"Standard" file name encryption. Where file names are less thn 156
"Standard" file name encryption. Where file names are less than 156
characters in length issues should not be encountered, irrespective of
cloud storage provider.

View File

@@ -22,7 +22,7 @@ Here is an example of how to make a remote called `remote`. First run:
This will guide you through an interactive setup process:
```
No remotes found - make a new one
No remotes found, make a new one?
n) New remote
r) Rename remote
c) Copy remote

View File

@@ -25,7 +25,7 @@ Here is an example of how to make a remote called `remote`. First run:
This will guide you through an interactive setup process:
```
No remotes found - make a new one
No remotes found, make a new one?
n) New remote
s) Set configuration password
q) Quit config

View File

@@ -23,7 +23,7 @@ Here is an example of how to make a remote called `remote`. First run:
This will guide you through an interactive setup process:
```
No remotes found - make a new one
No remotes found, make a new one?
n) New remote
s) Set configuration password
q) Quit config

View File

@@ -112,13 +112,13 @@ These flags are available for every command.
--rc-enable-metrics Enable prometheus metrics on /metrics
--rc-files string Path to local files to serve on the HTTP server
--rc-htpasswd string htpasswd file - if not provided no authentication is done
--rc-job-expire-duration duration expire finished async jobs older than this value (default 1m0s)
--rc-job-expire-interval duration interval to check for expired async jobs (default 10s)
--rc-job-expire-duration duration Expire finished async jobs older than this value (default 1m0s)
--rc-job-expire-interval duration Interval to check for expired async jobs (default 10s)
--rc-key string SSL PEM Private key
--rc-max-header-bytes int Maximum size of request header (default 4096)
--rc-no-auth Don't require auth for certain methods
--rc-pass string Password for authentication
--rc-realm string realm for authentication (default "rclone")
--rc-realm string Realm for authentication (default "rclone")
--rc-serve Enable the serving of remote objects
--rc-server-read-timeout duration Timeout for server reading data (default 1h0m0s)
--rc-server-write-timeout duration Timeout for server writing data (default 1h0m0s)
@@ -339,7 +339,7 @@ and may be set in the config file.
--ftp-idle-timeout Duration Max time before closing idle connections (default 1m0s)
--ftp-no-check-certificate Do not verify the TLS certificate of the server
--ftp-pass string FTP password (obscured)
--ftp-port string FTP port, leave blank to use default (21)
--ftp-port string FTP port number (default 21)
--ftp-shut-timeout Duration Maximum time to wait for data connection closing status (default 1m0s)
--ftp-tls Use Implicit FTPS (FTP over TLS)
--ftp-tls-cache-size int Size of TLS session cache for all control and data connections (default 32)
@@ -528,7 +528,7 @@ and may be set in the config file.
--sftp-md5sum-command string The command used to read md5 hashes
--sftp-pass string SSH password, leave blank to use ssh-agent (obscured)
--sftp-path-override string Override path used by SSH connection
--sftp-port string SSH port, leave blank to use default (22)
--sftp-port string SSH port number (default 22)
--sftp-pubkey-file string Optional path to public key file
--sftp-server-command string Specifies the path or command to run a sftp server on the remote host
--sftp-set-modtime Set the modified time on the remote if set (default true)

View File

@@ -27,7 +27,7 @@ For an anonymous FTP server, use `anonymous` as username and your email
address as password.
```
No remotes found - make a new one
No remotes found, make a new one?
n) New remote
r) Rename remote
c) Copy remote
@@ -51,11 +51,11 @@ Choose a number from below, or type in your own value
1 / Connect to ftp.example.com
\ "ftp.example.com"
host> ftp.example.com
FTP username, leave blank for current username, $USER
Enter a string value. Press Enter for the default ("").
FTP username
Enter a string value. Press Enter for the default ("$USER").
user>
FTP port, leave blank to use default (21)
Enter a string value. Press Enter for the default ("").
FTP port number
Enter a signed integer. Press Enter for the default (21).
port>
FTP password
y) Yes type in my own password

View File

@@ -26,7 +26,7 @@ Here is an example of how to make a remote called `remote`. First run:
This will guide you through an interactive setup process:
```
No remotes found - make a new one
No remotes found, make a new one?
n) New remote
s) Set configuration password
q) Quit config

View File

@@ -28,7 +28,7 @@ Now proceed to interactive or manual configuration.
Run `rclone config`:
```
No remotes found - make a new one
No remotes found, make a new one?
n) New remote
s) Set configuration password
q) Quit config

View File

@@ -19,7 +19,7 @@ Here is an example of how to make a remote called `remote`. First run:
This will guide you through an interactive setup process:
```
No remotes found - make a new one
No remotes found, make a new one?
n) New remote
s) Set configuration password
q) Quit config

View File

@@ -12,7 +12,26 @@ webservers such as Apache/Nginx/Caddy and will likely work with file
listings from most web servers. (If it doesn't then please file an
issue, or send a pull request!)
Paths are specified as `remote:` or `remote:path/to/dir`.
Paths are specified as `remote:` or `remote:path`.
The `remote:` represents the configured [url](#http-url), and any path following
it will be resolved relative to this url, according to the URL standard. This
means with remote url `https://beta.rclone.org/branch` and path `fix`, the
resolved URL will be `https://beta.rclone.org/branch/fix`, while with path
`/fix` the resolved URL will be `https://beta.rclone.org/fix` as the absolute
path is resolved from the root of the domain.
If the path following the `remote:` ends with `/` it will be assumed to point
to a directory. If the path does not end with `/`, then a HEAD request is sent
and the response used to decide if it it is treated as a file or a directory
(run with `-vv` to see details). When [--http-no-head](#http-no-head) is
specified, a path without ending `/` is always assumed to be a file. If rclone
incorrectly assumes the path is a file, the solution is to specify the path with
ending `/`. When you know the path is a directory, ending it with `/` is always
better as it avoids the initial HEAD request.
To just download a single file it is easier to use
[copyurl](/commands/rclone_copyurl/).
## Configuration
@@ -24,7 +43,7 @@ run:
This will guide you through an interactive setup process:
```
No remotes found - make a new one
No remotes found, make a new one?
n) New remote
s) Set configuration password
q) Quit config
@@ -81,25 +100,29 @@ Sync the remote `directory` to `/home/local/directory`, deleting any excess file
rclone sync -i remote:directory /home/local/directory
### Read only ###
### Read only
This remote is read only - you can't upload files to an HTTP server.
### Modified time ###
### Modified time
Most HTTP servers store time accurate to 1 second.
### Checksum ###
### Checksum
No checksums are stored.
### Usage without a config file ###
### Usage without a config file
Since the http remote only has one config parameter it is easy to use
without a config file:
rclone lsd --http-url https://beta.rclone.org :http:
or:
rclone lsd :http,url='https://beta.rclone.org':
{{< rem autogenerated options start" - DO NOT EDIT - instead edit fs.RegInfo in backend/http/http.go then run make backenddocs" >}}
### Standard options

View File

@@ -69,7 +69,7 @@ Here is an example of how to make a remote called `remote` with the default setu
This will guide you through an interactive setup process:
```
No remotes found - make a new one
No remotes found, make a new one?
n) New remote
s) Set configuration password
q) Quit config
@@ -148,9 +148,11 @@ To copy a local directory to an Jottacloud directory called backup
The official Jottacloud client registers a device for each computer you install it on,
and then creates a mountpoint for each folder you select for Backup.
The web interface uses a special device called Jotta for the Archive and Sync mountpoints.
In most cases you'll want to use the Jotta/Archive device/mountpoint, however if you want to access
files uploaded by any of the official clients rclone provides the option to select other devices
and mountpoints during config.
With rclone you'll want to use the Jotta/Archive device/mountpoint in most cases, however if you
want to access files uploaded by any of the official clients rclone provides the option to select
other devices and mountpoints during config. Note that uploading files is currently not supported
to other devices than Jotta.
The built-in Jotta device may also contain several other mountpoints, such as: Latest, Links, Shared and Trash.
These are special mountpoints with a different internal representation than the "regular" mountpoints.
@@ -178,9 +180,10 @@ flag.
Note that Jottacloud requires the MD5 hash before upload so if the
source does not have an MD5 checksum then the file will be cached
temporarily on disk (wherever the `TMPDIR` environment variable points
to) before it is uploaded. Small files will be cached in memory - see
the [--jottacloud-md5-memory-limit](#jottacloud-md5-memory-limit) flag.
temporarily on disk (in location given by
[--temp-dir](/docs/#temp-dir-dir)) before it is uploaded.
Small files will be cached in memory - see the
[--jottacloud-md5-memory-limit](#jottacloud-md5-memory-limit) flag.
When uploading from local disk the source checksum is always available,
so this does not apply. Starting with rclone version 1.52 the same is
true for crypted remotes (in older versions the crypt backend would not

View File

@@ -23,7 +23,7 @@ Here is an example of how to make a remote called `koofr`. First run:
This will guide you through an interactive setup process:
```
No remotes found - make a new one
No remotes found, make a new one?
n) New remote
s) Set configuration password
q) Quit config

View File

@@ -32,7 +32,7 @@ account and choose a tariff, then run
This will guide you through an interactive setup process:
```
No remotes found - make a new one
No remotes found, make a new one?
n) New remote
s) Set configuration password
q) Quit config

View File

@@ -27,7 +27,7 @@ Here is an example of how to make a remote called `remote`. First run:
This will guide you through an interactive setup process:
```
No remotes found - make a new one
No remotes found, make a new one?
n) New remote
s) Set configuration password
q) Quit config

View File

@@ -18,7 +18,7 @@ You can configure it as a remote like this with `rclone config` too if
you want to:
```
No remotes found - make a new one
No remotes found, make a new one?
n) New remote
s) Set configuration password
q) Quit config

View File

@@ -132,11 +132,13 @@ Client ID and Key by following the steps below:
2. Enter a name for your app, choose account type `Accounts in any organizational directory (Any Azure AD directory - Multitenant) and personal Microsoft accounts (e.g. Skype, Xbox)`, select `Web` in `Redirect URI`, then type (do not copy and paste) `http://localhost:53682/` and click Register. Copy and keep the `Application (client) ID` under the app name for later use.
3. Under `manage` select `Certificates & secrets`, click `New client secret`. Enter a description (can be anything) and set `Expires` to 24 months. Copy and keep that secret _Value_ for later use (you _won't_ be able to see this value afterwards).
4. Under `manage` select `API permissions`, click `Add a permission` and select `Microsoft Graph` then select `delegated permissions`.
5. Search and select the following permissions: `Files.Read`, `Files.ReadWrite`, `Files.Read.All`, `Files.ReadWrite.All`, `offline_access`, `User.Read`. Once selected click `Add permissions` at the bottom.
5. Search and select the following permissions: `Files.Read`, `Files.ReadWrite`, `Files.Read.All`, `Files.ReadWrite.All`, `offline_access`, `User.Read`, and optionally `Sites.Read.All` (see below). Once selected click `Add permissions` at the bottom.
Now the application is complete. Run `rclone config` to create or edit a OneDrive remote.
Supply the app ID and password as Client ID and Secret, respectively. rclone will walk you through the remaining steps.
The `Sites.Read.All` permission is required if you need to [search SharePoint sites when configuring the remote](https://github.com/rclone/rclone/pull/5883). However, if that permission is not assigned, you need to set `disable_site_permission` option to true in the advanced options.
### Modification time and hashes
OneDrive allows modification times to be set on objects accurate to 1
@@ -493,7 +495,7 @@ setting:
4. `Set-SPOTenant -EnableMinimumVersionRequirement $False`
5. `Disconnect-SPOService` (to disconnect from the server)
*Below are the steps for normal users to disable versioning. If you don't see the "No Versioning" option, make sure the above requirements are met.*
*Below are the steps for normal users to disable versioning. If you don't see the "No Versioning" option, make sure the above requirements are met.*
User [Weropol](https://github.com/Weropol) has found a method to disable
versioning on OneDrive
@@ -527,8 +529,8 @@ is a great way to see what it would do.
### Excessive throttling or blocked on SharePoint
If you experience excessive throttling or is being blocked on SharePoint then it may help to set the user agent explicitly with a flag like this: `--user-agent "ISV|rclone.org|rclone/v1.55.1"`
If you experience excessive throttling or is being blocked on SharePoint then it may help to set the user agent explicitly with a flag like this: `--user-agent "ISV|rclone.org|rclone/v1.55.1"`
The specific details can be found in the Microsoft document: [Avoid getting throttled or blocked in SharePoint Online](https://docs.microsoft.com/en-us/sharepoint/dev/general-development/how-to-avoid-getting-throttled-or-blocked-in-sharepoint-online#how-to-decorate-your-http-traffic-to-avoid-throttling)
### Unexpected file size/hash differences on Sharepoint ####
@@ -537,7 +539,7 @@ It is a
[known](https://github.com/OneDrive/onedrive-api-docs/issues/935#issuecomment-441741631)
issue that Sharepoint (not OneDrive or OneDrive for Business) silently modifies
uploaded files, mainly Office files (.docx, .xlsx, etc.), causing file size and
hash checks to fail. There are also other situations that will cause OneDrive to
hash checks to fail. There are also other situations that will cause OneDrive to
report inconsistent file sizes. To use rclone with such
affected files on Sharepoint, you
may disable these checks with the following command line arguments:
@@ -548,9 +550,9 @@ may disable these checks with the following command line arguments:
Alternatively, if you have write access to the OneDrive files, it may be possible
to fix this problem for certain files, by attempting the steps below.
Open the web interface for [OneDrive](https://onedrive.live.com) and find the
Open the web interface for [OneDrive](https://onedrive.live.com) and find the
affected files (which will be in the error messages/log for rclone). Simply click on
each of these files, causing OneDrive to open them on the web. This will cause each
each of these files, causing OneDrive to open them on the web. This will cause each
file to be converted in place to a format that is functionally equivalent
but which will no longer trigger the size discrepancy. Once all problematic files
are converted you will no longer need the ignore options above.

View File

@@ -21,7 +21,7 @@ Here is an example of how to make a remote called `remote`. First run:
This will guide you through an interactive setup process:
```
No remotes found - make a new one
No remotes found, make a new one?
n) New remote
s) Set configuration password
q) Quit config
@@ -80,7 +80,7 @@ List all the files in your pCloud
rclone ls remote:
To copy a local directory to an pCloud directory called backup
To copy a local directory to a pCloud directory called backup
rclone copy /home/source remote:backup

View File

@@ -21,7 +21,7 @@ Here is an example of how to make a remote called `remote`. First run:
This will guide you through an interactive setup process:
```
No remotes found - make a new one
No remotes found, make a new one?
n) New remote
s) Set configuration password
q) Quit config

View File

@@ -23,7 +23,7 @@ Here is an example of how to make a remote called `remote`. First run:
This will guide you through an interactive setup process:
```
No remotes found - make a new one
No remotes found, make a new one?
n) New remote
s) Set configuration password
q) Quit config

View File

@@ -17,7 +17,7 @@ Here is an example of making an QingStor configuration. First run
This will guide you through an interactive setup process.
```
No remotes found - make a new one
No remotes found, make a new one?
n) New remote
r) Rename remote
c) Copy remote

View File

@@ -294,7 +294,7 @@ Any config parameters you don't set will inherit the global defaults
which were set with command line flags or environment variables.
Note that it is possible to set some values as strings or integers -
see [data types](/#data-types) for more info. Here is an example
see [data types](#data-types) for more info. Here is an example
setting the equivalent of `--buffer-size` in string or integer format.
"_config":{"BufferSize": "42M"}
@@ -327,7 +327,7 @@ Any filter parameters you don't set will inherit the global defaults
which were set with command line flags or environment variables.
Note that it is possible to set some values as strings or integers -
see [data types](/#data-types) for more info. Here is an example
see [data types](#data-types) for more info. Here is an example
setting the equivalent of `--buffer-size` in string or integer format.
"_filter":{"MinSize": "42M"}
@@ -1824,10 +1824,10 @@ Here is how to use some of them:
- 30-second CPU profile: `go tool pprof http://localhost:5572/debug/pprof/profile`
- 5-second execution trace: `wget http://localhost:5572/debug/pprof/trace?seconds=5`
- Goroutine blocking profile
- Enable first with: `rclone rc debug/set-block-profile-rate rate=1` ([docs](#debug/set-block-profile-rate))
- Enable first with: `rclone rc debug/set-block-profile-rate rate=1` ([docs](#debug-set-block-profile-rate))
- `go tool pprof http://localhost:5572/debug/pprof/block`
- Contended mutexes:
- Enable first with: `rclone rc debug/set-mutex-profile-fraction rate=1` ([docs](#debug/set-mutex-profile-fraction))
- Enable first with: `rclone rc debug/set-mutex-profile-fraction rate=1` ([docs](#debug-set-mutex-profile-fraction))
- `go tool pprof http://localhost:5572/debug/pprof/mutex`
See the [net/http/pprof docs](https://golang.org/pkg/net/http/pprof/)

View File

@@ -58,7 +58,7 @@ First run
This will guide you through an interactive setup process.
```
No remotes found - make a new one
No remotes found, make a new one?
n) New remote
s) Set configuration password
q) Quit config
@@ -230,6 +230,8 @@ Choose a number from below, or type in your own value
\ "DEEP_ARCHIVE"
8 / Intelligent-Tiering storage class
\ "INTELLIGENT_TIERING"
9 / Glacier Instant Retrieval storage class
\ "GLACIER_IR"
storage_class> 1
Remote config
--------------------
@@ -340,7 +342,7 @@ instead of through directory listings. You can do a "top-up" sync very
cheaply by using `--max-age` and `--no-traverse` to copy only recent
files, eg
rclone copy --min-age 24h --no-traverse /path/to/source s3:bucket
rclone copy --max-age 24h --no-traverse /path/to/source s3:bucket
You'd then do a full `rclone sync` less often.
@@ -550,6 +552,15 @@ the object(s) in question before using rclone.
Note that rclone only speaks the S3 API it does not speak the Glacier
Vault API, so rclone cannot directly access Glacier Vaults.
### Object-lock enabled S3 bucket
According to AWS's [documentation on S3 Object Lock](https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lock-overview.html#object-lock-permission):
> If you configure a default retention period on a bucket, requests to upload objects in such a bucket must include the Content-MD5 header.
As mentioned in the [Hashes](#hashes) section, small files that are not uploaded as multipart, use a different tag, causing the upload to fail.
A simple solution is to set the `--s3-upload-cutoff 0` and force all the files to be uploaded as multipart.
{{< rem autogenerated options start" - DO NOT EDIT - instead edit fs.RegInfo in backend/s3/s3.go then run make backenddocs" >}}
### Standard options
@@ -1062,7 +1073,9 @@ Required when using an S3 clone.
- "s3.eu-central-1.wasabisys.com"
- Wasabi EU Central endpoint
- "s3.ap-northeast-1.wasabisys.com"
- Wasabi AP Northeast endpoint
- Wasabi AP Northeast 1 (Tokyo) endpoint
- "s3.ap-northeast-2.wasabisys.com"
- Wasabi AP Northeast 2 (Osaka) endpoint
#### --s3-location-constraint
@@ -1325,6 +1338,8 @@ The storage class to use when storing new objects in S3.
- Glacier Deep Archive storage class
- "INTELLIGENT_TIERING"
- Intelligent-Tiering storage class
- "GLACIER_IR"
- Glacier Instant Retrieval storage class
#### --s3-storage-class
@@ -2117,7 +2132,7 @@ To configure access to IBM COS S3, follow the steps below:
1. Run rclone config and select n for a new remote.
```
2018/02/14 14:13:11 NOTICE: Config file "C:\\Users\\a\\.config\\rclone\\rclone.conf" not found - using defaults
No remotes found - make a new one
No remotes found, make a new one?
n) New remote
s) Set configuration password
q) Quit config
@@ -2456,7 +2471,7 @@ Wasabi provides an S3 interface which can be configured for use with
rclone like this.
```
No remotes found - make a new one
No remotes found, make a new one?
n) New remote
s) Set configuration password
n/s> n
@@ -2568,7 +2583,7 @@ configuration. First run:
This will guide you through an interactive setup process.
```
No remotes found - make a new one
No remotes found, make a new one?
n) New remote
s) Set configuration password
q) Quit config
@@ -2678,7 +2693,7 @@ To configure access to Tencent COS, follow the steps below:
```
rclone config
No remotes found - make a new one
No remotes found, make a new one?
n) New remote
s) Set configuration password
q) Quit config

View File

@@ -29,7 +29,7 @@ This will guide you through an interactive setup process. To authenticate
you will need the URL of your server, your email (or username) and your password.
```
No remotes found - make a new one
No remotes found, make a new one?
n) New remote
s) Set configuration password
q) Quit config
@@ -118,7 +118,7 @@ excess files in the library.
Here's an example of a configuration in library mode with a user that has the two-factor authentication enabled. Your 2FA code will be asked at the end of the configuration, and will attempt to authenticate you:
```
No remotes found - make a new one
No remotes found, make a new one?
n) New remote
s) Set configuration password
q) Quit config

View File

@@ -38,7 +38,7 @@ Here is an example of making an SFTP configuration. First run
This will guide you through an interactive setup process.
```
No remotes found - make a new one
No remotes found, make a new one?
n) New remote
s) Set configuration password
q) Quit config
@@ -56,9 +56,11 @@ Choose a number from below, or type in your own value
1 / Connect to example.com
\ "example.com"
host> example.com
SSH username, leave blank for current username, $USER
SSH username
Enter a string value. Press Enter for the default ("$USER").
user> sftpuser
SSH port, leave blank to use default (22)
SSH port number
Enter a signed integer. Press Enter for the default (22).
port>
SSH password, leave blank to use ssh-agent.
y) Yes type in my own password
@@ -620,7 +622,7 @@ issue](https://github.com/pkg/sftp/issues/156) is fixed.
Note that since SFTP isn't HTTP based the following flags don't work
with it: `--dump-headers`, `--dump-bodies`, `--dump-auth`
Note that `--timeout` isn't supported (but `--contimeout` is).
Note that `--timeout` and `--contimeout` are both supported.
## C14 {#c14}

View File

@@ -20,7 +20,7 @@ Here is an example of how to make a remote called `remote`. First run:
This will guide you through an interactive setup process:
```
No remotes found - make a new one
No remotes found, make a new one?
n) New remote
s) Set configuration password
q) Quit config

View File

@@ -64,7 +64,7 @@ First, run:
This will guide you through an interactive setup process:
```
No remotes found - make a new one
No remotes found, make a new one?
n) New remote
s) Set configuration password
q) Quit config

View File

@@ -21,7 +21,7 @@ Here is an example of how to make a remote called `remote`. First run:
This will guide you through an interactive setup process:
```
No remotes found - make a new one
No remotes found, make a new one?
n) New remote
s) Set configuration password
q) Quit config

View File

@@ -26,7 +26,7 @@ Here is an example of making a swift configuration. First run
This will guide you through an interactive setup process.
```
No remotes found - make a new one
No remotes found, make a new one?
n) New remote
s) Set configuration password
q) Quit config

View File

@@ -25,7 +25,7 @@ This will guide you through an interactive setup process:
### Setup with access grant
```
No remotes found - make a new one
No remotes found, make a new one?
n) New remote
s) Set configuration password
q) Quit config
@@ -67,7 +67,7 @@ y/e/d> y
### Setup with API key and passphrase
```
No remotes found - make a new one
No remotes found, make a new one?
n) New remote
s) Set configuration password
q) Quit config

View File

@@ -34,7 +34,7 @@ First run:
This will guide you through an interactive setup process:
```
No remotes found - make a new one
No remotes found, make a new one?
n) New remote
s) Set configuration password
q) Quit config

View File

@@ -22,7 +22,7 @@ Here is an example of how to make a remote called `remote`. First run:
This will guide you through an interactive setup process:
```
No remotes found - make a new one
No remotes found, make a new one?
n) New remote
s) Set configuration password
q) Quit config

View File

@@ -16,7 +16,7 @@ Here is an example of making a yandex configuration. First run
This will guide you through an interactive setup process:
```
No remotes found - make a new one
No remotes found, make a new one?
n) New remote
s) Set configuration password
n/s> n
@@ -175,6 +175,15 @@ Leave blank to use the provider defaults.
- Type: string
- Default: ""
#### --yandex-hard-delete
Delete files permanently rather than putting them into the trash.
- Config: hard_delete
- Env Var: RCLONE_YANDEX_HARD_DELETE
- Type: bool
- Default: false
#### --yandex-encoding
This sets the encoding for the backend.

View File

@@ -16,7 +16,7 @@ Here is an example of making a zoho configuration. First run
This will guide you through an interactive setup process:
```
No remotes found - make a new one
No remotes found, make a new one?
n) New remote
s) Set configuration password
n/s> n

View File

@@ -148,6 +148,12 @@ h5 {
text-align: center;
}
/* Align menu items when icons have different sizes */
.menu .fa, .fab, .fad, .fal, .far, .fas {
width: 18px;
text-align: center;
}
/* Make primary buttons rclone colours. Should learn sass and do this the proper way! */
.btn-primary {
background-color: #3f79ad;

View File

@@ -51,7 +51,7 @@ const (
ConfigEncoding = "encoding"
// ConfigEncodingHelp is the help for ConfigEncoding
ConfigEncodingHelp = "This sets the encoding for the backend.\n\nSee the [encoding section in the overview](/overview/#encoding) for more info."
ConfigEncodingHelp = "The encoding for the backend.\n\nSee the [encoding section in the overview](/overview/#encoding) for more info."
// ConfigAuthorize indicates that we just want "rclone authorize"
ConfigAuthorize = "config_authorize"
@@ -570,9 +570,10 @@ func JSONListProviders() error {
// fsOption returns an Option describing the possible remotes
func fsOption() *fs.Option {
o := &fs.Option{
Name: "Storage",
Help: "Type of storage to configure.",
Default: "",
Name: "Storage",
Help: "Type of storage to configure.",
Default: "",
Required: true,
}
for _, item := range fs.Registry {
example := fs.OptionExample{

View File

@@ -61,15 +61,19 @@ func CommandDefault(commands []string, defaultIndex int) byte {
for {
fmt.Printf("%s> ", optHelp)
result := strings.ToLower(ReadLine())
if len(result) == 0 && defaultIndex >= 0 {
return optString[defaultIndex]
}
if len(result) != 1 {
continue
}
i := strings.Index(optString, string(result[0]))
if i >= 0 {
return result[0]
if len(result) == 0 {
if defaultIndex >= 0 {
return optString[defaultIndex]
}
fmt.Printf("This value is required and it has no default.\n")
} else if len(result) == 1 {
i := strings.Index(optString, string(result[0]))
if i >= 0 {
return result[0]
}
fmt.Printf("This value must be one of the following characters: %s.\n", strings.Join(opts, ", "))
} else {
fmt.Printf("This value must be a single character, one of the following: %s.\n", strings.Join(opts, ", "))
}
}
}
@@ -90,24 +94,31 @@ func Confirm(Default bool) bool {
return CommandDefault([]string{"yYes", "nNo"}, defaultIndex) == 'y'
}
// Choose one of the defaults or type a new string if newOk is set
func Choose(what string, defaults, help []string, newOk bool) string {
// Choose one of the choices, or default, or type a new string if newOk is set
func Choose(what string, kind string, choices, help []string, defaultValue string, required bool, newOk bool) string {
valueDescription := "an existing"
if newOk {
valueDescription = "your own"
}
fmt.Printf("Choose a number from below, or type in %s value.\n", valueDescription)
fmt.Printf("Choose a number from below, or type in %s %s.\n", valueDescription, kind)
// Empty input is allowed if not required is set, or if
// required is set but there is a default value to use.
if defaultValue != "" {
fmt.Printf("Press Enter for the default (%s).\n", defaultValue)
} else if !required {
fmt.Printf("Press Enter to leave empty.\n")
}
attributes := []string{terminal.HiRedFg, terminal.HiGreenFg}
for i, text := range defaults {
for i, text := range choices {
var lines []string
if help != nil {
if help != nil && help[i] != "" {
parts := strings.Split(help[i], "\n")
lines = append(lines, parts...)
lines = append(lines, fmt.Sprintf("(%s)", text))
}
lines = append(lines, fmt.Sprintf("%q", text))
pos := i + 1
terminal.WriteString(attributes[i%len(attributes)])
if len(lines) == 1 {
if len(lines) == 0 {
fmt.Printf("%2d > %s\n", pos, text)
} else {
mid := (len(lines) - 1) / 2
@@ -135,22 +146,108 @@ func Choose(what string, defaults, help []string, newOk bool) string {
result := ReadLine()
i, err := strconv.Atoi(result)
if err != nil {
if newOk {
return result
}
for _, v := range defaults {
for _, v := range choices {
if result == v {
return result
}
}
continue
}
if i >= 1 && i <= len(defaults) {
return defaults[i-1]
if result == "" {
// If empty string is in the predefined list of choices it has already been returned above.
// If parameter required is not set, then empty string is always a valid value.
if !required {
return result
}
// If parameter required is set, but there is a default, then empty input means default.
if defaultValue != "" {
return defaultValue
}
// If parameter required is set, and there is no default, then an input value is required.
fmt.Printf("This value is required and it has no default.\n")
} else if newOk {
// If legal input is not restricted to defined choices, then any nonzero input string is accepted.
return result
} else {
// A nonzero input string was specified but it did not match any of the strictly defined choices.
fmt.Printf("This value must match %s value.\n", valueDescription)
}
} else {
if i >= 1 && i <= len(choices) {
return choices[i-1]
}
fmt.Printf("No choices with this number.\n")
}
}
}
// Enter prompts for an input value of a specified type
func Enter(what string, kind string, defaultValue string, required bool) string {
// Empty input is allowed if not required is set, or if
// required is set but there is a default value to use.
fmt.Printf("Enter a %s.", kind)
if defaultValue != "" {
fmt.Printf(" Press Enter for the default (%s).\n", defaultValue)
} else if !required {
fmt.Println(" Press Enter to leave empty.")
} else {
fmt.Println()
}
for {
fmt.Printf("%s> ", what)
result := ReadLine()
if !required || result != "" {
return result
}
if defaultValue != "" {
return defaultValue
}
fmt.Printf("This value is required and it has no default.\n")
}
}
// ChoosePassword asks the user for a password
func ChoosePassword(defaultValue string, required bool) string {
fmt.Printf("Choose an alternative below.")
actions := []string{"yYes, type in my own password", "gGenerate random password"}
defaultAction := -1
if defaultValue != "" {
defaultAction = len(actions)
actions = append(actions, "nNo, keep existing")
fmt.Printf(" Press Enter for the default (%s).", string(actions[defaultAction][0]))
} else if !required {
defaultAction = len(actions)
actions = append(actions, "nNo, leave this optional password blank")
fmt.Printf(" Press Enter for the default (%s).", string(actions[defaultAction][0]))
}
fmt.Println()
var password string
var err error
switch i := CommandDefault(actions, defaultAction); i {
case 'y':
password = ChangePassword("the")
case 'g':
for {
fmt.Printf("Password strength in bits.\n64 is just about memorable\n128 is secure\n1024 is the maximum\n")
bits := ChooseNumber("Bits", 64, 1024)
password, err = Password(bits)
if err != nil {
log.Fatalf("Failed to make password: %v", err)
}
fmt.Printf("Your password is: %s\n", password)
fmt.Printf("Use this password? Please note that an obscured version of this \npassword (and not the " +
"password itself) will be stored under your \nconfiguration file, so keep this generated password " +
"in a safe place.\n")
if Confirm(true) {
break
}
}
case 'n':
return defaultValue
default:
fs.Errorf(nil, "Bad choice %c", i)
}
return obscure.MustObscure(password)
}
// ChooseNumber asks the user to enter a number between min and max
// inclusive prompting them with what.
func ChooseNumber(what string, min, max int) int {
@@ -188,7 +285,7 @@ func ShowRemotes() {
func ChooseRemote() string {
remotes := LoadedData().GetSectionList()
sort.Strings(remotes)
return Choose("remote", remotes, nil, false)
return Choose("remote", "value", remotes, nil, "", true, false)
}
// mustFindByName finds the RegInfo for the remote name passed in or
@@ -277,7 +374,7 @@ func backendConfig(ctx context.Context, name string, m configmap.Mapper, ri *fs.
fmt.Println(out.Option.Help)
in.Result = fmt.Sprint(Confirm(Default))
} else {
value := ChooseOption(out.Option)
value := ChooseOption(out.Option, name)
if value != "" {
err := out.Option.Set(value)
if err != nil {
@@ -316,67 +413,44 @@ func RemoteConfig(ctx context.Context, name string) error {
}
// ChooseOption asks the user to choose an option
func ChooseOption(o *fs.Option) string {
func ChooseOption(o *fs.Option, name string) string {
fmt.Printf("Option %s.\n", o.Name)
if o.Help != "" {
// Show help string without empty lines.
help := strings.Replace(strings.TrimSpace(o.Help), "\n\n", "\n", -1)
fmt.Println(help)
}
if o.IsPassword {
fmt.Printf("Choose an alternative below.")
actions := []string{"yYes type in my own password", "gGenerate random password"}
defaultAction := -1
if !o.Required {
defaultAction = len(actions)
actions = append(actions, "nNo leave this optional password blank")
fmt.Printf(" Press Enter for the default (%s).", string(actions[defaultAction][0]))
}
fmt.Println()
var password string
var err error
switch i := CommandDefault(actions, defaultAction); i {
case 'y':
password = ChangePassword("the")
case 'g':
for {
fmt.Printf("Password strength in bits.\n64 is just about memorable\n128 is secure\n1024 is the maximum\n")
bits := ChooseNumber("Bits", 64, 1024)
password, err = Password(bits)
if err != nil {
log.Fatalf("Failed to make password: %v", err)
}
fmt.Printf("Your password is: %s\n", password)
fmt.Printf("Use this password? Please note that an obscured version of this \npassword (and not the " +
"password itself) will be stored under your \nconfiguration file, so keep this generated password " +
"in a safe place.\n")
if Confirm(true) {
break
}
}
case 'n':
return ""
default:
fs.Errorf(nil, "Bad choice %c", i)
}
return obscure.MustObscure(password)
var defaultValue string
if o.Default == nil {
defaultValue = ""
} else {
defaultValue = fmt.Sprint(o.Default)
}
what := fmt.Sprintf("%T value", o.Default)
switch o.Default.(type) {
case bool:
what = "boolean value (true or false)"
case fs.SizeSuffix:
what = "size with suffix K,M,G,T"
case fs.Duration:
what = "duration s,m,h,d,w,M,y"
case int, int8, int16, int32, int64:
what = "signed integer"
case uint, byte, uint16, uint32, uint64:
what = "unsigned integer"
if o.IsPassword {
return ChoosePassword(defaultValue, o.Required)
}
what := "value"
if o.Default != "" {
switch o.Default.(type) {
case bool:
what = "boolean value (true or false)"
case fs.SizeSuffix:
what = "size with suffix K,M,G,T"
case fs.Duration:
what = "duration s,m,h,d,w,M,y"
case int, int8, int16, int32, int64:
what = "signed integer"
case uint, byte, uint16, uint32, uint64:
what = "unsigned integer"
default:
what = fmt.Sprintf("%T value", o.Default)
}
}
var in string
for {
fmt.Printf("Enter a %s. Press Enter for the default (%q).\n", what, fmt.Sprint(o.Default))
if len(o.Examples) > 0 {
var values []string
var help []string
@@ -384,27 +458,20 @@ func ChooseOption(o *fs.Option) string {
values = append(values, example.Value)
help = append(help, example.Help)
}
in = Choose(o.Name, values, help, !o.Exclusive)
in = Choose(o.Name, what, values, help, defaultValue, o.Required, !o.Exclusive)
} else {
fmt.Printf("%s> ", o.Name)
in = ReadLine()
in = Enter(o.Name, what, defaultValue, o.Required)
}
if in == "" {
if o.Required && fmt.Sprint(o.Default) == "" {
fmt.Printf("This value is required and it has no default.\n")
if in != "" {
newIn, err := configstruct.StringToInterface(o.Default, in)
if err != nil {
fmt.Printf("Failed to parse %q: %v\n", in, err)
continue
}
break
in = fmt.Sprint(newIn) // canonicalise
}
newIn, err := configstruct.StringToInterface(o.Default, in)
if err != nil {
fmt.Printf("Failed to parse %q: %v\n", in, err)
continue
}
in = fmt.Sprint(newIn) // canonicalise
break
return in
}
return in
}
// NewRemoteName asks the user for a name for a new remote
@@ -440,7 +507,7 @@ func NewRemote(ctx context.Context, name string) error {
// Set the type first
for {
newType = ChooseOption(fsOption())
newType = ChooseOption(fsOption(), name)
ri, err = fs.Find(newType)
if err != nil {
fmt.Printf("Bad remote %q: %v\n", newType, err)
@@ -553,7 +620,7 @@ func EditConfig(ctx context.Context) (err error) {
ShowRemotes()
fmt.Printf("\n")
} else {
fmt.Printf("No remotes found - make a new one\n")
fmt.Printf("No remotes found, make a new one?\n")
// take 2nd item and last 2 items of menu list
what = append(what[1:2], what[len(what)-2:]...)
}

View File

@@ -20,7 +20,17 @@ import (
"github.com/stretchr/testify/require"
)
func testConfigFile(t *testing.T, configFileName string) func() {
var simpleOptions = []fs.Option{{
Name: "bool",
Default: false,
IsPassword: false,
}, {
Name: "pass",
Default: "",
IsPassword: true,
}}
func testConfigFile(t *testing.T, options []fs.Option, configFileName string) func() {
ctx := context.Background()
ci := fs.GetConfig(ctx)
config.ClearConfigPassword()
@@ -46,24 +56,18 @@ func testConfigFile(t *testing.T, configFileName string) func() {
configfile.Install()
assert.Equal(t, []string{}, config.Data().GetSectionList())
// Fake a remote
fs.Register(&fs.RegInfo{
Name: "config_test_remote",
Options: fs.Options{
{
Name: "bool",
Default: false,
IsPassword: false,
},
{
Name: "pass",
Default: "",
IsPassword: true,
},
},
})
// Fake a filesystem/backend
backendName := "config_test_remote"
if regInfo, _ := fs.Find(backendName); regInfo != nil {
regInfo.Options = options
} else {
fs.Register(&fs.RegInfo{
Name: backendName,
Options: options,
})
}
// Undo the above
// Undo the above (except registered backend, unfortunately)
return func() {
err := os.Remove(path)
assert.NoError(t, err)
@@ -91,7 +95,7 @@ func makeReadLine(answers []string) func() string {
}
func TestCRUD(t *testing.T) {
defer testConfigFile(t, "crud.conf")()
defer testConfigFile(t, simpleOptions, "crud.conf")()
ctx := context.Background()
// script for creating remote
@@ -129,7 +133,7 @@ func TestCRUD(t *testing.T) {
}
func TestChooseOption(t *testing.T) {
defer testConfigFile(t, "crud.conf")()
defer testConfigFile(t, simpleOptions, "crud.conf")()
ctx := context.Background()
// script for creating remote
@@ -165,7 +169,7 @@ func TestChooseOption(t *testing.T) {
}
func TestNewRemoteName(t *testing.T) {
defer testConfigFile(t, "crud.conf")()
defer testConfigFile(t, simpleOptions, "crud.conf")()
ctx := context.Background()
// script for creating remote
@@ -189,7 +193,7 @@ func TestNewRemoteName(t *testing.T) {
func TestCreateUpdatePasswordRemote(t *testing.T) {
ctx := context.Background()
defer testConfigFile(t, "update.conf")()
defer testConfigFile(t, simpleOptions, "update.conf")()
for _, doObscure := range []bool{false, true} {
for _, noObscure := range []bool{false, true} {
@@ -244,5 +248,298 @@ func TestCreateUpdatePasswordRemote(t *testing.T) {
})
}
}
}
func TestDefaultRequired(t *testing.T) {
// By default options are optional (sic), regardless if a default value is defined.
// Setting Required=true means empty string is no longer allowed, except when
// a default value is set: Default value means empty string is always allowed!
options := []fs.Option{{
Name: "string_required",
Required: true,
}, {
Name: "string_default",
Default: "AAA",
}, {
Name: "string_required_default",
Default: "BBB",
Required: true,
}}
defer testConfigFile(t, options, "crud.conf")()
ctx := context.Background()
// script for creating remote
config.ReadLine = makeReadLine([]string{
"config_test_remote", // type
"111", // string_required
"222", // string_default
"333", // string_required_default
"y", // looks good, save
})
require.NoError(t, config.NewRemote(ctx, "test"))
assert.Equal(t, []string{"test"}, config.Data().GetSectionList())
assert.Equal(t, "config_test_remote", config.FileGet("test", "type"))
assert.Equal(t, "111", config.FileGet("test", "string_required"))
assert.Equal(t, "222", config.FileGet("test", "string_default"))
assert.Equal(t, "333", config.FileGet("test", "string_required_default"))
// delete remote
config.DeleteRemote("test")
assert.Equal(t, []string{}, config.Data().GetSectionList())
// script for creating remote
config.ReadLine = makeReadLine([]string{
"config_test_remote", // type
"", // string_required - invalid (empty string not allowed)
"111", // string_required - valid
"", // string_default (empty string allowed, means use default)
"", // string_required_default (empty string allowed, means use default)
"y", // looks good, save
})
require.NoError(t, config.NewRemote(ctx, "test"))
assert.Equal(t, []string{"test"}, config.Data().GetSectionList())
assert.Equal(t, "config_test_remote", config.FileGet("test", "type"))
assert.Equal(t, "111", config.FileGet("test", "string_required"))
assert.Equal(t, "", config.FileGet("test", "string_default"))
assert.Equal(t, "", config.FileGet("test", "string_required_default"))
}
func TestMultipleChoice(t *testing.T) {
// Multiple-choice options can be set to the number of a predefined choice, or
// its text. Unless Exclusive=true, tested later, any free text input is accepted.
//
// By default options are optional, regardless if a default value is defined.
// Setting Required=true means empty string is no longer allowed, except when
// a default value is set: Default value means empty string is always allowed!
options := []fs.Option{{
Name: "multiple_choice",
Examples: []fs.OptionExample{{
Value: "AAA",
Help: "This is value AAA",
}, {
Value: "BBB",
Help: "This is value BBB",
}, {
Value: "CCC",
Help: "This is value CCC",
}},
}, {
Name: "multiple_choice_required",
Required: true,
Examples: []fs.OptionExample{{
Value: "AAA",
Help: "This is value AAA",
}, {
Value: "BBB",
Help: "This is value BBB",
}, {
Value: "CCC",
Help: "This is value CCC",
}},
}, {
Name: "multiple_choice_default",
Default: "BBB",
Examples: []fs.OptionExample{{
Value: "AAA",
Help: "This is value AAA",
}, {
Value: "BBB",
Help: "This is value BBB",
}, {
Value: "CCC",
Help: "This is value CCC",
}},
}, {
Name: "multiple_choice_required_default",
Required: true,
Default: "BBB",
Examples: []fs.OptionExample{{
Value: "AAA",
Help: "This is value AAA",
}, {
Value: "BBB",
Help: "This is value BBB",
}, {
Value: "CCC",
Help: "This is value CCC",
}},
}}
defer testConfigFile(t, options, "crud.conf")()
ctx := context.Background()
// script for creating remote
config.ReadLine = makeReadLine([]string{
"config_test_remote", // type
"3", // multiple_choice
"3", // multiple_choice_required
"3", // multiple_choice_default
"3", // multiple_choice_required_default
"y", // looks good, save
})
require.NoError(t, config.NewRemote(ctx, "test"))
assert.Equal(t, []string{"test"}, config.Data().GetSectionList())
assert.Equal(t, "config_test_remote", config.FileGet("test", "type"))
assert.Equal(t, "CCC", config.FileGet("test", "multiple_choice"))
assert.Equal(t, "CCC", config.FileGet("test", "multiple_choice_required"))
assert.Equal(t, "CCC", config.FileGet("test", "multiple_choice_default"))
assert.Equal(t, "CCC", config.FileGet("test", "multiple_choice_required_default"))
// delete remote
config.DeleteRemote("test")
assert.Equal(t, []string{}, config.Data().GetSectionList())
// script for creating remote
config.ReadLine = makeReadLine([]string{
"config_test_remote", // type
"XXX", // multiple_choice
"XXX", // multiple_choice_required
"XXX", // multiple_choice_default
"XXX", // multiple_choice_required_default
"y", // looks good, save
})
require.NoError(t, config.NewRemote(ctx, "test"))
assert.Equal(t, []string{"test"}, config.Data().GetSectionList())
assert.Equal(t, "config_test_remote", config.FileGet("test", "type"))
assert.Equal(t, "XXX", config.FileGet("test", "multiple_choice"))
assert.Equal(t, "XXX", config.FileGet("test", "multiple_choice_required"))
assert.Equal(t, "XXX", config.FileGet("test", "multiple_choice_default"))
assert.Equal(t, "XXX", config.FileGet("test", "multiple_choice_required_default"))
// delete remote
config.DeleteRemote("test")
assert.Equal(t, []string{}, config.Data().GetSectionList())
// script for creating remote
config.ReadLine = makeReadLine([]string{
"config_test_remote", // type
"", // multiple_choice (empty string allowed)
"", // multiple_choice_required - invalid (empty string not allowed)
"XXX", // multiple_choice_required - valid (value not restricted to examples)
"", // multiple_choice_default (empty string allowed)
"", // multiple_choice_required_default (required does nothing when default is set)
"y", // looks good, save
})
require.NoError(t, config.NewRemote(ctx, "test"))
assert.Equal(t, []string{"test"}, config.Data().GetSectionList())
assert.Equal(t, "config_test_remote", config.FileGet("test", "type"))
assert.Equal(t, "", config.FileGet("test", "multiple_choice"))
assert.Equal(t, "XXX", config.FileGet("test", "multiple_choice_required"))
assert.Equal(t, "", config.FileGet("test", "multiple_choice_default"))
assert.Equal(t, "", config.FileGet("test", "multiple_choice_required_default"))
}
func TestMultipleChoiceExclusive(t *testing.T) {
// Setting Exclusive=true on multiple-choice option means any input
// value must be from the predefined list, but empty string is allowed.
// Setting a default value makes no difference.
options := []fs.Option{{
Name: "multiple_choice_exclusive",
Exclusive: true,
Examples: []fs.OptionExample{{
Value: "AAA",
Help: "This is value AAA",
}, {
Value: "BBB",
Help: "This is value BBB",
}, {
Value: "CCC",
Help: "This is value CCC",
}},
}, {
Name: "multiple_choice_exclusive_default",
Exclusive: true,
Default: "CCC",
Examples: []fs.OptionExample{{
Value: "AAA",
Help: "This is value AAA",
}, {
Value: "BBB",
Help: "This is value BBB",
}, {
Value: "CCC",
Help: "This is value CCC",
}},
}}
defer testConfigFile(t, options, "crud.conf")()
ctx := context.Background()
// script for creating remote
config.ReadLine = makeReadLine([]string{
"config_test_remote", // type
"XXX", // multiple_choice_exclusive - invalid (not a value from examples)
"", // multiple_choice_exclusive - valid (empty string allowed)
"YYY", // multiple_choice_exclusive_default - invalid (not a value from examples)
"", // multiple_choice_exclusive_default - valid (empty string allowed)
"y", // looks good, save
})
require.NoError(t, config.NewRemote(ctx, "test"))
assert.Equal(t, []string{"test"}, config.Data().GetSectionList())
assert.Equal(t, "config_test_remote", config.FileGet("test", "type"))
assert.Equal(t, "", config.FileGet("test", "multiple_choice_exclusive"))
assert.Equal(t, "", config.FileGet("test", "multiple_choice_exclusive_default"))
}
func TestMultipleChoiceExclusiveRequired(t *testing.T) {
// Setting Required=true together with Exclusive=true on multiple-choice option
// means empty string is no longer allowed, except when a default value is set
// (default value means empty string is always allowed).
options := []fs.Option{{
Name: "multiple_choice_exclusive_required",
Exclusive: true,
Required: true,
Examples: []fs.OptionExample{{
Value: "AAA",
Help: "This is value AAA",
}, {
Value: "BBB",
Help: "This is value BBB",
}, {
Value: "CCC",
Help: "This is value CCC",
}},
}, {
Name: "multiple_choice_exclusive_required_default",
Exclusive: true,
Required: true,
Default: "CCC",
Examples: []fs.OptionExample{{
Value: "AAA",
Help: "This is value AAA",
}, {
Value: "BBB",
Help: "This is value BBB",
}, {
Value: "CCC",
Help: "This is value CCC",
}},
}}
defer testConfigFile(t, options, "crud.conf")()
ctx := context.Background()
// script for creating remote
config.ReadLine = makeReadLine([]string{
"config_test_remote", // type
"XXX", // multiple_choice_exclusive_required - invalid (not a value from examples)
"", // multiple_choice_exclusive_required - invalid (empty string not allowed)
"CCC", // multiple_choice_exclusive_required - valid
"XXX", // multiple_choice_exclusive_required_default - invalid (not a value from examples)
"", // multiple_choice_exclusive_required_default - valid (empty string allowed)
"y", // looks good, save
})
require.NoError(t, config.NewRemote(ctx, "test"))
assert.Equal(t, []string{"test"}, config.Data().GetSectionList())
assert.Equal(t, "config_test_remote", config.FileGet("test", "type"))
assert.Equal(t, "CCC", config.FileGet("test", "multiple_choice_exclusive_required"))
assert.Equal(t, "", config.FileGet("test", "multiple_choice_exclusive_required_default"))
}

View File

@@ -158,12 +158,13 @@ func dedupeList(ctx context.Context, f fs.Fs, ht hash.Type, remote string, objs
}
// dedupeInteractive interactively dedupes the slice of objects
func dedupeInteractive(ctx context.Context, f fs.Fs, ht hash.Type, remote string, objs []fs.Object, byHash bool) {
func dedupeInteractive(ctx context.Context, f fs.Fs, ht hash.Type, remote string, objs []fs.Object, byHash bool) bool {
dedupeList(ctx, f, ht, remote, objs, byHash)
commands := []string{"sSkip and do nothing", "kKeep just one (choose which in next step)"}
if !byHash {
commands = append(commands, "rRename all to be different (by changing file.jpg to file-1.jpg)")
}
commands = append(commands, "qQuit")
switch config.Command(commands) {
case 's':
case 'k':
@@ -171,7 +172,10 @@ func dedupeInteractive(ctx context.Context, f fs.Fs, ht hash.Type, remote string
dedupeDeleteAllButOne(ctx, keep-1, remote, objs)
case 'r':
dedupeRename(ctx, f, remote, objs)
case 'q':
return false
}
return true
}
// DeduplicateMode is how the dedupe command chooses what to do
@@ -465,7 +469,9 @@ func Deduplicate(ctx context.Context, f fs.Fs, mode DeduplicateMode, byHash bool
}
switch mode {
case DeduplicateInteractive:
dedupeInteractive(ctx, f, ht, remote, objs, byHash)
if !dedupeInteractive(ctx, f, ht, remote, objs, byHash) {
return nil
}
case DeduplicateFirst:
dedupeDeleteAllButOne(ctx, 0, remote, objs)
case DeduplicateNewest:

View File

@@ -405,7 +405,7 @@ func Copy(ctx context.Context, f fs.Fs, dst fs.Object, remote string, src fs.Obj
if err == nil {
dst = newDst
in.ServerSideCopyEnd(dst.Size()) // account the bytes for the server-side transfer
err = in.Close()
_ = in.Close()
} else {
_ = in.Close()
}
@@ -598,6 +598,8 @@ func Move(ctx context.Context, fdst fs.Fs, dst fs.Object, remote string, src fs.
}
}
// Move dst <- src
in := tr.Account(ctx, nil) // account the transfer
in.ServerSideCopyStart()
newDst, err = doMove(ctx, src, remote)
switch err {
case nil:
@@ -606,13 +608,16 @@ func Move(ctx context.Context, fdst fs.Fs, dst fs.Object, remote string, src fs.
} else {
fs.Infof(src, "Moved (server-side)")
}
in.ServerSideCopyEnd(newDst.Size()) // account the bytes for the server-side transfer
_ = in.Close()
return newDst, nil
case fs.ErrorCantMove:
fs.Debugf(src, "Can't move, switching to copy")
_ = in.Close()
default:
err = fs.CountError(err)
fs.Errorf(src, "Couldn't move: %v", err)
_ = in.Close()
return newDst, err
}
}

View File

@@ -27,7 +27,7 @@ func AddFlags(flagSet *pflag.FlagSet) {
flags.StringVarP(flagSet, &Opt.WebGUIFetchURL, "rc-web-fetch-url", "", "https://api.github.com/repos/rclone/rclone-webui-react/releases/latest", "URL to fetch the releases for webgui")
flags.StringVarP(flagSet, &Opt.AccessControlAllowOrigin, "rc-allow-origin", "", "", "Set the allowed origin for CORS")
flags.BoolVarP(flagSet, &Opt.EnableMetrics, "rc-enable-metrics", "", false, "Enable prometheus metrics on /metrics")
flags.DurationVarP(flagSet, &Opt.JobExpireDuration, "rc-job-expire-duration", "", Opt.JobExpireDuration, "expire finished async jobs older than this value")
flags.DurationVarP(flagSet, &Opt.JobExpireInterval, "rc-job-expire-interval", "", Opt.JobExpireInterval, "interval to check for expired async jobs")
flags.DurationVarP(flagSet, &Opt.JobExpireDuration, "rc-job-expire-duration", "", Opt.JobExpireDuration, "Expire finished async jobs older than this value")
flags.DurationVarP(flagSet, &Opt.JobExpireInterval, "rc-job-expire-interval", "", Opt.JobExpireInterval, "Interval to check for expired async jobs")
httpflags.AddFlagsPrefix(flagSet, "rc-", &Opt.HTTPOptions)
}

View File

@@ -95,20 +95,20 @@ func newServer(ctx context.Context, opt *rc.Options, mux *http.ServeMux) *Server
fs.Errorf(nil, "Error while fetching the latest release of Web GUI: %v", err)
}
if opt.NoAuth {
opt.NoAuth = false
fs.Infof(nil, "Cannot run Web GUI without authentication, using default auth")
}
if opt.HTTPOptions.BasicUser == "" {
opt.HTTPOptions.BasicUser = "gui"
fs.Infof(nil, "No username specified. Using default username: %s \n", rcflags.Opt.HTTPOptions.BasicUser)
}
if opt.HTTPOptions.BasicPass == "" {
randomPass, err := random.Password(128)
if err != nil {
log.Fatalf("Failed to make password: %v", err)
fs.Logf(nil, "It is recommended to use web gui with auth.")
} else {
if opt.HTTPOptions.BasicUser == "" {
opt.HTTPOptions.BasicUser = "gui"
fs.Infof(nil, "No username specified. Using default username: %s \n", rcflags.Opt.HTTPOptions.BasicUser)
}
if opt.HTTPOptions.BasicPass == "" {
randomPass, err := random.Password(128)
if err != nil {
log.Fatalf("Failed to make password: %v", err)
}
opt.HTTPOptions.BasicPass = randomPass
fs.Infof(nil, "No password specified. Using random password: %s \n", randomPass)
}
opt.HTTPOptions.BasicPass = randomPass
fs.Infof(nil, "No password specified. Using random password: %s \n", randomPass)
}
opt.Serve = true

View File

@@ -124,20 +124,29 @@ const (
// Option is describes an option for the config wizard
//
// This also describes command line options and environment variables
//
// To create a multiple-choice option, specify the possible values
// in the Examples property. Whether the option's value is required
// to be one of these depends on other properties:
// - Default is to allow any value, either from specified examples,
// or any other value. To restrict exclusively to the specified
// examples, also set Exclusive=true.
// - If empty string should not be allowed then set Required=true,
// and do not set Default.
type Option struct {
Name string // name of the option in snake_case
Help string // Help, the first line only is used for the command line help
Provider string // Set to filter on provider
Default interface{} // default value, nil => ""
Help string // help, start with a single sentence on a single line that will be extracted for command line help
Provider string // set to filter on provider
Default interface{} // default value, nil => "", if set (and not to nil or "") then Required does nothing
Value interface{} // value to be set by flags
Examples OptionExamples `json:",omitempty"` // config examples
Examples OptionExamples `json:",omitempty"` // predefined values that can be selected from list (multiple-choice option)
ShortOpt string // the short option for this if required
Hide OptionVisibility // set this to hide the config from the configurator or the command line
Required bool // this option is required
Required bool // this option is required, meaning value cannot be empty unless there is a default
IsPassword bool // set if the option is a password
NoPrefix bool // set if the option for this should not use the backend prefix
Advanced bool // set if this is an advanced config option
Exclusive bool // set if the answer can only be one of the examples
Exclusive bool // set if the answer can only be one of the examples (empty string allowed unless Required or Default is set)
}
// BaseOption is an alias for Option used internally

View File

@@ -441,6 +441,10 @@ func Run(t *testing.T, opt *Opt) {
}
require.NoError(t, err, fmt.Sprintf("unexpected error: %v", err))
// Get fsInfo which contains type, etc. of the fs
fsInfo, _, _, _, err := fs.ConfigFs(subRemoteName)
require.NoError(t, err, fmt.Sprintf("unexpected error: %v", err))
// Skip the rest if it failed
skipIfNotOk(t)
@@ -1587,12 +1591,30 @@ func Run(t *testing.T, opt *Opt) {
t.Run("PublicLink", func(t *testing.T) {
skipIfNotOk(t)
doPublicLink := f.Features().PublicLink
if doPublicLink == nil {
publicLinkFunc := f.Features().PublicLink
if publicLinkFunc == nil {
t.Skip("FS has no PublicLinker interface")
}
type PublicLinkFunc func(ctx context.Context, remote string, expire fs.Duration, unlink bool) (link string, err error)
wrapPublicLinkFunc := func(f PublicLinkFunc) PublicLinkFunc {
return func(ctx context.Context, remote string, expire fs.Duration, unlink bool) (link string, err error) {
link, err = publicLinkFunc(ctx, remote, expire, unlink)
if err == nil {
return
}
// For OneDrive Personal, link expiry is a premium feature
// Don't let it fail the test (https://github.com/rclone/rclone/issues/5420)
if fsInfo.Name == "onedrive" && strings.Contains(err.Error(), "accountUpgradeRequired") {
t.Log("treating accountUpgradeRequired as success for PublicLink")
link, err = "bogus link to "+remote, nil
}
return
}
}
expiry := fs.Duration(60 * time.Second)
doPublicLink := wrapPublicLinkFunc(publicLinkFunc)
// if object not found
link, err := doPublicLink(ctx, file1.Path+"_does_not_exist", expiry, false)
@@ -1639,7 +1661,7 @@ func Run(t *testing.T, opt *Opt) {
_, err = subRemote.Put(ctx, buf, obji)
require.NoError(t, err)
link4, err := subRemote.Features().PublicLink(ctx, "", expiry, false)
link4, err := wrapPublicLinkFunc(subRemote.Features().PublicLink)(ctx, "", expiry, false)
require.NoError(t, err, "Sharing root in a sub-remote should work")
require.NotEqual(t, "", link4, "Link should not be empty")
}

View File

@@ -264,7 +264,7 @@ backends:
fastlist: true
- backend: "pcloud"
remote: "TestPcloud:"
fastlist: false
fastlist: true
- backend: "webdav"
remote: "TestWebdavNextcloud:"
ignore:

View File

@@ -39,9 +39,18 @@ func GetOSVersion() (osVersion, osKernel string) {
}
}
friendlyName := getRegistryVersionString("ReleaseId")
if osVersion != "" && friendlyName != "" {
osVersion += " " + friendlyName
if osVersion != "" {
// Include the friendly-name of the version, which is typically what is referred to.
// Until Windows 10 version 2004 (May 2020) this can be found from registry entry
// ReleaseID, after that we must use entry DisplayVersion (ReleaseId is stuck at 2009).
// Source: https://ss64.com/nt/ver.html
friendlyName := getRegistryVersionString("DisplayVersion")
if friendlyName == "" {
friendlyName = getRegistryVersionString("ReleaseId")
}
if friendlyName != "" {
osVersion += " " + friendlyName
}
}
updateRevision := getRegistryVersionInt("UBR")

View File

@@ -691,6 +691,11 @@ func newAuthServer(opt *Options, bindAddress, state, authURL string) *authServer
// Receive the auth request
func (s *authServer) handleAuth(w http.ResponseWriter, req *http.Request) {
if req.URL.Path != "/" {
fs.Debugf(nil, "Ignoring %s request on auth server to %q", req.Method, req.URL.Path)
http.NotFound(w, req)
return
}
fs.Debugf(nil, "Received %s request on auth server to %q", req.Method, req.URL.Path)
// Reply with the response to the user and to the channel
@@ -752,10 +757,6 @@ func (s *authServer) Init() error {
}
s.server.SetKeepAlivesEnabled(false)
mux.HandleFunc("/favicon.ico", func(w http.ResponseWriter, req *http.Request) {
http.Error(w, "", http.StatusNotFound)
return
})
mux.HandleFunc("/auth", func(w http.ResponseWriter, req *http.Request) {
state := req.FormValue("state")
if state != s.state {

View File

@@ -167,6 +167,10 @@ func DecodeJSON(resp *http.Response, result interface{}) (err error) {
func DecodeXML(resp *http.Response, result interface{}) (err error) {
defer fs.CheckClose(resp.Body, &err)
decoder := xml.NewDecoder(resp.Body)
// MEGAcmd has included escaped HTML entities in its XML output, so we have to be able to
// decode them.
decoder.Strict = false
decoder.Entity = xml.HTMLEntity
return decoder.Decode(result)
}

View File

@@ -4,13 +4,19 @@ This directory contains code to build rclone as a C library and the
shims for accessing rclone from C and other languages.
**Note** for the moment, the interfaces defined here are experimental
and may change in the future. Eventually they will stabilse and this
and may change in the future. Eventually they will stabilise and this
notice will be removed.
## C
The shims are a thin wrapper over the rclone RPC.
The implementation is based on cgo; to build it you need Go and a GCC compatible
C compiler (GCC or Clang). On Windows you can use the MinGW port of GCC,
installing it via the [MSYS2](https://www.msys2.org) distribution is recommended
(make sure you install GCC in the classic mingw64 subsystem, the ucrt64 version
is not compatible with cgo).
Build a shared library like this:
go build --buildmode=c-shared -o librclone.so github.com/rclone/rclone/librclone
@@ -20,9 +26,16 @@ Build a static library like this:
go build --buildmode=c-archive -o librclone.a github.com/rclone/rclone/librclone
Both the above commands will also generate `librclone.h` which should
be `#include`d in `C` programs wishing to use the library.
be `#include`d in `C` programs wishing to use the library (with some
[exceptions](#include-file)).
The library will depend on `libdl` and `libpthread`.
The library will depend on `libdl` and `libpthread` on Linux/macOS, unless
linking with a C standard library where their functionality is integrated,
which is the case for glibc version 2.34 and newer.
You may add arguments `-ldflags -s` to make the library file smaller. This will
omit symbol table and debug information, reducing size by about 25% on Linux and
50% on Windows.
### Documentation
@@ -33,9 +46,114 @@ For documentation see the Go documentation for:
- [RcloneRPC](https://pkg.go.dev/github.com/rclone/rclone/librclone#RcloneRPC)
- [RcloneFreeString](https://pkg.go.dev/github.com/rclone/rclone/librclone#RcloneFreeString)
### C Example
### Linux C example
There is an example program `ctest.c` with `Makefile` in the `ctest` subdirectory.
There is an example program `ctest.c`, with `Makefile`, in the `ctest`
subdirectory. It can be built on Linux/macOS, but not Windows without
changes - as described next.
### Windows C/C++ guidelines
The official [C example](#linux-c-example) is targeting Linux/macOS, and will
not work on Windows. It is very possible to use `librclone` from a C/C++
application on Windows, but there are some pitfalls that you can avoid by
following these guidelines:
- Build `librclone` as shared library, and use run-time dynamic linking (see [linking](#linking)).
- Do not try to unload the library with `FreeLibrary` (see [unloading](#unloading)).
- Deallocate returned strings with API function `RcloneFreeString` (see [memory management](#memory-management)).
- Define struct `RcloneRPCResult`, instead of including `librclone.h` (see [include file](#include-file)).
- Use UTF-8 encoded strings (see [encoding](#encoding)).
- Properly escape JSON strings, beware of the native path separator (see [escaping](#escaping)).
#### Linking
Use of different compilers, compiler versions, build configuration, and
dependency on different C runtime libraries for a library and the application
that references it, may easily break compatibility. When building the librclone
library with MinGW GCC compiler (via go build command), if you link it into an
application built with Visual C++ for example, there will be more than enough
differences to cause problems.
Linking with static library requires most compatibility, and is less likely to
work. Linking with shared library is therefore recommended. The library exposes
a plain C interface, and by using run-time dynamic linking (by using Windows API
functions `LoadLibrary` and `GetProcAddress`), you can make a boundary that
ensures compatibility (and in any case, you will not have an import library).
The only remaining concern is then memory allocations; you should make sure
memory is deallocated in the same library where it was allocated, as explained
[below](#memory-management).
#### Unloading
Do not try to unload the library with `FreeLibrary`, when using run-time dynamic
linking. The library includes Go-specific runtime components, with garbage
collection and other background threads, which do not handle unloading. Trying
to call `FreeLibrary` will crash the application. I.e. after you have loaded
`librclone.dll` into your application it must stay loaded until your application
exits.
#### Memory management
The output string returned from `RcloneRPC` is allocated within the `librclone`
library, and caller is responsible for freeing the memory. Due to C runtime
library differences, as mentioned [above](#linking), it is not recommended to do
this by calling `free` from the consuming application. You should instead use
the API function `RcloneFreeString`, which will call `free` from within the
`librclone` library, using the same runtime that allocated it in the first
place.
#### Include file
Do not include `librclone.h`. It contains some plain C, golang/cgo and GCC
specific type definitions that will not compile with all other compilers
without adjustments, where Visual C++ is one notable example. When using
run-time dynamic linking, you have no use of the extern declared functions
either.
The interface of librclone is so simple, that all you need is to define the
small struct `RcloneRPCResult`, from [librclone.go](librclone.go):
```C++
struct RcloneRPCResult {
char* Output;
int Status;
};
```
#### Encoding
The API uses plain C strings (type `char*`, called "narrow" strings), and rclone
assumes content is UTF-8 encoded. On Linux systems this normally matches the
standard string representation, and no special considerations must be made. On
Windows it is more complex.
On Windows, narrow strings are traditionally used with native non-Unicode
encoding, the so-called ANSI code page, while Unicode strings are instead
represented with the alternative `wchar_t*` type, called "wide" strings, and
encoded as UTF-16. This means, to correctly handle characters that are encoded
differently in UTF-8, you will need to perform conversion at some level:
Conversion between UTF-8 encoded narrow strings used by rclone, and either ANSI
encoded narrow strings or wide UTF-16 encoded strings used in runtime function,
Windows API, third party APIs, etc.
#### Escaping
The RPC method takes a string containing JSON. In addition to the normal
escaping of strings constants in your C/C++ source code, the JSON needs its
own escaping. This is not a Windows-specific issue, but there is the
additional challenge that native filesystem path separator is the same as
the escape character, and you may end up with strings like this:
```C++
const char* input = "{"
"\"fs\": \"C:\\\\Temp\","
"\"remote\": \"sub/folder\","
"\"opt\": \"{\\\"showHash\\\": true}\""
"}";
```
With C++11 you can use raw string literals to avoid the C++ escaping of string
constants, leaving escaping only necessary for the contained JSON.
## gomobile

View File

@@ -88,9 +88,11 @@ func RPC(method string, input string) (output string, status int) {
}()
// create a buffer to capture the output
err := json.NewDecoder(strings.NewReader(input)).Decode(&in)
if err != nil {
return writeError(method, in, fmt.Errorf("failed to read input JSON: %w", err), http.StatusBadRequest)
if input != "" {
err := json.NewDecoder(strings.NewReader(input)).Decode(&in)
if err != nil {
return writeError(method, in, fmt.Errorf("failed to read input JSON: %w", err), http.StatusBadRequest)
}
}
// Find the call

View File

@@ -389,3 +389,51 @@ func rcList(ctx context.Context, in rc.Params) (out rc.Params, err error) {
out["vfses"] = names
return out, nil
}
func init() {
rc.Add(rc.Call{
Path: "vfs/stats",
Title: "Stats for a VFS.",
Help: `
This returns stats for the selected VFS.
{
// Status of the disk cache - only present if --vfs-cache-mode > off
"diskCache": {
"bytesUsed": 0,
"erroredFiles": 0,
"files": 0,
"hashType": 1,
"outOfSpace": false,
"path": "/home/user/.cache/rclone/vfs/local/mnt/a",
"pathMeta": "/home/user/.cache/rclone/vfsMeta/local/mnt/a",
"uploadsInProgress": 0,
"uploadsQueued": 0
},
"fs": "/mnt/a",
"inUse": 1,
// Status of the in memory metadata cache
"metadataCache": {
"dirs": 1,
"files": 0
},
// Options as returned by options/get
"opt": {
"CacheMaxAge": 3600000000000,
// ...
"WriteWait": 1000000000
}
}
` + getVFSHelp,
Fn: rcStats,
})
}
func rcStats(ctx context.Context, in rc.Params) (out rc.Params, err error) {
vfs, err := getVFS(in)
if err != nil {
return nil, err
}
return vfs.Stats(), nil
}

View File

@@ -119,3 +119,15 @@ func TestRcList(t *testing.T) {
},
}, out)
}
func TestRcStats(t *testing.T) {
r, vfs, cleanup, call := rcNewRun(t, "vfs/stats")
defer cleanup()
out, err := call.Fn(context.Background(), nil)
require.NoError(t, err)
assert.Equal(t, fs.ConfigString(r.Fremote), out["fs"])
assert.Equal(t, int32(1), out["inUse"])
assert.Equal(t, 0, out["metadataCache"].(rc.Params)["files"])
assert.Equal(t, 1, out["metadataCache"].(rc.Params)["dirs"])
assert.Equal(t, vfs.Opt, out["opt"].(vfscommon.Options))
}

View File

@@ -36,6 +36,7 @@ import (
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/cache"
"github.com/rclone/rclone/fs/log"
"github.com/rclone/rclone/fs/rc"
"github.com/rclone/rclone/fs/walk"
"github.com/rclone/rclone/vfs/vfscache"
"github.com/rclone/rclone/vfs/vfscommon"
@@ -241,6 +242,32 @@ func New(f fs.Fs, opt *vfscommon.Options) *VFS {
return vfs
}
// Stats returns info about the VFS
func (vfs *VFS) Stats() (out rc.Params) {
out = make(rc.Params)
out["fs"] = fs.ConfigString(vfs.f)
out["opt"] = vfs.Opt
out["inUse"] = atomic.LoadInt32(&vfs.inUse)
var (
dirs int
files int
)
vfs.root.walk(func(d *Dir) {
dirs++
files += len(d.items)
})
inf := make(rc.Params)
out["metadataCache"] = inf
inf["dirs"] = dirs
inf["files"] = files
if vfs.cache != nil {
out["diskCache"] = vfs.cache.Stats()
}
return out
}
// Return the number of active cache entries and a VFS if any are in
// the cache.
func activeCacheEntries() (vfs *VFS, count int) {

View File

@@ -21,6 +21,7 @@ import (
"github.com/rclone/rclone/fs/fserrors"
"github.com/rclone/rclone/fs/hash"
"github.com/rclone/rclone/fs/operations"
"github.com/rclone/rclone/fs/rc"
"github.com/rclone/rclone/lib/encoder"
"github.com/rclone/rclone/lib/file"
"github.com/rclone/rclone/vfs/vfscache/writeback"
@@ -145,6 +146,29 @@ func New(ctx context.Context, fremote fs.Fs, opt *vfscommon.Options, avFn AddVir
return c, nil
}
// Stats returns info about the Cache
func (c *Cache) Stats() (out rc.Params) {
out = make(rc.Params)
// read only - no locking needed to read these
out["path"] = c.root
out["pathMeta"] = c.metaRoot
out["hashType"] = c.hashType
uploadsInProgress, uploadsQueued := c.writeback.Stats()
out["uploadsInProgress"] = uploadsInProgress
out["uploadsQueued"] = uploadsQueued
c.mu.Lock()
defer c.mu.Unlock()
out["files"] = len(c.item)
out["erroredFiles"] = len(c.errItems)
out["bytesUsed"] = c.used
out["outOfSpace"] = c.outOfSpace
return out
}
// createDir creates a directory path, along with any necessary parents
func createDir(dir string) error {
return file.MkdirAll(dir, 0700)
@@ -172,7 +196,6 @@ func createRootDirs(parentOSPath string, relativeDirOSPath string) (dataOSPath s
// Returns an os path for the data cache file.
func (c *Cache) createItemDir(name string) (string, error) {
parent := vfscommon.FindParent(name)
leaf := filepath.Base(name)
parentPath := c.toOSPath(parent)
err := createDir(parentPath)
if err != nil {
@@ -183,7 +206,7 @@ func (c *Cache) createItemDir(name string) (string, error) {
if err != nil {
return "", fmt.Errorf("failed to create metadata cache item directory: %w", err)
}
return filepath.Join(parentPath, leaf), nil
return c.toOSPath(name), nil
}
// getBackend gets a backend for a cache root dir

View File

@@ -701,3 +701,15 @@ func TestCacheDump(t *testing.T) {
out = c.Dump()
assert.Equal(t, "Cache{\n}\n", out)
}
func TestCacheStats(t *testing.T) {
_, c, cleanup := newTestCache(t)
defer cleanup()
out := c.Stats()
assert.Equal(t, int64(0), out["bytesUsed"])
assert.Equal(t, 0, out["erroredFiles"])
assert.Equal(t, 0, out["files"])
assert.Equal(t, 0, out["uploadsInProgress"])
assert.Equal(t, 0, out["uploadsQueued"])
}