mirror of
https://github.com/rclone/rclone.git
synced 2025-12-06 00:03:32 +00:00
Compare commits
72 Commits
fix-drive-
...
fix-5951-m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69fdcd4300 | ||
|
|
c0331c0c83 | ||
|
|
533377a955 | ||
|
|
f6f7bb35d5 | ||
|
|
a667e03fc9 | ||
|
|
1045344943 | ||
|
|
5e469db420 | ||
|
|
946e84d194 | ||
|
|
162aba60eb | ||
|
|
d8a874c32b | ||
|
|
9c451d9ac6 | ||
|
|
8f3f24672c | ||
|
|
0eb7b716d9 | ||
|
|
ee9684e60f | ||
|
|
e0cbe413e1 | ||
|
|
2523dd6220 | ||
|
|
c504d97017 | ||
|
|
b783f09fc6 | ||
|
|
a301478a13 | ||
|
|
63b450a2a5 | ||
|
|
843b77aaaa | ||
|
|
3641727edb | ||
|
|
38e2f835ed | ||
|
|
bd4bbed592 | ||
|
|
994b501188 | ||
|
|
dfa9381814 | ||
|
|
2a85feda4b | ||
|
|
ad46af9168 | ||
|
|
2fed02211c | ||
|
|
237daa8aaf | ||
|
|
8aeca6c033 | ||
|
|
fd82876086 | ||
|
|
be1a668e95 | ||
|
|
9d4eab32d8 | ||
|
|
b4ba7b69b8 | ||
|
|
deef659aef | ||
|
|
4b99e84242 | ||
|
|
06bdf7c64c | ||
|
|
e1225b5729 | ||
|
|
871cc2f62d | ||
|
|
bc23bf11db | ||
|
|
b55575e622 | ||
|
|
328f0e7135 | ||
|
|
a52814eed9 | ||
|
|
071a9e882d | ||
|
|
4e2ca3330c | ||
|
|
408d9f3e7a | ||
|
|
0681a5c86a | ||
|
|
df09c3f555 | ||
|
|
c41814fd2d | ||
|
|
c2557cc432 | ||
|
|
3425726c50 | ||
|
|
46175a22d8 | ||
|
|
bcf0e15ad7 | ||
|
|
b91c349cd5 | ||
|
|
d252816706 | ||
|
|
729117af68 | ||
|
|
cd4d8d55ec | ||
|
|
f26abc89a6 | ||
|
|
b5abbe819f | ||
|
|
a351484997 | ||
|
|
099eff8891 | ||
|
|
c4cb167d4a | ||
|
|
38e100ab19 | ||
|
|
db95a0d6c3 | ||
|
|
df07964db3 | ||
|
|
fbc4c4ad9a | ||
|
|
4454b3e1ae | ||
|
|
f9321fccbb | ||
|
|
3c2252b7c0 | ||
|
|
51c952654c | ||
|
|
80e47be65f |
100
.github/workflows/build.yml
vendored
100
.github/workflows/build.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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{},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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, '/')
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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/)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
6
docs/static/css/custom.css
vendored
6
docs/static/css/custom.css
vendored
@@ -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;
|
||||
|
||||
@@ -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{
|
||||
|
||||
255
fs/config/ui.go
255
fs/config/ui.go
@@ -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:]...)
|
||||
}
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -264,7 +264,7 @@ backends:
|
||||
fastlist: true
|
||||
- backend: "pcloud"
|
||||
remote: "TestPcloud:"
|
||||
fastlist: false
|
||||
fastlist: true
|
||||
- backend: "webdav"
|
||||
remote: "TestWebdavNextcloud:"
|
||||
ignore:
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
48
vfs/rc.go
48
vfs/rc.go
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
27
vfs/vfs.go
27
vfs/vfs.go
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user