mirror of
https://github.com/rclone/rclone.git
synced 2025-12-12 06:13:20 +00:00
Compare commits
95 Commits
fix-connec
...
v1.53-stab
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3dfa7d9a3 | ||
|
|
1936847548 | ||
|
|
89b4ccbbfa | ||
|
|
3c985a436b | ||
|
|
703f6002dd | ||
|
|
7de13fc426 | ||
|
|
c2f6d48d45 | ||
|
|
9d9999d17b | ||
|
|
15f31d3ca4 | ||
|
|
0ea51f74a1 | ||
|
|
6cd360233d | ||
|
|
687d2d495b | ||
|
|
50a107a5f3 | ||
|
|
2ed2861d09 | ||
|
|
e2cd449c62 | ||
|
|
98dbbc78ab | ||
|
|
53c4191350 | ||
|
|
e4ece15e68 | ||
|
|
fbf46908bf | ||
|
|
a96539eeec | ||
|
|
86cd5230d7 | ||
|
|
716019cf7d | ||
|
|
c59fe40795 | ||
|
|
ecd60f2430 | ||
|
|
d2a5640c3a | ||
|
|
8d3acfb38c | ||
|
|
200de46249 | ||
|
|
cee618bc03 | ||
|
|
db2aa771dc | ||
|
|
55bd60019e | ||
|
|
c8b11d27e1 | ||
|
|
4c215cc81e | ||
|
|
4df333255a | ||
|
|
843d684568 | ||
|
|
46ea3d93b5 | ||
|
|
89f2d43f17 | ||
|
|
cfc5d76fca | ||
|
|
0af493f693 | ||
|
|
51b3ee9a97 | ||
|
|
6a4b49479d | ||
|
|
4b03ee0f99 | ||
|
|
2f6231f7ac | ||
|
|
c0e6f54f01 | ||
|
|
def7b77d0f | ||
|
|
51b18a4a26 | ||
|
|
7cb76f9054 | ||
|
|
00ccc93482 | ||
|
|
f9fe494d93 | ||
|
|
4a0c266787 | ||
|
|
f48d0a518c | ||
|
|
99ff594773 | ||
|
|
6c140705e3 | ||
|
|
e76963a971 | ||
|
|
43ad7b10a2 | ||
|
|
f6970c65dd | ||
|
|
6012179c67 | ||
|
|
3ecdd4516f | ||
|
|
3b18ba1358 | ||
|
|
5fbbab58ed | ||
|
|
80b93beedf | ||
|
|
eb5c47fcfa | ||
|
|
c7335e780b | ||
|
|
878ebf3658 | ||
|
|
1c860ef252 | ||
|
|
a0494479f9 | ||
|
|
9a9a134188 | ||
|
|
41ccf01f29 | ||
|
|
06f3daa64b | ||
|
|
d5fe63c0a0 | ||
|
|
b7f0e776f6 | ||
|
|
b89f8c05cf | ||
|
|
b81dc16484 | ||
|
|
0e121eeddb | ||
|
|
0430163180 | ||
|
|
09a0dc1600 | ||
|
|
dd11778ac6 | ||
|
|
f36cbe5194 | ||
|
|
82a383588b | ||
|
|
8ae4d2cffe | ||
|
|
0f895c0697 | ||
|
|
937dd7fa1f | ||
|
|
33869387d1 | ||
|
|
3ec8e304b3 | ||
|
|
e62362094e | ||
|
|
6a0398211d | ||
|
|
e5a53d4c65 | ||
|
|
59d5767a07 | ||
|
|
087b5788e2 | ||
|
|
d944bfd936 | ||
|
|
d780fcf317 | ||
|
|
0a9b8eac80 | ||
|
|
1272a8f9a5 | ||
|
|
0b40eaedaf | ||
|
|
8340ff4fb9 | ||
|
|
f5abc168ed |
37
.github/ISSUE_TEMPLATE/Bug.md
vendored
37
.github/ISSUE_TEMPLATE/Bug.md
vendored
@@ -5,31 +5,19 @@ about: Report a problem with rclone
|
|||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
|
||||||
We understand you are having a problem with rclone; we want to help you with that!
|
Welcome :-) We understand you are having a problem with rclone; we want to help you with that!
|
||||||
|
|
||||||
**STOP and READ**
|
If you've just got a question or aren't sure if you've found a bug then please use the rclone forum:
|
||||||
**YOUR POST WILL BE REMOVED IF IT IS LOW QUALITY**:
|
|
||||||
Please show the effort you've put in to solving the problem and please be specific.
|
|
||||||
People are volunteering their time to help! Low effort posts are not likely to get good answers!
|
|
||||||
|
|
||||||
If you think you might have found a bug, try to replicate it with the latest beta (or stable).
|
|
||||||
The update instructions are available at https://rclone.org/commands/rclone_selfupdate/
|
|
||||||
|
|
||||||
If you can still replicate it or just got a question then please use the rclone forum:
|
|
||||||
|
|
||||||
https://forum.rclone.org/
|
https://forum.rclone.org/
|
||||||
|
|
||||||
for a quick response instead of filing an issue on this repo.
|
instead of filing an issue for a quick response.
|
||||||
|
|
||||||
If nothing else helps, then please fill in the info below which helps us help you.
|
If you think you might have found a bug, please can you try to replicate it with the latest beta?
|
||||||
|
|
||||||
**DO NOT REDACT** any information except passwords/keys/personal info.
|
https://beta.rclone.org/
|
||||||
|
|
||||||
You should use 3 backticks to begin and end your paste to make it readable.
|
If you can still replicate it with the latest beta, then please fill in the info below which makes our lives much easier. A log with -vv will make our day :-)
|
||||||
|
|
||||||
Make sure to include a log obtained with '-vv'.
|
|
||||||
|
|
||||||
You can also use '-vv --log-file bug.log' and a service such as https://pastebin.com or https://gist.github.com/
|
|
||||||
|
|
||||||
Thank you
|
Thank you
|
||||||
|
|
||||||
@@ -37,11 +25,6 @@ The Rclone Developers
|
|||||||
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
|
||||||
#### The associated forum post URL from `https://forum.rclone.org`
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### What is the problem you are having with rclone?
|
#### What is the problem you are having with rclone?
|
||||||
|
|
||||||
|
|
||||||
@@ -50,18 +33,18 @@ The Rclone Developers
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### Which OS you are using and how many bits (e.g. Windows 7, 64 bit)
|
#### Which OS you are using and how many bits (eg Windows 7, 64 bit)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### Which cloud storage system are you using? (e.g. Google Drive)
|
#### Which cloud storage system are you using? (eg Google Drive)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### The command you were trying to run (e.g. `rclone copy /tmp remote:tmp`)
|
#### The command you were trying to run (eg `rclone copy /tmp remote:tmp`)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### A log from the command with the `-vv` flag (e.g. output from `rclone -vv copy /tmp remote:tmp`)
|
#### A log from the command with the `-vv` flag (eg output from `rclone -vv copy /tmp remote:tmp`)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
16
.github/ISSUE_TEMPLATE/Feature.md
vendored
16
.github/ISSUE_TEMPLATE/Feature.md
vendored
@@ -7,16 +7,12 @@ about: Suggest a new feature or enhancement for rclone
|
|||||||
|
|
||||||
Welcome :-)
|
Welcome :-)
|
||||||
|
|
||||||
So you've got an idea to improve rclone? We love that!
|
So you've got an idea to improve rclone? We love that! You'll be glad to hear we've incorporated hundreds of ideas from contributors already.
|
||||||
You'll be glad to hear we've incorporated hundreds of ideas from contributors already.
|
|
||||||
|
|
||||||
Probably the latest beta (or stable) release has your feature, so try to update your rclone.
|
Here is a checklist of things to do:
|
||||||
The update instructions are available at https://rclone.org/commands/rclone_selfupdate/
|
|
||||||
|
|
||||||
If it still isn't there, here is a checklist of things to do:
|
1. Please search the old issues first for your idea and +1 or comment on an existing issue if possible.
|
||||||
|
2. Discuss on the forum first: https://forum.rclone.org/
|
||||||
1. Search the old issues for your idea and +1 or comment on an existing issue if possible.
|
|
||||||
2. Discuss on the forum: https://forum.rclone.org/
|
|
||||||
3. Make a feature request issue (this is the right place!).
|
3. Make a feature request issue (this is the right place!).
|
||||||
4. Be prepared to get involved making the feature :-)
|
4. Be prepared to get involved making the feature :-)
|
||||||
|
|
||||||
@@ -27,10 +23,6 @@ The Rclone Developers
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
|
|
||||||
#### The associated forum post URL from `https://forum.rclone.org`
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### What is your current rclone version (output from `rclone version`)?
|
#### What is your current rclone version (output from `rclone version`)?
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
145
.github/workflows/build.yml
vendored
145
.github/workflows/build.yml
vendored
@@ -19,12 +19,12 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
job_name: ['linux', 'mac_amd64', 'mac_arm64', 'windows_amd64', 'windows_386', 'other_os', 'go1.13', 'go1.14', 'go1.15']
|
job_name: ['linux', 'mac', 'windows_amd64', 'windows_386', 'other_os', 'go1.11', 'go1.12', 'go1.13', 'go1.14']
|
||||||
|
|
||||||
include:
|
include:
|
||||||
- job_name: linux
|
- job_name: linux
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
go: '1.16.x'
|
go: '1.15.x'
|
||||||
gotags: cmount
|
gotags: cmount
|
||||||
build_flags: '-include "^linux/"'
|
build_flags: '-include "^linux/"'
|
||||||
check: true
|
check: true
|
||||||
@@ -32,25 +32,18 @@ jobs:
|
|||||||
racequicktest: true
|
racequicktest: true
|
||||||
deploy: true
|
deploy: true
|
||||||
|
|
||||||
- job_name: mac_amd64
|
- job_name: mac
|
||||||
os: macOS-latest
|
os: macOS-latest
|
||||||
go: '1.16.x'
|
go: '1.15.x'
|
||||||
gotags: 'cmount'
|
gotags: 'cmount'
|
||||||
build_flags: '-include "^darwin/amd64" -cgo'
|
build_flags: '-include "^darwin/amd64" -cgo'
|
||||||
quicktest: true
|
quicktest: true
|
||||||
racequicktest: true
|
racequicktest: true
|
||||||
deploy: true
|
deploy: true
|
||||||
|
|
||||||
- job_name: mac_arm64
|
|
||||||
os: macOS-latest
|
|
||||||
go: '1.16.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'
|
|
||||||
deploy: true
|
|
||||||
|
|
||||||
- job_name: windows_amd64
|
- job_name: windows_amd64
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
go: '1.16.x'
|
go: '1.15.x'
|
||||||
gotags: cmount
|
gotags: cmount
|
||||||
build_flags: '-include "^windows/amd64" -cgo'
|
build_flags: '-include "^windows/amd64" -cgo'
|
||||||
build_args: '-buildmode exe'
|
build_args: '-buildmode exe'
|
||||||
@@ -60,7 +53,7 @@ jobs:
|
|||||||
|
|
||||||
- job_name: windows_386
|
- job_name: windows_386
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
go: '1.16.x'
|
go: '1.15.x'
|
||||||
gotags: cmount
|
gotags: cmount
|
||||||
goarch: '386'
|
goarch: '386'
|
||||||
cgo: '1'
|
cgo: '1'
|
||||||
@@ -71,11 +64,21 @@ jobs:
|
|||||||
|
|
||||||
- job_name: other_os
|
- job_name: other_os
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
go: '1.16.x'
|
go: '1.15.x'
|
||||||
build_flags: '-exclude "^(windows/|darwin/|linux/)"'
|
build_flags: '-exclude "^(windows/|darwin/amd64|linux/)"'
|
||||||
compile_all: true
|
compile_all: true
|
||||||
deploy: true
|
deploy: true
|
||||||
|
|
||||||
|
- job_name: go1.11
|
||||||
|
os: ubuntu-latest
|
||||||
|
go: '1.11.x'
|
||||||
|
quicktest: true
|
||||||
|
|
||||||
|
- job_name: go1.12
|
||||||
|
os: ubuntu-latest
|
||||||
|
go: '1.12.x'
|
||||||
|
quicktest: true
|
||||||
|
|
||||||
- job_name: go1.13
|
- job_name: go1.13
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
go: '1.13.x'
|
go: '1.13.x'
|
||||||
@@ -87,12 +90,6 @@ jobs:
|
|||||||
quicktest: true
|
quicktest: true
|
||||||
racequicktest: true
|
racequicktest: true
|
||||||
|
|
||||||
- job_name: go1.15
|
|
||||||
os: ubuntu-latest
|
|
||||||
go: '1.15.x'
|
|
||||||
quicktest: true
|
|
||||||
racequicktest: true
|
|
||||||
|
|
||||||
name: ${{ matrix.job_name }}
|
name: ${{ matrix.job_name }}
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
@@ -131,7 +128,7 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
brew update
|
brew update
|
||||||
brew install --cask macfuse
|
brew install --cask osxfuse
|
||||||
if: matrix.os == 'macOS-latest'
|
if: matrix.os == 'macOS-latest'
|
||||||
|
|
||||||
- name: Install Libraries on Windows
|
- name: Install Libraries on Windows
|
||||||
@@ -213,94 +210,46 @@ jobs:
|
|||||||
# Deploy binaries if enabled in config && not a PR && not a fork
|
# Deploy binaries if enabled in config && not a PR && not a fork
|
||||||
if: matrix.deploy && github.head_ref == '' && github.repository == 'rclone/rclone'
|
if: matrix.deploy && github.head_ref == '' && github.repository == 'rclone/rclone'
|
||||||
|
|
||||||
android:
|
xgo:
|
||||||
timeout-minutes: 30
|
timeout-minutes: 60
|
||||||
name: "android-all"
|
name: "xgo cross compile"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v1
|
||||||
|
|
||||||
# Upgrade together with NDK version
|
|
||||||
- name: Set up Go 1.14
|
|
||||||
uses: actions/setup-go@v1
|
|
||||||
with:
|
with:
|
||||||
go-version: 1.14
|
# Checkout into a fixed path to avoid import path problems on go < 1.11
|
||||||
|
path: ./src/github.com/rclone/rclone
|
||||||
|
|
||||||
# Upgrade together with Go version. Using a GitHub-provided version saves around 2 minutes.
|
- name: Set environment variables
|
||||||
- name: Force NDK version
|
|
||||||
run: echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;21.4.7075529" | grep -v = || true
|
|
||||||
|
|
||||||
- name: Go module cache
|
|
||||||
uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: ~/go/pkg/mod
|
|
||||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-go-
|
|
||||||
|
|
||||||
- name: Set global environment variables
|
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
echo "VERSION=$(make version)" >> $GITHUB_ENV
|
echo 'GOPATH=${{ runner.workspace }}' >> $GITHUB_ENV
|
||||||
|
echo '${{ runner.workspace }}/bin' >> $GITHUB_PATH
|
||||||
|
|
||||||
- name: build native rclone
|
- name: Cross-compile rclone
|
||||||
|
run: |
|
||||||
|
docker pull billziss/xgo-cgofuse
|
||||||
|
GO111MODULE=off go get -v github.com/karalabe/xgo # don't add to go.mod
|
||||||
|
# xgo \
|
||||||
|
# -image=billziss/xgo-cgofuse \
|
||||||
|
# -targets=darwin/amd64,linux/386,linux/amd64,windows/386,windows/amd64 \
|
||||||
|
# -tags cmount \
|
||||||
|
# -dest build \
|
||||||
|
# .
|
||||||
|
xgo \
|
||||||
|
-image=billziss/xgo-cgofuse \
|
||||||
|
-targets=android/*,ios/* \
|
||||||
|
-dest build \
|
||||||
|
.
|
||||||
|
|
||||||
|
- name: Build rclone
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
make
|
make
|
||||||
|
|
||||||
- name: arm-v7a Set environment variables
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
echo "CC=$(echo $ANDROID_HOME/ndk/21.4.7075529/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi16-clang)" >> $GITHUB_ENV
|
|
||||||
echo "CC_FOR_TARGET=$CC" >> $GITHUB_ENV
|
|
||||||
echo 'GOOS=android' >> $GITHUB_ENV
|
|
||||||
echo 'GOARCH=arm' >> $GITHUB_ENV
|
|
||||||
echo 'GOARM=7' >> $GITHUB_ENV
|
|
||||||
echo 'CGO_ENABLED=1' >> $GITHUB_ENV
|
|
||||||
echo 'CGO_LDFLAGS=-fuse-ld=lld -s -w' >> $GITHUB_ENV
|
|
||||||
- name: arm-v7a build
|
|
||||||
run: go build -v -tags android -trimpath -ldflags '-s -X github.com/rclone/rclone/fs.Version='${VERSION} -o build/rclone-android-16-armv7a .
|
|
||||||
|
|
||||||
- name: arm64-v8a Set environment variables
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
echo "CC=$(echo $ANDROID_HOME/ndk/21.4.7075529/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android21-clang)" >> $GITHUB_ENV
|
|
||||||
echo "CC_FOR_TARGET=$CC" >> $GITHUB_ENV
|
|
||||||
echo 'GOOS=android' >> $GITHUB_ENV
|
|
||||||
echo 'GOARCH=arm64' >> $GITHUB_ENV
|
|
||||||
echo 'CGO_ENABLED=1' >> $GITHUB_ENV
|
|
||||||
echo 'CGO_LDFLAGS=-fuse-ld=lld -s -w' >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: arm64-v8a build
|
|
||||||
run: go build -v -tags android -trimpath -ldflags '-s -X github.com/rclone/rclone/fs.Version='${VERSION} -o build/rclone-android-21-armv8a .
|
|
||||||
|
|
||||||
- name: x86 Set environment variables
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
echo "CC=$(echo $ANDROID_HOME/ndk/21.4.7075529/toolchains/llvm/prebuilt/linux-x86_64/bin/i686-linux-android16-clang)" >> $GITHUB_ENV
|
|
||||||
echo "CC_FOR_TARGET=$CC" >> $GITHUB_ENV
|
|
||||||
echo 'GOOS=android' >> $GITHUB_ENV
|
|
||||||
echo 'GOARCH=386' >> $GITHUB_ENV
|
|
||||||
echo 'CGO_ENABLED=1' >> $GITHUB_ENV
|
|
||||||
echo 'CGO_LDFLAGS=-fuse-ld=lld -s -w' >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: x86 build
|
|
||||||
run: go build -v -tags android -trimpath -ldflags '-s -X github.com/rclone/rclone/fs.Version='${VERSION} -o build/rclone-android-16-x86 .
|
|
||||||
|
|
||||||
- name: x64 Set environment variables
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
echo "CC=$(echo $ANDROID_HOME/ndk/21.4.7075529/toolchains/llvm/prebuilt/linux-x86_64/bin/x86_64-linux-android21-clang)" >> $GITHUB_ENV
|
|
||||||
echo "CC_FOR_TARGET=$CC" >> $GITHUB_ENV
|
|
||||||
echo 'GOOS=android' >> $GITHUB_ENV
|
|
||||||
echo 'GOARCH=amd64' >> $GITHUB_ENV
|
|
||||||
echo 'CGO_ENABLED=1' >> $GITHUB_ENV
|
|
||||||
echo 'CGO_LDFLAGS=-fuse-ld=lld -s -w' >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: x64 build
|
|
||||||
run: go build -v -tags android -trimpath -ldflags '-s -X github.com/rclone/rclone/fs.Version='${VERSION} -o build/rclone-android-21-x64 .
|
|
||||||
|
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
run: |
|
run: |
|
||||||
make ci_upload
|
make ci_upload
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,7 +1,6 @@
|
|||||||
*~
|
*~
|
||||||
_junk/
|
_junk/
|
||||||
rclone
|
rclone
|
||||||
rclone.exe
|
|
||||||
build
|
build
|
||||||
docs/public
|
docs/public
|
||||||
rclone.iml
|
rclone.iml
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ When filing an issue, please include the following information if
|
|||||||
possible as well as a description of the problem. Make sure you test
|
possible as well as a description of the problem. Make sure you test
|
||||||
with the [latest beta of rclone](https://beta.rclone.org/):
|
with the [latest beta of rclone](https://beta.rclone.org/):
|
||||||
|
|
||||||
* Rclone version (e.g. output from `rclone -V`)
|
* Rclone version (eg output from `rclone -V`)
|
||||||
* Which OS you are using and how many bits (e.g. Windows 7, 64 bit)
|
* Which OS you are using and how many bits (eg Windows 7, 64 bit)
|
||||||
* The command you were trying to run (e.g. `rclone copy /tmp remote:tmp`)
|
* The command you were trying to run (eg `rclone copy /tmp remote:tmp`)
|
||||||
* A log of the command with the `-vv` flag (e.g. output from `rclone -vv copy /tmp remote:tmp`)
|
* A log of the command with the `-vv` flag (eg output from `rclone -vv copy /tmp remote:tmp`)
|
||||||
* if the log contains secrets then edit the file with a text editor first to obscure them
|
* if the log contains secrets then edit the file with a text editor first to obscure them
|
||||||
|
|
||||||
## Submitting a pull request ##
|
## Submitting a pull request ##
|
||||||
@@ -33,11 +33,10 @@ page](https://github.com/rclone/rclone).
|
|||||||
|
|
||||||
Now in your terminal
|
Now in your terminal
|
||||||
|
|
||||||
git clone https://github.com/rclone/rclone.git
|
go get -u github.com/rclone/rclone
|
||||||
cd rclone
|
cd $GOPATH/src/github.com/rclone/rclone
|
||||||
git remote rename origin upstream
|
git remote rename origin upstream
|
||||||
git remote add origin git@github.com:YOURUSER/rclone.git
|
git remote add origin git@github.com:YOURUSER/rclone.git
|
||||||
go build
|
|
||||||
|
|
||||||
Make a branch to add your new feature
|
Make a branch to add your new feature
|
||||||
|
|
||||||
@@ -49,7 +48,7 @@ When ready - run the unit tests for the code you changed
|
|||||||
|
|
||||||
go test -v
|
go test -v
|
||||||
|
|
||||||
Note that you may need to make a test remote, e.g. `TestSwift` for some
|
Note that you may need to make a test remote, eg `TestSwift` for some
|
||||||
of the unit tests.
|
of the unit tests.
|
||||||
|
|
||||||
Note the top level Makefile targets
|
Note the top level Makefile targets
|
||||||
@@ -73,7 +72,7 @@ Make sure you
|
|||||||
|
|
||||||
When you are done with that
|
When you are done with that
|
||||||
|
|
||||||
git push -u origin my-new-feature
|
git push origin my-new-feature
|
||||||
|
|
||||||
Go to the GitHub website and click [Create pull
|
Go to the GitHub website and click [Create pull
|
||||||
request](https://help.github.com/articles/creating-a-pull-request/).
|
request](https://help.github.com/articles/creating-a-pull-request/).
|
||||||
@@ -87,7 +86,7 @@ git reset --soft HEAD~2 # This squashes the 2 latest commits together.
|
|||||||
git status # Check what will happen, if you made a mistake resetting, you can run git reset 'HEAD@{1}' to undo.
|
git status # Check what will happen, if you made a mistake resetting, you can run git reset 'HEAD@{1}' to undo.
|
||||||
git commit # Add a new commit message.
|
git commit # Add a new commit message.
|
||||||
git push --force # Push the squashed commit to your GitHub repo.
|
git push --force # Push the squashed commit to your GitHub repo.
|
||||||
# For more, see Stack Overflow, Git docs, or generally Duck around the web. jtagcat also recommends wizardzines.com
|
# For more, see Stack Overflow, Git docs, or generally Duck around the web. jtagcat also reccommends wizardzines.com
|
||||||
```
|
```
|
||||||
|
|
||||||
## CI for your fork ##
|
## CI for your fork ##
|
||||||
@@ -116,8 +115,8 @@ are skipped if `TestDrive:` isn't defined.
|
|||||||
cd backend/drive
|
cd backend/drive
|
||||||
go test -v
|
go test -v
|
||||||
|
|
||||||
You can then run the integration tests which test all of rclone's
|
You can then run the integration tests which tests all of rclone's
|
||||||
operations. Normally these get run against the local file system,
|
operations. Normally these get run against the local filing system,
|
||||||
but they can be run against any of the remotes.
|
but they can be run against any of the remotes.
|
||||||
|
|
||||||
cd fs/sync
|
cd fs/sync
|
||||||
@@ -128,7 +127,7 @@ but they can be run against any of the remotes.
|
|||||||
go test -v -remote TestDrive:
|
go test -v -remote TestDrive:
|
||||||
|
|
||||||
If you want to use the integration test framework to run these tests
|
If you want to use the integration test framework to run these tests
|
||||||
altogether with an HTML report and test retries then from the
|
all together with an HTML report and test retries then from the
|
||||||
project root:
|
project root:
|
||||||
|
|
||||||
go install github.com/rclone/rclone/fstest/test_all
|
go install github.com/rclone/rclone/fstest/test_all
|
||||||
@@ -171,7 +170,7 @@ with modules beneath.
|
|||||||
* log - logging facilities
|
* log - logging facilities
|
||||||
* march - iterates directories in lock step
|
* march - iterates directories in lock step
|
||||||
* object - in memory Fs objects
|
* object - in memory Fs objects
|
||||||
* operations - primitives for sync, e.g. Copy, Move
|
* operations - primitives for sync, eg Copy, Move
|
||||||
* sync - sync directories
|
* sync - sync directories
|
||||||
* walk - walk a directory
|
* walk - walk a directory
|
||||||
* fstest - provides integration test framework
|
* fstest - provides integration test framework
|
||||||
@@ -179,7 +178,7 @@ with modules beneath.
|
|||||||
* mockdir - mocks an fs.Directory
|
* mockdir - mocks an fs.Directory
|
||||||
* mockobject - mocks an fs.Object
|
* mockobject - mocks an fs.Object
|
||||||
* test_all - Runs integration tests for everything
|
* test_all - Runs integration tests for everything
|
||||||
* graphics - the images used in the website, etc.
|
* graphics - the images used in the website etc
|
||||||
* lib - libraries used by the backend
|
* lib - libraries used by the backend
|
||||||
* atexit - register functions to run when rclone exits
|
* atexit - register functions to run when rclone exits
|
||||||
* dircache - directory ID to name caching
|
* dircache - directory ID to name caching
|
||||||
@@ -203,12 +202,12 @@ for the flag help, the remainder is shown to the user in `rclone
|
|||||||
config` and is added to the docs with `make backenddocs`.
|
config` and is added to the docs with `make backenddocs`.
|
||||||
|
|
||||||
The only documentation you need to edit are the `docs/content/*.md`
|
The only documentation you need to edit are the `docs/content/*.md`
|
||||||
files. The `MANUAL.*`, `rclone.1`, web site, etc. are all auto generated
|
files. The MANUAL.*, rclone.1, web site etc are all auto generated
|
||||||
from those during the release process. See the `make doc` and `make
|
from those during the release process. See the `make doc` and `make
|
||||||
website` targets in the Makefile if you are interested in how. You
|
website` targets in the Makefile if you are interested in how. You
|
||||||
don't need to run these when adding a feature.
|
don't need to run these when adding a feature.
|
||||||
|
|
||||||
Documentation for rclone sub commands is with their code, e.g.
|
Documentation for rclone sub commands is with their code, eg
|
||||||
`cmd/ls/ls.go`.
|
`cmd/ls/ls.go`.
|
||||||
|
|
||||||
Note that you can use [GitHub's online editor](https://help.github.com/en/github/managing-files-in-a-repository/editing-files-in-another-users-repository)
|
Note that you can use [GitHub's online editor](https://help.github.com/en/github/managing-files-in-a-repository/editing-files-in-another-users-repository)
|
||||||
@@ -266,7 +265,7 @@ rclone uses the [go
|
|||||||
modules](https://tip.golang.org/cmd/go/#hdr-Modules__module_versions__and_more)
|
modules](https://tip.golang.org/cmd/go/#hdr-Modules__module_versions__and_more)
|
||||||
support in go1.11 and later to manage its dependencies.
|
support in go1.11 and later to manage its dependencies.
|
||||||
|
|
||||||
rclone can be built with modules outside of the `GOPATH`.
|
rclone can be built with modules outside of the GOPATH
|
||||||
|
|
||||||
To add a dependency `github.com/ncw/new_dependency` see the
|
To add a dependency `github.com/ncw/new_dependency` see the
|
||||||
instructions below. These will fetch the dependency and add it to
|
instructions below. These will fetch the dependency and add it to
|
||||||
@@ -334,8 +333,8 @@ Getting going
|
|||||||
* Try to implement as many optional methods as possible as it makes the remote more usable.
|
* Try to implement as many optional methods as possible as it makes the remote more usable.
|
||||||
* Use lib/encoder to make sure we can encode any path name and `rclone info` to help determine the encodings needed
|
* Use lib/encoder to make sure we can encode any path name and `rclone info` to help determine the encodings needed
|
||||||
* `rclone purge -v TestRemote:rclone-info`
|
* `rclone purge -v TestRemote:rclone-info`
|
||||||
* `rclone test info --all --remote-encoding None -vv --write-json remote.json TestRemote:rclone-info`
|
* `rclone info --remote-encoding None -vv --write-json remote.json TestRemote:rclone-info`
|
||||||
* `go run cmd/test/info/internal/build_csv/main.go -o remote.csv remote.json`
|
* `go run cmd/info/internal/build_csv/main.go -o remote.csv remote.json`
|
||||||
* open `remote.csv` in a spreadsheet and examine
|
* open `remote.csv` in a spreadsheet and examine
|
||||||
|
|
||||||
Unit tests
|
Unit tests
|
||||||
@@ -365,7 +364,7 @@ See the [testing](#testing) section for more information on integration tests.
|
|||||||
|
|
||||||
Add your fs to the docs - you'll need to pick an icon for it from
|
Add your fs to the docs - you'll need to pick an icon for it from
|
||||||
[fontawesome](http://fontawesome.io/icons/). Keep lists of remotes in
|
[fontawesome](http://fontawesome.io/icons/). Keep lists of remotes in
|
||||||
alphabetical order of full name of remote (e.g. `drive` is ordered as
|
alphabetical order of full name of remote (eg `drive` is ordered as
|
||||||
`Google Drive`) but with the local file system last.
|
`Google Drive`) but with the local file system last.
|
||||||
|
|
||||||
* `README.md` - main GitHub page
|
* `README.md` - main GitHub page
|
||||||
|
|||||||
@@ -16,8 +16,6 @@ RUN apk --no-cache add ca-certificates fuse tzdata && \
|
|||||||
|
|
||||||
COPY --from=builder /go/src/github.com/rclone/rclone/rclone /usr/local/bin/
|
COPY --from=builder /go/src/github.com/rclone/rclone/rclone /usr/local/bin/
|
||||||
|
|
||||||
RUN addgroup -g 1009 rclone && adduser -u 1009 -Ds /bin/sh -G rclone rclone
|
|
||||||
|
|
||||||
ENTRYPOINT [ "rclone" ]
|
ENTRYPOINT [ "rclone" ]
|
||||||
|
|
||||||
WORKDIR /data
|
WORKDIR /data
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ Current active maintainers of rclone are:
|
|||||||
| Fabian Möller | @B4dM4n | |
|
| Fabian Möller | @B4dM4n | |
|
||||||
| Alex Chen | @Cnly | onedrive backend |
|
| Alex Chen | @Cnly | onedrive backend |
|
||||||
| Sandeep Ummadi | @sandeepkru | azureblob backend |
|
| Sandeep Ummadi | @sandeepkru | azureblob backend |
|
||||||
| Sebastian Bünger | @buengese | jottacloud, yandex & compress backends |
|
| Sebastian Bünger | @buengese | jottacloud & yandex backends |
|
||||||
| Ivan Andreev | @ivandeex | chunker & mailru backends |
|
| Ivan Andreev | @ivandeex | chunker & mailru backends |
|
||||||
| Max Sum | @Max-Sum | union backend |
|
| Max Sum | @Max-Sum | union backend |
|
||||||
| Fred | @creativeprojects | seafile backend |
|
| Fred | @creativeprojects | seafile backend |
|
||||||
@@ -37,7 +37,7 @@ Rclone uses the labels like this:
|
|||||||
* `good first issue` - mark these if you find a small self contained issue - these get shown to new visitors to the project
|
* `good first issue` - mark these if you find a small self contained issue - these get shown to new visitors to the project
|
||||||
* `help` wanted - mark these if you find a self contained issue - these get shown to new visitors to the project
|
* `help` wanted - mark these if you find a self contained issue - these get shown to new visitors to the project
|
||||||
* `IMPORTANT` - note to maintainers not to forget to fix this for the release
|
* `IMPORTANT` - note to maintainers not to forget to fix this for the release
|
||||||
* `maintenance` - internal enhancement, code re-organisation, etc.
|
* `maintenance` - internal enhancement, code re-organisation etc
|
||||||
* `Needs Go 1.XX` - waiting for that version of Go to be released
|
* `Needs Go 1.XX` - waiting for that version of Go to be released
|
||||||
* `question` - not a `bug` or `enhancement` - direct to the forum for next time
|
* `question` - not a `bug` or `enhancement` - direct to the forum for next time
|
||||||
* `Remote: XXX` - which rclone backend this affects
|
* `Remote: XXX` - which rclone backend this affects
|
||||||
@@ -45,7 +45,7 @@ Rclone uses the labels like this:
|
|||||||
|
|
||||||
If it turns out to be a bug or an enhancement it should be tagged as such, with the appropriate other tags. Don't forget the "good first issue" tag to give new contributors something easy to do to get going.
|
If it turns out to be a bug or an enhancement it should be tagged as such, with the appropriate other tags. Don't forget the "good first issue" tag to give new contributors something easy to do to get going.
|
||||||
|
|
||||||
When a ticket is tagged it should be added to a milestone, either the next release, the one after, Soon or Help Wanted. Bugs can be added to the "Known Bugs" milestone if they aren't planned to be fixed or need to wait for something (e.g. the next go release).
|
When a ticket is tagged it should be added to a milestone, either the next release, the one after, Soon or Help Wanted. Bugs can be added to the "Known Bugs" milestone if they aren't planned to be fixed or need to wait for something (eg the next go release).
|
||||||
|
|
||||||
The milestones have these meanings:
|
The milestones have these meanings:
|
||||||
|
|
||||||
|
|||||||
4694
MANUAL.html
generated
4694
MANUAL.html
generated
File diff suppressed because it is too large
Load Diff
6316
MANUAL.txt
generated
6316
MANUAL.txt
generated
File diff suppressed because it is too large
Load Diff
13
Makefile
13
Makefile
@@ -93,7 +93,8 @@ build_dep:
|
|||||||
|
|
||||||
# Get the release dependencies we only install on linux
|
# Get the release dependencies we only install on linux
|
||||||
release_dep_linux:
|
release_dep_linux:
|
||||||
go run bin/get-github-release.go -extract nfpm goreleaser/nfpm 'nfpm_.*_Linux_x86_64\.tar\.gz'
|
go run bin/get-github-release.go -extract nfpm goreleaser/nfpm 'nfpm_.*_Linux_x86_64.tar.gz'
|
||||||
|
go run bin/get-github-release.go -extract github-release aktau/github-release 'linux-amd64-github-release.tar.bz2'
|
||||||
|
|
||||||
# Get the release dependencies we only install on Windows
|
# Get the release dependencies we only install on Windows
|
||||||
release_dep_windows:
|
release_dep_windows:
|
||||||
@@ -119,7 +120,7 @@ doc: rclone.1 MANUAL.html MANUAL.txt rcdocs commanddocs
|
|||||||
rclone.1: MANUAL.md
|
rclone.1: MANUAL.md
|
||||||
pandoc -s --from markdown-smart --to man MANUAL.md -o rclone.1
|
pandoc -s --from markdown-smart --to man MANUAL.md -o rclone.1
|
||||||
|
|
||||||
MANUAL.md: bin/make_manual.py docs/content/*.md commanddocs backenddocs rcdocs
|
MANUAL.md: bin/make_manual.py docs/content/*.md commanddocs backenddocs
|
||||||
./bin/make_manual.py
|
./bin/make_manual.py
|
||||||
|
|
||||||
MANUAL.html: MANUAL.md
|
MANUAL.html: MANUAL.md
|
||||||
@@ -187,10 +188,10 @@ upload_github:
|
|||||||
./bin/upload-github $(TAG)
|
./bin/upload-github $(TAG)
|
||||||
|
|
||||||
cross: doc
|
cross: doc
|
||||||
go run bin/cross-compile.go -release current $(BUILD_FLAGS) $(BUILDTAGS) $(BUILD_ARGS) $(TAG)
|
go run bin/cross-compile.go -release current $(BUILDTAGS) $(BUILD_ARGS) $(TAG)
|
||||||
|
|
||||||
beta:
|
beta:
|
||||||
go run bin/cross-compile.go $(BUILD_FLAGS) $(BUILDTAGS) $(BUILD_ARGS) $(TAG)
|
go run bin/cross-compile.go $(BUILDTAGS) $(BUILD_ARGS) $(TAG)
|
||||||
rclone -v copy build/ memstore:pub-rclone-org/$(TAG)
|
rclone -v copy build/ memstore:pub-rclone-org/$(TAG)
|
||||||
@echo Beta release ready at https://pub.rclone.org/$(TAG)/
|
@echo Beta release ready at https://pub.rclone.org/$(TAG)/
|
||||||
|
|
||||||
@@ -198,7 +199,7 @@ log_since_last_release:
|
|||||||
git log $(LAST_TAG)..
|
git log $(LAST_TAG)..
|
||||||
|
|
||||||
compile_all:
|
compile_all:
|
||||||
go run bin/cross-compile.go -compile-only $(BUILD_FLAGS) $(BUILDTAGS) $(BUILD_ARGS) $(TAG)
|
go run bin/cross-compile.go -compile-only $(BUILDTAGS) $(BUILD_ARGS) $(TAG)
|
||||||
|
|
||||||
ci_upload:
|
ci_upload:
|
||||||
sudo chown -R $$USER build
|
sudo chown -R $$USER build
|
||||||
@@ -232,7 +233,7 @@ tag: retag doc
|
|||||||
@echo "Edit the new changelog in docs/content/changelog.md"
|
@echo "Edit the new changelog in docs/content/changelog.md"
|
||||||
@echo "Then commit all the changes"
|
@echo "Then commit all the changes"
|
||||||
@echo git commit -m \"Version $(VERSION)\" -a -v
|
@echo git commit -m \"Version $(VERSION)\" -a -v
|
||||||
@echo "And finally run make retag before make cross, etc."
|
@echo "And finally run make retag before make cross etc"
|
||||||
|
|
||||||
retag:
|
retag:
|
||||||
@echo "Version is $(VERSION)"
|
@echo "Version is $(VERSION)"
|
||||||
|
|||||||
@@ -30,13 +30,11 @@ Rclone *("rsync for cloud storage")* is a command line program to sync files and
|
|||||||
* DigitalOcean Spaces [:page_facing_up:](https://rclone.org/s3/#digitalocean-spaces)
|
* DigitalOcean Spaces [:page_facing_up:](https://rclone.org/s3/#digitalocean-spaces)
|
||||||
* Dreamhost [:page_facing_up:](https://rclone.org/s3/#dreamhost)
|
* Dreamhost [:page_facing_up:](https://rclone.org/s3/#dreamhost)
|
||||||
* Dropbox [:page_facing_up:](https://rclone.org/dropbox/)
|
* Dropbox [:page_facing_up:](https://rclone.org/dropbox/)
|
||||||
* Enterprise File Fabric [:page_facing_up:](https://rclone.org/filefabric/)
|
|
||||||
* FTP [:page_facing_up:](https://rclone.org/ftp/)
|
* FTP [:page_facing_up:](https://rclone.org/ftp/)
|
||||||
* GetSky [:page_facing_up:](https://rclone.org/jottacloud/)
|
* GetSky [:page_facing_up:](https://rclone.org/jottacloud/)
|
||||||
* Google Cloud Storage [:page_facing_up:](https://rclone.org/googlecloudstorage/)
|
* Google Cloud Storage [:page_facing_up:](https://rclone.org/googlecloudstorage/)
|
||||||
* Google Drive [:page_facing_up:](https://rclone.org/drive/)
|
* Google Drive [:page_facing_up:](https://rclone.org/drive/)
|
||||||
* Google Photos [:page_facing_up:](https://rclone.org/googlephotos/)
|
* Google Photos [:page_facing_up:](https://rclone.org/googlephotos/)
|
||||||
* HDFS (Hadoop Distributed Filesystem) [:page_facing_up:](https://rclone.org/hdfs/)
|
|
||||||
* HTTP [:page_facing_up:](https://rclone.org/http/)
|
* HTTP [:page_facing_up:](https://rclone.org/http/)
|
||||||
* Hubic [:page_facing_up:](https://rclone.org/hubic/)
|
* Hubic [:page_facing_up:](https://rclone.org/hubic/)
|
||||||
* Jottacloud [:page_facing_up:](https://rclone.org/jottacloud/)
|
* Jottacloud [:page_facing_up:](https://rclone.org/jottacloud/)
|
||||||
@@ -70,7 +68,6 @@ Rclone *("rsync for cloud storage")* is a command line program to sync files and
|
|||||||
* Wasabi [:page_facing_up:](https://rclone.org/s3/#wasabi)
|
* Wasabi [:page_facing_up:](https://rclone.org/s3/#wasabi)
|
||||||
* WebDAV [:page_facing_up:](https://rclone.org/webdav/)
|
* WebDAV [:page_facing_up:](https://rclone.org/webdav/)
|
||||||
* Yandex Disk [:page_facing_up:](https://rclone.org/yandex/)
|
* Yandex Disk [:page_facing_up:](https://rclone.org/yandex/)
|
||||||
* Zoho WorkDrive [:page_facing_up:](https://rclone.org/zoho/)
|
|
||||||
* The local filesystem [:page_facing_up:](https://rclone.org/local/)
|
* The local filesystem [:page_facing_up:](https://rclone.org/local/)
|
||||||
|
|
||||||
Please see [the full list of all storage providers and their features](https://rclone.org/overview/)
|
Please see [the full list of all storage providers and their features](https://rclone.org/overview/)
|
||||||
@@ -85,7 +82,6 @@ Please see [the full list of all storage providers and their features](https://r
|
|||||||
* [Check](https://rclone.org/commands/rclone_check/) mode to check for file hash equality
|
* [Check](https://rclone.org/commands/rclone_check/) mode to check for file hash equality
|
||||||
* Can sync to and from network, e.g. two different cloud accounts
|
* Can sync to and from network, e.g. two different cloud accounts
|
||||||
* Optional large file chunking ([Chunker](https://rclone.org/chunker/))
|
* Optional large file chunking ([Chunker](https://rclone.org/chunker/))
|
||||||
* Optional transparent compression ([Compress](https://rclone.org/compress/))
|
|
||||||
* Optional encryption ([Crypt](https://rclone.org/crypt/))
|
* Optional encryption ([Crypt](https://rclone.org/crypt/))
|
||||||
* Optional cache ([Cache](https://rclone.org/cache/))
|
* Optional cache ([Cache](https://rclone.org/cache/))
|
||||||
* Optional FUSE mount ([rclone mount](https://rclone.org/commands/rclone_mount/))
|
* Optional FUSE mount ([rclone mount](https://rclone.org/commands/rclone_mount/))
|
||||||
|
|||||||
26
RELEASE.md
26
RELEASE.md
@@ -4,7 +4,7 @@ This file describes how to make the various kinds of releases
|
|||||||
|
|
||||||
## Extra required software for making a release
|
## Extra required software for making a release
|
||||||
|
|
||||||
* [gh the github cli](https://github.com/cli/cli) for uploading packages
|
* [github-release](https://github.com/aktau/github-release) for uploading packages
|
||||||
* pandoc for making the html and man pages
|
* pandoc for making the html and man pages
|
||||||
|
|
||||||
## Making a release
|
## Making a release
|
||||||
@@ -21,7 +21,7 @@ This file describes how to make the various kinds of releases
|
|||||||
* git status - to check for new man pages - git add them
|
* git status - to check for new man pages - git add them
|
||||||
* git commit -a -v -m "Version v1.XX.0"
|
* git commit -a -v -m "Version v1.XX.0"
|
||||||
* make retag
|
* make retag
|
||||||
* git push --follow-tags origin
|
* git push --tags origin master
|
||||||
* # Wait for the GitHub builds to complete then...
|
* # Wait for the GitHub builds to complete then...
|
||||||
* make fetch_binaries
|
* make fetch_binaries
|
||||||
* make tarball
|
* make tarball
|
||||||
@@ -48,8 +48,8 @@ If rclone needs a point release due to some horrendous bug:
|
|||||||
|
|
||||||
Set vars
|
Set vars
|
||||||
|
|
||||||
* BASE_TAG=v1.XX # e.g. v1.52
|
* BASE_TAG=v1.XX # eg v1.52
|
||||||
* NEW_TAG=${BASE_TAG}.Y # e.g. v1.52.1
|
* NEW_TAG=${BASE_TAG}.Y # eg v1.52.1
|
||||||
* echo $BASE_TAG $NEW_TAG # v1.52 v1.52.1
|
* echo $BASE_TAG $NEW_TAG # v1.52 v1.52.1
|
||||||
|
|
||||||
First make the release branch. If this is a second point release then
|
First make the release branch. If this is a second point release then
|
||||||
@@ -76,24 +76,6 @@ Now
|
|||||||
The rclone docker image should autobuild on via GitHub actions. If it doesn't
|
The rclone docker image should autobuild on via GitHub actions. If it doesn't
|
||||||
or needs to be updated then rebuild like this.
|
or needs to be updated then rebuild like this.
|
||||||
|
|
||||||
See: https://github.com/ilteoood/docker_buildx/issues/19
|
|
||||||
See: https://github.com/ilteoood/docker_buildx/blob/master/scripts/install_buildx.sh
|
|
||||||
|
|
||||||
```
|
|
||||||
git co v1.54.1
|
|
||||||
docker pull golang
|
|
||||||
export DOCKER_CLI_EXPERIMENTAL=enabled
|
|
||||||
docker buildx create --name actions_builder --use
|
|
||||||
docker run --rm --privileged docker/binfmt:820fdd95a9972a5308930a2bdfb8573dd4447ad3
|
|
||||||
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
|
|
||||||
SUPPORTED_PLATFORMS=$(docker buildx inspect --bootstrap | grep 'Platforms:*.*' | cut -d : -f2,3)
|
|
||||||
echo "Supported platforms: $SUPPORTED_PLATFORMS"
|
|
||||||
docker buildx build --platform linux/amd64,linux/386,linux/arm64,linux/arm/v7 -t rclone/rclone:1.54.1 -t rclone/rclone:1.54 -t rclone/rclone:1 -t rclone/rclone:latest --push .
|
|
||||||
docker buildx stop actions_builder
|
|
||||||
```
|
|
||||||
|
|
||||||
### Old build for linux/amd64 only
|
|
||||||
|
|
||||||
```
|
```
|
||||||
docker pull golang
|
docker pull golang
|
||||||
docker build --rm --ulimit memlock=67108864 -t rclone/rclone:1.52.0 -t rclone/rclone:1.52 -t rclone/rclone:1 -t rclone/rclone:latest .
|
docker build --rm --ulimit memlock=67108864 -t rclone/rclone:1.52.0 -t rclone/rclone:1.52 -t rclone/rclone:1 -t rclone/rclone:latest .
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package alias
|
package alias
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -35,7 +34,7 @@ type Options struct {
|
|||||||
// NewFs constructs an Fs from the path.
|
// NewFs constructs an Fs from the path.
|
||||||
//
|
//
|
||||||
// The returned Fs is the actual Fs, referenced by remote in the config
|
// The returned Fs is the actual Fs, referenced by remote in the config
|
||||||
func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, error) {
|
func NewFs(name, root string, m configmap.Mapper) (fs.Fs, error) {
|
||||||
// Parse config into Options struct
|
// Parse config into Options struct
|
||||||
opt := new(Options)
|
opt := new(Options)
|
||||||
err := configstruct.Set(m, opt)
|
err := configstruct.Set(m, opt)
|
||||||
@@ -48,5 +47,5 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
if strings.HasPrefix(opt.Remote, name+":") {
|
if strings.HasPrefix(opt.Remote, name+":") {
|
||||||
return nil, errors.New("can't point alias remote at itself - check the value of the remote setting")
|
return nil, errors.New("can't point alias remote at itself - check the value of the remote setting")
|
||||||
}
|
}
|
||||||
return cache.Get(ctx, fspath.JoinRootPath(opt.Remote, root))
|
return cache.Get(fspath.JoinRootPath(opt.Remote, root))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
_ "github.com/rclone/rclone/backend/local" // pull in test backend
|
_ "github.com/rclone/rclone/backend/local" // pull in test backend
|
||||||
"github.com/rclone/rclone/fs"
|
"github.com/rclone/rclone/fs"
|
||||||
"github.com/rclone/rclone/fs/config"
|
"github.com/rclone/rclone/fs/config"
|
||||||
"github.com/rclone/rclone/fs/config/configfile"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -20,7 +19,7 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func prepare(t *testing.T, root string) {
|
func prepare(t *testing.T, root string) {
|
||||||
configfile.LoadConfig(context.Background())
|
config.LoadConfig()
|
||||||
|
|
||||||
// Configure the remote
|
// Configure the remote
|
||||||
config.FileSet(remoteName, "type", "alias")
|
config.FileSet(remoteName, "type", "alias")
|
||||||
@@ -55,22 +54,21 @@ func TestNewFS(t *testing.T) {
|
|||||||
{"four/under four.txt", 9, false},
|
{"four/under four.txt", 9, false},
|
||||||
}},
|
}},
|
||||||
{"four", "..", "", true, []testEntry{
|
{"four", "..", "", true, []testEntry{
|
||||||
{"five", -1, true},
|
{"four", -1, true},
|
||||||
{"under four.txt", 9, false},
|
{"one%.txt", 6, false},
|
||||||
|
{"three", -1, true},
|
||||||
|
{"two.html", 7, false},
|
||||||
}},
|
}},
|
||||||
{"", "../../three", "", true, []testEntry{
|
{"four", "../three", "", true, []testEntry{
|
||||||
{"underthree.txt", 9, false},
|
{"underthree.txt", 9, false},
|
||||||
}},
|
}},
|
||||||
{"four", "../../five", "", true, []testEntry{
|
|
||||||
{"underfive.txt", 6, false},
|
|
||||||
}},
|
|
||||||
} {
|
} {
|
||||||
what := fmt.Sprintf("test %d remoteRoot=%q, fsRoot=%q, fsList=%q", testi, test.remoteRoot, test.fsRoot, test.fsList)
|
what := fmt.Sprintf("test %d remoteRoot=%q, fsRoot=%q, fsList=%q", testi, test.remoteRoot, test.fsRoot, test.fsList)
|
||||||
|
|
||||||
remoteRoot, err := filepath.Abs(filepath.FromSlash(path.Join("test/files", test.remoteRoot)))
|
remoteRoot, err := filepath.Abs(filepath.FromSlash(path.Join("test/files", test.remoteRoot)))
|
||||||
require.NoError(t, err, what)
|
require.NoError(t, err, what)
|
||||||
prepare(t, remoteRoot)
|
prepare(t, remoteRoot)
|
||||||
f, err := fs.NewFs(context.Background(), fmt.Sprintf("%s:%s", remoteName, test.fsRoot))
|
f, err := fs.NewFs(fmt.Sprintf("%s:%s", remoteName, test.fsRoot))
|
||||||
require.NoError(t, err, what)
|
require.NoError(t, err, what)
|
||||||
gotEntries, err := f.List(context.Background(), test.fsList)
|
gotEntries, err := f.List(context.Background(), test.fsList)
|
||||||
require.NoError(t, err, what)
|
require.NoError(t, err, what)
|
||||||
@@ -92,7 +90,7 @@ func TestNewFS(t *testing.T) {
|
|||||||
|
|
||||||
func TestNewFSNoRemote(t *testing.T) {
|
func TestNewFSNoRemote(t *testing.T) {
|
||||||
prepare(t, "")
|
prepare(t, "")
|
||||||
f, err := fs.NewFs(context.Background(), fmt.Sprintf("%s:", remoteName))
|
f, err := fs.NewFs(fmt.Sprintf("%s:", remoteName))
|
||||||
|
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
require.Nil(t, f)
|
require.Nil(t, f)
|
||||||
@@ -100,7 +98,7 @@ func TestNewFSNoRemote(t *testing.T) {
|
|||||||
|
|
||||||
func TestNewFSInvalidRemote(t *testing.T) {
|
func TestNewFSInvalidRemote(t *testing.T) {
|
||||||
prepare(t, "not_existing_test_remote:")
|
prepare(t, "not_existing_test_remote:")
|
||||||
f, err := fs.NewFs(context.Background(), fmt.Sprintf("%s:", remoteName))
|
f, err := fs.NewFs(fmt.Sprintf("%s:", remoteName))
|
||||||
|
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
require.Nil(t, f)
|
require.Nil(t, f)
|
||||||
|
|||||||
@@ -9,16 +9,13 @@ import (
|
|||||||
_ "github.com/rclone/rclone/backend/box"
|
_ "github.com/rclone/rclone/backend/box"
|
||||||
_ "github.com/rclone/rclone/backend/cache"
|
_ "github.com/rclone/rclone/backend/cache"
|
||||||
_ "github.com/rclone/rclone/backend/chunker"
|
_ "github.com/rclone/rclone/backend/chunker"
|
||||||
_ "github.com/rclone/rclone/backend/compress"
|
|
||||||
_ "github.com/rclone/rclone/backend/crypt"
|
_ "github.com/rclone/rclone/backend/crypt"
|
||||||
_ "github.com/rclone/rclone/backend/drive"
|
_ "github.com/rclone/rclone/backend/drive"
|
||||||
_ "github.com/rclone/rclone/backend/dropbox"
|
_ "github.com/rclone/rclone/backend/dropbox"
|
||||||
_ "github.com/rclone/rclone/backend/fichier"
|
_ "github.com/rclone/rclone/backend/fichier"
|
||||||
_ "github.com/rclone/rclone/backend/filefabric"
|
|
||||||
_ "github.com/rclone/rclone/backend/ftp"
|
_ "github.com/rclone/rclone/backend/ftp"
|
||||||
_ "github.com/rclone/rclone/backend/googlecloudstorage"
|
_ "github.com/rclone/rclone/backend/googlecloudstorage"
|
||||||
_ "github.com/rclone/rclone/backend/googlephotos"
|
_ "github.com/rclone/rclone/backend/googlephotos"
|
||||||
_ "github.com/rclone/rclone/backend/hdfs"
|
|
||||||
_ "github.com/rclone/rclone/backend/http"
|
_ "github.com/rclone/rclone/backend/http"
|
||||||
_ "github.com/rclone/rclone/backend/hubic"
|
_ "github.com/rclone/rclone/backend/hubic"
|
||||||
_ "github.com/rclone/rclone/backend/jottacloud"
|
_ "github.com/rclone/rclone/backend/jottacloud"
|
||||||
@@ -43,5 +40,4 @@ import (
|
|||||||
_ "github.com/rclone/rclone/backend/union"
|
_ "github.com/rclone/rclone/backend/union"
|
||||||
_ "github.com/rclone/rclone/backend/webdav"
|
_ "github.com/rclone/rclone/backend/webdav"
|
||||||
_ "github.com/rclone/rclone/backend/yandex"
|
_ "github.com/rclone/rclone/backend/yandex"
|
||||||
_ "github.com/rclone/rclone/backend/zoho"
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -70,8 +70,8 @@ func init() {
|
|||||||
Prefix: "acd",
|
Prefix: "acd",
|
||||||
Description: "Amazon Drive",
|
Description: "Amazon Drive",
|
||||||
NewFs: NewFs,
|
NewFs: NewFs,
|
||||||
Config: func(ctx context.Context, name string, m configmap.Mapper) {
|
Config: func(name string, m configmap.Mapper) {
|
||||||
err := oauthutil.Config(ctx, "amazon cloud drive", name, m, acdConfig, nil)
|
err := oauthutil.Config("amazon cloud drive", name, m, acdConfig, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to configure token: %v", err)
|
log.Fatalf("Failed to configure token: %v", err)
|
||||||
}
|
}
|
||||||
@@ -144,7 +144,6 @@ type Fs struct {
|
|||||||
name string // name of this remote
|
name string // name of this remote
|
||||||
features *fs.Features // optional features
|
features *fs.Features // optional features
|
||||||
opt Options // options for this Fs
|
opt Options // options for this Fs
|
||||||
ci *fs.ConfigInfo // global config
|
|
||||||
c *acd.Client // the connection to the acd server
|
c *acd.Client // the connection to the acd server
|
||||||
noAuthClient *http.Client // unauthenticated http client
|
noAuthClient *http.Client // unauthenticated http client
|
||||||
root string // the path we are working on
|
root string // the path we are working on
|
||||||
@@ -205,10 +204,7 @@ var retryErrorCodes = []int{
|
|||||||
|
|
||||||
// shouldRetry returns a boolean as to whether this resp and err
|
// shouldRetry returns a boolean as to whether this resp and err
|
||||||
// deserve to be retried. It returns the err as a convenience
|
// deserve to be retried. It returns the err as a convenience
|
||||||
func (f *Fs) shouldRetry(ctx context.Context, resp *http.Response, err error) (bool, error) {
|
func (f *Fs) shouldRetry(resp *http.Response, err error) (bool, error) {
|
||||||
if fserrors.ContextError(ctx, &err) {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
if resp.StatusCode == 401 {
|
if resp.StatusCode == 401 {
|
||||||
f.tokenRenewer.Invalidate()
|
f.tokenRenewer.Invalidate()
|
||||||
@@ -243,7 +239,8 @@ func filterRequest(req *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewFs constructs an Fs from the path, container:path
|
// NewFs constructs an Fs from the path, container:path
|
||||||
func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, error) {
|
func NewFs(name, root string, m configmap.Mapper) (fs.Fs, error) {
|
||||||
|
ctx := context.Background()
|
||||||
// Parse config into Options struct
|
// Parse config into Options struct
|
||||||
opt := new(Options)
|
opt := new(Options)
|
||||||
err := configstruct.Set(m, opt)
|
err := configstruct.Set(m, opt)
|
||||||
@@ -251,7 +248,7 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
root = parsePath(root)
|
root = parsePath(root)
|
||||||
baseClient := fshttp.NewClient(ctx)
|
baseClient := fshttp.NewClient(fs.Config)
|
||||||
if do, ok := baseClient.Transport.(interface {
|
if do, ok := baseClient.Transport.(interface {
|
||||||
SetRequestFilter(f func(req *http.Request))
|
SetRequestFilter(f func(req *http.Request))
|
||||||
}); ok {
|
}); ok {
|
||||||
@@ -259,31 +256,29 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
} else {
|
} else {
|
||||||
fs.Debugf(name+":", "Couldn't add request filter - large file downloads will fail")
|
fs.Debugf(name+":", "Couldn't add request filter - large file downloads will fail")
|
||||||
}
|
}
|
||||||
oAuthClient, ts, err := oauthutil.NewClientWithBaseClient(ctx, name, m, acdConfig, baseClient)
|
oAuthClient, ts, err := oauthutil.NewClientWithBaseClient(name, m, acdConfig, baseClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to configure Amazon Drive")
|
return nil, errors.Wrap(err, "failed to configure Amazon Drive")
|
||||||
}
|
}
|
||||||
|
|
||||||
c := acd.NewClient(oAuthClient)
|
c := acd.NewClient(oAuthClient)
|
||||||
ci := fs.GetConfig(ctx)
|
|
||||||
f := &Fs{
|
f := &Fs{
|
||||||
name: name,
|
name: name,
|
||||||
root: root,
|
root: root,
|
||||||
opt: *opt,
|
opt: *opt,
|
||||||
ci: ci,
|
|
||||||
c: c,
|
c: c,
|
||||||
pacer: fs.NewPacer(ctx, pacer.NewAmazonCloudDrive(pacer.MinSleep(minSleep))),
|
pacer: fs.NewPacer(pacer.NewAmazonCloudDrive(pacer.MinSleep(minSleep))),
|
||||||
noAuthClient: fshttp.NewClient(ctx),
|
noAuthClient: fshttp.NewClient(fs.Config),
|
||||||
}
|
}
|
||||||
f.features = (&fs.Features{
|
f.features = (&fs.Features{
|
||||||
CaseInsensitive: true,
|
CaseInsensitive: true,
|
||||||
ReadMimeType: true,
|
ReadMimeType: true,
|
||||||
CanHaveEmptyDirectories: true,
|
CanHaveEmptyDirectories: true,
|
||||||
}).Fill(ctx, f)
|
}).Fill(f)
|
||||||
|
|
||||||
// Renew the token in the background
|
// Renew the token in the background
|
||||||
f.tokenRenewer = oauthutil.NewRenew(f.String(), ts, func() error {
|
f.tokenRenewer = oauthutil.NewRenew(f.String(), ts, func() error {
|
||||||
_, err := f.getRootInfo(ctx)
|
_, err := f.getRootInfo()
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -291,14 +286,14 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
_, resp, err = f.c.Account.GetEndpoints()
|
_, resp, err = f.c.Account.GetEndpoints()
|
||||||
return f.shouldRetry(ctx, resp, err)
|
return f.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to get endpoints")
|
return nil, errors.Wrap(err, "failed to get endpoints")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get rootID
|
// Get rootID
|
||||||
rootInfo, err := f.getRootInfo(ctx)
|
rootInfo, err := f.getRootInfo()
|
||||||
if err != nil || rootInfo.Id == nil {
|
if err != nil || rootInfo.Id == nil {
|
||||||
return nil, errors.Wrap(err, "failed to get root")
|
return nil, errors.Wrap(err, "failed to get root")
|
||||||
}
|
}
|
||||||
@@ -340,11 +335,11 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
// getRootInfo gets the root folder info
|
// getRootInfo gets the root folder info
|
||||||
func (f *Fs) getRootInfo(ctx context.Context) (rootInfo *acd.Folder, err error) {
|
func (f *Fs) getRootInfo() (rootInfo *acd.Folder, err error) {
|
||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
rootInfo, resp, err = f.c.Nodes.GetRoot()
|
rootInfo, resp, err = f.c.Nodes.GetRoot()
|
||||||
return f.shouldRetry(ctx, resp, err)
|
return f.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
return rootInfo, err
|
return rootInfo, err
|
||||||
}
|
}
|
||||||
@@ -383,7 +378,7 @@ func (f *Fs) FindLeaf(ctx context.Context, pathID, leaf string) (pathIDOut strin
|
|||||||
var subFolder *acd.Folder
|
var subFolder *acd.Folder
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
subFolder, resp, err = folder.GetFolder(f.opt.Enc.FromStandardName(leaf))
|
subFolder, resp, err = folder.GetFolder(f.opt.Enc.FromStandardName(leaf))
|
||||||
return f.shouldRetry(ctx, resp, err)
|
return f.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == acd.ErrorNodeNotFound {
|
if err == acd.ErrorNodeNotFound {
|
||||||
@@ -410,7 +405,7 @@ func (f *Fs) CreateDir(ctx context.Context, pathID, leaf string) (newID string,
|
|||||||
var info *acd.Folder
|
var info *acd.Folder
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
info, resp, err = folder.CreateFolder(f.opt.Enc.FromStandardName(leaf))
|
info, resp, err = folder.CreateFolder(f.opt.Enc.FromStandardName(leaf))
|
||||||
return f.shouldRetry(ctx, resp, err)
|
return f.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
//fmt.Printf("...Error %v\n", err)
|
//fmt.Printf("...Error %v\n", err)
|
||||||
@@ -431,7 +426,7 @@ type listAllFn func(*acd.Node) bool
|
|||||||
// Lists the directory required calling the user function on each item found
|
// 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
|
// If the user fn ever returns true then it early exits with found = true
|
||||||
func (f *Fs) listAll(ctx context.Context, dirID string, title string, directoriesOnly bool, filesOnly bool, fn listAllFn) (found bool, err error) {
|
func (f *Fs) listAll(dirID string, title string, directoriesOnly bool, filesOnly bool, fn listAllFn) (found bool, err error) {
|
||||||
query := "parents:" + dirID
|
query := "parents:" + dirID
|
||||||
if directoriesOnly {
|
if directoriesOnly {
|
||||||
query += " AND kind:" + folderKind
|
query += " AND kind:" + folderKind
|
||||||
@@ -452,7 +447,7 @@ func (f *Fs) listAll(ctx context.Context, dirID string, title string, directorie
|
|||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
err = f.pacer.CallNoRetry(func() (bool, error) {
|
err = f.pacer.CallNoRetry(func() (bool, error) {
|
||||||
nodes, resp, err = f.c.Nodes.GetNodes(&opts)
|
nodes, resp, err = f.c.Nodes.GetNodes(&opts)
|
||||||
return f.shouldRetry(ctx, resp, err)
|
return f.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@@ -507,11 +502,11 @@ func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err e
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
maxTries := f.ci.LowLevelRetries
|
maxTries := fs.Config.LowLevelRetries
|
||||||
var iErr error
|
var iErr error
|
||||||
for tries := 1; tries <= maxTries; tries++ {
|
for tries := 1; tries <= maxTries; tries++ {
|
||||||
entries = nil
|
entries = nil
|
||||||
_, err = f.listAll(ctx, directoryID, "", false, false, func(node *acd.Node) bool {
|
_, err = f.listAll(directoryID, "", false, false, func(node *acd.Node) bool {
|
||||||
remote := path.Join(dir, *node.Name)
|
remote := path.Join(dir, *node.Name)
|
||||||
switch *node.Kind {
|
switch *node.Kind {
|
||||||
case folderKind:
|
case folderKind:
|
||||||
@@ -528,7 +523,7 @@ func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err e
|
|||||||
}
|
}
|
||||||
entries = append(entries, o)
|
entries = append(entries, o)
|
||||||
default:
|
default:
|
||||||
// ignore ASSET, etc.
|
// ignore ASSET etc
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
@@ -670,7 +665,7 @@ func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options .
|
|||||||
if ok {
|
if ok {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
return f.shouldRetry(ctx, resp, err)
|
return f.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -685,7 +680,7 @@ func (f *Fs) Mkdir(ctx context.Context, dir string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move src to this remote using server-side move operations.
|
// Move src to this remote using server side move operations.
|
||||||
//
|
//
|
||||||
// This is stored with the remote path given
|
// This is stored with the remote path given
|
||||||
//
|
//
|
||||||
@@ -711,7 +706,7 @@ func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
err = f.moveNode(ctx, srcObj.remote, dstLeaf, dstDirectoryID, srcObj.info, srcLeaf, srcDirectoryID, false)
|
err = f.moveNode(srcObj.remote, dstLeaf, dstDirectoryID, srcObj.info, srcLeaf, srcDirectoryID, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -722,7 +717,7 @@ func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object,
|
|||||||
dstObj fs.Object
|
dstObj fs.Object
|
||||||
srcErr, dstErr error
|
srcErr, dstErr error
|
||||||
)
|
)
|
||||||
for i := 1; i <= f.ci.LowLevelRetries; i++ {
|
for i := 1; i <= fs.Config.LowLevelRetries; i++ {
|
||||||
_, srcErr = srcObj.fs.NewObject(ctx, srcObj.remote) // try reading the object
|
_, srcErr = srcObj.fs.NewObject(ctx, srcObj.remote) // try reading the object
|
||||||
if srcErr != nil && srcErr != fs.ErrorObjectNotFound {
|
if srcErr != nil && srcErr != fs.ErrorObjectNotFound {
|
||||||
// exit if error on source
|
// exit if error on source
|
||||||
@@ -737,7 +732,7 @@ func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object,
|
|||||||
// finished if src not found and dst found
|
// finished if src not found and dst found
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
fs.Debugf(src, "Wait for directory listing to update after move %d/%d", i, f.ci.LowLevelRetries)
|
fs.Debugf(src, "Wait for directory listing to update after move %d/%d", i, fs.Config.LowLevelRetries)
|
||||||
time.Sleep(1 * time.Second)
|
time.Sleep(1 * time.Second)
|
||||||
}
|
}
|
||||||
return dstObj, dstErr
|
return dstObj, dstErr
|
||||||
@@ -750,7 +745,7 @@ func (f *Fs) DirCacheFlush() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DirMove moves src, srcRemote to this remote at dstRemote
|
// DirMove moves src, srcRemote to this remote at dstRemote
|
||||||
// using server-side move operations.
|
// using server side move operations.
|
||||||
//
|
//
|
||||||
// Will only be called if src.Fs().Name() == f.Name()
|
// Will only be called if src.Fs().Name() == f.Name()
|
||||||
//
|
//
|
||||||
@@ -806,7 +801,7 @@ func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string
|
|||||||
var jsonStr string
|
var jsonStr string
|
||||||
err = srcFs.pacer.Call(func() (bool, error) {
|
err = srcFs.pacer.Call(func() (bool, error) {
|
||||||
jsonStr, err = srcInfo.GetMetadata()
|
jsonStr, err = srcInfo.GetMetadata()
|
||||||
return srcFs.shouldRetry(ctx, nil, err)
|
return srcFs.shouldRetry(nil, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fs.Debugf(src, "DirMove error: error reading src metadata: %v", err)
|
fs.Debugf(src, "DirMove error: error reading src metadata: %v", err)
|
||||||
@@ -818,7 +813,7 @@ func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = f.moveNode(ctx, srcPath, dstLeaf, dstDirectoryID, srcInfo, srcLeaf, srcDirectoryID, true)
|
err = f.moveNode(srcPath, dstLeaf, dstDirectoryID, srcInfo, srcLeaf, srcDirectoryID, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -843,7 +838,7 @@ func (f *Fs) purgeCheck(ctx context.Context, dir string, check bool) error {
|
|||||||
if check {
|
if check {
|
||||||
// check directory is empty
|
// check directory is empty
|
||||||
empty := true
|
empty := true
|
||||||
_, err = f.listAll(ctx, rootID, "", false, false, func(node *acd.Node) bool {
|
_, err = f.listAll(rootID, "", false, false, func(node *acd.Node) bool {
|
||||||
switch *node.Kind {
|
switch *node.Kind {
|
||||||
case folderKind:
|
case folderKind:
|
||||||
empty = false
|
empty = false
|
||||||
@@ -868,7 +863,7 @@ func (f *Fs) purgeCheck(ctx context.Context, dir string, check bool) error {
|
|||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = node.Trash()
|
resp, err = node.Trash()
|
||||||
return f.shouldRetry(ctx, resp, err)
|
return f.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -898,7 +893,7 @@ func (f *Fs) Hashes() hash.Set {
|
|||||||
return hash.Set(hash.MD5)
|
return hash.Set(hash.MD5)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy src to this remote using server-side copy operations.
|
// Copy src to this remote using server side copy operations.
|
||||||
//
|
//
|
||||||
// This is stored with the remote path given
|
// This is stored with the remote path given
|
||||||
//
|
//
|
||||||
@@ -990,7 +985,7 @@ func (o *Object) readMetaData(ctx context.Context) (err error) {
|
|||||||
var info *acd.File
|
var info *acd.File
|
||||||
err = o.fs.pacer.Call(func() (bool, error) {
|
err = o.fs.pacer.Call(func() (bool, error) {
|
||||||
info, resp, err = folder.GetFile(o.fs.opt.Enc.FromStandardName(leaf))
|
info, resp, err = folder.GetFile(o.fs.opt.Enc.FromStandardName(leaf))
|
||||||
return o.fs.shouldRetry(ctx, resp, err)
|
return o.fs.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == acd.ErrorNodeNotFound {
|
if err == acd.ErrorNodeNotFound {
|
||||||
@@ -1047,7 +1042,7 @@ func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.Read
|
|||||||
} else {
|
} else {
|
||||||
in, resp, err = file.OpenTempURLHeaders(o.fs.noAuthClient, headers)
|
in, resp, err = file.OpenTempURLHeaders(o.fs.noAuthClient, headers)
|
||||||
}
|
}
|
||||||
return o.fs.shouldRetry(ctx, resp, err)
|
return o.fs.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
return in, err
|
return in, err
|
||||||
}
|
}
|
||||||
@@ -1070,7 +1065,7 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
|
|||||||
if ok {
|
if ok {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
return o.fs.shouldRetry(ctx, resp, err)
|
return o.fs.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1080,70 +1075,70 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Remove a node
|
// Remove a node
|
||||||
func (f *Fs) removeNode(ctx context.Context, info *acd.Node) error {
|
func (f *Fs) removeNode(info *acd.Node) error {
|
||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
var err error
|
var err error
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = info.Trash()
|
resp, err = info.Trash()
|
||||||
return f.shouldRetry(ctx, resp, err)
|
return f.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove an object
|
// Remove an object
|
||||||
func (o *Object) Remove(ctx context.Context) error {
|
func (o *Object) Remove(ctx context.Context) error {
|
||||||
return o.fs.removeNode(ctx, o.info)
|
return o.fs.removeNode(o.info)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore a node
|
// Restore a node
|
||||||
func (f *Fs) restoreNode(ctx context.Context, info *acd.Node) (newInfo *acd.Node, err error) {
|
func (f *Fs) restoreNode(info *acd.Node) (newInfo *acd.Node, err error) {
|
||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
newInfo, resp, err = info.Restore()
|
newInfo, resp, err = info.Restore()
|
||||||
return f.shouldRetry(ctx, resp, err)
|
return f.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
return newInfo, err
|
return newInfo, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Changes name of given node
|
// Changes name of given node
|
||||||
func (f *Fs) renameNode(ctx context.Context, info *acd.Node, newName string) (newInfo *acd.Node, err error) {
|
func (f *Fs) renameNode(info *acd.Node, newName string) (newInfo *acd.Node, err error) {
|
||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
newInfo, resp, err = info.Rename(f.opt.Enc.FromStandardName(newName))
|
newInfo, resp, err = info.Rename(f.opt.Enc.FromStandardName(newName))
|
||||||
return f.shouldRetry(ctx, resp, err)
|
return f.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
return newInfo, err
|
return newInfo, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replaces one parent with another, effectively moving the file. Leaves other
|
// Replaces one parent with another, effectively moving the file. Leaves other
|
||||||
// parents untouched. ReplaceParent cannot be used when the file is trashed.
|
// parents untouched. ReplaceParent cannot be used when the file is trashed.
|
||||||
func (f *Fs) replaceParent(ctx context.Context, info *acd.Node, oldParentID string, newParentID string) error {
|
func (f *Fs) replaceParent(info *acd.Node, oldParentID string, newParentID string) error {
|
||||||
return f.pacer.Call(func() (bool, error) {
|
return f.pacer.Call(func() (bool, error) {
|
||||||
resp, err := info.ReplaceParent(oldParentID, newParentID)
|
resp, err := info.ReplaceParent(oldParentID, newParentID)
|
||||||
return f.shouldRetry(ctx, resp, err)
|
return f.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adds one additional parent to object.
|
// Adds one additional parent to object.
|
||||||
func (f *Fs) addParent(ctx context.Context, info *acd.Node, newParentID string) error {
|
func (f *Fs) addParent(info *acd.Node, newParentID string) error {
|
||||||
return f.pacer.Call(func() (bool, error) {
|
return f.pacer.Call(func() (bool, error) {
|
||||||
resp, err := info.AddParent(newParentID)
|
resp, err := info.AddParent(newParentID)
|
||||||
return f.shouldRetry(ctx, resp, err)
|
return f.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove given parent from object, leaving the other possible
|
// Remove given parent from object, leaving the other possible
|
||||||
// parents untouched. Object can end up having no parents.
|
// parents untouched. Object can end up having no parents.
|
||||||
func (f *Fs) removeParent(ctx context.Context, info *acd.Node, parentID string) error {
|
func (f *Fs) removeParent(info *acd.Node, parentID string) error {
|
||||||
return f.pacer.Call(func() (bool, error) {
|
return f.pacer.Call(func() (bool, error) {
|
||||||
resp, err := info.RemoveParent(parentID)
|
resp, err := info.RemoveParent(parentID)
|
||||||
return f.shouldRetry(ctx, resp, err)
|
return f.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// moveNode moves the node given from the srcLeaf,srcDirectoryID to
|
// moveNode moves the node given from the srcLeaf,srcDirectoryID to
|
||||||
// the dstLeaf,dstDirectoryID
|
// the dstLeaf,dstDirectoryID
|
||||||
func (f *Fs) moveNode(ctx context.Context, name, dstLeaf, dstDirectoryID string, srcInfo *acd.Node, srcLeaf, srcDirectoryID string, useDirErrorMsgs bool) (err error) {
|
func (f *Fs) moveNode(name, dstLeaf, dstDirectoryID string, srcInfo *acd.Node, srcLeaf, srcDirectoryID string, useDirErrorMsgs bool) (err error) {
|
||||||
// fs.Debugf(name, "moveNode dst(%q,%s) <- src(%q,%s)", dstLeaf, dstDirectoryID, srcLeaf, srcDirectoryID)
|
// fs.Debugf(name, "moveNode dst(%q,%s) <- src(%q,%s)", dstLeaf, dstDirectoryID, srcLeaf, srcDirectoryID)
|
||||||
cantMove := fs.ErrorCantMove
|
cantMove := fs.ErrorCantMove
|
||||||
if useDirErrorMsgs {
|
if useDirErrorMsgs {
|
||||||
@@ -1157,7 +1152,7 @@ func (f *Fs) moveNode(ctx context.Context, name, dstLeaf, dstDirectoryID string,
|
|||||||
|
|
||||||
if srcLeaf != dstLeaf {
|
if srcLeaf != dstLeaf {
|
||||||
// fs.Debugf(name, "renaming")
|
// fs.Debugf(name, "renaming")
|
||||||
_, err = f.renameNode(ctx, srcInfo, dstLeaf)
|
_, err = f.renameNode(srcInfo, dstLeaf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fs.Debugf(name, "Move: quick path rename failed: %v", err)
|
fs.Debugf(name, "Move: quick path rename failed: %v", err)
|
||||||
goto OnConflict
|
goto OnConflict
|
||||||
@@ -1165,7 +1160,7 @@ func (f *Fs) moveNode(ctx context.Context, name, dstLeaf, dstDirectoryID string,
|
|||||||
}
|
}
|
||||||
if srcDirectoryID != dstDirectoryID {
|
if srcDirectoryID != dstDirectoryID {
|
||||||
// fs.Debugf(name, "trying parent replace: %s -> %s", oldParentID, newParentID)
|
// fs.Debugf(name, "trying parent replace: %s -> %s", oldParentID, newParentID)
|
||||||
err = f.replaceParent(ctx, srcInfo, srcDirectoryID, dstDirectoryID)
|
err = f.replaceParent(srcInfo, srcDirectoryID, dstDirectoryID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fs.Debugf(name, "Move: quick path parent replace failed: %v", err)
|
fs.Debugf(name, "Move: quick path parent replace failed: %v", err)
|
||||||
return err
|
return err
|
||||||
@@ -1178,13 +1173,13 @@ OnConflict:
|
|||||||
fs.Debugf(name, "Could not directly rename file, presumably because there was a file with the same name already. Instead, the file will now be trashed where such operations do not cause errors. It will be restored to the correct parent after. If any of the subsequent calls fails, the rename/move will be in an invalid state.")
|
fs.Debugf(name, "Could not directly rename file, presumably because there was a file with the same name already. Instead, the file will now be trashed where such operations do not cause errors. It will be restored to the correct parent after. If any of the subsequent calls fails, the rename/move will be in an invalid state.")
|
||||||
|
|
||||||
// fs.Debugf(name, "Trashing file")
|
// fs.Debugf(name, "Trashing file")
|
||||||
err = f.removeNode(ctx, srcInfo)
|
err = f.removeNode(srcInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fs.Debugf(name, "Move: remove node failed: %v", err)
|
fs.Debugf(name, "Move: remove node failed: %v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// fs.Debugf(name, "Renaming file")
|
// fs.Debugf(name, "Renaming file")
|
||||||
_, err = f.renameNode(ctx, srcInfo, dstLeaf)
|
_, err = f.renameNode(srcInfo, dstLeaf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fs.Debugf(name, "Move: rename node failed: %v", err)
|
fs.Debugf(name, "Move: rename node failed: %v", err)
|
||||||
return err
|
return err
|
||||||
@@ -1192,19 +1187,19 @@ OnConflict:
|
|||||||
// note: replacing parent is forbidden by API, modifying them individually is
|
// note: replacing parent is forbidden by API, modifying them individually is
|
||||||
// okay though
|
// okay though
|
||||||
// fs.Debugf(name, "Adding target parent")
|
// fs.Debugf(name, "Adding target parent")
|
||||||
err = f.addParent(ctx, srcInfo, dstDirectoryID)
|
err = f.addParent(srcInfo, dstDirectoryID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fs.Debugf(name, "Move: addParent failed: %v", err)
|
fs.Debugf(name, "Move: addParent failed: %v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// fs.Debugf(name, "removing original parent")
|
// fs.Debugf(name, "removing original parent")
|
||||||
err = f.removeParent(ctx, srcInfo, srcDirectoryID)
|
err = f.removeParent(srcInfo, srcDirectoryID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fs.Debugf(name, "Move: removeParent failed: %v", err)
|
fs.Debugf(name, "Move: removeParent failed: %v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// fs.Debugf(name, "Restoring")
|
// fs.Debugf(name, "Restoring")
|
||||||
_, err = f.restoreNode(ctx, srcInfo)
|
_, err = f.restoreNode(srcInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fs.Debugf(name, "Move: restoreNode node failed: %v", err)
|
fs.Debugf(name, "Move: restoreNode node failed: %v", err)
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
// Package azureblob provides an interface to the Microsoft Azure blob object storage system
|
// Package azureblob provides an interface to the Microsoft Azure blob object storage system
|
||||||
|
|
||||||
// +build !plan9,!solaris,!js,go1.14
|
// +build !plan9,!solaris,!js,go1.13
|
||||||
|
|
||||||
package azureblob
|
package azureblob
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/md5"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
@@ -21,9 +21,9 @@ import (
|
|||||||
|
|
||||||
"github.com/Azure/azure-pipeline-go/pipeline"
|
"github.com/Azure/azure-pipeline-go/pipeline"
|
||||||
"github.com/Azure/azure-storage-blob-go/azblob"
|
"github.com/Azure/azure-storage-blob-go/azblob"
|
||||||
"github.com/Azure/go-autorest/autorest/adal"
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/rclone/rclone/fs"
|
"github.com/rclone/rclone/fs"
|
||||||
|
"github.com/rclone/rclone/fs/accounting"
|
||||||
"github.com/rclone/rclone/fs/config"
|
"github.com/rclone/rclone/fs/config"
|
||||||
"github.com/rclone/rclone/fs/config/configmap"
|
"github.com/rclone/rclone/fs/config/configmap"
|
||||||
"github.com/rclone/rclone/fs/config/configstruct"
|
"github.com/rclone/rclone/fs/config/configstruct"
|
||||||
@@ -33,9 +33,10 @@ import (
|
|||||||
"github.com/rclone/rclone/fs/walk"
|
"github.com/rclone/rclone/fs/walk"
|
||||||
"github.com/rclone/rclone/lib/bucket"
|
"github.com/rclone/rclone/lib/bucket"
|
||||||
"github.com/rclone/rclone/lib/encoder"
|
"github.com/rclone/rclone/lib/encoder"
|
||||||
"github.com/rclone/rclone/lib/env"
|
|
||||||
"github.com/rclone/rclone/lib/pacer"
|
"github.com/rclone/rclone/lib/pacer"
|
||||||
"github.com/rclone/rclone/lib/pool"
|
"github.com/rclone/rclone/lib/pool"
|
||||||
|
"github.com/rclone/rclone/lib/readers"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -46,10 +47,13 @@ const (
|
|||||||
modTimeKey = "mtime"
|
modTimeKey = "mtime"
|
||||||
timeFormatIn = time.RFC3339
|
timeFormatIn = time.RFC3339
|
||||||
timeFormatOut = "2006-01-02T15:04:05.000000000Z07:00"
|
timeFormatOut = "2006-01-02T15:04:05.000000000Z07:00"
|
||||||
|
maxTotalParts = 50000 // in multipart upload
|
||||||
storageDefaultBaseURL = "blob.core.windows.net"
|
storageDefaultBaseURL = "blob.core.windows.net"
|
||||||
|
// maxUncommittedSize = 9 << 30 // can't upload bigger than this
|
||||||
defaultChunkSize = 4 * fs.MebiByte
|
defaultChunkSize = 4 * fs.MebiByte
|
||||||
maxChunkSize = 100 * fs.MebiByte
|
maxChunkSize = 100 * fs.MebiByte
|
||||||
uploadConcurrency = 4
|
defaultUploadCutoff = 256 * fs.MebiByte
|
||||||
|
maxUploadCutoff = 256 * fs.MebiByte
|
||||||
defaultAccessTier = azblob.AccessTierNone
|
defaultAccessTier = azblob.AccessTierNone
|
||||||
maxTryTimeout = time.Hour * 24 * 365 //max time of an azure web request response window (whether or not data is flowing)
|
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,
|
// Default storage account, key and blob endpoint for emulator support,
|
||||||
@@ -61,10 +65,6 @@ const (
|
|||||||
memoryPoolUseMmap = false
|
memoryPoolUseMmap = false
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
errCantUpdateArchiveTierBlobs = fserrors.NoRetryError(errors.New("can't update archive tier blob without --azureblob-archive-tier-delete"))
|
|
||||||
)
|
|
||||||
|
|
||||||
// Register with Fs
|
// Register with Fs
|
||||||
func init() {
|
func init() {
|
||||||
fs.Register(&fs.RegInfo{
|
fs.Register(&fs.RegInfo{
|
||||||
@@ -74,51 +74,12 @@ func init() {
|
|||||||
Options: []fs.Option{{
|
Options: []fs.Option{{
|
||||||
Name: "account",
|
Name: "account",
|
||||||
Help: "Storage Account Name (leave blank to use SAS URL or Emulator)",
|
Help: "Storage Account Name (leave blank to use SAS URL or Emulator)",
|
||||||
}, {
|
|
||||||
Name: "service_principal_file",
|
|
||||||
Help: `Path to file containing credentials for use with a service principal.
|
|
||||||
|
|
||||||
Leave blank normally. Needed only if you want to use a service principal instead of interactive login.
|
|
||||||
|
|
||||||
$ az sp create-for-rbac --name "<name>" \
|
|
||||||
--role "Storage Blob Data Owner" \
|
|
||||||
--scopes "/subscriptions/<subscription>/resourceGroups/<resource-group>/providers/Microsoft.Storage/storageAccounts/<storage-account>/blobServices/default/containers/<container>" \
|
|
||||||
> azure-principal.json
|
|
||||||
|
|
||||||
See [Use Azure CLI to assign an Azure role for access to blob and queue data](https://docs.microsoft.com/en-us/azure/storage/common/storage-auth-aad-rbac-cli)
|
|
||||||
for more details.
|
|
||||||
`,
|
|
||||||
}, {
|
}, {
|
||||||
Name: "key",
|
Name: "key",
|
||||||
Help: "Storage Account Key (leave blank to use SAS URL or Emulator)",
|
Help: "Storage Account Key (leave blank to use SAS URL or Emulator)",
|
||||||
}, {
|
}, {
|
||||||
Name: "sas_url",
|
Name: "sas_url",
|
||||||
Help: "SAS URL for container level access only\n(leave blank if using account/key or Emulator)",
|
Help: "SAS URL for container level access only\n(leave blank if using account/key or Emulator)",
|
||||||
}, {
|
|
||||||
Name: "use_msi",
|
|
||||||
Help: `Use a managed service identity to authenticate (only works in Azure)
|
|
||||||
|
|
||||||
When true, use a [managed service identity](https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/)
|
|
||||||
to authenticate to Azure Storage instead of a SAS token or account key.
|
|
||||||
|
|
||||||
If the VM(SS) on which this program is running has a system-assigned identity, it will
|
|
||||||
be used by default. If the resource has no system-assigned but exactly one user-assigned identity,
|
|
||||||
the user-assigned identity will be used by default. If the resource has multiple user-assigned
|
|
||||||
identities, the identity to use must be explicitly specified using exactly one of the msi_object_id,
|
|
||||||
msi_client_id, or msi_mi_res_id parameters.`,
|
|
||||||
Default: false,
|
|
||||||
}, {
|
|
||||||
Name: "msi_object_id",
|
|
||||||
Help: "Object ID of the user-assigned MSI to use, if any. Leave blank if msi_client_id or msi_mi_res_id specified.",
|
|
||||||
Advanced: true,
|
|
||||||
}, {
|
|
||||||
Name: "msi_client_id",
|
|
||||||
Help: "Object ID of the user-assigned MSI to use, if any. Leave blank if msi_object_id or msi_mi_res_id specified.",
|
|
||||||
Advanced: true,
|
|
||||||
}, {
|
|
||||||
Name: "msi_mi_res_id",
|
|
||||||
Help: "Azure resource ID of the user-assigned MSI to use, if any. Leave blank if msi_client_id or msi_object_id specified.",
|
|
||||||
Advanced: true,
|
|
||||||
}, {
|
}, {
|
||||||
Name: "use_emulator",
|
Name: "use_emulator",
|
||||||
Help: "Uses local storage emulator if provided as 'true' (leave blank if using real azure storage endpoint)",
|
Help: "Uses local storage emulator if provided as 'true' (leave blank if using real azure storage endpoint)",
|
||||||
@@ -129,7 +90,8 @@ msi_client_id, or msi_mi_res_id parameters.`,
|
|||||||
Advanced: true,
|
Advanced: true,
|
||||||
}, {
|
}, {
|
||||||
Name: "upload_cutoff",
|
Name: "upload_cutoff",
|
||||||
Help: "Cutoff for switching to chunked upload (<= 256MB). (Deprecated)",
|
Help: "Cutoff for switching to chunked upload (<= 256MB).",
|
||||||
|
Default: defaultUploadCutoff,
|
||||||
Advanced: true,
|
Advanced: true,
|
||||||
}, {
|
}, {
|
||||||
Name: "chunk_size",
|
Name: "chunk_size",
|
||||||
@@ -167,24 +129,6 @@ If blobs are in "archive tier" at remote, trying to perform data transfer
|
|||||||
operations from remote will not be allowed. User should first restore by
|
operations from remote will not be allowed. User should first restore by
|
||||||
tiering blob to "Hot" or "Cool".`,
|
tiering blob to "Hot" or "Cool".`,
|
||||||
Advanced: true,
|
Advanced: true,
|
||||||
}, {
|
|
||||||
Name: "archive_tier_delete",
|
|
||||||
Default: false,
|
|
||||||
Help: fmt.Sprintf(`Delete archive tier blobs before overwriting.
|
|
||||||
|
|
||||||
Archive tier blobs cannot be updated. So without this flag, if you
|
|
||||||
attempt to update an archive tier blob, then rclone will produce the
|
|
||||||
error:
|
|
||||||
|
|
||||||
%v
|
|
||||||
|
|
||||||
With this flag set then before rclone attempts to overwrite an archive
|
|
||||||
tier blob, it will delete the existing blob before uploading its
|
|
||||||
replacement. This has the potential for data loss if the upload fails
|
|
||||||
(unlike updating a normal blob) and also may cost more since deleting
|
|
||||||
archive tier blobs early may be chargable.
|
|
||||||
`, errCantUpdateArchiveTierBlobs),
|
|
||||||
Advanced: true,
|
|
||||||
}, {
|
}, {
|
||||||
Name: "disable_checksum",
|
Name: "disable_checksum",
|
||||||
Help: `Don't store MD5 checksum with object metadata.
|
Help: `Don't store MD5 checksum with object metadata.
|
||||||
@@ -217,23 +161,6 @@ This option controls how often unused buffers will be removed from the pool.`,
|
|||||||
encoder.EncodeDel |
|
encoder.EncodeDel |
|
||||||
encoder.EncodeBackSlash |
|
encoder.EncodeBackSlash |
|
||||||
encoder.EncodeRightPeriod),
|
encoder.EncodeRightPeriod),
|
||||||
}, {
|
|
||||||
Name: "public_access",
|
|
||||||
Help: "Public access level of a container: blob, container.",
|
|
||||||
Default: string(azblob.PublicAccessNone),
|
|
||||||
Examples: []fs.OptionExample{
|
|
||||||
{
|
|
||||||
Value: string(azblob.PublicAccessNone),
|
|
||||||
Help: "The container and its blobs can be accessed only with an authorized request. It's a default value",
|
|
||||||
}, {
|
|
||||||
Value: string(azblob.PublicAccessBlob),
|
|
||||||
Help: "Blob data within this container can be read via anonymous request.",
|
|
||||||
}, {
|
|
||||||
Value: string(azblob.PublicAccessContainer),
|
|
||||||
Help: "Allow full public read access for container and blob data.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Advanced: true,
|
|
||||||
}},
|
}},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -241,24 +168,18 @@ This option controls how often unused buffers will be removed from the pool.`,
|
|||||||
// Options defines the configuration for this backend
|
// Options defines the configuration for this backend
|
||||||
type Options struct {
|
type Options struct {
|
||||||
Account string `config:"account"`
|
Account string `config:"account"`
|
||||||
ServicePrincipalFile string `config:"service_principal_file"`
|
|
||||||
Key string `config:"key"`
|
Key string `config:"key"`
|
||||||
UseMSI bool `config:"use_msi"`
|
|
||||||
MSIObjectID string `config:"msi_object_id"`
|
|
||||||
MSIClientID string `config:"msi_client_id"`
|
|
||||||
MSIResourceID string `config:"msi_mi_res_id"`
|
|
||||||
Endpoint string `config:"endpoint"`
|
Endpoint string `config:"endpoint"`
|
||||||
SASURL string `config:"sas_url"`
|
SASURL string `config:"sas_url"`
|
||||||
|
UploadCutoff fs.SizeSuffix `config:"upload_cutoff"`
|
||||||
ChunkSize fs.SizeSuffix `config:"chunk_size"`
|
ChunkSize fs.SizeSuffix `config:"chunk_size"`
|
||||||
ListChunkSize uint `config:"list_chunk"`
|
ListChunkSize uint `config:"list_chunk"`
|
||||||
AccessTier string `config:"access_tier"`
|
AccessTier string `config:"access_tier"`
|
||||||
ArchiveTierDelete bool `config:"archive_tier_delete"`
|
|
||||||
UseEmulator bool `config:"use_emulator"`
|
UseEmulator bool `config:"use_emulator"`
|
||||||
DisableCheckSum bool `config:"disable_checksum"`
|
DisableCheckSum bool `config:"disable_checksum"`
|
||||||
MemoryPoolFlushTime fs.Duration `config:"memory_pool_flush_time"`
|
MemoryPoolFlushTime fs.Duration `config:"memory_pool_flush_time"`
|
||||||
MemoryPoolUseMmap bool `config:"memory_pool_use_mmap"`
|
MemoryPoolUseMmap bool `config:"memory_pool_use_mmap"`
|
||||||
Enc encoder.MultiEncoder `config:"encoding"`
|
Enc encoder.MultiEncoder `config:"encoding"`
|
||||||
PublicAccess string `config:"public_access"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fs represents a remote azure server
|
// Fs represents a remote azure server
|
||||||
@@ -266,7 +187,6 @@ type Fs struct {
|
|||||||
name string // name of this remote
|
name string // name of this remote
|
||||||
root string // the path we are working on if any
|
root string // the path we are working on if any
|
||||||
opt Options // parsed config options
|
opt Options // parsed config options
|
||||||
ci *fs.ConfigInfo // global config
|
|
||||||
features *fs.Features // optional features
|
features *fs.Features // optional features
|
||||||
client *http.Client // http client we are using
|
client *http.Client // http client we are using
|
||||||
svcURL *azblob.ServiceURL // reference to serviceURL
|
svcURL *azblob.ServiceURL // reference to serviceURL
|
||||||
@@ -277,10 +197,8 @@ type Fs struct {
|
|||||||
isLimited bool // if limited to one container
|
isLimited bool // if limited to one container
|
||||||
cache *bucket.Cache // cache for container creation status
|
cache *bucket.Cache // cache for container creation status
|
||||||
pacer *fs.Pacer // To pace and retry the API calls
|
pacer *fs.Pacer // To pace and retry the API calls
|
||||||
imdsPacer *fs.Pacer // Same but for IMDS
|
|
||||||
uploadToken *pacer.TokenDispenser // control concurrency
|
uploadToken *pacer.TokenDispenser // control concurrency
|
||||||
pool *pool.Pool // memory pool
|
pool *pool.Pool // memory pool
|
||||||
publicAccess azblob.PublicAccessType // Container Public Access Level
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Object describes an azure object
|
// Object describes an azure object
|
||||||
@@ -354,22 +272,9 @@ func validateAccessTier(tier string) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// validatePublicAccess checks if azureblob supports use supplied public access level
|
|
||||||
func validatePublicAccess(publicAccess string) bool {
|
|
||||||
switch publicAccess {
|
|
||||||
case string(azblob.PublicAccessNone),
|
|
||||||
string(azblob.PublicAccessBlob),
|
|
||||||
string(azblob.PublicAccessContainer):
|
|
||||||
// valid cases
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// retryErrorCodes is a slice of error codes that we will retry
|
// retryErrorCodes is a slice of error codes that we will retry
|
||||||
var retryErrorCodes = []int{
|
var retryErrorCodes = []int{
|
||||||
401, // Unauthorized (e.g. "Token has expired")
|
401, // Unauthorized (eg "Token has expired")
|
||||||
408, // Request Timeout
|
408, // Request Timeout
|
||||||
429, // Rate exceeded.
|
429, // Rate exceeded.
|
||||||
500, // Get occasional 500 Internal Server Error
|
500, // Get occasional 500 Internal Server Error
|
||||||
@@ -379,10 +284,7 @@ var retryErrorCodes = []int{
|
|||||||
|
|
||||||
// shouldRetry returns a boolean as to whether this resp and err
|
// shouldRetry returns a boolean as to whether this resp and err
|
||||||
// deserve to be retried. It returns the err as a convenience
|
// deserve to be retried. It returns the err as a convenience
|
||||||
func (f *Fs) shouldRetry(ctx context.Context, err error) (bool, error) {
|
func (f *Fs) shouldRetry(err error) (bool, error) {
|
||||||
if fserrors.ContextError(ctx, &err) {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
// FIXME interpret special errors - more to do here
|
// FIXME interpret special errors - more to do here
|
||||||
if storageErr, ok := err.(azblob.StorageError); ok {
|
if storageErr, ok := err.(azblob.StorageError); ok {
|
||||||
switch storageErr.ServiceCode() {
|
switch storageErr.ServiceCode() {
|
||||||
@@ -397,8 +299,6 @@ func (f *Fs) shouldRetry(ctx context.Context, err error) (bool, error) {
|
|||||||
return true, err
|
return true, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if httpErr, ok := err.(httpError); ok {
|
|
||||||
return fserrors.ShouldRetryHTTP(httpErr.Response, retryErrorCodes), err
|
|
||||||
}
|
}
|
||||||
return fserrors.ShouldRetry(err), err
|
return fserrors.ShouldRetry(err), err
|
||||||
}
|
}
|
||||||
@@ -422,6 +322,21 @@ func (f *Fs) setUploadChunkSize(cs fs.SizeSuffix) (old fs.SizeSuffix, err error)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkUploadCutoff(cs fs.SizeSuffix) error {
|
||||||
|
if cs > maxUploadCutoff {
|
||||||
|
return errors.Errorf("%v must be less than or equal to %v", cs, maxUploadCutoff)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fs) setUploadCutoff(cs fs.SizeSuffix) (old fs.SizeSuffix, err error) {
|
||||||
|
err = checkUploadCutoff(cs)
|
||||||
|
if err == nil {
|
||||||
|
old, f.opt.UploadCutoff = f.opt.UploadCutoff, cs
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// httpClientFactory creates a Factory object that sends HTTP requests
|
// httpClientFactory creates a Factory object that sends HTTP requests
|
||||||
// to an rclone's http.Client.
|
// to an rclone's http.Client.
|
||||||
//
|
//
|
||||||
@@ -438,50 +353,6 @@ func httpClientFactory(client *http.Client) pipeline.Factory {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
type servicePrincipalCredentials struct {
|
|
||||||
AppID string `json:"appId"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
Tenant string `json:"tenant"`
|
|
||||||
}
|
|
||||||
|
|
||||||
const azureActiveDirectoryEndpoint = "https://login.microsoftonline.com/"
|
|
||||||
const azureStorageEndpoint = "https://storage.azure.com/"
|
|
||||||
|
|
||||||
// newServicePrincipalTokenRefresher takes the client ID and secret, and returns a refresh-able access token.
|
|
||||||
func newServicePrincipalTokenRefresher(ctx context.Context, credentialsData []byte) (azblob.TokenRefresher, error) {
|
|
||||||
var spCredentials servicePrincipalCredentials
|
|
||||||
if err := json.Unmarshal(credentialsData, &spCredentials); err != nil {
|
|
||||||
return nil, errors.Wrap(err, "error parsing credentials from JSON file")
|
|
||||||
}
|
|
||||||
oauthConfig, err := adal.NewOAuthConfig(azureActiveDirectoryEndpoint, spCredentials.Tenant)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "error creating oauth config")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create service principal token for Azure Storage.
|
|
||||||
servicePrincipalToken, err := adal.NewServicePrincipalToken(
|
|
||||||
*oauthConfig,
|
|
||||||
spCredentials.AppID,
|
|
||||||
spCredentials.Password,
|
|
||||||
azureStorageEndpoint)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "error creating service principal token")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wrap token inside a refresher closure.
|
|
||||||
var tokenRefresher azblob.TokenRefresher = func(credential azblob.TokenCredential) time.Duration {
|
|
||||||
if err := servicePrincipalToken.Refresh(); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
refreshedToken := servicePrincipalToken.Token()
|
|
||||||
credential.SetToken(refreshedToken.AccessToken)
|
|
||||||
exp := refreshedToken.Expires().Sub(time.Now().Add(2 * time.Minute))
|
|
||||||
return exp
|
|
||||||
}
|
|
||||||
|
|
||||||
return tokenRefresher, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// newPipeline creates a Pipeline using the specified credentials and options.
|
// newPipeline creates a Pipeline using the specified credentials and options.
|
||||||
//
|
//
|
||||||
// this code was copied from azblob.NewPipeline
|
// this code was copied from azblob.NewPipeline
|
||||||
@@ -508,7 +379,8 @@ func (f *Fs) setRoot(root string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewFs constructs an Fs from the path, container:path
|
// NewFs constructs an Fs from the path, container:path
|
||||||
func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, error) {
|
func NewFs(name, root string, m configmap.Mapper) (fs.Fs, error) {
|
||||||
|
ctx := context.Background()
|
||||||
// Parse config into Options struct
|
// Parse config into Options struct
|
||||||
opt := new(Options)
|
opt := new(Options)
|
||||||
err := configstruct.Set(m, opt)
|
err := configstruct.Set(m, opt)
|
||||||
@@ -516,6 +388,10 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = checkUploadCutoff(opt.UploadCutoff)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "azure: upload cutoff")
|
||||||
|
}
|
||||||
err = checkUploadChunkSize(opt.ChunkSize)
|
err = checkUploadChunkSize(opt.ChunkSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "azure: chunk size")
|
return nil, errors.Wrap(err, "azure: chunk size")
|
||||||
@@ -534,31 +410,21 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
string(azblob.AccessTierHot), string(azblob.AccessTierCool), string(azblob.AccessTierArchive))
|
string(azblob.AccessTierHot), string(azblob.AccessTierCool), string(azblob.AccessTierArchive))
|
||||||
}
|
}
|
||||||
|
|
||||||
if !validatePublicAccess((opt.PublicAccess)) {
|
|
||||||
return nil, errors.Errorf("Azure Blob: Supported public access level are %s and %s",
|
|
||||||
string(azblob.PublicAccessBlob), string(azblob.PublicAccessContainer))
|
|
||||||
}
|
|
||||||
|
|
||||||
ci := fs.GetConfig(ctx)
|
|
||||||
f := &Fs{
|
f := &Fs{
|
||||||
name: name,
|
name: name,
|
||||||
opt: *opt,
|
opt: *opt,
|
||||||
ci: ci,
|
pacer: fs.NewPacer(pacer.NewS3(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))),
|
||||||
pacer: fs.NewPacer(ctx, pacer.NewS3(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))),
|
uploadToken: pacer.NewTokenDispenser(fs.Config.Transfers),
|
||||||
imdsPacer: fs.NewPacer(ctx, pacer.NewAzureIMDS()),
|
client: fshttp.NewClient(fs.Config),
|
||||||
uploadToken: pacer.NewTokenDispenser(ci.Transfers),
|
|
||||||
client: fshttp.NewClient(ctx),
|
|
||||||
cache: bucket.NewCache(),
|
cache: bucket.NewCache(),
|
||||||
cntURLcache: make(map[string]*azblob.ContainerURL, 1),
|
cntURLcache: make(map[string]*azblob.ContainerURL, 1),
|
||||||
pool: pool.New(
|
pool: pool.New(
|
||||||
time.Duration(opt.MemoryPoolFlushTime),
|
time.Duration(opt.MemoryPoolFlushTime),
|
||||||
int(opt.ChunkSize),
|
int(opt.ChunkSize),
|
||||||
ci.Transfers,
|
fs.Config.Transfers,
|
||||||
opt.MemoryPoolUseMmap,
|
opt.MemoryPoolUseMmap,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
f.publicAccess = azblob.PublicAccessType(opt.PublicAccess)
|
|
||||||
f.imdsPacer.SetRetries(5) // per IMDS documentation
|
|
||||||
f.setRoot(root)
|
f.setRoot(root)
|
||||||
f.features = (&fs.Features{
|
f.features = (&fs.Features{
|
||||||
ReadMimeType: true,
|
ReadMimeType: true,
|
||||||
@@ -567,7 +433,7 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
BucketBasedRootOK: true,
|
BucketBasedRootOK: true,
|
||||||
SetTier: true,
|
SetTier: true,
|
||||||
GetTier: true,
|
GetTier: true,
|
||||||
}).Fill(ctx, f)
|
}).Fill(f)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
u *url.URL
|
u *url.URL
|
||||||
@@ -585,76 +451,6 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
}
|
}
|
||||||
pipeline := f.newPipeline(credential, azblob.PipelineOptions{Retry: azblob.RetryOptions{TryTimeout: maxTryTimeout}})
|
pipeline := f.newPipeline(credential, azblob.PipelineOptions{Retry: azblob.RetryOptions{TryTimeout: maxTryTimeout}})
|
||||||
serviceURL = azblob.NewServiceURL(*u, pipeline)
|
serviceURL = azblob.NewServiceURL(*u, pipeline)
|
||||||
case opt.UseMSI:
|
|
||||||
var token adal.Token
|
|
||||||
var userMSI *userMSI = &userMSI{}
|
|
||||||
if len(opt.MSIClientID) > 0 || len(opt.MSIObjectID) > 0 || len(opt.MSIResourceID) > 0 {
|
|
||||||
// Specifying a user-assigned identity. Exactly one of the above IDs must be specified.
|
|
||||||
// Validate and ensure exactly one is set. (To do: better validation.)
|
|
||||||
if len(opt.MSIClientID) > 0 {
|
|
||||||
if len(opt.MSIObjectID) > 0 || len(opt.MSIResourceID) > 0 {
|
|
||||||
return nil, errors.New("more than one user-assigned identity ID is set")
|
|
||||||
}
|
|
||||||
userMSI.Type = msiClientID
|
|
||||||
userMSI.Value = opt.MSIClientID
|
|
||||||
}
|
|
||||||
if len(opt.MSIObjectID) > 0 {
|
|
||||||
if len(opt.MSIClientID) > 0 || len(opt.MSIResourceID) > 0 {
|
|
||||||
return nil, errors.New("more than one user-assigned identity ID is set")
|
|
||||||
}
|
|
||||||
userMSI.Type = msiObjectID
|
|
||||||
userMSI.Value = opt.MSIObjectID
|
|
||||||
}
|
|
||||||
if len(opt.MSIResourceID) > 0 {
|
|
||||||
if len(opt.MSIClientID) > 0 || len(opt.MSIObjectID) > 0 {
|
|
||||||
return nil, errors.New("more than one user-assigned identity ID is set")
|
|
||||||
}
|
|
||||||
userMSI.Type = msiResourceID
|
|
||||||
userMSI.Value = opt.MSIResourceID
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
userMSI = nil
|
|
||||||
}
|
|
||||||
err = f.imdsPacer.Call(func() (bool, error) {
|
|
||||||
// Retry as specified by the documentation:
|
|
||||||
// https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/how-to-use-vm-token#retry-guidance
|
|
||||||
token, err = GetMSIToken(ctx, userMSI)
|
|
||||||
return f.shouldRetry(ctx, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrapf(err, "Failed to acquire MSI token")
|
|
||||||
}
|
|
||||||
|
|
||||||
u, err = url.Parse(fmt.Sprintf("https://%s.%s", opt.Account, opt.Endpoint))
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "failed to make azure storage url from account and endpoint")
|
|
||||||
}
|
|
||||||
credential := azblob.NewTokenCredential(token.AccessToken, func(credential azblob.TokenCredential) time.Duration {
|
|
||||||
fs.Debugf(f, "Token refresher called.")
|
|
||||||
var refreshedToken adal.Token
|
|
||||||
err := f.imdsPacer.Call(func() (bool, error) {
|
|
||||||
refreshedToken, err = GetMSIToken(ctx, userMSI)
|
|
||||||
return f.shouldRetry(ctx, err)
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
// Failed to refresh.
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
credential.SetToken(refreshedToken.AccessToken)
|
|
||||||
now := time.Now().UTC()
|
|
||||||
// Refresh one minute before expiry.
|
|
||||||
refreshAt := refreshedToken.Expires().UTC().Add(-1 * time.Minute)
|
|
||||||
fs.Debugf(f, "Acquired new token that expires at %v; refreshing in %d s", refreshedToken.Expires(),
|
|
||||||
int(refreshAt.Sub(now).Seconds()))
|
|
||||||
if now.After(refreshAt) {
|
|
||||||
// Acquired a causality violation.
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return refreshAt.Sub(now)
|
|
||||||
})
|
|
||||||
pipeline := f.newPipeline(credential, azblob.PipelineOptions{Retry: azblob.RetryOptions{TryTimeout: maxTryTimeout}})
|
|
||||||
serviceURL = azblob.NewServiceURL(*u, pipeline)
|
|
||||||
case opt.Account != "" && opt.Key != "":
|
case opt.Account != "" && opt.Key != "":
|
||||||
credential, err := azblob.NewSharedKeyCredential(opt.Account, opt.Key)
|
credential, err := azblob.NewSharedKeyCredential(opt.Account, opt.Key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -686,27 +482,8 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
} else {
|
} else {
|
||||||
serviceURL = azblob.NewServiceURL(*u, pipeline)
|
serviceURL = azblob.NewServiceURL(*u, pipeline)
|
||||||
}
|
}
|
||||||
case opt.ServicePrincipalFile != "":
|
|
||||||
// Create a standard URL.
|
|
||||||
u, err = url.Parse(fmt.Sprintf("https://%s.%s", opt.Account, opt.Endpoint))
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "failed to make azure storage url from account and endpoint")
|
|
||||||
}
|
|
||||||
// Try loading service principal credentials from file.
|
|
||||||
loadedCreds, err := ioutil.ReadFile(env.ShellExpand(opt.ServicePrincipalFile))
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "error opening service principal credentials file")
|
|
||||||
}
|
|
||||||
// Create a token refresher from service principal credentials.
|
|
||||||
tokenRefresher, err := newServicePrincipalTokenRefresher(ctx, loadedCreds)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "failed to create a service principal token")
|
|
||||||
}
|
|
||||||
options := azblob.PipelineOptions{Retry: azblob.RetryOptions{TryTimeout: maxTryTimeout}}
|
|
||||||
pipe := f.newPipeline(azblob.NewTokenCredential("", tokenRefresher), options)
|
|
||||||
serviceURL = azblob.NewServiceURL(*u, pipe)
|
|
||||||
default:
|
default:
|
||||||
return nil, errors.New("No authentication method configured")
|
return nil, errors.New("Need account+key or connectionString or sasURL")
|
||||||
}
|
}
|
||||||
f.svcURL = &serviceURL
|
f.svcURL = &serviceURL
|
||||||
|
|
||||||
@@ -747,7 +524,7 @@ func (f *Fs) cntURL(container string) (containerURL *azblob.ContainerURL) {
|
|||||||
// Return an Object from a path
|
// Return an Object from a path
|
||||||
//
|
//
|
||||||
// If it can't be found it returns the error fs.ErrorObjectNotFound.
|
// If it can't be found it returns the error fs.ErrorObjectNotFound.
|
||||||
func (f *Fs) newObjectWithInfo(remote string, info *azblob.BlobItemInternal) (fs.Object, error) {
|
func (f *Fs) newObjectWithInfo(remote string, info *azblob.BlobItem) (fs.Object, error) {
|
||||||
o := &Object{
|
o := &Object{
|
||||||
fs: f,
|
fs: f,
|
||||||
remote: remote,
|
remote: remote,
|
||||||
@@ -804,7 +581,7 @@ func isDirectoryMarker(size int64, metadata azblob.Metadata, remote string) bool
|
|||||||
}
|
}
|
||||||
|
|
||||||
// listFn is called from list to handle an object
|
// listFn is called from list to handle an object
|
||||||
type listFn func(remote string, object *azblob.BlobItemInternal, isDirectory bool) error
|
type listFn func(remote string, object *azblob.BlobItem, isDirectory bool) error
|
||||||
|
|
||||||
// list lists the objects into the function supplied from
|
// list lists the objects into the function supplied from
|
||||||
// the container and root supplied
|
// the container and root supplied
|
||||||
@@ -844,7 +621,7 @@ func (f *Fs) list(ctx context.Context, container, directory, prefix string, addC
|
|||||||
err := f.pacer.Call(func() (bool, error) {
|
err := f.pacer.Call(func() (bool, error) {
|
||||||
var err error
|
var err error
|
||||||
response, err = f.cntURL(container).ListBlobsHierarchySegment(ctx, marker, delimiter, options)
|
response, err = f.cntURL(container).ListBlobsHierarchySegment(ctx, marker, delimiter, options)
|
||||||
return f.shouldRetry(ctx, err)
|
return f.shouldRetry(err)
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -903,7 +680,7 @@ func (f *Fs) list(ctx context.Context, container, directory, prefix string, addC
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Convert a list item into a DirEntry
|
// Convert a list item into a DirEntry
|
||||||
func (f *Fs) itemToDirEntry(remote string, object *azblob.BlobItemInternal, isDirectory bool) (fs.DirEntry, error) {
|
func (f *Fs) itemToDirEntry(remote string, object *azblob.BlobItem, isDirectory bool) (fs.DirEntry, error) {
|
||||||
if isDirectory {
|
if isDirectory {
|
||||||
d := fs.NewDir(remote, time.Time{})
|
d := fs.NewDir(remote, time.Time{})
|
||||||
return d, nil
|
return d, nil
|
||||||
@@ -915,27 +692,9 @@ func (f *Fs) itemToDirEntry(remote string, object *azblob.BlobItemInternal, isDi
|
|||||||
return o, nil
|
return o, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check to see if this is a limited container and the container is not found
|
|
||||||
func (f *Fs) containerOK(container string) bool {
|
|
||||||
if !f.isLimited {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
f.cntURLcacheMu.Lock()
|
|
||||||
defer f.cntURLcacheMu.Unlock()
|
|
||||||
for limitedContainer := range f.cntURLcache {
|
|
||||||
if container == limitedContainer {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// listDir lists a single directory
|
// listDir lists a single directory
|
||||||
func (f *Fs) listDir(ctx context.Context, container, directory, prefix string, addContainer bool) (entries fs.DirEntries, err error) {
|
func (f *Fs) listDir(ctx context.Context, container, directory, prefix string, addContainer bool) (entries fs.DirEntries, err error) {
|
||||||
if !f.containerOK(container) {
|
err = f.list(ctx, container, directory, prefix, addContainer, false, f.opt.ListChunkSize, func(remote string, object *azblob.BlobItem, isDirectory bool) error {
|
||||||
return nil, fs.ErrorDirNotFound
|
|
||||||
}
|
|
||||||
err = f.list(ctx, container, directory, prefix, addContainer, false, f.opt.ListChunkSize, func(remote string, object *azblob.BlobItemInternal, isDirectory bool) error {
|
|
||||||
entry, err := f.itemToDirEntry(remote, object, isDirectory)
|
entry, err := f.itemToDirEntry(remote, object, isDirectory)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1016,7 +775,7 @@ func (f *Fs) ListR(ctx context.Context, dir string, callback fs.ListRCallback) (
|
|||||||
container, directory := f.split(dir)
|
container, directory := f.split(dir)
|
||||||
list := walk.NewListRHelper(callback)
|
list := walk.NewListRHelper(callback)
|
||||||
listR := func(container, directory, prefix string, addContainer bool) error {
|
listR := func(container, directory, prefix string, addContainer bool) error {
|
||||||
return f.list(ctx, container, directory, prefix, addContainer, true, f.opt.ListChunkSize, func(remote string, object *azblob.BlobItemInternal, isDirectory bool) error {
|
return f.list(ctx, container, directory, prefix, addContainer, true, f.opt.ListChunkSize, func(remote string, object *azblob.BlobItem, isDirectory bool) error {
|
||||||
entry, err := f.itemToDirEntry(remote, object, isDirectory)
|
entry, err := f.itemToDirEntry(remote, object, isDirectory)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1043,9 +802,6 @@ func (f *Fs) ListR(ctx context.Context, dir string, callback fs.ListRCallback) (
|
|||||||
f.cache.MarkOK(container)
|
f.cache.MarkOK(container)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if !f.containerOK(container) {
|
|
||||||
return fs.ErrorDirNotFound
|
|
||||||
}
|
|
||||||
err = listR(container, directory, f.rootDirectory, f.rootContainer == "")
|
err = listR(container, directory, f.rootDirectory, f.rootContainer == "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1070,7 +826,7 @@ func (f *Fs) listContainersToFn(fn listContainerFn) error {
|
|||||||
err := f.pacer.Call(func() (bool, error) {
|
err := f.pacer.Call(func() (bool, error) {
|
||||||
var err error
|
var err error
|
||||||
response, err = f.svcURL.ListContainersSegment(ctx, marker, params)
|
response, err = f.svcURL.ListContainersSegment(ctx, marker, params)
|
||||||
return f.shouldRetry(ctx, err)
|
return f.shouldRetry(err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1122,7 +878,7 @@ func (f *Fs) makeContainer(ctx context.Context, container string) error {
|
|||||||
}
|
}
|
||||||
// now try to create the container
|
// now try to create the container
|
||||||
return f.pacer.Call(func() (bool, error) {
|
return f.pacer.Call(func() (bool, error) {
|
||||||
_, err := f.cntURL(container).Create(ctx, azblob.Metadata{}, f.publicAccess)
|
_, err := f.cntURL(container).Create(ctx, azblob.Metadata{}, azblob.PublicAccessNone)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if storageErr, ok := err.(azblob.StorageError); ok {
|
if storageErr, ok := err.(azblob.StorageError); ok {
|
||||||
switch storageErr.ServiceCode() {
|
switch storageErr.ServiceCode() {
|
||||||
@@ -1139,7 +895,7 @@ func (f *Fs) makeContainer(ctx context.Context, container string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return f.shouldRetry(ctx, err)
|
return f.shouldRetry(err)
|
||||||
})
|
})
|
||||||
}, nil)
|
}, nil)
|
||||||
}
|
}
|
||||||
@@ -1147,7 +903,7 @@ func (f *Fs) makeContainer(ctx context.Context, container string) error {
|
|||||||
// isEmpty checks to see if a given (container, directory) is empty and returns an error if not
|
// isEmpty checks to see if a given (container, directory) is empty and returns an error if not
|
||||||
func (f *Fs) isEmpty(ctx context.Context, container, directory string) (err error) {
|
func (f *Fs) isEmpty(ctx context.Context, container, directory string) (err error) {
|
||||||
empty := true
|
empty := true
|
||||||
err = f.list(ctx, container, directory, f.rootDirectory, f.rootContainer == "", true, 1, func(remote string, object *azblob.BlobItemInternal, isDirectory bool) error {
|
err = f.list(ctx, container, directory, f.rootDirectory, f.rootContainer == "", true, 1, func(remote string, object *azblob.BlobItem, isDirectory bool) error {
|
||||||
empty = false
|
empty = false
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
@@ -1177,10 +933,10 @@ func (f *Fs) deleteContainer(ctx context.Context, container string) error {
|
|||||||
return false, fs.ErrorDirNotFound
|
return false, fs.ErrorDirNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
return f.shouldRetry(ctx, err)
|
return f.shouldRetry(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return f.shouldRetry(ctx, err)
|
return f.shouldRetry(err)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -1220,7 +976,7 @@ func (f *Fs) Purge(ctx context.Context, dir string) error {
|
|||||||
return f.deleteContainer(ctx, container)
|
return f.deleteContainer(ctx, container)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy src to this remote using server-side copy operations.
|
// Copy src to this remote using server side copy operations.
|
||||||
//
|
//
|
||||||
// This is stored with the remote path given
|
// This is stored with the remote path given
|
||||||
//
|
//
|
||||||
@@ -1252,8 +1008,8 @@ func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object,
|
|||||||
var startCopy *azblob.BlobStartCopyFromURLResponse
|
var startCopy *azblob.BlobStartCopyFromURLResponse
|
||||||
|
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
startCopy, err = dstBlobURL.StartCopyFromURL(ctx, *source, nil, azblob.ModifiedAccessConditions{}, options, azblob.AccessTierType(f.opt.AccessTier), nil)
|
startCopy, err = dstBlobURL.StartCopyFromURL(ctx, *source, nil, azblob.ModifiedAccessConditions{}, options)
|
||||||
return f.shouldRetry(ctx, err)
|
return f.shouldRetry(err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -1262,7 +1018,7 @@ func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object,
|
|||||||
copyStatus := startCopy.CopyStatus()
|
copyStatus := startCopy.CopyStatus()
|
||||||
for copyStatus == azblob.CopyStatusPending {
|
for copyStatus == azblob.CopyStatusPending {
|
||||||
time.Sleep(1 * time.Second)
|
time.Sleep(1 * time.Second)
|
||||||
getMetadata, err := dstBlobURL.GetProperties(ctx, options, azblob.ClientProvidedKeyOptions{})
|
getMetadata, err := dstBlobURL.GetProperties(ctx, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -1280,7 +1036,7 @@ func (f *Fs) getMemoryPool(size int64) *pool.Pool {
|
|||||||
return pool.New(
|
return pool.New(
|
||||||
time.Duration(f.opt.MemoryPoolFlushTime),
|
time.Duration(f.opt.MemoryPoolFlushTime),
|
||||||
int(size),
|
int(size),
|
||||||
f.ci.Transfers,
|
fs.Config.Transfers,
|
||||||
f.opt.MemoryPoolUseMmap,
|
f.opt.MemoryPoolUseMmap,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1367,7 +1123,7 @@ func (o *Object) decodeMetaDataFromPropertiesResponse(info *azblob.BlobGetProper
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *Object) decodeMetaDataFromBlob(info *azblob.BlobItemInternal) (err error) {
|
func (o *Object) decodeMetaDataFromBlob(info *azblob.BlobItem) (err error) {
|
||||||
metadata := info.Metadata
|
metadata := info.Metadata
|
||||||
size := *info.Properties.ContentLength
|
size := *info.Properties.ContentLength
|
||||||
if isDirectoryMarker(size, metadata, o.remote) {
|
if isDirectoryMarker(size, metadata, o.remote) {
|
||||||
@@ -1413,8 +1169,8 @@ func (o *Object) readMetaData() (err error) {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
var blobProperties *azblob.BlobGetPropertiesResponse
|
var blobProperties *azblob.BlobGetPropertiesResponse
|
||||||
err = o.fs.pacer.Call(func() (bool, error) {
|
err = o.fs.pacer.Call(func() (bool, error) {
|
||||||
blobProperties, err = blob.GetProperties(ctx, options, azblob.ClientProvidedKeyOptions{})
|
blobProperties, err = blob.GetProperties(ctx, options)
|
||||||
return o.fs.shouldRetry(ctx, err)
|
return o.fs.shouldRetry(err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// On directories - GetProperties does not work and current SDK does not populate service code correctly hence check regular http response as well
|
// On directories - GetProperties does not work and current SDK does not populate service code correctly hence check regular http response as well
|
||||||
@@ -1448,8 +1204,8 @@ func (o *Object) SetModTime(ctx context.Context, modTime time.Time) error {
|
|||||||
|
|
||||||
blob := o.getBlobReference()
|
blob := o.getBlobReference()
|
||||||
err := o.fs.pacer.Call(func() (bool, error) {
|
err := o.fs.pacer.Call(func() (bool, error) {
|
||||||
_, err := blob.SetMetadata(ctx, o.meta, azblob.BlobAccessConditions{}, azblob.ClientProvidedKeyOptions{})
|
_, err := blob.SetMetadata(ctx, o.meta, azblob.BlobAccessConditions{})
|
||||||
return o.fs.shouldRetry(ctx, err)
|
return o.fs.shouldRetry(err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1489,15 +1245,15 @@ func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.Read
|
|||||||
}
|
}
|
||||||
blob := o.getBlobReference()
|
blob := o.getBlobReference()
|
||||||
ac := azblob.BlobAccessConditions{}
|
ac := azblob.BlobAccessConditions{}
|
||||||
var downloadResponse *azblob.DownloadResponse
|
var dowloadResponse *azblob.DownloadResponse
|
||||||
err = o.fs.pacer.Call(func() (bool, error) {
|
err = o.fs.pacer.Call(func() (bool, error) {
|
||||||
downloadResponse, err = blob.Download(ctx, offset, count, ac, false, azblob.ClientProvidedKeyOptions{})
|
dowloadResponse, err = blob.Download(ctx, offset, count, ac, false)
|
||||||
return o.fs.shouldRetry(ctx, err)
|
return o.fs.shouldRetry(err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to open for download")
|
return nil, errors.Wrap(err, "failed to open for download")
|
||||||
}
|
}
|
||||||
in = downloadResponse.Body(azblob.RetryReaderOptions{})
|
in = dowloadResponse.Body(azblob.RetryReaderOptions{})
|
||||||
return in, nil
|
return in, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1522,6 +1278,12 @@ func init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// readSeeker joins an io.Reader and an io.Seeker
|
||||||
|
type readSeeker struct {
|
||||||
|
io.Reader
|
||||||
|
io.Seeker
|
||||||
|
}
|
||||||
|
|
||||||
// increment the slice passed in as LSB binary
|
// increment the slice passed in as LSB binary
|
||||||
func increment(xs []byte) {
|
func increment(xs []byte) {
|
||||||
for i, digit := range xs {
|
for i, digit := range xs {
|
||||||
@@ -1534,69 +1296,153 @@ func increment(xs []byte) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// poolWrapper wraps a pool.Pool as an azblob.TransferManager
|
var warnStreamUpload sync.Once
|
||||||
type poolWrapper struct {
|
|
||||||
pool *pool.Pool
|
|
||||||
bufToken chan struct{}
|
|
||||||
runToken chan struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// newPoolWrapper creates an azblob.TransferManager that will use a
|
// uploadMultipart uploads a file using multipart upload
|
||||||
// pool.Pool with maximum concurrency as specified.
|
//
|
||||||
func (f *Fs) newPoolWrapper(concurrency int) azblob.TransferManager {
|
// Write a larger blob, using CreateBlockBlob, PutBlock, and PutBlockList.
|
||||||
return &poolWrapper{
|
func (o *Object) uploadMultipart(ctx context.Context, in io.Reader, size int64, blob *azblob.BlobURL, httpHeaders *azblob.BlobHTTPHeaders) (err error) {
|
||||||
pool: f.pool,
|
// Calculate correct chunkSize
|
||||||
bufToken: make(chan struct{}, concurrency),
|
chunkSize := int64(o.fs.opt.ChunkSize)
|
||||||
runToken: make(chan struct{}, concurrency),
|
totalParts := -1
|
||||||
|
|
||||||
|
// Note that the max size of file is 4.75 TB (100 MB X 50,000
|
||||||
|
// blocks) and this is bigger than the max uncommitted block
|
||||||
|
// size (9.52 TB) so we do not need to part commit block lists
|
||||||
|
// or garbage collect uncommitted blocks.
|
||||||
|
//
|
||||||
|
// See: https://docs.microsoft.com/en-gb/rest/api/storageservices/put-block
|
||||||
|
|
||||||
|
// size can be -1 here meaning we don't know the size of the incoming file. We use ChunkSize
|
||||||
|
// buffers here (default 4MB). With a maximum number of parts (50,000) this will be a file of
|
||||||
|
// 195GB which seems like a not too unreasonable limit.
|
||||||
|
if size == -1 {
|
||||||
|
warnStreamUpload.Do(func() {
|
||||||
|
fs.Logf(o, "Streaming uploads using chunk size %v will have maximum file size of %v",
|
||||||
|
o.fs.opt.ChunkSize, fs.SizeSuffix(chunkSize*maxTotalParts))
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Adjust partSize until the number of parts is small enough.
|
||||||
|
if size/chunkSize >= maxTotalParts {
|
||||||
|
// Calculate partition size rounded up to the nearest MB
|
||||||
|
chunkSize = (((size / maxTotalParts) >> 20) + 1) << 20
|
||||||
|
}
|
||||||
|
if chunkSize > int64(maxChunkSize) {
|
||||||
|
return errors.Errorf("can't upload as it is too big %v - takes more than %d chunks of %v", fs.SizeSuffix(size), totalParts, fs.SizeSuffix(chunkSize/2))
|
||||||
|
}
|
||||||
|
totalParts = int(size / chunkSize)
|
||||||
|
if size%chunkSize != 0 {
|
||||||
|
totalParts++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get implements TransferManager.Get().
|
fs.Debugf(o, "Multipart upload session started for %d parts of size %v", totalParts, fs.SizeSuffix(chunkSize))
|
||||||
func (pw *poolWrapper) Get() []byte {
|
|
||||||
pw.bufToken <- struct{}{}
|
// unwrap the accounting from the input, we use wrap to put it
|
||||||
return pw.pool.Get()
|
// back on after the buffering
|
||||||
|
in, wrap := accounting.UnWrap(in)
|
||||||
|
|
||||||
|
// Upload the chunks
|
||||||
|
var (
|
||||||
|
g, gCtx = errgroup.WithContext(ctx)
|
||||||
|
remaining = size // remaining size in file for logging only, -1 if size < 0
|
||||||
|
position = int64(0) // position in file
|
||||||
|
memPool = o.fs.getMemoryPool(chunkSize) // pool to get memory from
|
||||||
|
finished = false // set when we have read EOF
|
||||||
|
blocks []string // list of blocks for finalize
|
||||||
|
blockBlobURL = blob.ToBlockBlobURL() // Get BlockBlobURL, we will use default pipeline here
|
||||||
|
ac = azblob.LeaseAccessConditions{} // Use default lease access conditions
|
||||||
|
binaryBlockID = make([]byte, 8) // block counter as LSB first 8 bytes
|
||||||
|
)
|
||||||
|
for part := 0; !finished; part++ {
|
||||||
|
// Get a block of memory from the pool and a token which limits concurrency
|
||||||
|
o.fs.uploadToken.Get()
|
||||||
|
buf := memPool.Get()
|
||||||
|
|
||||||
|
free := func() {
|
||||||
|
memPool.Put(buf) // return the buf
|
||||||
|
o.fs.uploadToken.Put() // return the token
|
||||||
}
|
}
|
||||||
|
|
||||||
// Put implements TransferManager.Put().
|
// Fail fast, in case an errgroup managed function returns an error
|
||||||
func (pw *poolWrapper) Put(b []byte) {
|
// gCtx is cancelled. There is no point in uploading all the other parts.
|
||||||
pw.pool.Put(b)
|
if gCtx.Err() != nil {
|
||||||
<-pw.bufToken
|
free()
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run implements TransferManager.Run().
|
// Read the chunk
|
||||||
func (pw *poolWrapper) Run(f func()) {
|
n, err := readers.ReadFill(in, buf) // this can never return 0, nil
|
||||||
pw.runToken <- struct{}{}
|
if err == io.EOF {
|
||||||
go func() {
|
if n == 0 { // end if no data
|
||||||
f()
|
free()
|
||||||
<-pw.runToken
|
break
|
||||||
}()
|
}
|
||||||
|
finished = true
|
||||||
|
} else if err != nil {
|
||||||
|
free()
|
||||||
|
return errors.Wrap(err, "multipart upload failed to read source")
|
||||||
|
}
|
||||||
|
buf = buf[:n]
|
||||||
|
|
||||||
|
// increment the blockID and save the blocks for finalize
|
||||||
|
increment(binaryBlockID)
|
||||||
|
blockID := base64.StdEncoding.EncodeToString(binaryBlockID)
|
||||||
|
blocks = append(blocks, blockID)
|
||||||
|
|
||||||
|
// Transfer the chunk
|
||||||
|
fs.Debugf(o, "Uploading part %d/%d offset %v/%v part size %v", part+1, totalParts, fs.SizeSuffix(position), fs.SizeSuffix(size), fs.SizeSuffix(chunkSize))
|
||||||
|
g.Go(func() (err error) {
|
||||||
|
defer free()
|
||||||
|
|
||||||
|
// Upload the block, with MD5 for check
|
||||||
|
md5sum := md5.Sum(buf)
|
||||||
|
transactionalMD5 := md5sum[:]
|
||||||
|
err = o.fs.pacer.Call(func() (bool, error) {
|
||||||
|
bufferReader := bytes.NewReader(buf)
|
||||||
|
wrappedReader := wrap(bufferReader)
|
||||||
|
rs := readSeeker{wrappedReader, bufferReader}
|
||||||
|
_, err = blockBlobURL.StageBlock(ctx, blockID, &rs, ac, transactionalMD5)
|
||||||
|
return o.fs.shouldRetry(err)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "multipart upload failed to upload part")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// ready for next block
|
||||||
|
if size >= 0 {
|
||||||
|
remaining -= chunkSize
|
||||||
|
}
|
||||||
|
position += chunkSize
|
||||||
|
}
|
||||||
|
err = g.Wait()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close implements TransferManager.Close().
|
// Finalise the upload session
|
||||||
func (pw *poolWrapper) Close() {
|
err = o.fs.pacer.Call(func() (bool, error) {
|
||||||
|
_, err := blockBlobURL.CommitBlockList(ctx, blocks, *httpHeaders, o.meta, azblob.BlobAccessConditions{})
|
||||||
|
return o.fs.shouldRetry(err)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "multipart upload failed to finalize")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the object with the contents of the io.Reader, modTime and size
|
// Update the object with the contents of the io.Reader, modTime and size
|
||||||
//
|
//
|
||||||
// The new object may have been created if an error is returned
|
// The new object may have been created if an error is returned
|
||||||
func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (err error) {
|
func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (err error) {
|
||||||
if o.accessTier == azblob.AccessTierArchive {
|
|
||||||
if o.fs.opt.ArchiveTierDelete {
|
|
||||||
fs.Debugf(o, "deleting archive tier blob before updating")
|
|
||||||
err = o.Remove(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "failed to delete archive blob before updating")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return errCantUpdateArchiveTierBlobs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
container, _ := o.split()
|
container, _ := o.split()
|
||||||
err = o.fs.makeContainer(ctx, container)
|
err = o.fs.makeContainer(ctx, container)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
size := src.Size()
|
||||||
// Update Mod time
|
// Update Mod time
|
||||||
o.updateMetadataWithModTime(src.ModTime(ctx))
|
o.updateMetadataWithModTime(src.ModTime(ctx))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1605,10 +1451,11 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
|
|||||||
|
|
||||||
blob := o.getBlobReference()
|
blob := o.getBlobReference()
|
||||||
httpHeaders := azblob.BlobHTTPHeaders{}
|
httpHeaders := azblob.BlobHTTPHeaders{}
|
||||||
httpHeaders.ContentType = fs.MimeType(ctx, src)
|
httpHeaders.ContentType = fs.MimeType(ctx, o)
|
||||||
|
// Compute the Content-MD5 of the file, for multiparts uploads it
|
||||||
// Compute the Content-MD5 of the file. As we stream all uploads it
|
|
||||||
// will be set in PutBlockList API call using the 'x-ms-blob-content-md5' header
|
// will be set in PutBlockList API call using the 'x-ms-blob-content-md5' header
|
||||||
|
// Note: If multipart, an MD5 checksum will also be computed for each uploaded block
|
||||||
|
// in order to validate its integrity during transport
|
||||||
if !o.fs.opt.DisableCheckSum {
|
if !o.fs.opt.DisableCheckSum {
|
||||||
if sourceMD5, _ := src.Hash(ctx, hash.MD5); sourceMD5 != "" {
|
if sourceMD5, _ := src.Hash(ctx, hash.MD5); sourceMD5 != "" {
|
||||||
sourceMD5bytes, err := hex.DecodeString(sourceMD5)
|
sourceMD5bytes, err := hex.DecodeString(sourceMD5)
|
||||||
@@ -1622,18 +1469,31 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
|
|||||||
|
|
||||||
putBlobOptions := azblob.UploadStreamToBlockBlobOptions{
|
putBlobOptions := azblob.UploadStreamToBlockBlobOptions{
|
||||||
BufferSize: int(o.fs.opt.ChunkSize),
|
BufferSize: int(o.fs.opt.ChunkSize),
|
||||||
MaxBuffers: uploadConcurrency,
|
MaxBuffers: 4,
|
||||||
Metadata: o.meta,
|
Metadata: o.meta,
|
||||||
BlobHTTPHeaders: httpHeaders,
|
BlobHTTPHeaders: httpHeaders,
|
||||||
TransferManager: o.fs.newPoolWrapper(uploadConcurrency),
|
}
|
||||||
|
// FIXME Until https://github.com/Azure/azure-storage-blob-go/pull/75
|
||||||
|
// is merged the SDK can't upload a single blob of exactly the chunk
|
||||||
|
// size, so upload with a multpart upload to work around.
|
||||||
|
// See: https://github.com/rclone/rclone/issues/2653
|
||||||
|
multipartUpload := size < 0 || size >= int64(o.fs.opt.UploadCutoff)
|
||||||
|
if size == int64(o.fs.opt.ChunkSize) {
|
||||||
|
multipartUpload = true
|
||||||
|
fs.Debugf(o, "Setting multipart upload for file of chunk size (%d) to work around SDK bug", size)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't retry, return a retry error instead
|
// Don't retry, return a retry error instead
|
||||||
err = o.fs.pacer.CallNoRetry(func() (bool, error) {
|
err = o.fs.pacer.CallNoRetry(func() (bool, error) {
|
||||||
// Stream contents of the reader object to the given blob URL
|
if multipartUpload {
|
||||||
|
// If a large file upload in chunks
|
||||||
|
err = o.uploadMultipart(ctx, in, size, &blob, &httpHeaders)
|
||||||
|
} else {
|
||||||
|
// Write a small blob in one transaction
|
||||||
blockBlobURL := blob.ToBlockBlobURL()
|
blockBlobURL := blob.ToBlockBlobURL()
|
||||||
_, err = azblob.UploadStreamToBlockBlob(ctx, in, blockBlobURL, putBlobOptions)
|
_, err = azblob.UploadStreamToBlockBlob(ctx, in, blockBlobURL, putBlobOptions)
|
||||||
return o.fs.shouldRetry(ctx, err)
|
}
|
||||||
|
return o.fs.shouldRetry(err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1661,7 +1521,7 @@ func (o *Object) Remove(ctx context.Context) error {
|
|||||||
ac := azblob.BlobAccessConditions{}
|
ac := azblob.BlobAccessConditions{}
|
||||||
return o.fs.pacer.Call(func() (bool, error) {
|
return o.fs.pacer.Call(func() (bool, error) {
|
||||||
_, err := blob.Delete(ctx, snapShotOptions, ac)
|
_, err := blob.Delete(ctx, snapShotOptions, ac)
|
||||||
return o.fs.shouldRetry(ctx, err)
|
return o.fs.shouldRetry(err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1690,7 +1550,7 @@ func (o *Object) SetTier(tier string) error {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
err := o.fs.pacer.Call(func() (bool, error) {
|
err := o.fs.pacer.Call(func() (bool, error) {
|
||||||
_, err := blob.SetTier(ctx, desiredAccessTier, azblob.LeaseAccessConditions{})
|
_, err := blob.SetTier(ctx, desiredAccessTier, azblob.LeaseAccessConditions{})
|
||||||
return o.fs.shouldRetry(ctx, err)
|
return o.fs.shouldRetry(err)
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// +build !plan9,!solaris,!js,go1.14
|
// +build !plan9,!solaris,!js,go1.13
|
||||||
|
|
||||||
package azureblob
|
package azureblob
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
// Test AzureBlob filesystem interface
|
// Test AzureBlob filesystem interface
|
||||||
|
|
||||||
// +build !plan9,!solaris,!js,go1.14
|
// +build !plan9,!solaris,!js,go1.13
|
||||||
|
|
||||||
package azureblob
|
package azureblob
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/rclone/rclone/fs"
|
"github.com/rclone/rclone/fs"
|
||||||
"github.com/rclone/rclone/fstest/fstests"
|
"github.com/rclone/rclone/fstest/fstests"
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestIntegration runs integration tests against the remote
|
// TestIntegration runs integration tests against the remote
|
||||||
@@ -29,36 +27,11 @@ func (f *Fs) SetUploadChunkSize(cs fs.SizeSuffix) (fs.SizeSuffix, error) {
|
|||||||
return f.setUploadChunkSize(cs)
|
return f.setUploadChunkSize(cs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *Fs) SetUploadCutoff(cs fs.SizeSuffix) (fs.SizeSuffix, error) {
|
||||||
|
return f.setUploadCutoff(cs)
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
_ fstests.SetUploadChunkSizer = (*Fs)(nil)
|
_ fstests.SetUploadChunkSizer = (*Fs)(nil)
|
||||||
|
_ fstests.SetUploadCutoffer = (*Fs)(nil)
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestServicePrincipalFileSuccess checks that, given a proper JSON file, we can create a token.
|
|
||||||
func TestServicePrincipalFileSuccess(t *testing.T) {
|
|
||||||
ctx := context.TODO()
|
|
||||||
credentials := `
|
|
||||||
{
|
|
||||||
"appId": "my application (client) ID",
|
|
||||||
"password": "my secret",
|
|
||||||
"tenant": "my active directory tenant ID"
|
|
||||||
}
|
|
||||||
`
|
|
||||||
tokenRefresher, err := newServicePrincipalTokenRefresher(ctx, []byte(credentials))
|
|
||||||
if assert.NoError(t, err) {
|
|
||||||
assert.NotNil(t, tokenRefresher)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestServicePrincipalFileFailure checks that, given a JSON file with a missing secret, it returns an error.
|
|
||||||
func TestServicePrincipalFileFailure(t *testing.T) {
|
|
||||||
ctx := context.TODO()
|
|
||||||
credentials := `
|
|
||||||
{
|
|
||||||
"appId": "my application (client) ID",
|
|
||||||
"tenant": "my active directory tenant ID"
|
|
||||||
}
|
|
||||||
`
|
|
||||||
_, err := newServicePrincipalTokenRefresher(ctx, []byte(credentials))
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.EqualError(t, err, "error creating service principal token: parameter 'secret' cannot be empty")
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Build for azureblob for unsupported platforms to stop go complaining
|
// Build for azureblob for unsupported platforms to stop go complaining
|
||||||
// about "no buildable Go source files "
|
// about "no buildable Go source files "
|
||||||
|
|
||||||
// +build plan9 solaris js !go1.14
|
// +build plan9 solaris js !go1.13
|
||||||
|
|
||||||
package azureblob
|
package azureblob
|
||||||
|
|||||||
@@ -1,137 +0,0 @@
|
|||||||
// +build !plan9,!solaris,!js,go1.14
|
|
||||||
|
|
||||||
package azureblob
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/Azure/go-autorest/autorest/adal"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/rclone/rclone/fs"
|
|
||||||
"github.com/rclone/rclone/fs/fshttp"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
azureResource = "https://storage.azure.com"
|
|
||||||
imdsAPIVersion = "2018-02-01"
|
|
||||||
msiEndpointDefault = "http://169.254.169.254/metadata/identity/oauth2/token"
|
|
||||||
)
|
|
||||||
|
|
||||||
// This custom type is used to add the port the test server has bound to
|
|
||||||
// to the request context.
|
|
||||||
type testPortKey string
|
|
||||||
|
|
||||||
type msiIdentifierType int
|
|
||||||
|
|
||||||
const (
|
|
||||||
msiClientID msiIdentifierType = iota
|
|
||||||
msiObjectID
|
|
||||||
msiResourceID
|
|
||||||
)
|
|
||||||
|
|
||||||
type userMSI struct {
|
|
||||||
Type msiIdentifierType
|
|
||||||
Value string
|
|
||||||
}
|
|
||||||
|
|
||||||
type httpError struct {
|
|
||||||
Response *http.Response
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e httpError) Error() string {
|
|
||||||
return fmt.Sprintf("HTTP error %v (%v)", e.Response.StatusCode, e.Response.Status)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMSIToken attempts to obtain an MSI token from the Azure Instance
|
|
||||||
// Metadata Service.
|
|
||||||
func GetMSIToken(ctx context.Context, identity *userMSI) (adal.Token, error) {
|
|
||||||
// Attempt to get an MSI token; silently continue if unsuccessful.
|
|
||||||
// This code has been lovingly stolen from azcopy's OAuthTokenManager.
|
|
||||||
result := adal.Token{}
|
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", msiEndpointDefault, nil)
|
|
||||||
if err != nil {
|
|
||||||
fs.Debugf(nil, "Failed to create request: %v", err)
|
|
||||||
return result, err
|
|
||||||
}
|
|
||||||
params := req.URL.Query()
|
|
||||||
params.Set("resource", azureResource)
|
|
||||||
params.Set("api-version", imdsAPIVersion)
|
|
||||||
|
|
||||||
// Specify user-assigned identity if requested.
|
|
||||||
if identity != nil {
|
|
||||||
switch identity.Type {
|
|
||||||
case msiClientID:
|
|
||||||
params.Set("client_id", identity.Value)
|
|
||||||
case msiObjectID:
|
|
||||||
params.Set("object_id", identity.Value)
|
|
||||||
case msiResourceID:
|
|
||||||
params.Set("mi_res_id", identity.Value)
|
|
||||||
default:
|
|
||||||
// If this happens, the calling function and this one don't agree on
|
|
||||||
// what valid ID types exist.
|
|
||||||
return result, fmt.Errorf("unknown MSI identity type specified")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
req.URL.RawQuery = params.Encode()
|
|
||||||
|
|
||||||
// The Metadata header is required by all calls to IMDS.
|
|
||||||
req.Header.Set("Metadata", "true")
|
|
||||||
|
|
||||||
// If this function is run in a test, query the test server instead of IMDS.
|
|
||||||
testPort, isTest := ctx.Value(testPortKey("testPort")).(int)
|
|
||||||
if isTest {
|
|
||||||
req.URL.Host = fmt.Sprintf("localhost:%d", testPort)
|
|
||||||
req.Host = req.URL.Host
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send request
|
|
||||||
httpClient := fshttp.NewClient(ctx)
|
|
||||||
resp, err := httpClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return result, errors.Wrap(err, "MSI is not enabled on this VM")
|
|
||||||
}
|
|
||||||
defer func() { // resp and Body should not be nil
|
|
||||||
_, err = io.Copy(ioutil.Discard, resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
fs.Debugf(nil, "Unable to drain IMDS response: %v", err)
|
|
||||||
}
|
|
||||||
err = resp.Body.Close()
|
|
||||||
if err != nil {
|
|
||||||
fs.Debugf(nil, "Unable to close IMDS response: %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
// Check if the status code indicates success
|
|
||||||
// The request returns 200 currently, add 201 and 202 as well for possible extension.
|
|
||||||
switch resp.StatusCode {
|
|
||||||
case 200, 201, 202:
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
body, _ := ioutil.ReadAll(resp.Body)
|
|
||||||
fs.Errorf(nil, "Couldn't obtain OAuth token from IMDS; server returned status code %d and body: %v", resp.StatusCode, string(body))
|
|
||||||
return result, httpError{Response: resp}
|
|
||||||
}
|
|
||||||
|
|
||||||
b, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return result, errors.Wrap(err, "Couldn't read IMDS response")
|
|
||||||
}
|
|
||||||
// Remove BOM, if any. azcopy does this so I'm following along.
|
|
||||||
b = bytes.TrimPrefix(b, []byte("\xef\xbb\xbf"))
|
|
||||||
|
|
||||||
// This would be a good place to persist the token if a large number of rclone
|
|
||||||
// invocations are being made in a short amount of time. If the token is
|
|
||||||
// persisted, the azureblob code will need to check for expiry before every
|
|
||||||
// storage API call.
|
|
||||||
err = json.Unmarshal(b, &result)
|
|
||||||
if err != nil {
|
|
||||||
return result, errors.Wrap(err, "Couldn't unmarshal IMDS response")
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
// +build !plan9,!solaris,!js,go1.14
|
|
||||||
|
|
||||||
package azureblob
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/Azure/go-autorest/autorest/adal"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func handler(t *testing.T, actual *map[string]string) http.HandlerFunc {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
err := r.ParseForm()
|
|
||||||
require.NoError(t, err)
|
|
||||||
parameters := r.URL.Query()
|
|
||||||
(*actual)["path"] = r.URL.Path
|
|
||||||
(*actual)["Metadata"] = r.Header.Get("Metadata")
|
|
||||||
(*actual)["method"] = r.Method
|
|
||||||
for paramName := range parameters {
|
|
||||||
(*actual)[paramName] = parameters.Get(paramName)
|
|
||||||
}
|
|
||||||
// Make response.
|
|
||||||
response := adal.Token{}
|
|
||||||
responseBytes, err := json.Marshal(response)
|
|
||||||
require.NoError(t, err)
|
|
||||||
_, err = w.Write(responseBytes)
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManagedIdentity(t *testing.T) {
|
|
||||||
// test user-assigned identity specifiers to use
|
|
||||||
testMSIClientID := "d859b29f-5c9c-42f8-a327-ec1bc6408d79"
|
|
||||||
testMSIObjectID := "9ffeb650-3ca0-4278-962b-5a38d520591a"
|
|
||||||
testMSIResourceID := "/subscriptions/fe714c49-b8a4-4d49-9388-96a20daa318f/resourceGroups/somerg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/someidentity"
|
|
||||||
tests := []struct {
|
|
||||||
identity *userMSI
|
|
||||||
identityParameterName string
|
|
||||||
expectedAbsent []string
|
|
||||||
}{
|
|
||||||
{&userMSI{msiClientID, testMSIClientID}, "client_id", []string{"object_id", "mi_res_id"}},
|
|
||||||
{&userMSI{msiObjectID, testMSIObjectID}, "object_id", []string{"client_id", "mi_res_id"}},
|
|
||||||
{&userMSI{msiResourceID, testMSIResourceID}, "mi_res_id", []string{"object_id", "client_id"}},
|
|
||||||
{nil, "(default)", []string{"object_id", "client_id", "mi_res_id"}},
|
|
||||||
}
|
|
||||||
alwaysExpected := map[string]string{
|
|
||||||
"path": "/metadata/identity/oauth2/token",
|
|
||||||
"resource": "https://storage.azure.com",
|
|
||||||
"Metadata": "true",
|
|
||||||
"api-version": "2018-02-01",
|
|
||||||
"method": "GET",
|
|
||||||
}
|
|
||||||
for _, test := range tests {
|
|
||||||
actual := make(map[string]string, 10)
|
|
||||||
testServer := httptest.NewServer(handler(t, &actual))
|
|
||||||
defer testServer.Close()
|
|
||||||
testServerPort, err := strconv.Atoi(strings.Split(testServer.URL, ":")[2])
|
|
||||||
require.NoError(t, err)
|
|
||||||
ctx := context.WithValue(context.TODO(), testPortKey("testPort"), testServerPort)
|
|
||||||
_, err = GetMSIToken(ctx, test.identity)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Validate expected query parameters present
|
|
||||||
expected := make(map[string]string)
|
|
||||||
for k, v := range alwaysExpected {
|
|
||||||
expected[k] = v
|
|
||||||
}
|
|
||||||
if test.identity != nil {
|
|
||||||
expected[test.identityParameterName] = test.identity.Value
|
|
||||||
}
|
|
||||||
|
|
||||||
for key := range expected {
|
|
||||||
value, exists := actual[key]
|
|
||||||
if assert.Truef(t, exists, "test of %s: query parameter %s was not passed",
|
|
||||||
test.identityParameterName, key) {
|
|
||||||
assert.Equalf(t, expected[key], value,
|
|
||||||
"test of %s: parameter %s has incorrect value", test.identityParameterName, key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate unexpected query parameters absent
|
|
||||||
for _, key := range test.expectedAbsent {
|
|
||||||
_, exists := actual[key]
|
|
||||||
assert.Falsef(t, exists, "query parameter %s was unexpectedly passed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func errorHandler(resultCode int) http.HandlerFunc {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
http.Error(w, "Test error generated", resultCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIMDSErrors(t *testing.T) {
|
|
||||||
errorCodes := []int{404, 429, 500}
|
|
||||||
for _, code := range errorCodes {
|
|
||||||
testServer := httptest.NewServer(errorHandler(code))
|
|
||||||
defer testServer.Close()
|
|
||||||
testServerPort, err := strconv.Atoi(strings.Split(testServer.URL, ":")[2])
|
|
||||||
require.NoError(t, err)
|
|
||||||
ctx := context.WithValue(context.TODO(), testPortKey("testPort"), testServerPort)
|
|
||||||
_, err = GetMSIToken(ctx, nil)
|
|
||||||
require.Error(t, err)
|
|
||||||
httpErr, ok := err.(httpError)
|
|
||||||
require.Truef(t, ok, "HTTP error %d did not result in an httpError object", code)
|
|
||||||
assert.Equalf(t, httpErr.Response.StatusCode, code, "desired error %d but didn't get it", code)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
166
backend/b2/b2.go
166
backend/b2/b2.go
@@ -44,10 +44,8 @@ const (
|
|||||||
timeHeader = headerPrefix + timeKey
|
timeHeader = headerPrefix + timeKey
|
||||||
sha1Key = "large_file_sha1"
|
sha1Key = "large_file_sha1"
|
||||||
sha1Header = "X-Bz-Content-Sha1"
|
sha1Header = "X-Bz-Content-Sha1"
|
||||||
|
sha1InfoHeader = headerPrefix + sha1Key
|
||||||
testModeHeader = "X-Bz-Test-Mode"
|
testModeHeader = "X-Bz-Test-Mode"
|
||||||
idHeader = "X-Bz-File-Id"
|
|
||||||
nameHeader = "X-Bz-File-Name"
|
|
||||||
timestampHeader = "X-Bz-Upload-Timestamp"
|
|
||||||
retryAfterHeader = "Retry-After"
|
retryAfterHeader = "Retry-After"
|
||||||
minSleep = 10 * time.Millisecond
|
minSleep = 10 * time.Millisecond
|
||||||
maxSleep = 5 * time.Minute
|
maxSleep = 5 * time.Minute
|
||||||
@@ -123,7 +121,7 @@ This value should be set no larger than 4.657GiB (== 5GB).`,
|
|||||||
Name: "copy_cutoff",
|
Name: "copy_cutoff",
|
||||||
Help: `Cutoff for switching to multipart copy
|
Help: `Cutoff for switching to multipart copy
|
||||||
|
|
||||||
Any files larger than this that need to be server-side copied will be
|
Any files larger than this that need to be server side copied will be
|
||||||
copied in chunks of this size.
|
copied in chunks of this size.
|
||||||
|
|
||||||
The minimum is 0 and the maximum is 4.6GB.`,
|
The minimum is 0 and the maximum is 4.6GB.`,
|
||||||
@@ -155,9 +153,7 @@ to start uploading.`,
|
|||||||
|
|
||||||
This is usually set to a Cloudflare CDN URL as Backblaze offers
|
This is usually set to a Cloudflare CDN URL as Backblaze offers
|
||||||
free egress for data downloaded through the Cloudflare network.
|
free egress for data downloaded through the Cloudflare network.
|
||||||
Rclone works with private buckets by sending an "Authorization" header.
|
This is probably only useful for a public bucket.
|
||||||
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.`,
|
||||||
Advanced: true,
|
Advanced: true,
|
||||||
}, {
|
}, {
|
||||||
@@ -218,7 +214,6 @@ type Fs struct {
|
|||||||
name string // name of this remote
|
name string // name of this remote
|
||||||
root string // the path we are working on if any
|
root string // the path we are working on if any
|
||||||
opt Options // parsed config options
|
opt Options // parsed config options
|
||||||
ci *fs.ConfigInfo // global config
|
|
||||||
features *fs.Features // optional features
|
features *fs.Features // optional features
|
||||||
srv *rest.Client // the connection to the b2 server
|
srv *rest.Client // the connection to the b2 server
|
||||||
rootBucket string // bucket part of root (if any)
|
rootBucket string // bucket part of root (if any)
|
||||||
@@ -295,7 +290,7 @@ func (o *Object) split() (bucket, bucketPath string) {
|
|||||||
|
|
||||||
// retryErrorCodes is a slice of error codes that we will retry
|
// retryErrorCodes is a slice of error codes that we will retry
|
||||||
var retryErrorCodes = []int{
|
var retryErrorCodes = []int{
|
||||||
401, // Unauthorized (e.g. "Token has expired")
|
401, // Unauthorized (eg "Token has expired")
|
||||||
408, // Request Timeout
|
408, // Request Timeout
|
||||||
429, // Rate exceeded.
|
429, // Rate exceeded.
|
||||||
500, // Get occasional 500 Internal Server Error
|
500, // Get occasional 500 Internal Server Error
|
||||||
@@ -305,10 +300,7 @@ var retryErrorCodes = []int{
|
|||||||
|
|
||||||
// shouldRetryNoAuth returns a boolean as to whether this resp and err
|
// shouldRetryNoAuth returns a boolean as to whether this resp and err
|
||||||
// deserve to be retried. It returns the err as a convenience
|
// deserve to be retried. It returns the err as a convenience
|
||||||
func (f *Fs) shouldRetryNoReauth(ctx context.Context, resp *http.Response, err error) (bool, error) {
|
func (f *Fs) shouldRetryNoReauth(resp *http.Response, err error) (bool, error) {
|
||||||
if fserrors.ContextError(ctx, &err) {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
// For 429 or 503 errors look at the Retry-After: header and
|
// For 429 or 503 errors look at the Retry-After: header and
|
||||||
// set the retry appropriately, starting with a minimum of 1
|
// set the retry appropriately, starting with a minimum of 1
|
||||||
// second if it isn't set.
|
// second if it isn't set.
|
||||||
@@ -339,7 +331,7 @@ func (f *Fs) shouldRetry(ctx context.Context, resp *http.Response, err error) (b
|
|||||||
}
|
}
|
||||||
return true, err
|
return true, err
|
||||||
}
|
}
|
||||||
return f.shouldRetryNoReauth(ctx, resp, err)
|
return f.shouldRetryNoReauth(resp, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// errorHandler parses a non 2xx error response into an error
|
// errorHandler parses a non 2xx error response into an error
|
||||||
@@ -399,17 +391,14 @@ func (f *Fs) setRoot(root string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewFs constructs an Fs from the path, bucket:path
|
// NewFs constructs an Fs from the path, bucket:path
|
||||||
func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, error) {
|
func NewFs(name, root string, m configmap.Mapper) (fs.Fs, error) {
|
||||||
|
ctx := context.Background()
|
||||||
// Parse config into Options struct
|
// Parse config into Options struct
|
||||||
opt := new(Options)
|
opt := new(Options)
|
||||||
err := configstruct.Set(m, opt)
|
err := configstruct.Set(m, opt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if opt.UploadCutoff < opt.ChunkSize {
|
|
||||||
opt.UploadCutoff = opt.ChunkSize
|
|
||||||
fs.Infof(nil, "b2: raising upload cutoff to chunk size: %v", opt.UploadCutoff)
|
|
||||||
}
|
|
||||||
err = checkUploadCutoff(opt, opt.UploadCutoff)
|
err = checkUploadCutoff(opt, opt.UploadCutoff)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "b2: upload cutoff")
|
return nil, errors.Wrap(err, "b2: upload cutoff")
|
||||||
@@ -427,22 +416,20 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
if opt.Endpoint == "" {
|
if opt.Endpoint == "" {
|
||||||
opt.Endpoint = defaultEndpoint
|
opt.Endpoint = defaultEndpoint
|
||||||
}
|
}
|
||||||
ci := fs.GetConfig(ctx)
|
|
||||||
f := &Fs{
|
f := &Fs{
|
||||||
name: name,
|
name: name,
|
||||||
opt: *opt,
|
opt: *opt,
|
||||||
ci: ci,
|
srv: rest.NewClient(fshttp.NewClient(fs.Config)).SetErrorHandler(errorHandler),
|
||||||
srv: rest.NewClient(fshttp.NewClient(ctx)).SetErrorHandler(errorHandler),
|
|
||||||
cache: bucket.NewCache(),
|
cache: bucket.NewCache(),
|
||||||
_bucketID: make(map[string]string, 1),
|
_bucketID: make(map[string]string, 1),
|
||||||
_bucketType: make(map[string]string, 1),
|
_bucketType: make(map[string]string, 1),
|
||||||
uploads: make(map[string][]*api.GetUploadURLResponse),
|
uploads: make(map[string][]*api.GetUploadURLResponse),
|
||||||
pacer: fs.NewPacer(ctx, pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))),
|
pacer: fs.NewPacer(pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))),
|
||||||
uploadToken: pacer.NewTokenDispenser(ci.Transfers),
|
uploadToken: pacer.NewTokenDispenser(fs.Config.Transfers),
|
||||||
pool: pool.New(
|
pool: pool.New(
|
||||||
time.Duration(opt.MemoryPoolFlushTime),
|
time.Duration(opt.MemoryPoolFlushTime),
|
||||||
int(opt.ChunkSize),
|
int(opt.ChunkSize),
|
||||||
ci.Transfers,
|
fs.Config.Transfers,
|
||||||
opt.MemoryPoolUseMmap,
|
opt.MemoryPoolUseMmap,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
@@ -452,7 +439,7 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
WriteMimeType: true,
|
WriteMimeType: true,
|
||||||
BucketBased: true,
|
BucketBased: true,
|
||||||
BucketBasedRootOK: true,
|
BucketBasedRootOK: true,
|
||||||
}).Fill(ctx, f)
|
}).Fill(f)
|
||||||
// Set the test flag if required
|
// Set the test flag if required
|
||||||
if opt.TestMode != "" {
|
if opt.TestMode != "" {
|
||||||
testMode := strings.TrimSpace(opt.TestMode)
|
testMode := strings.TrimSpace(opt.TestMode)
|
||||||
@@ -482,10 +469,13 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
f.setRoot(newRoot)
|
f.setRoot(newRoot)
|
||||||
_, err := f.NewObject(ctx, leaf)
|
_, err := f.NewObject(ctx, leaf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if err == fs.ErrorObjectNotFound {
|
||||||
// File doesn't exist so return old f
|
// File doesn't exist so return old f
|
||||||
f.setRoot(oldRoot)
|
f.setRoot(oldRoot)
|
||||||
return f, nil
|
return f, nil
|
||||||
}
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
// return an error with an fs which points to the parent
|
// return an error with an fs which points to the parent
|
||||||
return f, fs.ErrorIsFile
|
return f, fs.ErrorIsFile
|
||||||
}
|
}
|
||||||
@@ -507,7 +497,7 @@ func (f *Fs) authorizeAccount(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
err := f.pacer.Call(func() (bool, error) {
|
err := f.pacer.Call(func() (bool, error) {
|
||||||
resp, err := f.srv.CallJSON(ctx, &opts, nil, &f.info)
|
resp, err := f.srv.CallJSON(ctx, &opts, nil, &f.info)
|
||||||
return f.shouldRetryNoReauth(ctx, resp, err)
|
return f.shouldRetryNoReauth(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to authenticate")
|
return errors.Wrap(err, "failed to authenticate")
|
||||||
@@ -712,7 +702,7 @@ func (f *Fs) list(ctx context.Context, bucket, directory, prefix string, addBuck
|
|||||||
remote := file.Name[len(prefix):]
|
remote := file.Name[len(prefix):]
|
||||||
// Check for directory
|
// Check for directory
|
||||||
isDirectory := remote == "" || strings.HasSuffix(remote, "/")
|
isDirectory := remote == "" || strings.HasSuffix(remote, "/")
|
||||||
if isDirectory && len(remote) > 1 {
|
if isDirectory {
|
||||||
remote = remote[:len(remote)-1]
|
remote = remote[:len(remote)-1]
|
||||||
}
|
}
|
||||||
if addBucket {
|
if addBucket {
|
||||||
@@ -1178,10 +1168,10 @@ func (f *Fs) purge(ctx context.Context, dir string, oldOnly bool) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Delete Config.Transfers in parallel
|
// Delete Config.Transfers in parallel
|
||||||
toBeDeleted := make(chan *api.File, f.ci.Transfers)
|
toBeDeleted := make(chan *api.File, fs.Config.Transfers)
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
wg.Add(f.ci.Transfers)
|
wg.Add(fs.Config.Transfers)
|
||||||
for i := 0; i < f.ci.Transfers; i++ {
|
for i := 0; i < fs.Config.Transfers; i++ {
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
for object := range toBeDeleted {
|
for object := range toBeDeleted {
|
||||||
@@ -1193,7 +1183,7 @@ func (f *Fs) purge(ctx context.Context, dir string, oldOnly bool) error {
|
|||||||
tr := accounting.Stats(ctx).NewCheckingTransfer(oi)
|
tr := accounting.Stats(ctx).NewCheckingTransfer(oi)
|
||||||
err = f.deleteByID(ctx, object.ID, object.Name)
|
err = f.deleteByID(ctx, object.ID, object.Name)
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
tr.Done(ctx, err)
|
tr.Done(err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
@@ -1221,7 +1211,7 @@ func (f *Fs) purge(ctx context.Context, dir string, oldOnly bool) error {
|
|||||||
toBeDeleted <- object
|
toBeDeleted <- object
|
||||||
}
|
}
|
||||||
last = remote
|
last = remote
|
||||||
tr.Done(ctx, nil)
|
tr.Done(nil)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}))
|
}))
|
||||||
@@ -1244,7 +1234,7 @@ func (f *Fs) CleanUp(ctx context.Context) error {
|
|||||||
return f.purge(ctx, "", true)
|
return f.purge(ctx, "", true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// copy does a server-side copy from dstObj <- srcObj
|
// copy does a server side copy from dstObj <- srcObj
|
||||||
//
|
//
|
||||||
// If newInfo is nil then the metadata will be copied otherwise it
|
// If newInfo is nil then the metadata will be copied otherwise it
|
||||||
// will be replaced with newInfo
|
// will be replaced with newInfo
|
||||||
@@ -1301,7 +1291,7 @@ func (f *Fs) copy(ctx context.Context, dstObj *Object, srcObj *Object, newInfo *
|
|||||||
return dstObj.decodeMetaDataFileInfo(&response)
|
return dstObj.decodeMetaDataFileInfo(&response)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy src to this remote using server-side copy operations.
|
// Copy src to this remote using server side copy operations.
|
||||||
//
|
//
|
||||||
// This is stored with the remote path given
|
// This is stored with the remote path given
|
||||||
//
|
//
|
||||||
@@ -1450,7 +1440,7 @@ func (o *Object) Size() int64 {
|
|||||||
// Make sure it is lower case
|
// Make sure it is lower case
|
||||||
//
|
//
|
||||||
// Remove unverified prefix - see https://www.backblaze.com/b2/docs/uploading.html
|
// Remove unverified prefix - see https://www.backblaze.com/b2/docs/uploading.html
|
||||||
// Some tools (e.g. Cyberduck) use this
|
// Some tools (eg Cyberduck) use this
|
||||||
func cleanSHA1(sha1 string) (out string) {
|
func cleanSHA1(sha1 string) (out string) {
|
||||||
out = strings.ToLower(sha1)
|
out = strings.ToLower(sha1)
|
||||||
const unverified = "unverified:"
|
const unverified = "unverified:"
|
||||||
@@ -1504,11 +1494,8 @@ func (o *Object) decodeMetaDataFileInfo(info *api.FileInfo) (err error) {
|
|||||||
return o.decodeMetaDataRaw(info.ID, info.SHA1, info.Size, info.UploadTimestamp, info.Info, info.ContentType)
|
return o.decodeMetaDataRaw(info.ID, info.SHA1, info.Size, info.UploadTimestamp, info.Info, info.ContentType)
|
||||||
}
|
}
|
||||||
|
|
||||||
// getMetaDataListing gets the metadata from the object unconditionally from the listing
|
// getMetaData gets the metadata from the object unconditionally
|
||||||
//
|
func (o *Object) getMetaData(ctx context.Context) (info *api.File, err error) {
|
||||||
// Note that listing is a class C transaction which costs more than
|
|
||||||
// the B transaction used in getMetaData
|
|
||||||
func (o *Object) getMetaDataListing(ctx context.Context) (info *api.File, err error) {
|
|
||||||
bucket, bucketPath := o.split()
|
bucket, bucketPath := o.split()
|
||||||
maxSearched := 1
|
maxSearched := 1
|
||||||
var timestamp api.Timestamp
|
var timestamp api.Timestamp
|
||||||
@@ -1541,19 +1528,6 @@ func (o *Object) getMetaDataListing(ctx context.Context) (info *api.File, err er
|
|||||||
return info, nil
|
return info, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getMetaData gets the metadata from the object unconditionally
|
|
||||||
func (o *Object) getMetaData(ctx context.Context) (info *api.File, err error) {
|
|
||||||
// If using versions and have a version suffix, need to list the directory to find the correct versions
|
|
||||||
if o.fs.opt.Versions {
|
|
||||||
timestamp, _ := api.RemoveVersion(o.remote)
|
|
||||||
if !timestamp.IsZero() {
|
|
||||||
return o.getMetaDataListing(ctx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_, info, err = o.getOrHead(ctx, "HEAD", nil)
|
|
||||||
return info, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// readMetaData gets the metadata if it hasn't already been fetched
|
// readMetaData gets the metadata if it hasn't already been fetched
|
||||||
//
|
//
|
||||||
// Sets
|
// Sets
|
||||||
@@ -1683,11 +1657,12 @@ func (file *openFile) Close() (err error) {
|
|||||||
// Check it satisfies the interfaces
|
// Check it satisfies the interfaces
|
||||||
var _ io.ReadCloser = &openFile{}
|
var _ io.ReadCloser = &openFile{}
|
||||||
|
|
||||||
func (o *Object) getOrHead(ctx context.Context, method string, options []fs.OpenOption) (resp *http.Response, info *api.File, err error) {
|
// Open an object for read
|
||||||
|
func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.ReadCloser, err error) {
|
||||||
|
fs.FixRangeOption(options, o.size)
|
||||||
opts := rest.Opts{
|
opts := rest.Opts{
|
||||||
Method: method,
|
Method: "GET",
|
||||||
Options: options,
|
Options: options,
|
||||||
NoResponse: method == "HEAD",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use downloadUrl from backblaze if downloadUrl is not set
|
// Use downloadUrl from backblaze if downloadUrl is not set
|
||||||
@@ -1705,74 +1680,37 @@ func (o *Object) getOrHead(ctx context.Context, method string, options []fs.Open
|
|||||||
bucket, bucketPath := o.split()
|
bucket, bucketPath := o.split()
|
||||||
opts.Path += "/file/" + urlEncode(o.fs.opt.Enc.FromStandardName(bucket)) + "/" + urlEncode(o.fs.opt.Enc.FromStandardPath(bucketPath))
|
opts.Path += "/file/" + urlEncode(o.fs.opt.Enc.FromStandardName(bucket)) + "/" + urlEncode(o.fs.opt.Enc.FromStandardPath(bucketPath))
|
||||||
}
|
}
|
||||||
|
var resp *http.Response
|
||||||
err = o.fs.pacer.Call(func() (bool, error) {
|
err = o.fs.pacer.Call(func() (bool, error) {
|
||||||
resp, err = o.fs.srv.Call(ctx, &opts)
|
resp, err = o.fs.srv.Call(ctx, &opts)
|
||||||
return o.fs.shouldRetry(ctx, resp, err)
|
return o.fs.shouldRetry(ctx, resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// 404 for files, 400 for directories
|
return nil, errors.Wrap(err, "failed to open for download")
|
||||||
if resp != nil && (resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusBadRequest) {
|
|
||||||
return nil, nil, fs.ErrorObjectNotFound
|
|
||||||
}
|
|
||||||
return nil, nil, errors.Wrapf(err, "failed to %s for download", method)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NB resp may be Open here - don't return err != nil without closing
|
// Parse the time out of the headers if possible
|
||||||
|
err = o.parseTimeString(resp.Header.Get(timeHeader))
|
||||||
// Convert the Headers into an api.File
|
|
||||||
var uploadTimestamp api.Timestamp
|
|
||||||
err = uploadTimestamp.UnmarshalJSON([]byte(resp.Header.Get(timestampHeader)))
|
|
||||||
if err != nil {
|
|
||||||
fs.Debugf(o, "Bad "+timestampHeader+" header: %v", err)
|
|
||||||
}
|
|
||||||
var Info = make(map[string]string)
|
|
||||||
for k, vs := range resp.Header {
|
|
||||||
k = strings.ToLower(k)
|
|
||||||
for _, v := range vs {
|
|
||||||
if strings.HasPrefix(k, headerPrefix) {
|
|
||||||
Info[k[len(headerPrefix):]] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
info = &api.File{
|
|
||||||
ID: resp.Header.Get(idHeader),
|
|
||||||
Name: resp.Header.Get(nameHeader),
|
|
||||||
Action: "upload",
|
|
||||||
Size: resp.ContentLength,
|
|
||||||
UploadTimestamp: uploadTimestamp,
|
|
||||||
SHA1: resp.Header.Get(sha1Header),
|
|
||||||
ContentType: resp.Header.Get("Content-Type"),
|
|
||||||
Info: Info,
|
|
||||||
}
|
|
||||||
// When reading files from B2 via cloudflare using
|
|
||||||
// --b2-download-url cloudflare strips the Content-Length
|
|
||||||
// headers (presumably so it can inject stuff) so use the old
|
|
||||||
// length read from the listing.
|
|
||||||
if info.Size < 0 {
|
|
||||||
info.Size = o.size
|
|
||||||
}
|
|
||||||
return resp, info, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open an object for read
|
|
||||||
func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.ReadCloser, err error) {
|
|
||||||
fs.FixRangeOption(options, o.size)
|
|
||||||
|
|
||||||
resp, info, err := o.getOrHead(ctx, "GET", options)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't check length or hash or metadata on partial content
|
|
||||||
if resp.StatusCode == http.StatusPartialContent {
|
|
||||||
return resp.Body, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err = o.decodeMetaData(info)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = resp.Body.Close()
|
_ = resp.Body.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
// Read sha1 from header if it isn't set
|
||||||
|
if o.sha1 == "" {
|
||||||
|
o.sha1 = resp.Header.Get(sha1Header)
|
||||||
|
fs.Debugf(o, "Reading sha1 from header - %q", o.sha1)
|
||||||
|
// if sha1 header is "none" (in big files), then need
|
||||||
|
// to read it from the metadata
|
||||||
|
if o.sha1 == "none" {
|
||||||
|
o.sha1 = resp.Header.Get(sha1InfoHeader)
|
||||||
|
fs.Debugf(o, "Reading sha1 from info - %q", o.sha1)
|
||||||
|
}
|
||||||
|
o.sha1 = cleanSHA1(o.sha1)
|
||||||
|
}
|
||||||
|
// Don't check length or hash on partial content
|
||||||
|
if resp.StatusCode == http.StatusPartialContent {
|
||||||
|
return resp.Body, nil
|
||||||
|
}
|
||||||
return newOpenFile(o, resp), nil
|
return newOpenFile(o, resp), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -84,20 +84,20 @@ func init() {
|
|||||||
Name: "box",
|
Name: "box",
|
||||||
Description: "Box",
|
Description: "Box",
|
||||||
NewFs: NewFs,
|
NewFs: NewFs,
|
||||||
Config: func(ctx context.Context, name string, m configmap.Mapper) {
|
Config: func(name string, m configmap.Mapper) {
|
||||||
jsonFile, ok := m.Get("box_config_file")
|
jsonFile, ok := m.Get("box_config_file")
|
||||||
boxSubType, boxSubTypeOk := m.Get("box_sub_type")
|
boxSubType, boxSubTypeOk := m.Get("box_sub_type")
|
||||||
boxAccessToken, boxAccessTokenOk := m.Get("access_token")
|
boxAccessToken, boxAccessTokenOk := m.Get("access_token")
|
||||||
var err error
|
var err error
|
||||||
// If using box config.json, use JWT auth
|
// If using box config.json, use JWT auth
|
||||||
if ok && boxSubTypeOk && jsonFile != "" && boxSubType != "" {
|
if ok && boxSubTypeOk && jsonFile != "" && boxSubType != "" {
|
||||||
err = refreshJWTToken(ctx, jsonFile, boxSubType, name, m)
|
err = refreshJWTToken(jsonFile, boxSubType, name, m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to configure token with jwt authentication: %v", err)
|
log.Fatalf("Failed to configure token with jwt authentication: %v", err)
|
||||||
}
|
}
|
||||||
// Else, if not using an access token, use oauth2
|
// Else, if not using an access token, use oauth2
|
||||||
} else if boxAccessToken == "" || !boxAccessTokenOk {
|
} else if boxAccessToken == "" || !boxAccessTokenOk {
|
||||||
err = oauthutil.Config(ctx, "box", name, m, oauthConfig, nil)
|
err = oauthutil.Config("box", name, m, oauthConfig, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to configure token with oauth authentication: %v", err)
|
log.Fatalf("Failed to configure token with oauth authentication: %v", err)
|
||||||
}
|
}
|
||||||
@@ -153,7 +153,7 @@ func init() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func refreshJWTToken(ctx context.Context, jsonFile string, boxSubType string, name string, m configmap.Mapper) error {
|
func refreshJWTToken(jsonFile string, boxSubType string, name string, m configmap.Mapper) error {
|
||||||
jsonFile = env.ShellExpand(jsonFile)
|
jsonFile = env.ShellExpand(jsonFile)
|
||||||
boxConfig, err := getBoxConfig(jsonFile)
|
boxConfig, err := getBoxConfig(jsonFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -169,7 +169,7 @@ func refreshJWTToken(ctx context.Context, jsonFile string, boxSubType string, na
|
|||||||
}
|
}
|
||||||
signingHeaders := getSigningHeaders(boxConfig)
|
signingHeaders := getSigningHeaders(boxConfig)
|
||||||
queryParams := getQueryParams(boxConfig)
|
queryParams := getQueryParams(boxConfig)
|
||||||
client := fshttp.NewClient(ctx)
|
client := fshttp.NewClient(fs.Config)
|
||||||
err = jwtutil.Config("box", name, claims, signingHeaders, queryParams, privateKey, m, client)
|
err = jwtutil.Config("box", name, claims, signingHeaders, queryParams, privateKey, m, client)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -317,13 +317,10 @@ var retryErrorCodes = []int{
|
|||||||
|
|
||||||
// shouldRetry returns a boolean as to whether this resp and err
|
// shouldRetry returns a boolean as to whether this resp and err
|
||||||
// deserve to be retried. It returns the err as a convenience
|
// deserve to be retried. It returns the err as a convenience
|
||||||
func shouldRetry(ctx context.Context, resp *http.Response, err error) (bool, error) {
|
func shouldRetry(resp *http.Response, err error) (bool, error) {
|
||||||
if fserrors.ContextError(ctx, &err) {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
authRetry := false
|
authRetry := false
|
||||||
|
|
||||||
if resp != nil && resp.StatusCode == 401 && strings.Contains(resp.Header.Get("Www-Authenticate"), "expired_token") {
|
if resp != nil && resp.StatusCode == 401 && len(resp.Header["Www-Authenticate"]) == 1 && strings.Index(resp.Header["Www-Authenticate"][0], "expired_token") >= 0 {
|
||||||
authRetry = true
|
authRetry = true
|
||||||
fs.Debugf(nil, "Should retry: %v", err)
|
fs.Debugf(nil, "Should retry: %v", err)
|
||||||
}
|
}
|
||||||
@@ -342,7 +339,7 @@ func (f *Fs) readMetaDataForPath(ctx context.Context, path string) (info *api.It
|
|||||||
}
|
}
|
||||||
|
|
||||||
found, err := f.listAll(ctx, directoryID, false, true, func(item *api.Item) bool {
|
found, err := f.listAll(ctx, directoryID, false, true, func(item *api.Item) bool {
|
||||||
if strings.EqualFold(item.Name, leaf) {
|
if item.Name == leaf {
|
||||||
info = item
|
info = item
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -375,7 +372,8 @@ func errorHandler(resp *http.Response) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewFs constructs an Fs from the path, container:path
|
// NewFs constructs an Fs from the path, container:path
|
||||||
func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, error) {
|
func NewFs(name, root string, m configmap.Mapper) (fs.Fs, error) {
|
||||||
|
ctx := context.Background()
|
||||||
// Parse config into Options struct
|
// Parse config into Options struct
|
||||||
opt := new(Options)
|
opt := new(Options)
|
||||||
err := configstruct.Set(m, opt)
|
err := configstruct.Set(m, opt)
|
||||||
@@ -389,29 +387,28 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
|
|
||||||
root = parsePath(root)
|
root = parsePath(root)
|
||||||
|
|
||||||
client := fshttp.NewClient(ctx)
|
client := fshttp.NewClient(fs.Config)
|
||||||
var ts *oauthutil.TokenSource
|
var ts *oauthutil.TokenSource
|
||||||
// If not using an accessToken, create an oauth client and tokensource
|
// If not using an accessToken, create an oauth client and tokensource
|
||||||
if opt.AccessToken == "" {
|
if opt.AccessToken == "" {
|
||||||
client, ts, err = oauthutil.NewClient(ctx, name, m, oauthConfig)
|
client, ts, err = oauthutil.NewClient(name, m, oauthConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to configure Box")
|
return nil, errors.Wrap(err, "failed to configure Box")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ci := fs.GetConfig(ctx)
|
|
||||||
f := &Fs{
|
f := &Fs{
|
||||||
name: name,
|
name: name,
|
||||||
root: root,
|
root: root,
|
||||||
opt: *opt,
|
opt: *opt,
|
||||||
srv: rest.NewClient(client).SetRoot(rootURL),
|
srv: rest.NewClient(client).SetRoot(rootURL),
|
||||||
pacer: fs.NewPacer(ctx, pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))),
|
pacer: fs.NewPacer(pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))),
|
||||||
uploadToken: pacer.NewTokenDispenser(ci.Transfers),
|
uploadToken: pacer.NewTokenDispenser(fs.Config.Transfers),
|
||||||
}
|
}
|
||||||
f.features = (&fs.Features{
|
f.features = (&fs.Features{
|
||||||
CaseInsensitive: true,
|
CaseInsensitive: true,
|
||||||
CanHaveEmptyDirectories: true,
|
CanHaveEmptyDirectories: true,
|
||||||
}).Fill(ctx, f)
|
}).Fill(f)
|
||||||
f.srv.SetErrorHandler(errorHandler)
|
f.srv.SetErrorHandler(errorHandler)
|
||||||
|
|
||||||
// If using an accessToken, set the Authorization header
|
// If using an accessToken, set the Authorization header
|
||||||
@@ -427,7 +424,7 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
// should do so whether there are uploads pending or not.
|
// should do so whether there are uploads pending or not.
|
||||||
if ok && boxSubTypeOk && jsonFile != "" && boxSubType != "" {
|
if ok && boxSubTypeOk && jsonFile != "" && boxSubType != "" {
|
||||||
f.tokenRenewer = oauthutil.NewRenew(f.String(), ts, func() error {
|
f.tokenRenewer = oauthutil.NewRenew(f.String(), ts, func() error {
|
||||||
err := refreshJWTToken(ctx, jsonFile, boxSubType, name, m)
|
err := refreshJWTToken(jsonFile, boxSubType, name, m)
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
f.tokenRenewer.Start()
|
f.tokenRenewer.Start()
|
||||||
@@ -466,7 +463,7 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
f.features.Fill(ctx, &tempF)
|
f.features.Fill(&tempF)
|
||||||
// XXX: update the old f here instead of returning tempF, since
|
// XXX: update the old f here instead of returning tempF, since
|
||||||
// `features` were already filled with functions having *f as a receiver.
|
// `features` were already filled with functions having *f as a receiver.
|
||||||
// See https://github.com/rclone/rclone/issues/2182
|
// See https://github.com/rclone/rclone/issues/2182
|
||||||
@@ -517,7 +514,7 @@ func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) {
|
|||||||
func (f *Fs) FindLeaf(ctx context.Context, pathID, leaf string) (pathIDOut string, found bool, err error) {
|
func (f *Fs) FindLeaf(ctx context.Context, pathID, leaf string) (pathIDOut string, found bool, err error) {
|
||||||
// Find the leaf in pathID
|
// 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, func(item *api.Item) bool {
|
||||||
if strings.EqualFold(item.Name, leaf) {
|
if item.Name == leaf {
|
||||||
pathIDOut = item.ID
|
pathIDOut = item.ID
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -551,7 +548,7 @@ func (f *Fs) CreateDir(ctx context.Context, pathID, leaf string) (newID string,
|
|||||||
}
|
}
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, &mkdir, &info)
|
resp, err = f.srv.CallJSON(ctx, &opts, &mkdir, &info)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
//fmt.Printf("...Error %v\n", err)
|
//fmt.Printf("...Error %v\n", err)
|
||||||
@@ -588,7 +585,7 @@ OUTER:
|
|||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return found, errors.Wrap(err, "couldn't list files")
|
return found, errors.Wrap(err, "couldn't list files")
|
||||||
@@ -743,7 +740,7 @@ func (f *Fs) deleteObject(ctx context.Context, id string) error {
|
|||||||
}
|
}
|
||||||
return f.pacer.Call(func() (bool, error) {
|
return f.pacer.Call(func() (bool, error) {
|
||||||
resp, err := f.srv.Call(ctx, &opts)
|
resp, err := f.srv.Call(ctx, &opts)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -770,7 +767,7 @@ func (f *Fs) purgeCheck(ctx context.Context, dir string, check bool) error {
|
|||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.Call(ctx, &opts)
|
resp, err = f.srv.Call(ctx, &opts)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "rmdir failed")
|
return errors.Wrap(err, "rmdir failed")
|
||||||
@@ -794,7 +791,7 @@ func (f *Fs) Precision() time.Duration {
|
|||||||
return time.Second
|
return time.Second
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy src to this remote using server-side copy operations.
|
// Copy src to this remote using server side copy operations.
|
||||||
//
|
//
|
||||||
// This is stored with the remote path given
|
// This is stored with the remote path given
|
||||||
//
|
//
|
||||||
@@ -842,7 +839,7 @@ func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object,
|
|||||||
var info *api.Item
|
var info *api.Item
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, ©File, &info)
|
resp, err = f.srv.CallJSON(ctx, &opts, ©File, &info)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -880,7 +877,7 @@ func (f *Fs) move(ctx context.Context, endpoint, id, leaf, directoryID string) (
|
|||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, &move, &info)
|
resp, err = f.srv.CallJSON(ctx, &opts, &move, &info)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -898,7 +895,7 @@ func (f *Fs) About(ctx context.Context) (usage *fs.Usage, err error) {
|
|||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, &user)
|
resp, err = f.srv.CallJSON(ctx, &opts, nil, &user)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to read user info")
|
return nil, errors.Wrap(err, "failed to read user info")
|
||||||
@@ -912,7 +909,7 @@ func (f *Fs) About(ctx context.Context) (usage *fs.Usage, err error) {
|
|||||||
return usage, nil
|
return usage, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move src to this remote using server-side move operations.
|
// Move src to this remote using server side move operations.
|
||||||
//
|
//
|
||||||
// This is stored with the remote path given
|
// This is stored with the remote path given
|
||||||
//
|
//
|
||||||
@@ -948,7 +945,7 @@ func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DirMove moves src, srcRemote to this remote at dstRemote
|
// DirMove moves src, srcRemote to this remote at dstRemote
|
||||||
// using server-side move operations.
|
// using server side move operations.
|
||||||
//
|
//
|
||||||
// Will only be called if src.Fs().Name() == f.Name()
|
// Will only be called if src.Fs().Name() == f.Name()
|
||||||
//
|
//
|
||||||
@@ -1011,12 +1008,12 @@ func (f *Fs) PublicLink(ctx context.Context, remote string, expire fs.Duration,
|
|||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, &shareLink, &info)
|
resp, err = f.srv.CallJSON(ctx, &opts, &shareLink, &info)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
return info.SharedLink.URL, err
|
return info.SharedLink.URL, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// deletePermanently permanently deletes a trashed file
|
// deletePermanently permenently deletes a trashed file
|
||||||
func (f *Fs) deletePermanently(ctx context.Context, itemType, id string) error {
|
func (f *Fs) deletePermanently(ctx context.Context, itemType, id string) error {
|
||||||
opts := rest.Opts{
|
opts := rest.Opts{
|
||||||
Method: "DELETE",
|
Method: "DELETE",
|
||||||
@@ -1029,7 +1026,7 @@ func (f *Fs) deletePermanently(ctx context.Context, itemType, id string) error {
|
|||||||
}
|
}
|
||||||
return f.pacer.Call(func() (bool, error) {
|
return f.pacer.Call(func() (bool, error) {
|
||||||
resp, err := f.srv.Call(ctx, &opts)
|
resp, err := f.srv.Call(ctx, &opts)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1051,7 +1048,7 @@ func (f *Fs) CleanUp(ctx context.Context) (err error) {
|
|||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "couldn't list trash")
|
return errors.Wrap(err, "couldn't list trash")
|
||||||
@@ -1185,7 +1182,7 @@ func (o *Object) setModTime(ctx context.Context, modTime time.Time) (*api.Item,
|
|||||||
var info *api.Item
|
var info *api.Item
|
||||||
err := o.fs.pacer.Call(func() (bool, error) {
|
err := o.fs.pacer.Call(func() (bool, error) {
|
||||||
resp, err := o.fs.srv.CallJSON(ctx, &opts, &update, &info)
|
resp, err := o.fs.srv.CallJSON(ctx, &opts, &update, &info)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
return info, err
|
return info, err
|
||||||
}
|
}
|
||||||
@@ -1218,7 +1215,7 @@ func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.Read
|
|||||||
}
|
}
|
||||||
err = o.fs.pacer.Call(func() (bool, error) {
|
err = o.fs.pacer.Call(func() (bool, error) {
|
||||||
resp, err = o.fs.srv.Call(ctx, &opts)
|
resp, err = o.fs.srv.Call(ctx, &opts)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -1258,7 +1255,7 @@ func (o *Object) upload(ctx context.Context, in io.Reader, leaf, directoryID str
|
|||||||
}
|
}
|
||||||
err = o.fs.pacer.CallNoRetry(func() (bool, error) {
|
err = o.fs.pacer.CallNoRetry(func() (bool, error) {
|
||||||
resp, err = o.fs.srv.CallJSON(ctx, &opts, &upload, &result)
|
resp, err = o.fs.srv.CallJSON(ctx, &opts, &upload, &result)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// multipart upload for box
|
// multpart upload for box
|
||||||
|
|
||||||
package box
|
package box
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ func (o *Object) createUploadSession(ctx context.Context, leaf, directoryID stri
|
|||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
err = o.fs.pacer.Call(func() (bool, error) {
|
err = o.fs.pacer.Call(func() (bool, error) {
|
||||||
resp, err = o.fs.srv.CallJSON(ctx, &opts, &request, &response)
|
resp, err = o.fs.srv.CallJSON(ctx, &opts, &request, &response)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -74,7 +74,7 @@ func (o *Object) uploadPart(ctx context.Context, SessionID string, offset, total
|
|||||||
err = o.fs.pacer.Call(func() (bool, error) {
|
err = o.fs.pacer.Call(func() (bool, error) {
|
||||||
opts.Body = wrap(bytes.NewReader(chunk))
|
opts.Body = wrap(bytes.NewReader(chunk))
|
||||||
resp, err = o.fs.srv.CallJSON(ctx, &opts, nil, &response)
|
resp, err = o.fs.srv.CallJSON(ctx, &opts, nil, &response)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -109,10 +109,10 @@ outer:
|
|||||||
err = o.fs.pacer.Call(func() (bool, error) {
|
err = o.fs.pacer.Call(func() (bool, error) {
|
||||||
resp, err = o.fs.srv.CallJSON(ctx, &opts, &request, nil)
|
resp, err = o.fs.srv.CallJSON(ctx, &opts, &request, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
}
|
}
|
||||||
body, err = rest.ReadBody(resp)
|
body, err = rest.ReadBody(resp)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
delay := defaultDelay
|
delay := defaultDelay
|
||||||
var why string
|
var why string
|
||||||
@@ -167,7 +167,7 @@ func (o *Object) abortUpload(ctx context.Context, SessionID string) (err error)
|
|||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
err = o.fs.pacer.Call(func() (bool, error) {
|
err = o.fs.pacer.Call(func() (bool, error) {
|
||||||
resp, err = o.fs.srv.Call(ctx, &opts)
|
resp, err = o.fs.srv.Call(ctx, &opts)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
33
backend/cache/cache.go
vendored
33
backend/cache/cache.go
vendored
@@ -68,7 +68,7 @@ func init() {
|
|||||||
CommandHelp: commandHelp,
|
CommandHelp: commandHelp,
|
||||||
Options: []fs.Option{{
|
Options: []fs.Option{{
|
||||||
Name: "remote",
|
Name: "remote",
|
||||||
Help: "Remote to cache.\nNormally should contain a ':' and a path, e.g. \"myremote:path/to/dir\",\n\"myremote:bucket\" or maybe \"myremote:\" (not recommended).",
|
Help: "Remote to cache.\nNormally should contain a ':' and a path, eg \"myremote:path/to/dir\",\n\"myremote:bucket\" or maybe \"myremote:\" (not recommended).",
|
||||||
Required: true,
|
Required: true,
|
||||||
}, {
|
}, {
|
||||||
Name: "plex_url",
|
Name: "plex_url",
|
||||||
@@ -109,7 +109,7 @@ will need to be cleared or unexpected EOF errors will occur.`,
|
|||||||
}},
|
}},
|
||||||
}, {
|
}, {
|
||||||
Name: "info_age",
|
Name: "info_age",
|
||||||
Help: `How long to cache file structure information (directory listings, file size, times, etc.).
|
Help: `How long to cache file structure information (directory listings, file size, times etc).
|
||||||
If all write operations are done through the cache then you can safely make
|
If all write operations are done through the cache then you can safely make
|
||||||
this value very large as the cache store will also be updated in real time.`,
|
this value very large as the cache store will also be updated in real time.`,
|
||||||
Default: DefCacheInfoAge,
|
Default: DefCacheInfoAge,
|
||||||
@@ -340,7 +340,7 @@ func parseRootPath(path string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewFs constructs an Fs from the path, container:path
|
// NewFs constructs an Fs from the path, container:path
|
||||||
func NewFs(ctx context.Context, name, rootPath string, m configmap.Mapper) (fs.Fs, error) {
|
func NewFs(name, rootPath string, m configmap.Mapper) (fs.Fs, error) {
|
||||||
// Parse config into Options struct
|
// Parse config into Options struct
|
||||||
opt := new(Options)
|
opt := new(Options)
|
||||||
err := configstruct.Set(m, opt)
|
err := configstruct.Set(m, opt)
|
||||||
@@ -362,7 +362,7 @@ func NewFs(ctx context.Context, name, rootPath string, m configmap.Mapper) (fs.F
|
|||||||
}
|
}
|
||||||
|
|
||||||
remotePath := fspath.JoinRootPath(opt.Remote, rootPath)
|
remotePath := fspath.JoinRootPath(opt.Remote, rootPath)
|
||||||
wrappedFs, wrapErr := cache.Get(ctx, remotePath)
|
wrappedFs, wrapErr := cache.Get(remotePath)
|
||||||
if wrapErr != nil && wrapErr != fs.ErrorIsFile {
|
if wrapErr != nil && wrapErr != fs.ErrorIsFile {
|
||||||
return nil, errors.Wrapf(wrapErr, "failed to make remote %q to wrap", remotePath)
|
return nil, errors.Wrapf(wrapErr, "failed to make remote %q to wrap", remotePath)
|
||||||
}
|
}
|
||||||
@@ -479,7 +479,7 @@ func NewFs(ctx context.Context, name, rootPath string, m configmap.Mapper) (fs.F
|
|||||||
return nil, errors.Wrapf(err, "failed to create cache directory %v", f.opt.TempWritePath)
|
return nil, errors.Wrapf(err, "failed to create cache directory %v", f.opt.TempWritePath)
|
||||||
}
|
}
|
||||||
f.opt.TempWritePath = filepath.ToSlash(f.opt.TempWritePath)
|
f.opt.TempWritePath = filepath.ToSlash(f.opt.TempWritePath)
|
||||||
f.tempFs, err = cache.Get(ctx, f.opt.TempWritePath)
|
f.tempFs, err = cache.Get(f.opt.TempWritePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(err, "failed to create temp fs: %v", err)
|
return nil, errors.Wrapf(err, "failed to create temp fs: %v", err)
|
||||||
}
|
}
|
||||||
@@ -506,13 +506,13 @@ func NewFs(ctx context.Context, name, rootPath string, m configmap.Mapper) (fs.F
|
|||||||
if doChangeNotify := wrappedFs.Features().ChangeNotify; doChangeNotify != nil {
|
if doChangeNotify := wrappedFs.Features().ChangeNotify; doChangeNotify != nil {
|
||||||
pollInterval := make(chan time.Duration, 1)
|
pollInterval := make(chan time.Duration, 1)
|
||||||
pollInterval <- time.Duration(f.opt.ChunkCleanInterval)
|
pollInterval <- time.Duration(f.opt.ChunkCleanInterval)
|
||||||
doChangeNotify(ctx, f.receiveChangeNotify, pollInterval)
|
doChangeNotify(context.Background(), f.receiveChangeNotify, pollInterval)
|
||||||
}
|
}
|
||||||
|
|
||||||
f.features = (&fs.Features{
|
f.features = (&fs.Features{
|
||||||
CanHaveEmptyDirectories: true,
|
CanHaveEmptyDirectories: true,
|
||||||
DuplicateFiles: false, // storage doesn't permit this
|
DuplicateFiles: false, // storage doesn't permit this
|
||||||
}).Fill(ctx, f).Mask(ctx, wrappedFs).WrapsFs(f, wrappedFs)
|
}).Fill(f).Mask(wrappedFs).WrapsFs(f, wrappedFs)
|
||||||
// override only those features that use a temp fs and it doesn't support them
|
// override only those features that use a temp fs and it doesn't support them
|
||||||
//f.features.ChangeNotify = f.ChangeNotify
|
//f.features.ChangeNotify = f.ChangeNotify
|
||||||
if f.opt.TempWritePath != "" {
|
if f.opt.TempWritePath != "" {
|
||||||
@@ -581,7 +581,7 @@ Some valid examples are:
|
|||||||
"0:10" -> the first ten chunks
|
"0:10" -> the first ten chunks
|
||||||
|
|
||||||
Any parameter with a key that starts with "file" can be used to
|
Any parameter with a key that starts with "file" can be used to
|
||||||
specify files to fetch, e.g.
|
specify files to fetch, eg
|
||||||
|
|
||||||
rclone rc cache/fetch chunks=0 file=hello file2=home/goodbye
|
rclone rc cache/fetch chunks=0 file=hello file2=home/goodbye
|
||||||
|
|
||||||
@@ -1236,7 +1236,7 @@ func (f *Fs) Rmdir(ctx context.Context, dir string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DirMove moves src, srcRemote to this remote at dstRemote
|
// DirMove moves src, srcRemote to this remote at dstRemote
|
||||||
// using server-side move operations.
|
// using server side move operations.
|
||||||
func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string) error {
|
func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string) error {
|
||||||
fs.Debugf(f, "move dir '%s'/'%s' -> '%s'/'%s'", src.Root(), srcRemote, f.Root(), dstRemote)
|
fs.Debugf(f, "move dir '%s'/'%s' -> '%s'/'%s'", src.Root(), srcRemote, f.Root(), dstRemote)
|
||||||
|
|
||||||
@@ -1517,7 +1517,7 @@ func (f *Fs) PutStream(ctx context.Context, in io.Reader, src fs.ObjectInfo, opt
|
|||||||
return f.put(ctx, in, src, options, do)
|
return f.put(ctx, in, src, options, do)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy src to this remote using server-side copy operations.
|
// Copy src to this remote using server side copy operations.
|
||||||
func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object, error) {
|
func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object, error) {
|
||||||
fs.Debugf(f, "copy obj '%s' -> '%s'", src, remote)
|
fs.Debugf(f, "copy obj '%s' -> '%s'", src, remote)
|
||||||
|
|
||||||
@@ -1594,7 +1594,7 @@ func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object,
|
|||||||
return co, nil
|
return co, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move src to this remote using server-side move operations.
|
// Move src to this remote using server side move operations.
|
||||||
func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object, error) {
|
func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object, error) {
|
||||||
fs.Debugf(f, "moving obj '%s' -> %s", src, remote)
|
fs.Debugf(f, "moving obj '%s' -> %s", src, remote)
|
||||||
|
|
||||||
@@ -1895,16 +1895,6 @@ func (f *Fs) Disconnect(ctx context.Context) error {
|
|||||||
return do(ctx)
|
return do(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shutdown the backend, closing any background tasks and any
|
|
||||||
// cached connections.
|
|
||||||
func (f *Fs) Shutdown(ctx context.Context) error {
|
|
||||||
do := f.Fs.Features().Shutdown
|
|
||||||
if do == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return do(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
var commandHelp = []fs.CommandHelp{
|
var commandHelp = []fs.CommandHelp{
|
||||||
{
|
{
|
||||||
Name: "stats",
|
Name: "stats",
|
||||||
@@ -1949,5 +1939,4 @@ var (
|
|||||||
_ fs.Disconnecter = (*Fs)(nil)
|
_ fs.Disconnecter = (*Fs)(nil)
|
||||||
_ fs.Commander = (*Fs)(nil)
|
_ fs.Commander = (*Fs)(nil)
|
||||||
_ fs.MergeDirser = (*Fs)(nil)
|
_ fs.MergeDirser = (*Fs)(nil)
|
||||||
_ fs.Shutdowner = (*Fs)(nil)
|
|
||||||
)
|
)
|
||||||
|
|||||||
13
backend/cache/cache_internal_test.go
vendored
13
backend/cache/cache_internal_test.go
vendored
@@ -892,7 +892,7 @@ func (r *run) newCacheFs(t *testing.T, remote, id string, needRemote, purge bool
|
|||||||
m.Set("type", "cache")
|
m.Set("type", "cache")
|
||||||
m.Set("remote", localRemote+":"+filepath.Join(os.TempDir(), localRemote))
|
m.Set("remote", localRemote+":"+filepath.Join(os.TempDir(), localRemote))
|
||||||
} else {
|
} else {
|
||||||
remoteType := config.FileGet(remote, "type")
|
remoteType := config.FileGet(remote, "type", "")
|
||||||
if remoteType == "" {
|
if remoteType == "" {
|
||||||
t.Skipf("skipped due to invalid remote type for %v", remote)
|
t.Skipf("skipped due to invalid remote type for %v", remote)
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -903,14 +903,14 @@ func (r *run) newCacheFs(t *testing.T, remote, id string, needRemote, purge bool
|
|||||||
m.Set("password", cryptPassword1)
|
m.Set("password", cryptPassword1)
|
||||||
m.Set("password2", cryptPassword2)
|
m.Set("password2", cryptPassword2)
|
||||||
}
|
}
|
||||||
remoteRemote := config.FileGet(remote, "remote")
|
remoteRemote := config.FileGet(remote, "remote", "")
|
||||||
if remoteRemote == "" {
|
if remoteRemote == "" {
|
||||||
t.Skipf("skipped due to invalid remote wrapper for %v", remote)
|
t.Skipf("skipped due to invalid remote wrapper for %v", remote)
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
remoteRemoteParts := strings.Split(remoteRemote, ":")
|
remoteRemoteParts := strings.Split(remoteRemote, ":")
|
||||||
remoteWrapping := remoteRemoteParts[0]
|
remoteWrapping := remoteRemoteParts[0]
|
||||||
remoteType := config.FileGet(remoteWrapping, "type")
|
remoteType := config.FileGet(remoteWrapping, "type", "")
|
||||||
if remoteType != "cache" {
|
if remoteType != "cache" {
|
||||||
t.Skipf("skipped due to invalid remote type for %v: '%v'", remoteWrapping, remoteType)
|
t.Skipf("skipped due to invalid remote type for %v: '%v'", remoteWrapping, remoteType)
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -925,15 +925,14 @@ func (r *run) newCacheFs(t *testing.T, remote, id string, needRemote, purge bool
|
|||||||
boltDb, err := cache.GetPersistent(runInstance.dbPath, runInstance.chunkPath, &cache.Features{PurgeDb: true})
|
boltDb, err := cache.GetPersistent(runInstance.dbPath, runInstance.chunkPath, &cache.Features{PurgeDb: true})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
ci := fs.GetConfig(context.Background())
|
fs.Config.LowLevelRetries = 1
|
||||||
ci.LowLevelRetries = 1
|
|
||||||
|
|
||||||
// Instantiate root
|
// Instantiate root
|
||||||
if purge {
|
if purge {
|
||||||
boltDb.PurgeTempUploads()
|
boltDb.PurgeTempUploads()
|
||||||
_ = os.RemoveAll(path.Join(runInstance.tmpUploadDir, id))
|
_ = os.RemoveAll(path.Join(runInstance.tmpUploadDir, id))
|
||||||
}
|
}
|
||||||
f, err := cache.NewFs(context.Background(), remote, id, m)
|
f, err := cache.NewFs(remote, id, m)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
cfs, err := r.getCacheFs(f)
|
cfs, err := r.getCacheFs(f)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -1034,7 +1033,7 @@ func (r *run) updateObjectRemote(t *testing.T, f fs.Fs, remote string, data1 []b
|
|||||||
objInfo1 := object.NewStaticObjectInfo(remote, time.Now(), int64(len(data1)), true, nil, f)
|
objInfo1 := object.NewStaticObjectInfo(remote, time.Now(), int64(len(data1)), true, nil, f)
|
||||||
objInfo2 := object.NewStaticObjectInfo(remote, time.Now(), int64(len(data2)), true, nil, f)
|
objInfo2 := object.NewStaticObjectInfo(remote, time.Now(), int64(len(data2)), true, nil, f)
|
||||||
|
|
||||||
_, err = f.Put(context.Background(), in1, objInfo1)
|
obj, err = f.Put(context.Background(), in1, objInfo1)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
obj, err = f.NewObject(context.Background(), remote)
|
obj, err = f.NewObject(context.Background(), remote)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
@@ -42,13 +42,12 @@ import (
|
|||||||
// used mostly for consistency checks (lazily for performance reasons).
|
// used mostly for consistency checks (lazily for performance reasons).
|
||||||
// Other formats can be developed that use an external meta store
|
// Other formats can be developed that use an external meta store
|
||||||
// free of these limitations, but this needs some support from
|
// free of these limitations, but this needs some support from
|
||||||
// rclone core (e.g. metadata store interfaces).
|
// rclone core (eg. metadata store interfaces).
|
||||||
//
|
//
|
||||||
// The following types of chunks are supported:
|
// The following types of chunks are supported:
|
||||||
// data and control, active and temporary.
|
// data and control, active and temporary.
|
||||||
// Chunk type is identified by matching chunk file name
|
// Chunk type is identified by matching chunk file name
|
||||||
// based on the chunk name format configured by user and transaction
|
// based on the chunk name format configured by user.
|
||||||
// style being used.
|
|
||||||
//
|
//
|
||||||
// Both data and control chunks can be either temporary (aka hidden)
|
// Both data and control chunks can be either temporary (aka hidden)
|
||||||
// or active (non-temporary aka normal aka permanent).
|
// or active (non-temporary aka normal aka permanent).
|
||||||
@@ -64,12 +63,6 @@ import (
|
|||||||
// which is transparently converted to the new format. In its maximum
|
// which is transparently converted to the new format. In its maximum
|
||||||
// length of 13 decimals it makes a 7-digit base-36 number.
|
// length of 13 decimals it makes a 7-digit base-36 number.
|
||||||
//
|
//
|
||||||
// When transactions is set to the norename style, data chunks will
|
|
||||||
// keep their temporary chunk names (with the transacion identifier
|
|
||||||
// suffix). To distinguish them from temporary chunks, the txn field
|
|
||||||
// of the metadata file is set to match the transaction identifier of
|
|
||||||
// the data chunks.
|
|
||||||
//
|
|
||||||
// Chunker can tell data chunks from control chunks by the characters
|
// Chunker can tell data chunks from control chunks by the characters
|
||||||
// located in the "hash placeholder" position of configured format.
|
// located in the "hash placeholder" position of configured format.
|
||||||
// Data chunks have decimal digits there.
|
// Data chunks have decimal digits there.
|
||||||
@@ -104,11 +97,10 @@ var (
|
|||||||
//
|
//
|
||||||
// And still chunker's primary function is to chunk large files
|
// And still chunker's primary function is to chunk large files
|
||||||
// rather than serve as a generic metadata container.
|
// rather than serve as a generic metadata container.
|
||||||
const maxMetadataSize = 1023
|
const maxMetadataSize = 255
|
||||||
const maxMetadataSizeWritten = 255
|
|
||||||
|
|
||||||
// Current/highest supported metadata format.
|
// Current/highest supported metadata format.
|
||||||
const metadataVersion = 2
|
const metadataVersion = 1
|
||||||
|
|
||||||
// optimizeFirstChunk enables the following optimization in the Put:
|
// optimizeFirstChunk enables the following optimization in the Put:
|
||||||
// If a single chunk is expected, put the first chunk using the
|
// If a single chunk is expected, put the first chunk using the
|
||||||
@@ -150,7 +142,7 @@ func init() {
|
|||||||
Name: "remote",
|
Name: "remote",
|
||||||
Required: true,
|
Required: true,
|
||||||
Help: `Remote to chunk/unchunk.
|
Help: `Remote to chunk/unchunk.
|
||||||
Normally should contain a ':' and a path, e.g. "myremote:path/to/dir",
|
Normally should contain a ':' and a path, eg "myremote:path/to/dir",
|
||||||
"myremote:bucket" or maybe "myremote:" (not recommended).`,
|
"myremote:bucket" or maybe "myremote:" (not recommended).`,
|
||||||
}, {
|
}, {
|
||||||
Name: "chunk_size",
|
Name: "chunk_size",
|
||||||
@@ -160,7 +152,6 @@ Normally should contain a ':' and a path, e.g. "myremote:path/to/dir",
|
|||||||
}, {
|
}, {
|
||||||
Name: "name_format",
|
Name: "name_format",
|
||||||
Advanced: true,
|
Advanced: true,
|
||||||
Hide: fs.OptionHideCommandLine,
|
|
||||||
Default: `*.rclone_chunk.###`,
|
Default: `*.rclone_chunk.###`,
|
||||||
Help: `String format of chunk file names.
|
Help: `String format of chunk file names.
|
||||||
The two placeholders are: base file name (*) and chunk number (#...).
|
The two placeholders are: base file name (*) and chunk number (#...).
|
||||||
@@ -171,14 +162,12 @@ Possible chunk files are ignored if their name does not match given format.`,
|
|||||||
}, {
|
}, {
|
||||||
Name: "start_from",
|
Name: "start_from",
|
||||||
Advanced: true,
|
Advanced: true,
|
||||||
Hide: fs.OptionHideCommandLine,
|
|
||||||
Default: 1,
|
Default: 1,
|
||||||
Help: `Minimum valid chunk number. Usually 0 or 1.
|
Help: `Minimum valid chunk number. Usually 0 or 1.
|
||||||
By default chunk numbers start from 1.`,
|
By default chunk numbers start from 1.`,
|
||||||
}, {
|
}, {
|
||||||
Name: "meta_format",
|
Name: "meta_format",
|
||||||
Advanced: true,
|
Advanced: true,
|
||||||
Hide: fs.OptionHideCommandLine,
|
|
||||||
Default: "simplejson",
|
Default: "simplejson",
|
||||||
Help: `Format of the metadata object or "none". By default "simplejson".
|
Help: `Format of the metadata object or "none". By default "simplejson".
|
||||||
Metadata is a small JSON file named after the composite file.`,
|
Metadata is a small JSON file named after the composite file.`,
|
||||||
@@ -231,37 +220,12 @@ It has the following fields: ver, size, nchunks, md5, sha1.`,
|
|||||||
Help: "Warn user, skip incomplete file and proceed.",
|
Help: "Warn user, skip incomplete file and proceed.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, {
|
|
||||||
Name: "transactions",
|
|
||||||
Advanced: true,
|
|
||||||
Default: "rename",
|
|
||||||
Help: `Choose how chunker should handle temporary files during transactions.`,
|
|
||||||
Hide: fs.OptionHideCommandLine,
|
|
||||||
Examples: []fs.OptionExample{
|
|
||||||
{
|
|
||||||
Value: "rename",
|
|
||||||
Help: "Rename temporary files after a successful transaction.",
|
|
||||||
}, {
|
|
||||||
Value: "norename",
|
|
||||||
Help: `Leave temporary file names and write transaction ID to metadata file.
|
|
||||||
Metadata is required for no rename transactions (meta format cannot be "none").
|
|
||||||
If you are using norename transactions you should be careful not to downgrade Rclone
|
|
||||||
as older versions of Rclone don't support this transaction style and will misinterpret
|
|
||||||
files manipulated by norename transactions.
|
|
||||||
This method is EXPERIMENTAL, don't use on production systems.`,
|
|
||||||
}, {
|
|
||||||
Value: "auto",
|
|
||||||
Help: `Rename or norename will be used depending on capabilities of the backend.
|
|
||||||
If meta format is set to "none", rename transactions will always be used.
|
|
||||||
This method is EXPERIMENTAL, don't use on production systems.`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}},
|
}},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewFs constructs an Fs from the path, container:path
|
// NewFs constructs an Fs from the path, container:path
|
||||||
func NewFs(ctx context.Context, name, rpath string, m configmap.Mapper) (fs.Fs, error) {
|
func NewFs(name, rpath string, m configmap.Mapper) (fs.Fs, error) {
|
||||||
// Parse config into Options struct
|
// Parse config into Options struct
|
||||||
opt := new(Options)
|
opt := new(Options)
|
||||||
err := configstruct.Set(m, opt)
|
err := configstruct.Set(m, opt)
|
||||||
@@ -277,18 +241,21 @@ func NewFs(ctx context.Context, name, rpath string, m configmap.Mapper) (fs.Fs,
|
|||||||
return nil, errors.New("can't point remote at itself - check the value of the remote setting")
|
return nil, errors.New("can't point remote at itself - check the value of the remote setting")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseName, basePath, err := fspath.SplitFs(remote)
|
baseName, basePath, err := fspath.Parse(remote)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(err, "failed to parse remote %q to wrap", remote)
|
return nil, errors.Wrapf(err, "failed to parse remote %q to wrap", remote)
|
||||||
}
|
}
|
||||||
|
if baseName != "" {
|
||||||
|
baseName += ":"
|
||||||
|
}
|
||||||
// Look for a file first
|
// Look for a file first
|
||||||
remotePath := fspath.JoinRootPath(basePath, rpath)
|
remotePath := fspath.JoinRootPath(basePath, rpath)
|
||||||
baseFs, err := cache.Get(ctx, baseName+remotePath)
|
baseFs, err := cache.Get(baseName + remotePath)
|
||||||
if err != fs.ErrorIsFile && err != nil {
|
if err != fs.ErrorIsFile && err != nil {
|
||||||
return nil, errors.Wrapf(err, "failed to make remote %q to wrap", baseName+remotePath)
|
return nil, errors.Wrapf(err, "failed to make remote %q to wrap", baseName+remotePath)
|
||||||
}
|
}
|
||||||
if !operations.CanServerSideMove(baseFs) {
|
if !operations.CanServerSideMove(baseFs) {
|
||||||
return nil, errors.New("can't use chunker on a backend which doesn't support server-side move or copy")
|
return nil, errors.New("can't use chunker on a backend which doesn't support server side move or copy")
|
||||||
}
|
}
|
||||||
|
|
||||||
f := &Fs{
|
f := &Fs{
|
||||||
@@ -300,7 +267,7 @@ func NewFs(ctx context.Context, name, rpath string, m configmap.Mapper) (fs.Fs,
|
|||||||
cache.PinUntilFinalized(f.base, f)
|
cache.PinUntilFinalized(f.base, f)
|
||||||
f.dirSort = true // processEntries requires that meta Objects prerun data chunks atm.
|
f.dirSort = true // processEntries requires that meta Objects prerun data chunks atm.
|
||||||
|
|
||||||
if err := f.configure(opt.NameFormat, opt.MetaFormat, opt.HashType, opt.Transactions); err != nil {
|
if err := f.configure(opt.NameFormat, opt.MetaFormat, opt.HashType); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,7 +278,7 @@ func NewFs(ctx context.Context, name, rpath string, m configmap.Mapper) (fs.Fs,
|
|||||||
// (yet can't satisfy fstest.CheckListing, will ignore)
|
// (yet can't satisfy fstest.CheckListing, will ignore)
|
||||||
if err == nil && !f.useMeta && strings.Contains(rpath, "/") {
|
if err == nil && !f.useMeta && strings.Contains(rpath, "/") {
|
||||||
firstChunkPath := f.makeChunkName(remotePath, 0, "", "")
|
firstChunkPath := f.makeChunkName(remotePath, 0, "", "")
|
||||||
_, testErr := cache.Get(ctx, baseName+firstChunkPath)
|
_, testErr := cache.Get(baseName + firstChunkPath)
|
||||||
if testErr == fs.ErrorIsFile {
|
if testErr == fs.ErrorIsFile {
|
||||||
err = testErr
|
err = testErr
|
||||||
}
|
}
|
||||||
@@ -324,12 +291,12 @@ func NewFs(ctx context.Context, name, rpath string, m configmap.Mapper) (fs.Fs,
|
|||||||
f.features = (&fs.Features{
|
f.features = (&fs.Features{
|
||||||
CaseInsensitive: true,
|
CaseInsensitive: true,
|
||||||
DuplicateFiles: true,
|
DuplicateFiles: true,
|
||||||
ReadMimeType: false, // Object.MimeType not supported
|
ReadMimeType: true,
|
||||||
WriteMimeType: true,
|
WriteMimeType: true,
|
||||||
BucketBased: true,
|
BucketBased: true,
|
||||||
CanHaveEmptyDirectories: true,
|
CanHaveEmptyDirectories: true,
|
||||||
ServerSideAcrossConfigs: true,
|
ServerSideAcrossConfigs: true,
|
||||||
}).Fill(ctx, f).Mask(ctx, baseFs).WrapsFs(f, baseFs)
|
}).Fill(f).Mask(baseFs).WrapsFs(f, baseFs)
|
||||||
|
|
||||||
f.features.Disable("ListR") // Recursive listing may cause chunker skip files
|
f.features.Disable("ListR") // Recursive listing may cause chunker skip files
|
||||||
|
|
||||||
@@ -345,7 +312,6 @@ type Options struct {
|
|||||||
MetaFormat string `config:"meta_format"`
|
MetaFormat string `config:"meta_format"`
|
||||||
HashType string `config:"hash_type"`
|
HashType string `config:"hash_type"`
|
||||||
FailHard bool `config:"fail_hard"`
|
FailHard bool `config:"fail_hard"`
|
||||||
Transactions string `config:"transactions"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fs represents a wrapped fs.Fs
|
// Fs represents a wrapped fs.Fs
|
||||||
@@ -367,13 +333,12 @@ type Fs struct {
|
|||||||
opt Options // copy of Options
|
opt Options // copy of Options
|
||||||
features *fs.Features // optional features
|
features *fs.Features // optional features
|
||||||
dirSort bool // reserved for future, ignored
|
dirSort bool // reserved for future, ignored
|
||||||
useNoRename bool // can be set with the transactions option
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// configure sets up chunker for given name format, meta format and hash type.
|
// configure sets up chunker for given name format, meta format and hash type.
|
||||||
// It also seeds the source of random transaction identifiers.
|
// It also seeds the source of random transaction identifiers.
|
||||||
// configure must be called only from NewFs or by unit tests.
|
// configure must be called only from NewFs or by unit tests.
|
||||||
func (f *Fs) configure(nameFormat, metaFormat, hashType, transactionMode string) error {
|
func (f *Fs) configure(nameFormat, metaFormat, hashType string) error {
|
||||||
if err := f.setChunkNameFormat(nameFormat); err != nil {
|
if err := f.setChunkNameFormat(nameFormat); err != nil {
|
||||||
return errors.Wrapf(err, "invalid name format '%s'", nameFormat)
|
return errors.Wrapf(err, "invalid name format '%s'", nameFormat)
|
||||||
}
|
}
|
||||||
@@ -383,9 +348,6 @@ func (f *Fs) configure(nameFormat, metaFormat, hashType, transactionMode string)
|
|||||||
if err := f.setHashType(hashType); err != nil {
|
if err := f.setHashType(hashType); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := f.setTransactionMode(transactionMode); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
randomSeed := time.Now().UnixNano()
|
randomSeed := time.Now().UnixNano()
|
||||||
f.xactIDRand = rand.New(rand.NewSource(randomSeed))
|
f.xactIDRand = rand.New(rand.NewSource(randomSeed))
|
||||||
@@ -445,27 +407,6 @@ func (f *Fs) setHashType(hashType string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fs) setTransactionMode(transactionMode string) error {
|
|
||||||
switch transactionMode {
|
|
||||||
case "rename":
|
|
||||||
f.useNoRename = false
|
|
||||||
case "norename":
|
|
||||||
if !f.useMeta {
|
|
||||||
return errors.New("incompatible transaction options")
|
|
||||||
}
|
|
||||||
f.useNoRename = true
|
|
||||||
case "auto":
|
|
||||||
f.useNoRename = !f.CanQuickRename()
|
|
||||||
if f.useNoRename && !f.useMeta {
|
|
||||||
f.useNoRename = false
|
|
||||||
return errors.New("using norename transactions requires metadata")
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unsupported transaction mode '%s'", transactionMode)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// setChunkNameFormat converts pattern based chunk name format
|
// setChunkNameFormat converts pattern based chunk name format
|
||||||
// into Printf format and Regular expressions for data and
|
// into Printf format and Regular expressions for data and
|
||||||
// control chunks.
|
// control chunks.
|
||||||
@@ -525,7 +466,7 @@ func (f *Fs) setChunkNameFormat(pattern string) error {
|
|||||||
// filePath can be name, relative or absolute path of main file.
|
// filePath can be name, relative or absolute path of main file.
|
||||||
//
|
//
|
||||||
// chunkNo must be a zero based index of data chunk.
|
// chunkNo must be a zero based index of data chunk.
|
||||||
// Negative chunkNo e.g. -1 indicates a control chunk.
|
// Negative chunkNo eg. -1 indicates a control chunk.
|
||||||
// ctrlType is type of control chunk (must be valid).
|
// ctrlType is type of control chunk (must be valid).
|
||||||
// ctrlType must be "" for data chunks.
|
// ctrlType must be "" for data chunks.
|
||||||
//
|
//
|
||||||
@@ -748,7 +689,6 @@ func (f *Fs) processEntries(ctx context.Context, origEntries fs.DirEntries, dirP
|
|||||||
byRemote := make(map[string]*Object)
|
byRemote := make(map[string]*Object)
|
||||||
badEntry := make(map[string]bool)
|
badEntry := make(map[string]bool)
|
||||||
isSubdir := make(map[string]bool)
|
isSubdir := make(map[string]bool)
|
||||||
txnByRemote := map[string]string{}
|
|
||||||
|
|
||||||
var tempEntries fs.DirEntries
|
var tempEntries fs.DirEntries
|
||||||
for _, dirOrObject := range sortedEntries {
|
for _, dirOrObject := range sortedEntries {
|
||||||
@@ -761,18 +701,12 @@ func (f *Fs) processEntries(ctx context.Context, origEntries fs.DirEntries, dirP
|
|||||||
object := f.newObject("", entry, nil)
|
object := f.newObject("", entry, nil)
|
||||||
byRemote[remote] = object
|
byRemote[remote] = object
|
||||||
tempEntries = append(tempEntries, object)
|
tempEntries = append(tempEntries, object)
|
||||||
if f.useNoRename {
|
|
||||||
txnByRemote[remote], err = object.readXactID(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
// this is some kind of chunk
|
// this is some kind of chunk
|
||||||
// metobject should have been created above if present
|
// metobject should have been created above if present
|
||||||
|
isSpecial := xactID != "" || ctrlType != ""
|
||||||
mainObject := byRemote[mainRemote]
|
mainObject := byRemote[mainRemote]
|
||||||
isSpecial := xactID != txnByRemote[mainRemote] || ctrlType != ""
|
|
||||||
if mainObject == nil && f.useMeta && !isSpecial {
|
if mainObject == nil && f.useMeta && !isSpecial {
|
||||||
fs.Debugf(f, "skip orphan data chunk %q", remote)
|
fs.Debugf(f, "skip orphan data chunk %q", remote)
|
||||||
break
|
break
|
||||||
@@ -791,9 +725,6 @@ func (f *Fs) processEntries(ctx context.Context, origEntries fs.DirEntries, dirP
|
|||||||
fs.Infof(f, "ignore non-data chunk %q", remote)
|
fs.Infof(f, "ignore non-data chunk %q", remote)
|
||||||
}
|
}
|
||||||
// need to read metadata to ensure actual object type
|
// need to read metadata to ensure actual object type
|
||||||
// no need to read if metaobject is too big or absent,
|
|
||||||
// use the fact that before calling validate()
|
|
||||||
// the `size` field caches metaobject size, if any
|
|
||||||
if f.useMeta && mainObject != nil && mainObject.size <= maxMetadataSize {
|
if f.useMeta && mainObject != nil && mainObject.size <= maxMetadataSize {
|
||||||
mainObject.unsure = true
|
mainObject.unsure = true
|
||||||
}
|
}
|
||||||
@@ -873,9 +804,7 @@ func (f *Fs) scanObject(ctx context.Context, remote string, quickScan bool) (fs.
|
|||||||
var (
|
var (
|
||||||
o *Object
|
o *Object
|
||||||
baseObj fs.Object
|
baseObj fs.Object
|
||||||
currentXactID string
|
|
||||||
err error
|
err error
|
||||||
sameMain bool
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if f.useMeta {
|
if f.useMeta {
|
||||||
@@ -889,7 +818,6 @@ func (f *Fs) scanObject(ctx context.Context, remote string, quickScan bool) (fs.
|
|||||||
// as a hard limit. Anything larger than that is treated as a
|
// as a hard limit. Anything larger than that is treated as a
|
||||||
// non-chunked file without even checking its contents, so it's
|
// non-chunked file without even checking its contents, so it's
|
||||||
// paramount to prevent metadata from exceeding the maximum size.
|
// paramount to prevent metadata from exceeding the maximum size.
|
||||||
// Anything smaller is additionally checked for format.
|
|
||||||
o = f.newObject("", baseObj, nil)
|
o = f.newObject("", baseObj, nil)
|
||||||
if o.size > maxMetadataSize {
|
if o.size > maxMetadataSize {
|
||||||
return o, nil
|
return o, nil
|
||||||
@@ -919,36 +847,20 @@ func (f *Fs) scanObject(ctx context.Context, remote string, quickScan bool) (fs.
|
|||||||
return nil, errors.Wrap(err, "can't detect composite file")
|
return nil, errors.Wrap(err, "can't detect composite file")
|
||||||
}
|
}
|
||||||
|
|
||||||
if f.useNoRename {
|
|
||||||
currentXactID, err = o.readXactID(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
caseInsensitive := f.features.CaseInsensitive
|
|
||||||
|
|
||||||
for _, dirOrObject := range entries {
|
for _, dirOrObject := range entries {
|
||||||
entry, ok := dirOrObject.(fs.Object)
|
entry, ok := dirOrObject.(fs.Object)
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
entryRemote := entry.Remote()
|
entryRemote := entry.Remote()
|
||||||
if !caseInsensitive && !strings.Contains(entryRemote, remote) {
|
if !strings.Contains(entryRemote, remote) {
|
||||||
continue // bypass regexp to save cpu
|
continue // bypass regexp to save cpu
|
||||||
}
|
}
|
||||||
mainRemote, chunkNo, ctrlType, xactID := f.parseChunkName(entryRemote)
|
mainRemote, chunkNo, ctrlType, xactID := f.parseChunkName(entryRemote)
|
||||||
if mainRemote == "" {
|
if mainRemote == "" || mainRemote != remote {
|
||||||
continue // skip non-chunks
|
continue // skip non-conforming chunks
|
||||||
}
|
}
|
||||||
if caseInsensitive {
|
if ctrlType != "" || xactID != "" {
|
||||||
sameMain = strings.EqualFold(mainRemote, remote)
|
|
||||||
} else {
|
|
||||||
sameMain = mainRemote == remote
|
|
||||||
}
|
|
||||||
if !sameMain {
|
|
||||||
continue // skip alien chunks
|
|
||||||
}
|
|
||||||
if ctrlType != "" || xactID != currentXactID {
|
|
||||||
if f.useMeta {
|
if f.useMeta {
|
||||||
// temporary/control chunk calls for lazy metadata read
|
// temporary/control chunk calls for lazy metadata read
|
||||||
o.unsure = true
|
o.unsure = true
|
||||||
@@ -994,22 +906,11 @@ func (f *Fs) scanObject(ctx context.Context, remote string, quickScan bool) (fs.
|
|||||||
return o, nil
|
return o, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// readMetadata reads composite object metadata and caches results,
|
|
||||||
// in case of critical errors metadata is not cached.
|
|
||||||
// Returns ErrMetaUnknown if an unsupported metadata format is detected.
|
|
||||||
// If object is not chunked but marked by List or NewObject for recheck,
|
|
||||||
// readMetadata will attempt to parse object as composite with fallback
|
|
||||||
// to non-chunked representation if the attempt fails.
|
|
||||||
func (o *Object) readMetadata(ctx context.Context) error {
|
func (o *Object) readMetadata(ctx context.Context) error {
|
||||||
// return quickly if metadata is absent or has been already cached
|
|
||||||
if !o.f.useMeta {
|
|
||||||
o.isFull = true
|
|
||||||
}
|
|
||||||
if o.isFull {
|
if o.isFull {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if !o.isComposite() && !o.unsure {
|
if !o.f.useMeta || (!o.isComposite() && !o.unsure) {
|
||||||
// this for sure is a non-chunked standalone file
|
|
||||||
o.isFull = true
|
o.isFull = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -1027,7 +928,6 @@ func (o *Object) readMetadata(ctx context.Context) error {
|
|||||||
return ErrMetaTooBig
|
return ErrMetaTooBig
|
||||||
}
|
}
|
||||||
|
|
||||||
// size is within limits, perform consistency checks
|
|
||||||
reader, err := metaObject.Open(ctx)
|
reader, err := metaObject.Open(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1063,70 +963,22 @@ func (o *Object) readMetadata(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
o.md5 = metaInfo.md5
|
o.md5 = metaInfo.md5
|
||||||
o.sha1 = metaInfo.sha1
|
o.sha1 = metaInfo.sha1
|
||||||
o.xactID = metaInfo.xactID
|
|
||||||
}
|
}
|
||||||
|
|
||||||
o.isFull = true // cache results
|
o.isFull = true
|
||||||
o.xIDCached = true
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// readXactID returns the transaction ID stored in the passed metadata object
|
|
||||||
func (o *Object) readXactID(ctx context.Context) (xactID string, err error) {
|
|
||||||
// if xactID has already been read and cahced return it now
|
|
||||||
if o.xIDCached {
|
|
||||||
return o.xactID, nil
|
|
||||||
}
|
|
||||||
// Avoid reading metadata for backends that don't use xactID to identify permanent chunks
|
|
||||||
if !o.f.useNoRename {
|
|
||||||
return "", errors.New("readXactID requires norename transactions")
|
|
||||||
}
|
|
||||||
if o.main == nil {
|
|
||||||
return "", errors.New("readXactID requires valid metaobject")
|
|
||||||
}
|
|
||||||
if o.main.Size() > maxMetadataSize {
|
|
||||||
return "", nil // this was likely not a metadata object, return empty xactID but don't throw error
|
|
||||||
}
|
|
||||||
reader, err := o.main.Open(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
data, err := ioutil.ReadAll(reader)
|
|
||||||
_ = reader.Close() // ensure file handle is freed on windows
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch o.f.opt.MetaFormat {
|
|
||||||
case "simplejson":
|
|
||||||
if data != nil && len(data) > maxMetadataSizeWritten {
|
|
||||||
return "", nil // this was likely not a metadata object, return empty xactID but don't throw error
|
|
||||||
}
|
|
||||||
var metadata metaSimpleJSON
|
|
||||||
err = json.Unmarshal(data, &metadata)
|
|
||||||
if err != nil {
|
|
||||||
return "", nil // this was likely not a metadata object, return empty xactID but don't throw error
|
|
||||||
}
|
|
||||||
xactID = metadata.XactID
|
|
||||||
}
|
|
||||||
o.xactID = xactID
|
|
||||||
o.xIDCached = true
|
|
||||||
return xactID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// put implements Put, PutStream, PutUnchecked, Update
|
// put implements Put, PutStream, PutUnchecked, Update
|
||||||
func (f *Fs) put(
|
func (f *Fs) put(
|
||||||
ctx context.Context, in io.Reader, src fs.ObjectInfo, remote string, options []fs.OpenOption,
|
ctx context.Context, in io.Reader, src fs.ObjectInfo, remote string, options []fs.OpenOption,
|
||||||
basePut putFn, action string, target fs.Object) (obj fs.Object, err error) {
|
basePut putFn, action string, target fs.Object) (obj fs.Object, err error) {
|
||||||
|
|
||||||
// Perform consistency checks
|
|
||||||
if err := f.forbidChunk(src, remote); err != nil {
|
if err := f.forbidChunk(src, remote); err != nil {
|
||||||
return nil, errors.Wrap(err, action+" refused")
|
return nil, errors.Wrap(err, action+" refused")
|
||||||
}
|
}
|
||||||
if target == nil {
|
if target == nil {
|
||||||
// Get target object with a quick directory scan
|
// Get target object with a quick directory scan
|
||||||
// skip metadata check if target object does not exist.
|
|
||||||
// ignore not-chunked objects, skip chunk size checks.
|
|
||||||
if obj, err := f.scanObject(ctx, remote, true); err == nil {
|
if obj, err := f.scanObject(ctx, remote, true); err == nil {
|
||||||
target = obj
|
target = obj
|
||||||
}
|
}
|
||||||
@@ -1139,7 +991,6 @@ func (f *Fs) put(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare to upload
|
|
||||||
c := f.newChunkingReader(src)
|
c := f.newChunkingReader(src)
|
||||||
wrapIn := c.wrapStream(ctx, in, src)
|
wrapIn := c.wrapStream(ctx, in, src)
|
||||||
|
|
||||||
@@ -1210,7 +1061,7 @@ func (f *Fs) put(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Wrapped remote may or may not have seen EOF from chunking reader,
|
// Wrapped remote may or may not have seen EOF from chunking reader,
|
||||||
// e.g. the box multi-uploader reads exactly the chunk size specified
|
// eg. the box multi-uploader reads exactly the chunk size specified
|
||||||
// and skips the "EOF" read. Hence, switch to next limit here.
|
// and skips the "EOF" read. Hence, switch to next limit here.
|
||||||
if !(c.chunkLimit == 0 || c.chunkLimit == c.chunkSize || c.sizeTotal == -1 || c.done) {
|
if !(c.chunkLimit == 0 || c.chunkLimit == c.chunkSize || c.sizeTotal == -1 || c.done) {
|
||||||
silentlyRemove(ctx, chunk)
|
silentlyRemove(ctx, chunk)
|
||||||
@@ -1266,8 +1117,7 @@ func (f *Fs) put(
|
|||||||
// If previous object was chunked, remove its chunks
|
// If previous object was chunked, remove its chunks
|
||||||
f.removeOldChunks(ctx, baseRemote)
|
f.removeOldChunks(ctx, baseRemote)
|
||||||
|
|
||||||
if !f.useNoRename {
|
// Rename data chunks from temporary to final names
|
||||||
// The transaction suffix will be removed for backends with quick rename operations
|
|
||||||
for chunkNo, chunk := range c.chunks {
|
for chunkNo, chunk := range c.chunks {
|
||||||
chunkRemote := f.makeChunkName(baseRemote, chunkNo, "", "")
|
chunkRemote := f.makeChunkName(baseRemote, chunkNo, "", "")
|
||||||
chunkMoved, errMove := f.baseMove(ctx, chunk, chunkRemote, delFailed)
|
chunkMoved, errMove := f.baseMove(ctx, chunk, chunkRemote, delFailed)
|
||||||
@@ -1276,8 +1126,6 @@ func (f *Fs) put(
|
|||||||
}
|
}
|
||||||
c.chunks[chunkNo] = chunkMoved
|
c.chunks[chunkNo] = chunkMoved
|
||||||
}
|
}
|
||||||
xactID = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if !f.useMeta {
|
if !f.useMeta {
|
||||||
// Remove stale metadata, if any
|
// Remove stale metadata, if any
|
||||||
@@ -1296,7 +1144,7 @@ func (f *Fs) put(
|
|||||||
switch f.opt.MetaFormat {
|
switch f.opt.MetaFormat {
|
||||||
case "simplejson":
|
case "simplejson":
|
||||||
c.updateHashes()
|
c.updateHashes()
|
||||||
metadata, err = marshalSimpleJSON(ctx, sizeTotal, len(c.chunks), c.md5, c.sha1, xactID)
|
metadata, err = marshalSimpleJSON(ctx, sizeTotal, len(c.chunks), c.md5, c.sha1)
|
||||||
}
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
metaInfo := f.wrapInfo(src, baseRemote, int64(len(metadata)))
|
metaInfo := f.wrapInfo(src, baseRemote, int64(len(metadata)))
|
||||||
@@ -1308,7 +1156,6 @@ func (f *Fs) put(
|
|||||||
|
|
||||||
o := f.newObject("", metaObject, c.chunks)
|
o := f.newObject("", metaObject, c.chunks)
|
||||||
o.size = sizeTotal
|
o.size = sizeTotal
|
||||||
o.xactID = xactID
|
|
||||||
return o, nil
|
return o, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1350,12 +1197,6 @@ func (c *chunkingReader) wrapStream(ctx context.Context, in io.Reader, src fs.Ob
|
|||||||
|
|
||||||
switch {
|
switch {
|
||||||
case c.fs.useMD5:
|
case c.fs.useMD5:
|
||||||
srcObj := fs.UnWrapObjectInfo(src)
|
|
||||||
if srcObj != nil && srcObj.Fs().Features().SlowHash {
|
|
||||||
fs.Debugf(src, "skip slow MD5 on source file, hashing in-transit")
|
|
||||||
c.hasher = md5.New()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if c.md5, _ = src.Hash(ctx, hash.MD5); c.md5 == "" {
|
if c.md5, _ = src.Hash(ctx, hash.MD5); c.md5 == "" {
|
||||||
if c.fs.hashFallback {
|
if c.fs.hashFallback {
|
||||||
c.sha1, _ = src.Hash(ctx, hash.SHA1)
|
c.sha1, _ = src.Hash(ctx, hash.SHA1)
|
||||||
@@ -1364,12 +1205,6 @@ func (c *chunkingReader) wrapStream(ctx context.Context, in io.Reader, src fs.Ob
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
case c.fs.useSHA1:
|
case c.fs.useSHA1:
|
||||||
srcObj := fs.UnWrapObjectInfo(src)
|
|
||||||
if srcObj != nil && srcObj.Fs().Features().SlowHash {
|
|
||||||
fs.Debugf(src, "skip slow SHA1 on source file, hashing in-transit")
|
|
||||||
c.hasher = sha1.New()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if c.sha1, _ = src.Hash(ctx, hash.SHA1); c.sha1 == "" {
|
if c.sha1, _ = src.Hash(ctx, hash.SHA1); c.sha1 == "" {
|
||||||
if c.fs.hashFallback {
|
if c.fs.hashFallback {
|
||||||
c.md5, _ = src.Hash(ctx, hash.MD5)
|
c.md5, _ = src.Hash(ctx, hash.MD5)
|
||||||
@@ -1403,7 +1238,7 @@ func (c *chunkingReader) Read(buf []byte) (bytesRead int, err error) {
|
|||||||
if c.chunkLimit <= 0 {
|
if c.chunkLimit <= 0 {
|
||||||
// Chunk complete - switch to next one.
|
// Chunk complete - switch to next one.
|
||||||
// Note #1:
|
// Note #1:
|
||||||
// We might not get here because some remotes (e.g. box multi-uploader)
|
// We might not get here because some remotes (eg. box multi-uploader)
|
||||||
// read the specified size exactly and skip the concluding EOF Read.
|
// read the specified size exactly and skip the concluding EOF Read.
|
||||||
// Then a check in the put loop will kick in.
|
// Then a check in the put loop will kick in.
|
||||||
// Note #2:
|
// Note #2:
|
||||||
@@ -1442,7 +1277,7 @@ func (c *chunkingReader) accountBytes(bytesRead int64) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// dummyRead updates accounting, hashsums, etc. by simulating reads
|
// dummyRead updates accounting, hashsums etc by simulating reads
|
||||||
func (c *chunkingReader) dummyRead(in io.Reader, size int64) error {
|
func (c *chunkingReader) dummyRead(in io.Reader, size int64) error {
|
||||||
if c.hasher == nil && c.readCount+size > maxMetadataSize {
|
if c.hasher == nil && c.readCount+size > maxMetadataSize {
|
||||||
c.accountBytes(size)
|
c.accountBytes(size)
|
||||||
@@ -1594,7 +1429,7 @@ func (f *Fs) Purge(ctx context.Context, dir string) error {
|
|||||||
// However, if rclone dies unexpectedly, it can leave hidden temporary
|
// However, if rclone dies unexpectedly, it can leave hidden temporary
|
||||||
// chunks, which cannot be discovered using the `list` command.
|
// chunks, which cannot be discovered using the `list` command.
|
||||||
// Remove does not try to search for such chunks or to delete them.
|
// Remove does not try to search for such chunks or to delete them.
|
||||||
// Sometimes this can lead to strange results e.g. when `list` shows that
|
// Sometimes this can lead to strange results eg. when `list` shows that
|
||||||
// directory is empty but `rmdir` refuses to remove it because on the
|
// directory is empty but `rmdir` refuses to remove it because on the
|
||||||
// level of wrapped remote it's actually *not* empty.
|
// level of wrapped remote it's actually *not* empty.
|
||||||
// As a workaround users can use `purge` to forcibly remove it.
|
// As a workaround users can use `purge` to forcibly remove it.
|
||||||
@@ -1712,7 +1547,7 @@ func (f *Fs) copyOrMove(ctx context.Context, o *Object, remote string, do copyMo
|
|||||||
var metadata []byte
|
var metadata []byte
|
||||||
switch f.opt.MetaFormat {
|
switch f.opt.MetaFormat {
|
||||||
case "simplejson":
|
case "simplejson":
|
||||||
metadata, err = marshalSimpleJSON(ctx, newObj.size, len(newChunks), md5, sha1, o.xactID)
|
metadata, err = marshalSimpleJSON(ctx, newObj.size, len(newChunks), md5, sha1)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
metaInfo := f.wrapInfo(metaObject, "", int64(len(metadata)))
|
metaInfo := f.wrapInfo(metaObject, "", int64(len(metadata)))
|
||||||
err = newObj.main.Update(ctx, bytes.NewReader(metadata), metaInfo)
|
err = newObj.main.Update(ctx, bytes.NewReader(metadata), metaInfo)
|
||||||
@@ -1746,8 +1581,6 @@ func (f *Fs) okForServerSide(ctx context.Context, src fs.Object, opName string)
|
|||||||
diff = "chunk sizes"
|
diff = "chunk sizes"
|
||||||
case f.opt.NameFormat != obj.f.opt.NameFormat:
|
case f.opt.NameFormat != obj.f.opt.NameFormat:
|
||||||
diff = "chunk name formats"
|
diff = "chunk name formats"
|
||||||
case f.opt.StartFrom != obj.f.opt.StartFrom:
|
|
||||||
diff = "chunk numbering"
|
|
||||||
case f.opt.MetaFormat != obj.f.opt.MetaFormat:
|
case f.opt.MetaFormat != obj.f.opt.MetaFormat:
|
||||||
diff = "meta formats"
|
diff = "meta formats"
|
||||||
}
|
}
|
||||||
@@ -1791,7 +1624,7 @@ func (f *Fs) okForServerSide(ctx context.Context, src fs.Object, opName string)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy src to this remote using server-side copy operations.
|
// Copy src to this remote using server side copy operations.
|
||||||
//
|
//
|
||||||
// This is stored with the remote path given
|
// This is stored with the remote path given
|
||||||
//
|
//
|
||||||
@@ -1812,7 +1645,7 @@ func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object,
|
|||||||
return f.copyOrMove(ctx, obj, remote, baseCopy, md5, sha1, "copy")
|
return f.copyOrMove(ctx, obj, remote, baseCopy, md5, sha1, "copy")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move src to this remote using server-side move operations.
|
// Move src to this remote using server side move operations.
|
||||||
//
|
//
|
||||||
// This is stored with the remote path given
|
// This is stored with the remote path given
|
||||||
//
|
//
|
||||||
@@ -1857,7 +1690,7 @@ func (f *Fs) baseMove(ctx context.Context, src fs.Object, remote string, delMode
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DirMove moves src, srcRemote to this remote at dstRemote
|
// DirMove moves src, srcRemote to this remote at dstRemote
|
||||||
// using server-side move operations.
|
// using server side move operations.
|
||||||
//
|
//
|
||||||
// Will only be called if src.Fs().Name() == f.Name()
|
// Will only be called if src.Fs().Name() == f.Name()
|
||||||
//
|
//
|
||||||
@@ -1928,13 +1761,7 @@ func (f *Fs) ChangeNotify(ctx context.Context, notifyFunc func(string, fs.EntryT
|
|||||||
//fs.Debugf(f, "ChangeNotify: path %q entryType %d", path, entryType)
|
//fs.Debugf(f, "ChangeNotify: path %q entryType %d", path, entryType)
|
||||||
if entryType == fs.EntryObject {
|
if entryType == fs.EntryObject {
|
||||||
mainPath, _, _, xactID := f.parseChunkName(path)
|
mainPath, _, _, xactID := f.parseChunkName(path)
|
||||||
metaXactID := ""
|
if mainPath != "" && xactID == "" {
|
||||||
if f.useNoRename {
|
|
||||||
metaObject, _ := f.base.NewObject(ctx, mainPath)
|
|
||||||
dummyObject := f.newObject("", metaObject, nil)
|
|
||||||
metaXactID, _ = dummyObject.readXactID(ctx)
|
|
||||||
}
|
|
||||||
if mainPath != "" && xactID == metaXactID {
|
|
||||||
path = mainPath
|
path = mainPath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1943,16 +1770,6 @@ func (f *Fs) ChangeNotify(ctx context.Context, notifyFunc func(string, fs.EntryT
|
|||||||
do(ctx, wrappedNotifyFunc, pollIntervalChan)
|
do(ctx, wrappedNotifyFunc, pollIntervalChan)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shutdown the backend, closing any background tasks and any
|
|
||||||
// cached connections.
|
|
||||||
func (f *Fs) Shutdown(ctx context.Context) error {
|
|
||||||
do := f.base.Features().Shutdown
|
|
||||||
if do == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return do(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Object represents a composite file wrapping one or more data chunks
|
// Object represents a composite file wrapping one or more data chunks
|
||||||
type Object struct {
|
type Object struct {
|
||||||
remote string
|
remote string
|
||||||
@@ -1960,9 +1777,7 @@ type Object struct {
|
|||||||
chunks []fs.Object // active data chunks if file is composite, or wrapped file as a single chunk if meta format is 'none'
|
chunks []fs.Object // active data chunks if file is composite, or wrapped file as a single chunk if meta format is 'none'
|
||||||
size int64 // cached total size of chunks in a composite file or -1 for non-chunked files
|
size int64 // cached total size of chunks in a composite file or -1 for non-chunked files
|
||||||
isFull bool // true if metadata has been read
|
isFull bool // true if metadata has been read
|
||||||
xIDCached bool // true if xactID has been read
|
|
||||||
unsure bool // true if need to read metadata to detect object type
|
unsure bool // true if need to read metadata to detect object type
|
||||||
xactID string // transaction ID for "norename" or empty string for "renamed" chunks
|
|
||||||
md5 string
|
md5 string
|
||||||
sha1 string
|
sha1 string
|
||||||
f *Fs
|
f *Fs
|
||||||
@@ -1984,9 +1799,6 @@ func (o *Object) addChunk(chunk fs.Object, chunkNo int) error {
|
|||||||
copy(newChunks, o.chunks)
|
copy(newChunks, o.chunks)
|
||||||
o.chunks = newChunks
|
o.chunks = newChunks
|
||||||
}
|
}
|
||||||
if o.chunks[chunkNo] != nil {
|
|
||||||
return fmt.Errorf("duplicate chunk number %d", chunkNo+o.f.opt.StartFrom)
|
|
||||||
}
|
|
||||||
o.chunks[chunkNo] = chunk
|
o.chunks[chunkNo] = chunk
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -2293,7 +2105,6 @@ type ObjectInfo struct {
|
|||||||
src fs.ObjectInfo
|
src fs.ObjectInfo
|
||||||
fs *Fs
|
fs *Fs
|
||||||
nChunks int // number of data chunks
|
nChunks int // number of data chunks
|
||||||
xactID string // transaction ID for "norename" or empty string for "renamed" chunks
|
|
||||||
size int64 // overrides source size by the total size of data chunks
|
size int64 // overrides source size by the total size of data chunks
|
||||||
remote string // overrides remote name
|
remote string // overrides remote name
|
||||||
md5 string // overrides MD5 checksum
|
md5 string // overrides MD5 checksum
|
||||||
@@ -2394,7 +2205,6 @@ type metaSimpleJSON struct {
|
|||||||
// optional extra fields
|
// optional extra fields
|
||||||
MD5 string `json:"md5,omitempty"`
|
MD5 string `json:"md5,omitempty"`
|
||||||
SHA1 string `json:"sha1,omitempty"`
|
SHA1 string `json:"sha1,omitempty"`
|
||||||
XactID string `json:"txn,omitempty"` // transaction ID for norename transactions
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// marshalSimpleJSON
|
// marshalSimpleJSON
|
||||||
@@ -2404,11 +2214,8 @@ type metaSimpleJSON struct {
|
|||||||
// - if file contents can be mistaken as meta object
|
// - if file contents can be mistaken as meta object
|
||||||
// - if consistent hashing is On but wrapped remote can't provide given hash
|
// - if consistent hashing is On but wrapped remote can't provide given hash
|
||||||
//
|
//
|
||||||
func marshalSimpleJSON(ctx context.Context, size int64, nChunks int, md5, sha1, xactID string) ([]byte, error) {
|
func marshalSimpleJSON(ctx context.Context, size int64, nChunks int, md5, sha1 string) ([]byte, error) {
|
||||||
version := metadataVersion
|
version := metadataVersion
|
||||||
if xactID == "" && version == 2 {
|
|
||||||
version = 1
|
|
||||||
}
|
|
||||||
metadata := metaSimpleJSON{
|
metadata := metaSimpleJSON{
|
||||||
// required core fields
|
// required core fields
|
||||||
Version: &version,
|
Version: &version,
|
||||||
@@ -2417,20 +2224,17 @@ func marshalSimpleJSON(ctx context.Context, size int64, nChunks int, md5, sha1,
|
|||||||
// optional extra fields
|
// optional extra fields
|
||||||
MD5: md5,
|
MD5: md5,
|
||||||
SHA1: sha1,
|
SHA1: sha1,
|
||||||
XactID: xactID,
|
|
||||||
}
|
}
|
||||||
data, err := json.Marshal(&metadata)
|
data, err := json.Marshal(&metadata)
|
||||||
if err == nil && data != nil && len(data) >= maxMetadataSizeWritten {
|
if err == nil && data != nil && len(data) >= maxMetadataSize {
|
||||||
// be a nitpicker, never produce something you can't consume
|
// be a nitpicker, never produce something you can't consume
|
||||||
return nil, errors.New("metadata can't be this big, please report to rclone developers")
|
return nil, errors.New("metadata can't be this big, please report to rclone developers")
|
||||||
}
|
}
|
||||||
return data, err
|
return data, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// unmarshalSimpleJSON parses metadata.
|
// unmarshalSimpleJSON
|
||||||
//
|
//
|
||||||
// In case of errors returns a flag telling whether input has been
|
|
||||||
// produced by incompatible version of rclone vs wasn't metadata at all.
|
|
||||||
// Only metadata format version 1 is supported atm.
|
// Only metadata format version 1 is supported atm.
|
||||||
// Future releases will transparently migrate older metadata objects.
|
// Future releases will transparently migrate older metadata objects.
|
||||||
// New format will have a higher version number and cannot be correctly
|
// New format will have a higher version number and cannot be correctly
|
||||||
@@ -2440,7 +2244,7 @@ func marshalSimpleJSON(ctx context.Context, size int64, nChunks int, md5, sha1,
|
|||||||
func unmarshalSimpleJSON(ctx context.Context, metaObject fs.Object, data []byte) (info *ObjectInfo, madeByChunker bool, err error) {
|
func unmarshalSimpleJSON(ctx context.Context, metaObject fs.Object, data []byte) (info *ObjectInfo, madeByChunker bool, err error) {
|
||||||
// Be strict about JSON format
|
// Be strict about JSON format
|
||||||
// to reduce possibility that a random small file resembles metadata.
|
// to reduce possibility that a random small file resembles metadata.
|
||||||
if data != nil && len(data) > maxMetadataSizeWritten {
|
if data != nil && len(data) > maxMetadataSize {
|
||||||
return nil, false, ErrMetaTooBig
|
return nil, false, ErrMetaTooBig
|
||||||
}
|
}
|
||||||
if data == nil || len(data) < 2 || data[0] != '{' || data[len(data)-1] != '}' {
|
if data == nil || len(data) < 2 || data[0] != '{' || data[len(data)-1] != '}' {
|
||||||
@@ -2495,7 +2299,6 @@ func unmarshalSimpleJSON(ctx context.Context, metaObject fs.Object, data []byte)
|
|||||||
info.nChunks = *metadata.ChunkNum
|
info.nChunks = *metadata.ChunkNum
|
||||||
info.md5 = metadata.MD5
|
info.md5 = metadata.MD5
|
||||||
info.sha1 = metadata.SHA1
|
info.sha1 = metadata.SHA1
|
||||||
info.xactID = metadata.XactID
|
|
||||||
return info, true, nil
|
return info, true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2528,11 +2331,6 @@ func (f *Fs) Precision() time.Duration {
|
|||||||
return f.base.Precision()
|
return f.base.Precision()
|
||||||
}
|
}
|
||||||
|
|
||||||
// CanQuickRename returns true if the Fs supports a quick rename operation
|
|
||||||
func (f *Fs) CanQuickRename() bool {
|
|
||||||
return f.base.Features().Move != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the interfaces are satisfied
|
// Check the interfaces are satisfied
|
||||||
var (
|
var (
|
||||||
_ fs.Fs = (*Fs)(nil)
|
_ fs.Fs = (*Fs)(nil)
|
||||||
@@ -2548,7 +2346,6 @@ var (
|
|||||||
_ fs.Abouter = (*Fs)(nil)
|
_ fs.Abouter = (*Fs)(nil)
|
||||||
_ fs.Wrapper = (*Fs)(nil)
|
_ fs.Wrapper = (*Fs)(nil)
|
||||||
_ fs.ChangeNotifier = (*Fs)(nil)
|
_ fs.ChangeNotifier = (*Fs)(nil)
|
||||||
_ fs.Shutdowner = (*Fs)(nil)
|
|
||||||
_ fs.ObjectInfo = (*ObjectInfo)(nil)
|
_ fs.ObjectInfo = (*ObjectInfo)(nil)
|
||||||
_ fs.Object = (*Object)(nil)
|
_ fs.Object = (*Object)(nil)
|
||||||
_ fs.ObjectUnWrapper = (*Object)(nil)
|
_ fs.ObjectUnWrapper = (*Object)(nil)
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import (
|
|||||||
|
|
||||||
"github.com/rclone/rclone/fs"
|
"github.com/rclone/rclone/fs"
|
||||||
"github.com/rclone/rclone/fs/hash"
|
"github.com/rclone/rclone/fs/hash"
|
||||||
"github.com/rclone/rclone/fs/object"
|
|
||||||
"github.com/rclone/rclone/fs/operations"
|
"github.com/rclone/rclone/fs/operations"
|
||||||
"github.com/rclone/rclone/fstest"
|
"github.com/rclone/rclone/fstest"
|
||||||
"github.com/rclone/rclone/fstest/fstests"
|
"github.com/rclone/rclone/fstest/fstests"
|
||||||
@@ -468,15 +467,9 @@ func testPreventCorruption(t *testing.T, f *Fs) {
|
|||||||
return obj
|
return obj
|
||||||
}
|
}
|
||||||
billyObj := newFile("billy")
|
billyObj := newFile("billy")
|
||||||
billyTxn := billyObj.(*Object).xactID
|
|
||||||
if f.useNoRename {
|
|
||||||
require.True(t, billyTxn != "")
|
|
||||||
} else {
|
|
||||||
require.True(t, billyTxn == "")
|
|
||||||
}
|
|
||||||
|
|
||||||
billyChunkName := func(chunkNo int) string {
|
billyChunkName := func(chunkNo int) string {
|
||||||
return f.makeChunkName(billyObj.Remote(), chunkNo, "", billyTxn)
|
return f.makeChunkName(billyObj.Remote(), chunkNo, "", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
err := f.Mkdir(ctx, billyChunkName(1))
|
err := f.Mkdir(ctx, billyChunkName(1))
|
||||||
@@ -493,13 +486,11 @@ func testPreventCorruption(t *testing.T, f *Fs) {
|
|||||||
// accessing chunks in strict mode is prohibited
|
// accessing chunks in strict mode is prohibited
|
||||||
f.opt.FailHard = true
|
f.opt.FailHard = true
|
||||||
billyChunk4Name := billyChunkName(4)
|
billyChunk4Name := billyChunkName(4)
|
||||||
_, err = f.base.NewObject(ctx, billyChunk4Name)
|
billyChunk4, err := f.NewObject(ctx, billyChunk4Name)
|
||||||
require.NoError(t, err)
|
|
||||||
_, err = f.NewObject(ctx, billyChunk4Name)
|
|
||||||
assertOverlapError(err)
|
assertOverlapError(err)
|
||||||
|
|
||||||
f.opt.FailHard = false
|
f.opt.FailHard = false
|
||||||
billyChunk4, err := f.NewObject(ctx, billyChunk4Name)
|
billyChunk4, err = f.NewObject(ctx, billyChunk4Name)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
require.NotNil(t, billyChunk4)
|
require.NotNil(t, billyChunk4)
|
||||||
|
|
||||||
@@ -528,8 +519,7 @@ func testPreventCorruption(t *testing.T, f *Fs) {
|
|||||||
|
|
||||||
// recreate billy in case it was anyhow corrupted
|
// recreate billy in case it was anyhow corrupted
|
||||||
willyObj := newFile("willy")
|
willyObj := newFile("willy")
|
||||||
willyTxn := willyObj.(*Object).xactID
|
willyChunkName := f.makeChunkName(willyObj.Remote(), 1, "", "")
|
||||||
willyChunkName := f.makeChunkName(willyObj.Remote(), 1, "", willyTxn)
|
|
||||||
f.opt.FailHard = false
|
f.opt.FailHard = false
|
||||||
willyChunk, err := f.NewObject(ctx, willyChunkName)
|
willyChunk, err := f.NewObject(ctx, willyChunkName)
|
||||||
f.opt.FailHard = true
|
f.opt.FailHard = true
|
||||||
@@ -570,20 +560,17 @@ func testChunkNumberOverflow(t *testing.T, f *Fs) {
|
|||||||
modTime := fstest.Time("2001-02-03T04:05:06.499999999Z")
|
modTime := fstest.Time("2001-02-03T04:05:06.499999999Z")
|
||||||
contents := random.String(100)
|
contents := random.String(100)
|
||||||
|
|
||||||
newFile := func(f fs.Fs, name string) (obj fs.Object, filename string, txnID string) {
|
newFile := func(f fs.Fs, name string) (fs.Object, string) {
|
||||||
filename = path.Join(dir, name)
|
filename := path.Join(dir, name)
|
||||||
item := fstest.Item{Path: filename, ModTime: modTime}
|
item := fstest.Item{Path: filename, ModTime: modTime}
|
||||||
_, obj = fstests.PutTestContents(ctx, t, f, &item, contents, true)
|
_, obj := fstests.PutTestContents(ctx, t, f, &item, contents, true)
|
||||||
require.NotNil(t, obj)
|
require.NotNil(t, obj)
|
||||||
if chunkObj, isChunkObj := obj.(*Object); isChunkObj {
|
return obj, filename
|
||||||
txnID = chunkObj.xactID
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
f.opt.FailHard = false
|
f.opt.FailHard = false
|
||||||
file, fileName, fileTxn := newFile(f, "wreaker")
|
file, fileName := newFile(f, "wreaker")
|
||||||
wreak, _, _ := newFile(f.base, f.makeChunkName("wreaker", wreakNumber, "", fileTxn))
|
wreak, _ := newFile(f.base, f.makeChunkName("wreaker", wreakNumber, "", ""))
|
||||||
|
|
||||||
f.opt.FailHard = false
|
f.opt.FailHard = false
|
||||||
fstest.CheckListingWithRoot(t, f, dir, nil, nil, f.Precision())
|
fstest.CheckListingWithRoot(t, f, dir, nil, nil, f.Precision())
|
||||||
@@ -662,7 +649,7 @@ func testMetadataInput(t *testing.T, f *Fs) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
metaData, err := marshalSimpleJSON(ctx, 3, 1, "", "", "")
|
metaData, err := marshalSimpleJSON(ctx, 3, 1, "", "")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
todaysMeta := string(metaData)
|
todaysMeta := string(metaData)
|
||||||
runSubtest(todaysMeta, "today")
|
runSubtest(todaysMeta, "today")
|
||||||
@@ -676,174 +663,6 @@ func testMetadataInput(t *testing.T, f *Fs) {
|
|||||||
runSubtest(futureMeta, "future")
|
runSubtest(futureMeta, "future")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test that chunker refuses to change on objects with future/unknown metadata
|
|
||||||
func testFutureProof(t *testing.T, f *Fs) {
|
|
||||||
if f.opt.MetaFormat == "none" {
|
|
||||||
t.Skip("this test requires metadata support")
|
|
||||||
}
|
|
||||||
|
|
||||||
saveOpt := f.opt
|
|
||||||
ctx := context.Background()
|
|
||||||
f.opt.FailHard = true
|
|
||||||
const dir = "future"
|
|
||||||
const file = dir + "/test"
|
|
||||||
defer func() {
|
|
||||||
f.opt.FailHard = false
|
|
||||||
_ = operations.Purge(ctx, f.base, dir)
|
|
||||||
f.opt = saveOpt
|
|
||||||
}()
|
|
||||||
|
|
||||||
modTime := fstest.Time("2001-02-03T04:05:06.499999999Z")
|
|
||||||
putPart := func(name string, part int, data, msg string) {
|
|
||||||
if part > 0 {
|
|
||||||
name = f.makeChunkName(name, part-1, "", "")
|
|
||||||
}
|
|
||||||
item := fstest.Item{Path: name, ModTime: modTime}
|
|
||||||
_, obj := fstests.PutTestContents(ctx, t, f.base, &item, data, true)
|
|
||||||
assert.NotNil(t, obj, msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// simulate chunked object from future
|
|
||||||
meta := `{"ver":999,"nchunks":3,"size":9,"garbage":"litter","sha1":"0707f2970043f9f7c22029482db27733deaec029"}`
|
|
||||||
putPart(file, 0, meta, "metaobject")
|
|
||||||
putPart(file, 1, "abc", "chunk1")
|
|
||||||
putPart(file, 2, "def", "chunk2")
|
|
||||||
putPart(file, 3, "ghi", "chunk3")
|
|
||||||
|
|
||||||
// List should succeed
|
|
||||||
ls, err := f.List(ctx, dir)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, 1, len(ls))
|
|
||||||
assert.Equal(t, int64(9), ls[0].Size())
|
|
||||||
|
|
||||||
// NewObject should succeed
|
|
||||||
obj, err := f.NewObject(ctx, file)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, file, obj.Remote())
|
|
||||||
assert.Equal(t, int64(9), obj.Size())
|
|
||||||
|
|
||||||
// Hash must fail
|
|
||||||
_, err = obj.Hash(ctx, hash.SHA1)
|
|
||||||
assert.Equal(t, ErrMetaUnknown, err)
|
|
||||||
|
|
||||||
// Move must fail
|
|
||||||
mobj, err := operations.Move(ctx, f, nil, file+"2", obj)
|
|
||||||
assert.Nil(t, mobj)
|
|
||||||
assert.Error(t, err)
|
|
||||||
if err != nil {
|
|
||||||
assert.Contains(t, err.Error(), "please upgrade rclone")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Put must fail
|
|
||||||
oi := object.NewStaticObjectInfo(file, modTime, 3, true, nil, nil)
|
|
||||||
buf := bytes.NewBufferString("abc")
|
|
||||||
_, err = f.Put(ctx, buf, oi)
|
|
||||||
assert.Error(t, err)
|
|
||||||
|
|
||||||
// Rcat must fail
|
|
||||||
in := ioutil.NopCloser(bytes.NewBufferString("abc"))
|
|
||||||
robj, err := operations.Rcat(ctx, f, file, in, modTime)
|
|
||||||
assert.Nil(t, robj)
|
|
||||||
assert.NotNil(t, err)
|
|
||||||
if err != nil {
|
|
||||||
assert.Contains(t, err.Error(), "please upgrade rclone")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The newer method of doing transactions without renaming should still be able to correctly process chunks that were created with renaming
|
|
||||||
// If you attempt to do the inverse, however, the data chunks will be ignored causing commands to perform incorrectly
|
|
||||||
func testBackwardsCompatibility(t *testing.T, f *Fs) {
|
|
||||||
if !f.useMeta {
|
|
||||||
t.Skip("Can't do norename transactions without metadata")
|
|
||||||
}
|
|
||||||
const dir = "backcomp"
|
|
||||||
ctx := context.Background()
|
|
||||||
saveOpt := f.opt
|
|
||||||
saveUseNoRename := f.useNoRename
|
|
||||||
defer func() {
|
|
||||||
f.opt.FailHard = false
|
|
||||||
_ = operations.Purge(ctx, f.base, dir)
|
|
||||||
f.opt = saveOpt
|
|
||||||
f.useNoRename = saveUseNoRename
|
|
||||||
}()
|
|
||||||
f.opt.ChunkSize = fs.SizeSuffix(10)
|
|
||||||
|
|
||||||
modTime := fstest.Time("2001-02-03T04:05:06.499999999Z")
|
|
||||||
contents := random.String(250)
|
|
||||||
newFile := func(f fs.Fs, name string) (fs.Object, string) {
|
|
||||||
filename := path.Join(dir, name)
|
|
||||||
item := fstest.Item{Path: filename, ModTime: modTime}
|
|
||||||
_, obj := fstests.PutTestContents(ctx, t, f, &item, contents, true)
|
|
||||||
require.NotNil(t, obj)
|
|
||||||
return obj, filename
|
|
||||||
}
|
|
||||||
|
|
||||||
f.opt.FailHard = false
|
|
||||||
f.useNoRename = false
|
|
||||||
file, fileName := newFile(f, "renamefile")
|
|
||||||
|
|
||||||
f.opt.FailHard = false
|
|
||||||
item := fstest.NewItem(fileName, contents, modTime)
|
|
||||||
|
|
||||||
var items []fstest.Item
|
|
||||||
items = append(items, item)
|
|
||||||
|
|
||||||
f.useNoRename = true
|
|
||||||
fstest.CheckListingWithRoot(t, f, dir, items, nil, f.Precision())
|
|
||||||
_, err := f.NewObject(ctx, fileName)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
f.opt.FailHard = true
|
|
||||||
_, err = f.List(ctx, dir)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
f.opt.FailHard = false
|
|
||||||
_ = file.Remove(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testChunkerServerSideMove(t *testing.T, f *Fs) {
|
|
||||||
if !f.useMeta {
|
|
||||||
t.Skip("Can't test norename transactions without metadata")
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
const dir = "servermovetest"
|
|
||||||
subRemote := fmt.Sprintf("%s:%s/%s", f.Name(), f.Root(), dir)
|
|
||||||
|
|
||||||
subFs1, err := fs.NewFs(ctx, subRemote+"/subdir1")
|
|
||||||
assert.NoError(t, err)
|
|
||||||
fs1, isChunkerFs := subFs1.(*Fs)
|
|
||||||
assert.True(t, isChunkerFs)
|
|
||||||
fs1.useNoRename = false
|
|
||||||
fs1.opt.ChunkSize = fs.SizeSuffix(3)
|
|
||||||
|
|
||||||
subFs2, err := fs.NewFs(ctx, subRemote+"/subdir2")
|
|
||||||
assert.NoError(t, err)
|
|
||||||
fs2, isChunkerFs := subFs2.(*Fs)
|
|
||||||
assert.True(t, isChunkerFs)
|
|
||||||
fs2.useNoRename = true
|
|
||||||
fs2.opt.ChunkSize = fs.SizeSuffix(3)
|
|
||||||
|
|
||||||
modTime := fstest.Time("2001-02-03T04:05:06.499999999Z")
|
|
||||||
item := fstest.Item{Path: "movefile", ModTime: modTime}
|
|
||||||
contents := "abcdef"
|
|
||||||
_, file := fstests.PutTestContents(ctx, t, fs1, &item, contents, true)
|
|
||||||
|
|
||||||
dstOverwritten, _ := fs2.NewObject(ctx, "movefile")
|
|
||||||
dstFile, err := operations.Move(ctx, fs2, dstOverwritten, "movefile", file)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, int64(len(contents)), dstFile.Size())
|
|
||||||
|
|
||||||
r, err := dstFile.Open(ctx)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.NotNil(t, r)
|
|
||||||
data, err := ioutil.ReadAll(r)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, contents, string(data))
|
|
||||||
_ = r.Close()
|
|
||||||
_ = operations.Purge(ctx, f.base, dir)
|
|
||||||
}
|
|
||||||
|
|
||||||
// InternalTest dispatches all internal tests
|
// InternalTest dispatches all internal tests
|
||||||
func (f *Fs) InternalTest(t *testing.T) {
|
func (f *Fs) InternalTest(t *testing.T) {
|
||||||
t.Run("PutLarge", func(t *testing.T) {
|
t.Run("PutLarge", func(t *testing.T) {
|
||||||
@@ -867,15 +686,6 @@ func (f *Fs) InternalTest(t *testing.T) {
|
|||||||
t.Run("MetadataInput", func(t *testing.T) {
|
t.Run("MetadataInput", func(t *testing.T) {
|
||||||
testMetadataInput(t, f)
|
testMetadataInput(t, f)
|
||||||
})
|
})
|
||||||
t.Run("FutureProof", func(t *testing.T) {
|
|
||||||
testFutureProof(t, f)
|
|
||||||
})
|
|
||||||
t.Run("BackwardsCompatibility", func(t *testing.T) {
|
|
||||||
testBackwardsCompatibility(t, f)
|
|
||||||
})
|
|
||||||
t.Run("ChunkerServerSideMove", func(t *testing.T) {
|
|
||||||
testChunkerServerSideMove(t, f)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ fstests.InternalTester = (*Fs)(nil)
|
var _ fstests.InternalTester = (*Fs)(nil)
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ import (
|
|||||||
|
|
||||||
// Command line flags
|
// Command line flags
|
||||||
var (
|
var (
|
||||||
// Invalid characters are not supported by some remotes, e.g. Mailru.
|
// Invalid characters are not supported by some remotes, eg. Mailru.
|
||||||
// We enable testing with invalid characters when -remote is not set, so
|
// We enable testing with invalid characters when -remote is not set, so
|
||||||
// chunker overlays a local directory, but invalid characters are disabled
|
// chunker overlays a local directory, but invalid characters are disabled
|
||||||
// by default when -remote is set, e.g. when test_all runs backend tests.
|
// by default when -remote is set, eg. when test_all runs backend tests.
|
||||||
// You can still test with invalid characters using the below flag.
|
// You can still test with invalid characters using the below flag.
|
||||||
UseBadChars = flag.Bool("bad-chars", false, "Set to test bad characters in file names when -remote is set")
|
UseBadChars = flag.Bool("bad-chars", false, "Set to test bad characters in file names when -remote is set")
|
||||||
)
|
)
|
||||||
|
|||||||
1
backend/compress/.gitignore
vendored
1
backend/compress/.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
test
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,65 +0,0 @@
|
|||||||
// Test Crypt filesystem interface
|
|
||||||
package compress
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
_ "github.com/rclone/rclone/backend/drive"
|
|
||||||
_ "github.com/rclone/rclone/backend/local"
|
|
||||||
_ "github.com/rclone/rclone/backend/s3"
|
|
||||||
_ "github.com/rclone/rclone/backend/swift"
|
|
||||||
"github.com/rclone/rclone/fstest"
|
|
||||||
"github.com/rclone/rclone/fstest/fstests"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestIntegration runs integration tests against the remote
|
|
||||||
func TestIntegration(t *testing.T) {
|
|
||||||
opt := fstests.Opt{
|
|
||||||
RemoteName: *fstest.RemoteName,
|
|
||||||
NilObject: (*Object)(nil),
|
|
||||||
UnimplementableFsMethods: []string{
|
|
||||||
"OpenWriterAt",
|
|
||||||
"MergeDirs",
|
|
||||||
"DirCacheFlush",
|
|
||||||
"PutUnchecked",
|
|
||||||
"PutStream",
|
|
||||||
"UserInfo",
|
|
||||||
"Disconnect",
|
|
||||||
},
|
|
||||||
TiersToTest: []string{"STANDARD", "STANDARD_IA"},
|
|
||||||
UnimplementableObjectMethods: []string{}}
|
|
||||||
fstests.Run(t, &opt)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestRemoteGzip tests GZIP compression
|
|
||||||
func TestRemoteGzip(t *testing.T) {
|
|
||||||
if *fstest.RemoteName != "" {
|
|
||||||
t.Skip("Skipping as -remote set")
|
|
||||||
}
|
|
||||||
tempdir := filepath.Join(os.TempDir(), "rclone-compress-test-gzip")
|
|
||||||
name := "TestCompressGzip"
|
|
||||||
fstests.Run(t, &fstests.Opt{
|
|
||||||
RemoteName: name + ":",
|
|
||||||
NilObject: (*Object)(nil),
|
|
||||||
UnimplementableFsMethods: []string{
|
|
||||||
"OpenWriterAt",
|
|
||||||
"MergeDirs",
|
|
||||||
"DirCacheFlush",
|
|
||||||
"PutUnchecked",
|
|
||||||
"PutStream",
|
|
||||||
"UserInfo",
|
|
||||||
"Disconnect",
|
|
||||||
},
|
|
||||||
UnimplementableObjectMethods: []string{
|
|
||||||
"GetTier",
|
|
||||||
"SetTier",
|
|
||||||
},
|
|
||||||
ExtraConfig: []fstests.ExtraConfigItem{
|
|
||||||
{Name: name, Key: "type", Value: "compress"},
|
|
||||||
{Name: name, Key: "remote", Value: tempdir},
|
|
||||||
{Name: name, Key: "compression_mode", Value: "gzip"},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -147,7 +147,7 @@ func newCipher(mode NameEncryptionMode, password, salt string, dirNameEncrypt bo
|
|||||||
// If salt is "" we use a fixed salt just to make attackers lives
|
// If salt is "" we use a fixed salt just to make attackers lives
|
||||||
// slighty harder than using no salt.
|
// slighty harder than using no salt.
|
||||||
//
|
//
|
||||||
// Note that empty password makes all 0x00 keys which is used in the
|
// Note that empty passsword makes all 0x00 keys which is used in the
|
||||||
// tests.
|
// tests.
|
||||||
func (c *Cipher) Key(password, salt string) (err error) {
|
func (c *Cipher) Key(password, salt string) (err error) {
|
||||||
const keySize = len(c.dataKey) + len(c.nameKey) + len(c.nameTweak)
|
const keySize = len(c.dataKey) + len(c.nameKey) + len(c.nameTweak)
|
||||||
@@ -633,8 +633,11 @@ func (fh *encrypter) Read(p []byte) (n int, err error) {
|
|||||||
}
|
}
|
||||||
// possibly err != nil here, but we will process the
|
// possibly err != nil here, but we will process the
|
||||||
// data and the next call to ReadFull will return 0, err
|
// data and the next call to ReadFull will return 0, err
|
||||||
|
// Write nonce to start of block
|
||||||
|
copy(fh.buf, fh.nonce[:])
|
||||||
// Encrypt the block using the nonce
|
// Encrypt the block using the nonce
|
||||||
secretbox.Seal(fh.buf[:0], readBuf[:n], fh.nonce.pointer(), &fh.c.dataKey)
|
block := fh.buf
|
||||||
|
secretbox.Seal(block[:0], readBuf[:n], fh.nonce.pointer(), &fh.c.dataKey)
|
||||||
fh.bufIndex = 0
|
fh.bufIndex = 0
|
||||||
fh.bufSize = blockHeaderSize + n
|
fh.bufSize = blockHeaderSize + n
|
||||||
fh.nonce.increment()
|
fh.nonce.increment()
|
||||||
@@ -779,7 +782,8 @@ func (fh *decrypter) fillBuffer() (err error) {
|
|||||||
return ErrorEncryptedFileBadHeader
|
return ErrorEncryptedFileBadHeader
|
||||||
}
|
}
|
||||||
// Decrypt the block using the nonce
|
// Decrypt the block using the nonce
|
||||||
_, ok := secretbox.Open(fh.buf[:0], readBuf[:n], fh.nonce.pointer(), &fh.c.dataKey)
|
block := fh.buf
|
||||||
|
_, ok := secretbox.Open(block[:0], readBuf[:n], fh.nonce.pointer(), &fh.c.dataKey)
|
||||||
if !ok {
|
if !ok {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err // return pending error as it is likely more accurate
|
return err // return pending error as it is likely more accurate
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ func init() {
|
|||||||
CommandHelp: commandHelp,
|
CommandHelp: commandHelp,
|
||||||
Options: []fs.Option{{
|
Options: []fs.Option{{
|
||||||
Name: "remote",
|
Name: "remote",
|
||||||
Help: "Remote to encrypt/decrypt.\nNormally should contain a ':' and a path, e.g. \"myremote:path/to/dir\",\n\"myremote:bucket\" or maybe \"myremote:\" (not recommended).",
|
Help: "Remote to encrypt/decrypt.\nNormally should contain a ':' and a path, eg \"myremote:path/to/dir\",\n\"myremote:bucket\" or maybe \"myremote:\" (not recommended).",
|
||||||
Required: true,
|
Required: true,
|
||||||
}, {
|
}, {
|
||||||
Name: "filename_encryption",
|
Name: "filename_encryption",
|
||||||
@@ -76,7 +76,7 @@ NB If filename_encryption is "off" then this option will do nothing.`,
|
|||||||
}, {
|
}, {
|
||||||
Name: "server_side_across_configs",
|
Name: "server_side_across_configs",
|
||||||
Default: false,
|
Default: false,
|
||||||
Help: `Allow server-side operations (e.g. copy) to work across different crypt configs.
|
Help: `Allow server side operations (eg copy) to work across different crypt configs.
|
||||||
|
|
||||||
Normally this option is not what you want, but if you have two crypts
|
Normally this option is not what you want, but if you have two crypts
|
||||||
pointing to the same backend you can use it.
|
pointing to the same backend you can use it.
|
||||||
@@ -101,21 +101,6 @@ names, or for debugging purposes.`,
|
|||||||
Default: false,
|
Default: false,
|
||||||
Hide: fs.OptionHideConfigurator,
|
Hide: fs.OptionHideConfigurator,
|
||||||
Advanced: true,
|
Advanced: true,
|
||||||
}, {
|
|
||||||
Name: "no_data_encryption",
|
|
||||||
Help: "Option to either encrypt file data or leave it unencrypted.",
|
|
||||||
Default: false,
|
|
||||||
Advanced: true,
|
|
||||||
Examples: []fs.OptionExample{
|
|
||||||
{
|
|
||||||
Value: "true",
|
|
||||||
Help: "Don't encrypt file data, leave it unencrypted.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value: "false",
|
|
||||||
Help: "Encrypt file data.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}},
|
}},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -159,7 +144,7 @@ func NewCipher(m configmap.Mapper) (*Cipher, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewFs constructs an Fs from the path, container:path
|
// NewFs constructs an Fs from the path, container:path
|
||||||
func NewFs(ctx context.Context, name, rpath string, m configmap.Mapper) (fs.Fs, error) {
|
func NewFs(name, rpath string, m configmap.Mapper) (fs.Fs, error) {
|
||||||
// Parse config into Options struct
|
// Parse config into Options struct
|
||||||
opt := new(Options)
|
opt := new(Options)
|
||||||
err := configstruct.Set(m, opt)
|
err := configstruct.Set(m, opt)
|
||||||
@@ -174,21 +159,21 @@ func NewFs(ctx context.Context, name, rpath string, m configmap.Mapper) (fs.Fs,
|
|||||||
if strings.HasPrefix(remote, name+":") {
|
if strings.HasPrefix(remote, name+":") {
|
||||||
return nil, errors.New("can't point crypt remote at itself - check the value of the remote setting")
|
return nil, errors.New("can't point crypt remote at itself - check the value of the remote setting")
|
||||||
}
|
}
|
||||||
// Make sure to remove trailing . referring to the current dir
|
// Make sure to remove trailing . reffering to the current dir
|
||||||
if path.Base(rpath) == "." {
|
if path.Base(rpath) == "." {
|
||||||
rpath = strings.TrimSuffix(rpath, ".")
|
rpath = strings.TrimSuffix(rpath, ".")
|
||||||
}
|
}
|
||||||
// Look for a file first
|
// Look for a file first
|
||||||
var wrappedFs fs.Fs
|
var wrappedFs fs.Fs
|
||||||
if rpath == "" {
|
if rpath == "" {
|
||||||
wrappedFs, err = cache.Get(ctx, remote)
|
wrappedFs, err = cache.Get(remote)
|
||||||
} else {
|
} else {
|
||||||
remotePath := fspath.JoinRootPath(remote, cipher.EncryptFileName(rpath))
|
remotePath := fspath.JoinRootPath(remote, cipher.EncryptFileName(rpath))
|
||||||
wrappedFs, err = cache.Get(ctx, remotePath)
|
wrappedFs, err = cache.Get(remotePath)
|
||||||
// if that didn't produce a file, look for a directory
|
// if that didn't produce a file, look for a directory
|
||||||
if err != fs.ErrorIsFile {
|
if err != fs.ErrorIsFile {
|
||||||
remotePath = fspath.JoinRootPath(remote, cipher.EncryptDirName(rpath))
|
remotePath = fspath.JoinRootPath(remote, cipher.EncryptDirName(rpath))
|
||||||
wrappedFs, err = cache.Get(ctx, remotePath)
|
wrappedFs, err = cache.Get(remotePath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err != fs.ErrorIsFile && err != nil {
|
if err != fs.ErrorIsFile && err != nil {
|
||||||
@@ -214,7 +199,7 @@ func NewFs(ctx context.Context, name, rpath string, m configmap.Mapper) (fs.Fs,
|
|||||||
SetTier: true,
|
SetTier: true,
|
||||||
GetTier: true,
|
GetTier: true,
|
||||||
ServerSideAcrossConfigs: opt.ServerSideAcrossConfigs,
|
ServerSideAcrossConfigs: opt.ServerSideAcrossConfigs,
|
||||||
}).Fill(ctx, f).Mask(ctx, wrappedFs).WrapsFs(f, wrappedFs)
|
}).Fill(f).Mask(wrappedFs).WrapsFs(f, wrappedFs)
|
||||||
|
|
||||||
return f, err
|
return f, err
|
||||||
}
|
}
|
||||||
@@ -224,7 +209,6 @@ type Options struct {
|
|||||||
Remote string `config:"remote"`
|
Remote string `config:"remote"`
|
||||||
FilenameEncryption string `config:"filename_encryption"`
|
FilenameEncryption string `config:"filename_encryption"`
|
||||||
DirectoryNameEncryption bool `config:"directory_name_encryption"`
|
DirectoryNameEncryption bool `config:"directory_name_encryption"`
|
||||||
NoDataEncryption bool `config:"no_data_encryption"`
|
|
||||||
Password string `config:"password"`
|
Password string `config:"password"`
|
||||||
Password2 string `config:"password2"`
|
Password2 string `config:"password2"`
|
||||||
ServerSideAcrossConfigs bool `config:"server_side_across_configs"`
|
ServerSideAcrossConfigs bool `config:"server_side_across_configs"`
|
||||||
@@ -362,10 +346,6 @@ type putFn func(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ..
|
|||||||
|
|
||||||
// put implements Put or PutStream
|
// put implements Put or PutStream
|
||||||
func (f *Fs) put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options []fs.OpenOption, put putFn) (fs.Object, error) {
|
func (f *Fs) put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options []fs.OpenOption, put putFn) (fs.Object, error) {
|
||||||
if f.opt.NoDataEncryption {
|
|
||||||
return put(ctx, in, f.newObjectInfo(src, nonce{}), options...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encrypt the data into wrappedIn
|
// Encrypt the data into wrappedIn
|
||||||
wrappedIn, encrypter, err := f.cipher.encryptData(in)
|
wrappedIn, encrypter, err := f.cipher.encryptData(in)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -404,8 +384,7 @@ func (f *Fs) put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options [
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to read destination hash")
|
return nil, errors.Wrap(err, "failed to read destination hash")
|
||||||
}
|
}
|
||||||
if srcHash != "" && dstHash != "" {
|
if srcHash != "" && dstHash != "" && srcHash != dstHash {
|
||||||
if srcHash != dstHash {
|
|
||||||
// remove object
|
// remove object
|
||||||
err = o.Remove(ctx)
|
err = o.Remove(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -413,8 +392,6 @@ func (f *Fs) put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options [
|
|||||||
}
|
}
|
||||||
return nil, errors.Errorf("corrupted on transfer: %v crypted hash differ %q vs %q", ht, srcHash, dstHash)
|
return nil, errors.Errorf("corrupted on transfer: %v crypted hash differ %q vs %q", ht, srcHash, dstHash)
|
||||||
}
|
}
|
||||||
fs.Debugf(src, "%v = %s OK", ht, srcHash)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return f.newObject(o), nil
|
return f.newObject(o), nil
|
||||||
@@ -467,7 +444,7 @@ func (f *Fs) Purge(ctx context.Context, dir string) error {
|
|||||||
return do(ctx, f.cipher.EncryptDirName(dir))
|
return do(ctx, f.cipher.EncryptDirName(dir))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy src to this remote using server-side copy operations.
|
// Copy src to this remote using server side copy operations.
|
||||||
//
|
//
|
||||||
// This is stored with the remote path given
|
// This is stored with the remote path given
|
||||||
//
|
//
|
||||||
@@ -492,7 +469,7 @@ func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object,
|
|||||||
return f.newObject(oResult), nil
|
return f.newObject(oResult), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move src to this remote using server-side move operations.
|
// Move src to this remote using server side move operations.
|
||||||
//
|
//
|
||||||
// This is stored with the remote path given
|
// This is stored with the remote path given
|
||||||
//
|
//
|
||||||
@@ -518,7 +495,7 @@ func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DirMove moves src, srcRemote to this remote at dstRemote
|
// DirMove moves src, srcRemote to this remote at dstRemote
|
||||||
// using server-side move operations.
|
// using server side move operations.
|
||||||
//
|
//
|
||||||
// Will only be called if src.Fs().Name() == f.Name()
|
// Will only be called if src.Fs().Name() == f.Name()
|
||||||
//
|
//
|
||||||
@@ -640,10 +617,6 @@ func (f *Fs) computeHashWithNonce(ctx context.Context, nonce nonce, src fs.Objec
|
|||||||
//
|
//
|
||||||
// Note that we break lots of encapsulation in this function.
|
// Note that we break lots of encapsulation in this function.
|
||||||
func (f *Fs) ComputeHash(ctx context.Context, o *Object, src fs.Object, hashType hash.Type) (hashStr string, err error) {
|
func (f *Fs) ComputeHash(ctx context.Context, o *Object, src fs.Object, hashType hash.Type) (hashStr string, err error) {
|
||||||
if f.opt.NoDataEncryption {
|
|
||||||
return src.Hash(ctx, hashType)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read the nonce - opening the file is sufficient to read the nonce in
|
// Read the nonce - opening the file is sufficient to read the nonce in
|
||||||
// use a limited read so we only read the header
|
// use a limited read so we only read the header
|
||||||
in, err := o.Object.Open(ctx, &fs.RangeOption{Start: 0, End: int64(fileHeaderSize) - 1})
|
in, err := o.Object.Open(ctx, &fs.RangeOption{Start: 0, End: int64(fileHeaderSize) - 1})
|
||||||
@@ -849,14 +822,10 @@ func (o *Object) Remote() string {
|
|||||||
|
|
||||||
// Size returns the size of the file
|
// Size returns the size of the file
|
||||||
func (o *Object) Size() int64 {
|
func (o *Object) Size() int64 {
|
||||||
size := o.Object.Size()
|
size, err := o.f.cipher.DecryptedSize(o.Object.Size())
|
||||||
if !o.f.opt.NoDataEncryption {
|
|
||||||
var err error
|
|
||||||
size, err = o.f.cipher.DecryptedSize(size)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fs.Debugf(o, "Bad size for decrypt: %v", err)
|
fs.Debugf(o, "Bad size for decrypt: %v", err)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return size
|
return size
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -873,10 +842,6 @@ func (o *Object) UnWrap() fs.Object {
|
|||||||
|
|
||||||
// Open opens the file for read. Call Close() on the returned io.ReadCloser
|
// Open opens the file for read. Call Close() on the returned io.ReadCloser
|
||||||
func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (rc io.ReadCloser, err error) {
|
func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (rc io.ReadCloser, err error) {
|
||||||
if o.f.opt.NoDataEncryption {
|
|
||||||
return o.Object.Open(ctx, options...)
|
|
||||||
}
|
|
||||||
|
|
||||||
var openOptions []fs.OpenOption
|
var openOptions []fs.OpenOption
|
||||||
var offset, limit int64 = 0, -1
|
var offset, limit int64 = 0, -1
|
||||||
for _, option := range options {
|
for _, option := range options {
|
||||||
@@ -952,16 +917,6 @@ func (f *Fs) Disconnect(ctx context.Context) error {
|
|||||||
return do(ctx)
|
return do(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shutdown the backend, closing any background tasks and any
|
|
||||||
// cached connections.
|
|
||||||
func (f *Fs) Shutdown(ctx context.Context) error {
|
|
||||||
do := f.Fs.Features().Shutdown
|
|
||||||
if do == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return do(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ObjectInfo describes a wrapped fs.ObjectInfo for being the source
|
// ObjectInfo describes a wrapped fs.ObjectInfo for being the source
|
||||||
//
|
//
|
||||||
// This encrypts the remote name and adjusts the size
|
// This encrypts the remote name and adjusts the size
|
||||||
@@ -1070,7 +1025,6 @@ var (
|
|||||||
_ fs.PublicLinker = (*Fs)(nil)
|
_ fs.PublicLinker = (*Fs)(nil)
|
||||||
_ fs.UserInfoer = (*Fs)(nil)
|
_ fs.UserInfoer = (*Fs)(nil)
|
||||||
_ fs.Disconnecter = (*Fs)(nil)
|
_ fs.Disconnecter = (*Fs)(nil)
|
||||||
_ fs.Shutdowner = (*Fs)(nil)
|
|
||||||
_ fs.ObjectInfo = (*ObjectInfo)(nil)
|
_ fs.ObjectInfo = (*ObjectInfo)(nil)
|
||||||
_ fs.Object = (*Object)(nil)
|
_ fs.Object = (*Object)(nil)
|
||||||
_ fs.ObjectUnWrapper = (*Object)(nil)
|
_ fs.ObjectUnWrapper = (*Object)(nil)
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ func (o testWrapper) UnWrap() fs.Object {
|
|||||||
// Create a temporary local fs to upload things from
|
// Create a temporary local fs to upload things from
|
||||||
|
|
||||||
func makeTempLocalFs(t *testing.T) (localFs fs.Fs, cleanup func()) {
|
func makeTempLocalFs(t *testing.T) (localFs fs.Fs, cleanup func()) {
|
||||||
localFs, err := fs.TemporaryLocalFs(context.Background())
|
localFs, err := fs.TemporaryLocalFs()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
cleanup = func() {
|
cleanup = func() {
|
||||||
require.NoError(t, localFs.Rmdir(context.Background(), ""))
|
require.NoError(t, localFs.Rmdir(context.Background(), ""))
|
||||||
@@ -87,7 +87,7 @@ func testObjectInfo(t *testing.T, f *Fs, wrap bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// wrap the object in a crypt for upload using the nonce we
|
// wrap the object in a crypt for upload using the nonce we
|
||||||
// saved from the encrypter
|
// saved from the encryptor
|
||||||
src := f.newObjectInfo(oi, nonce)
|
src := f.newObjectInfo(oi, nonce)
|
||||||
|
|
||||||
// Test ObjectInfo methods
|
// Test ObjectInfo methods
|
||||||
|
|||||||
@@ -91,26 +91,3 @@ func TestObfuscate(t *testing.T) {
|
|||||||
UnimplementableObjectMethods: []string{"MimeType"},
|
UnimplementableObjectMethods: []string{"MimeType"},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestNoDataObfuscate runs integration tests against the remote
|
|
||||||
func TestNoDataObfuscate(t *testing.T) {
|
|
||||||
if *fstest.RemoteName != "" {
|
|
||||||
t.Skip("Skipping as -remote set")
|
|
||||||
}
|
|
||||||
tempdir := filepath.Join(os.TempDir(), "rclone-crypt-test-obfuscate")
|
|
||||||
name := "TestCrypt4"
|
|
||||||
fstests.Run(t, &fstests.Opt{
|
|
||||||
RemoteName: name + ":",
|
|
||||||
NilObject: (*crypt.Object)(nil),
|
|
||||||
ExtraConfig: []fstests.ExtraConfigItem{
|
|
||||||
{Name: name, Key: "type", Value: "crypt"},
|
|
||||||
{Name: name, Key: "remote", Value: tempdir},
|
|
||||||
{Name: name, Key: "password", Value: obscure.MustObscure("potato2")},
|
|
||||||
{Name: name, Key: "filename_encryption", Value: "obfuscate"},
|
|
||||||
{Name: name, Key: "no_data_encryption", Value: "true"},
|
|
||||||
},
|
|
||||||
SkipBadWindowsCharacters: true,
|
|
||||||
UnimplementableFsMethods: []string{"OpenWriterAt"},
|
|
||||||
UnimplementableObjectMethods: []string{"MimeType"},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -7,8 +7,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"mime"
|
"mime"
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -111,7 +109,6 @@ func TestInternalParseExtensions(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestInternalFindExportFormat(t *testing.T) {
|
func TestInternalFindExportFormat(t *testing.T) {
|
||||||
ctx := context.Background()
|
|
||||||
item := &drive.File{
|
item := &drive.File{
|
||||||
Name: "file",
|
Name: "file",
|
||||||
MimeType: "application/vnd.google-apps.document",
|
MimeType: "application/vnd.google-apps.document",
|
||||||
@@ -129,7 +126,7 @@ func TestInternalFindExportFormat(t *testing.T) {
|
|||||||
} {
|
} {
|
||||||
f := new(Fs)
|
f := new(Fs)
|
||||||
f.exportExtensions = test.extensions
|
f.exportExtensions = test.extensions
|
||||||
gotExtension, gotFilename, gotMimeType, gotIsDocument := f.findExportFormat(ctx, item)
|
gotExtension, gotFilename, gotMimeType, gotIsDocument := f.findExportFormat(item)
|
||||||
assert.Equal(t, test.wantExtension, gotExtension)
|
assert.Equal(t, test.wantExtension, gotExtension)
|
||||||
if test.wantExtension != "" {
|
if test.wantExtension != "" {
|
||||||
assert.Equal(t, item.Name+gotExtension, gotFilename)
|
assert.Equal(t, item.Name+gotExtension, gotFilename)
|
||||||
@@ -197,7 +194,7 @@ func (f *Fs) InternalTestDocumentImport(t *testing.T) {
|
|||||||
testFilesPath, err := filepath.Abs(filepath.FromSlash("test/files"))
|
testFilesPath, err := filepath.Abs(filepath.FromSlash("test/files"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
testFilesFs, err := fs.NewFs(context.Background(), testFilesPath)
|
testFilesFs, err := fs.NewFs(testFilesPath)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
_, f.importMimeTypes, err = parseExtensions("odt,ods,doc")
|
_, f.importMimeTypes, err = parseExtensions("odt,ods,doc")
|
||||||
@@ -211,7 +208,7 @@ func (f *Fs) InternalTestDocumentUpdate(t *testing.T) {
|
|||||||
testFilesPath, err := filepath.Abs(filepath.FromSlash("test/files"))
|
testFilesPath, err := filepath.Abs(filepath.FromSlash("test/files"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
testFilesFs, err := fs.NewFs(context.Background(), testFilesPath)
|
testFilesFs, err := fs.NewFs(testFilesPath)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
_, f.importMimeTypes, err = parseExtensions("odt,ods,doc")
|
_, f.importMimeTypes, err = parseExtensions("odt,ods,doc")
|
||||||
@@ -275,15 +272,14 @@ func (f *Fs) InternalTestDocumentLink(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestIntegration/FsMkdir/FsPutFiles/Internal/Shortcuts
|
||||||
|
func (f *Fs) InternalTestShortcuts(t *testing.T) {
|
||||||
const (
|
const (
|
||||||
// from fstest/fstests/fstests.go
|
// from fstest/fstests/fstests.go
|
||||||
existingDir = "hello? sausage"
|
existingDir = "hello? sausage"
|
||||||
existingFile = `hello? sausage/êé/Hello, 世界/ " ' @ < > & ? + ≠/z.txt`
|
existingFile = `hello? sausage/êé/Hello, 世界/ " ' @ < > & ? + ≠/z.txt`
|
||||||
existingSubDir = "êé"
|
existingSubDir = "êé"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestIntegration/FsMkdir/FsPutFiles/Internal/Shortcuts
|
|
||||||
func (f *Fs) InternalTestShortcuts(t *testing.T) {
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
srcObj, err := f.NewObject(ctx, existingFile)
|
srcObj, err := f.NewObject(ctx, existingFile)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -412,55 +408,6 @@ func (f *Fs) InternalTestUnTrash(t *testing.T) {
|
|||||||
require.NoError(t, f.Purge(ctx, "trashDir"))
|
require.NoError(t, f.Purge(ctx, "trashDir"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestIntegration/FsMkdir/FsPutFiles/Internal/CopyID
|
|
||||||
func (f *Fs) InternalTestCopyID(t *testing.T) {
|
|
||||||
ctx := context.Background()
|
|
||||||
obj, err := f.NewObject(ctx, existingFile)
|
|
||||||
require.NoError(t, err)
|
|
||||||
o := obj.(*Object)
|
|
||||||
|
|
||||||
dir, err := ioutil.TempDir("", "rclone-drive-copyid-test")
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer func() {
|
|
||||||
_ = os.RemoveAll(dir)
|
|
||||||
}()
|
|
||||||
|
|
||||||
checkFile := func(name string) {
|
|
||||||
filePath := filepath.Join(dir, name)
|
|
||||||
fi, err := os.Stat(filePath)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, int64(100), fi.Size())
|
|
||||||
err = os.Remove(filePath)
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("BadID", func(t *testing.T) {
|
|
||||||
err = f.copyID(ctx, "ID-NOT-FOUND", dir+"/")
|
|
||||||
require.Error(t, err)
|
|
||||||
assert.Contains(t, err.Error(), "couldn't find id")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Directory", func(t *testing.T) {
|
|
||||||
rootID, err := f.dirCache.RootID(ctx, false)
|
|
||||||
require.NoError(t, err)
|
|
||||||
err = f.copyID(ctx, rootID, dir+"/")
|
|
||||||
require.Error(t, err)
|
|
||||||
assert.Contains(t, err.Error(), "can't copy directory")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("WithoutDestName", func(t *testing.T) {
|
|
||||||
err = f.copyID(ctx, o.id, dir+"/")
|
|
||||||
require.NoError(t, err)
|
|
||||||
checkFile(path.Base(existingFile))
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("WithDestName", func(t *testing.T) {
|
|
||||||
err = f.copyID(ctx, o.id, dir+"/potato.txt")
|
|
||||||
require.NoError(t, err)
|
|
||||||
checkFile("potato.txt")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Fs) InternalTest(t *testing.T) {
|
func (f *Fs) InternalTest(t *testing.T) {
|
||||||
// These tests all depend on each other so run them as nested tests
|
// These tests all depend on each other so run them as nested tests
|
||||||
t.Run("DocumentImport", func(t *testing.T) {
|
t.Run("DocumentImport", func(t *testing.T) {
|
||||||
@@ -477,7 +424,6 @@ func (f *Fs) InternalTest(t *testing.T) {
|
|||||||
})
|
})
|
||||||
t.Run("Shortcuts", f.InternalTestShortcuts)
|
t.Run("Shortcuts", f.InternalTestShortcuts)
|
||||||
t.Run("UnTrash", f.InternalTestUnTrash)
|
t.Run("UnTrash", f.InternalTestUnTrash)
|
||||||
t.Run("CopyID", f.InternalTestCopyID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ fstests.InternalTester = (*Fs)(nil)
|
var _ fstests.InternalTester = (*Fs)(nil)
|
||||||
|
|||||||
@@ -77,10 +77,11 @@ func (f *Fs) Upload(ctx context.Context, in io.Reader, size int64, contentType,
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
var req *http.Request
|
var req *http.Request
|
||||||
req, err = http.NewRequestWithContext(ctx, method, urls, body)
|
req, err = http.NewRequest(method, urls, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
req = req.WithContext(ctx) // go1.13 can use NewRequestWithContext
|
||||||
googleapi.Expand(req.URL, map[string]string{
|
googleapi.Expand(req.URL, map[string]string{
|
||||||
"fileId": fileID,
|
"fileId": fileID,
|
||||||
})
|
})
|
||||||
@@ -94,7 +95,7 @@ func (f *Fs) Upload(ctx context.Context, in io.Reader, size int64, contentType,
|
|||||||
defer googleapi.CloseBody(res)
|
defer googleapi.CloseBody(res)
|
||||||
err = googleapi.CheckResponse(res)
|
err = googleapi.CheckResponse(res)
|
||||||
}
|
}
|
||||||
return f.shouldRetry(ctx, err)
|
return f.shouldRetry(err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -113,7 +114,8 @@ func (f *Fs) Upload(ctx context.Context, in io.Reader, size int64, contentType,
|
|||||||
|
|
||||||
// Make an http.Request for the range passed in
|
// Make an http.Request for the range passed in
|
||||||
func (rx *resumableUpload) makeRequest(ctx context.Context, start int64, body io.ReadSeeker, reqSize int64) *http.Request {
|
func (rx *resumableUpload) makeRequest(ctx context.Context, start int64, body io.ReadSeeker, reqSize int64) *http.Request {
|
||||||
req, _ := http.NewRequestWithContext(ctx, "POST", rx.URI, body)
|
req, _ := http.NewRequest("POST", rx.URI, body)
|
||||||
|
req = req.WithContext(ctx) // go1.13 can use NewRequestWithContext
|
||||||
req.ContentLength = reqSize
|
req.ContentLength = reqSize
|
||||||
totalSize := "*"
|
totalSize := "*"
|
||||||
if rx.ContentLength >= 0 {
|
if rx.ContentLength >= 0 {
|
||||||
@@ -202,7 +204,7 @@ func (rx *resumableUpload) Upload(ctx context.Context) (*drive.File, error) {
|
|||||||
err = rx.f.pacer.Call(func() (bool, error) {
|
err = rx.f.pacer.Call(func() (bool, error) {
|
||||||
fs.Debugf(rx.remote, "Sending chunk %d length %d", start, reqSize)
|
fs.Debugf(rx.remote, "Sending chunk %d length %d", start, reqSize)
|
||||||
StatusCode, err = rx.transferChunk(ctx, start, chunk, reqSize)
|
StatusCode, err = rx.transferChunk(ctx, start, chunk, reqSize)
|
||||||
again, err := rx.f.shouldRetry(ctx, err)
|
again, err := rx.f.shouldRetry(err)
|
||||||
if StatusCode == statusResumeIncomplete || StatusCode == http.StatusCreated || StatusCode == http.StatusOK {
|
if StatusCode == statusResumeIncomplete || StatusCode == http.StatusCreated || StatusCode == http.StatusOK {
|
||||||
again = false
|
again = false
|
||||||
err = nil
|
err = nil
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,44 +0,0 @@
|
|||||||
package dropbox
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestInternalCheckPathLength(t *testing.T) {
|
|
||||||
rep := func(n int, r rune) (out string) {
|
|
||||||
rs := make([]rune, n)
|
|
||||||
for i := range rs {
|
|
||||||
rs[i] = r
|
|
||||||
}
|
|
||||||
return string(rs)
|
|
||||||
}
|
|
||||||
for _, test := range []struct {
|
|
||||||
in string
|
|
||||||
ok bool
|
|
||||||
}{
|
|
||||||
{in: "", ok: true},
|
|
||||||
{in: rep(maxFileNameLength, 'a'), ok: true},
|
|
||||||
{in: rep(maxFileNameLength+1, 'a'), ok: false},
|
|
||||||
{in: rep(maxFileNameLength, '£'), ok: true},
|
|
||||||
{in: rep(maxFileNameLength+1, '£'), ok: false},
|
|
||||||
{in: rep(maxFileNameLength, '☺'), ok: true},
|
|
||||||
{in: rep(maxFileNameLength+1, '☺'), ok: false},
|
|
||||||
{in: rep(maxFileNameLength, '你'), ok: true},
|
|
||||||
{in: rep(maxFileNameLength+1, '你'), ok: false},
|
|
||||||
{in: "/ok/ok", ok: true},
|
|
||||||
{in: "/ok/" + rep(maxFileNameLength, 'a') + "/ok", ok: true},
|
|
||||||
{in: "/ok/" + rep(maxFileNameLength+1, 'a') + "/ok", ok: false},
|
|
||||||
{in: "/ok/" + rep(maxFileNameLength, '£') + "/ok", ok: true},
|
|
||||||
{in: "/ok/" + rep(maxFileNameLength+1, '£') + "/ok", ok: false},
|
|
||||||
{in: "/ok/" + rep(maxFileNameLength, '☺') + "/ok", ok: true},
|
|
||||||
{in: "/ok/" + rep(maxFileNameLength+1, '☺') + "/ok", ok: false},
|
|
||||||
{in: "/ok/" + rep(maxFileNameLength, '你') + "/ok", ok: true},
|
|
||||||
{in: "/ok/" + rep(maxFileNameLength+1, '你') + "/ok", ok: false},
|
|
||||||
} {
|
|
||||||
|
|
||||||
err := checkPathLength(test.in)
|
|
||||||
assert.Equal(t, test.ok, err == nil, test.in)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -28,10 +28,7 @@ var retryErrorCodes = []int{
|
|||||||
|
|
||||||
// shouldRetry returns a boolean as to whether this resp and err
|
// shouldRetry returns a boolean as to whether this resp and err
|
||||||
// deserve to be retried. It returns the err as a convenience
|
// deserve to be retried. It returns the err as a convenience
|
||||||
func shouldRetry(ctx context.Context, resp *http.Response, err error) (bool, error) {
|
func shouldRetry(resp *http.Response, err error) (bool, error) {
|
||||||
if fserrors.ContextError(ctx, &err) {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
// Detect this error which the integration tests provoke
|
// Detect this error which the integration tests provoke
|
||||||
// error HTTP error 403 (403 Forbidden) returned body: "{\"message\":\"Flood detected: IP Locked #374\",\"status\":\"KO\"}"
|
// error HTTP error 403 (403 Forbidden) returned body: "{\"message\":\"Flood detected: IP Locked #374\",\"status\":\"KO\"}"
|
||||||
//
|
//
|
||||||
@@ -51,41 +48,6 @@ func shouldRetry(ctx context.Context, resp *http.Response, err error) (bool, err
|
|||||||
|
|
||||||
var isAlphaNumeric = regexp.MustCompile(`^[a-zA-Z0-9]+$`).MatchString
|
var isAlphaNumeric = regexp.MustCompile(`^[a-zA-Z0-9]+$`).MatchString
|
||||||
|
|
||||||
func (f *Fs) createObject(ctx context.Context, remote string) (o *Object, leaf string, directoryID string, err error) {
|
|
||||||
// Create the directory for the object if it doesn't exist
|
|
||||||
leaf, directoryID, err = f.dirCache.FindPath(ctx, remote, true)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Temporary Object under construction
|
|
||||||
o = &Object{
|
|
||||||
fs: f,
|
|
||||||
remote: remote,
|
|
||||||
}
|
|
||||||
return o, leaf, directoryID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Fs) readFileInfo(ctx context.Context, url string) (*File, error) {
|
|
||||||
request := FileInfoRequest{
|
|
||||||
URL: url,
|
|
||||||
}
|
|
||||||
opts := rest.Opts{
|
|
||||||
Method: "POST",
|
|
||||||
Path: "/file/info.cgi",
|
|
||||||
}
|
|
||||||
|
|
||||||
var file File
|
|
||||||
err := f.pacer.Call(func() (bool, error) {
|
|
||||||
resp, err := f.rest.CallJSON(ctx, &opts, &request, &file)
|
|
||||||
return shouldRetry(ctx, resp, err)
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "couldn't read file info")
|
|
||||||
}
|
|
||||||
|
|
||||||
return &file, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Fs) getDownloadToken(ctx context.Context, url string) (*GetTokenResponse, error) {
|
func (f *Fs) getDownloadToken(ctx context.Context, url string) (*GetTokenResponse, error) {
|
||||||
request := DownloadRequest{
|
request := DownloadRequest{
|
||||||
URL: url,
|
URL: url,
|
||||||
@@ -99,7 +61,7 @@ func (f *Fs) getDownloadToken(ctx context.Context, url string) (*GetTokenRespons
|
|||||||
var token GetTokenResponse
|
var token GetTokenResponse
|
||||||
err := f.pacer.Call(func() (bool, error) {
|
err := f.pacer.Call(func() (bool, error) {
|
||||||
resp, err := f.rest.CallJSON(ctx, &opts, &request, &token)
|
resp, err := f.rest.CallJSON(ctx, &opts, &request, &token)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "couldn't list files")
|
return nil, errors.Wrap(err, "couldn't list files")
|
||||||
@@ -127,7 +89,7 @@ func (f *Fs) listSharedFiles(ctx context.Context, id string) (entries fs.DirEntr
|
|||||||
var sharedFiles SharedFolderResponse
|
var sharedFiles SharedFolderResponse
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err := f.rest.CallJSON(ctx, &opts, nil, &sharedFiles)
|
resp, err := f.rest.CallJSON(ctx, &opts, nil, &sharedFiles)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "couldn't list files")
|
return nil, errors.Wrap(err, "couldn't list files")
|
||||||
@@ -156,7 +118,7 @@ func (f *Fs) listFiles(ctx context.Context, directoryID int) (filesList *FilesLi
|
|||||||
filesList = &FilesList{}
|
filesList = &FilesList{}
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err := f.rest.CallJSON(ctx, &opts, &request, filesList)
|
resp, err := f.rest.CallJSON(ctx, &opts, &request, filesList)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "couldn't list files")
|
return nil, errors.Wrap(err, "couldn't list files")
|
||||||
@@ -184,7 +146,7 @@ func (f *Fs) listFolders(ctx context.Context, directoryID int) (foldersList *Fol
|
|||||||
foldersList = &FoldersList{}
|
foldersList = &FoldersList{}
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err := f.rest.CallJSON(ctx, &opts, &request, foldersList)
|
resp, err := f.rest.CallJSON(ctx, &opts, &request, foldersList)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "couldn't list folders")
|
return nil, errors.Wrap(err, "couldn't list folders")
|
||||||
@@ -278,7 +240,7 @@ func (f *Fs) makeFolder(ctx context.Context, leaf string, folderID int) (respons
|
|||||||
response = &MakeFolderResponse{}
|
response = &MakeFolderResponse{}
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err := f.rest.CallJSON(ctx, &opts, &request, response)
|
resp, err := f.rest.CallJSON(ctx, &opts, &request, response)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "couldn't create folder")
|
return nil, errors.Wrap(err, "couldn't create folder")
|
||||||
@@ -305,7 +267,7 @@ func (f *Fs) removeFolder(ctx context.Context, name string, folderID int) (respo
|
|||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.rest.CallJSON(ctx, &opts, request, response)
|
resp, err = f.rest.CallJSON(ctx, &opts, request, response)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "couldn't remove folder")
|
return nil, errors.Wrap(err, "couldn't remove folder")
|
||||||
@@ -334,7 +296,7 @@ func (f *Fs) deleteFile(ctx context.Context, url string) (response *GenericOKRes
|
|||||||
response = &GenericOKResponse{}
|
response = &GenericOKResponse{}
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err := f.rest.CallJSON(ctx, &opts, request, response)
|
resp, err := f.rest.CallJSON(ctx, &opts, request, response)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -346,56 +308,6 @@ func (f *Fs) deleteFile(ctx context.Context, url string) (response *GenericOKRes
|
|||||||
return response, nil
|
return response, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fs) moveFile(ctx context.Context, url string, folderID int, rename string) (response *MoveFileResponse, err error) {
|
|
||||||
request := &MoveFileRequest{
|
|
||||||
URLs: []string{url},
|
|
||||||
FolderID: folderID,
|
|
||||||
Rename: rename,
|
|
||||||
}
|
|
||||||
|
|
||||||
opts := rest.Opts{
|
|
||||||
Method: "POST",
|
|
||||||
Path: "/file/mv.cgi",
|
|
||||||
}
|
|
||||||
|
|
||||||
response = &MoveFileResponse{}
|
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
|
||||||
resp, err := f.rest.CallJSON(ctx, &opts, request, response)
|
|
||||||
return shouldRetry(ctx, resp, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "couldn't copy file")
|
|
||||||
}
|
|
||||||
|
|
||||||
return response, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Fs) copyFile(ctx context.Context, url string, folderID int, rename string) (response *CopyFileResponse, err error) {
|
|
||||||
request := &CopyFileRequest{
|
|
||||||
URLs: []string{url},
|
|
||||||
FolderID: folderID,
|
|
||||||
Rename: rename,
|
|
||||||
}
|
|
||||||
|
|
||||||
opts := rest.Opts{
|
|
||||||
Method: "POST",
|
|
||||||
Path: "/file/cp.cgi",
|
|
||||||
}
|
|
||||||
|
|
||||||
response = &CopyFileResponse{}
|
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
|
||||||
resp, err := f.rest.CallJSON(ctx, &opts, request, response)
|
|
||||||
return shouldRetry(ctx, resp, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "couldn't copy file")
|
|
||||||
}
|
|
||||||
|
|
||||||
return response, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Fs) getUploadNode(ctx context.Context) (response *GetUploadNodeResponse, err error) {
|
func (f *Fs) getUploadNode(ctx context.Context) (response *GetUploadNodeResponse, err error) {
|
||||||
// fs.Debugf(f, "Requesting Upload node")
|
// fs.Debugf(f, "Requesting Upload node")
|
||||||
|
|
||||||
@@ -408,7 +320,7 @@ func (f *Fs) getUploadNode(ctx context.Context) (response *GetUploadNodeResponse
|
|||||||
response = &GetUploadNodeResponse{}
|
response = &GetUploadNodeResponse{}
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err := f.rest.CallJSON(ctx, &opts, nil, response)
|
resp, err := f.rest.CallJSON(ctx, &opts, nil, response)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "didnt got an upload node")
|
return nil, errors.Wrap(err, "didnt got an upload node")
|
||||||
@@ -451,7 +363,7 @@ func (f *Fs) uploadFile(ctx context.Context, in io.Reader, size int64, fileName,
|
|||||||
|
|
||||||
err = f.pacer.CallNoRetry(func() (bool, error) {
|
err = f.pacer.CallNoRetry(func() (bool, error) {
|
||||||
resp, err := f.rest.CallJSON(ctx, &opts, nil, nil)
|
resp, err := f.rest.CallJSON(ctx, &opts, nil, nil)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -485,7 +397,7 @@ func (f *Fs) endUpload(ctx context.Context, uploadID string, nodeurl string) (re
|
|||||||
response = &EndFileUploadResponse{}
|
response = &EndFileUploadResponse{}
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err := f.rest.CallJSON(ctx, &opts, nil, response)
|
resp, err := f.rest.CallJSON(ctx, &opts, nil, response)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ func init() {
|
|||||||
fs.Register(&fs.RegInfo{
|
fs.Register(&fs.RegInfo{
|
||||||
Name: "fichier",
|
Name: "fichier",
|
||||||
Description: "1Fichier",
|
Description: "1Fichier",
|
||||||
Config: func(ctx context.Context, name string, config configmap.Mapper) {
|
Config: func(name string, config configmap.Mapper) {
|
||||||
},
|
},
|
||||||
NewFs: NewFs,
|
NewFs: NewFs,
|
||||||
Options: []fs.Option{{
|
Options: []fs.Option{{
|
||||||
@@ -167,7 +167,7 @@ func (f *Fs) Features() *fs.Features {
|
|||||||
//
|
//
|
||||||
// On Windows avoid single character remote names as they can be mixed
|
// On Windows avoid single character remote names as they can be mixed
|
||||||
// up with drive letters.
|
// up with drive letters.
|
||||||
func NewFs(ctx context.Context, name string, root string, config configmap.Mapper) (fs.Fs, error) {
|
func NewFs(name string, root string, config configmap.Mapper) (fs.Fs, error) {
|
||||||
opt := new(Options)
|
opt := new(Options)
|
||||||
err := configstruct.Set(config, opt)
|
err := configstruct.Set(config, opt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -186,17 +186,16 @@ func NewFs(ctx context.Context, name string, root string, config configmap.Mappe
|
|||||||
name: name,
|
name: name,
|
||||||
root: root,
|
root: root,
|
||||||
opt: *opt,
|
opt: *opt,
|
||||||
pacer: fs.NewPacer(ctx, pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant), pacer.AttackConstant(attackConstant))),
|
pacer: fs.NewPacer(pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant), pacer.AttackConstant(attackConstant))),
|
||||||
baseClient: &http.Client{},
|
baseClient: &http.Client{},
|
||||||
}
|
}
|
||||||
|
|
||||||
f.features = (&fs.Features{
|
f.features = (&fs.Features{
|
||||||
DuplicateFiles: true,
|
DuplicateFiles: true,
|
||||||
CanHaveEmptyDirectories: true,
|
CanHaveEmptyDirectories: true,
|
||||||
ReadMimeType: true,
|
}).Fill(f)
|
||||||
}).Fill(ctx, f)
|
|
||||||
|
|
||||||
client := fshttp.NewClient(ctx)
|
client := fshttp.NewClient(fs.Config)
|
||||||
|
|
||||||
f.rest = rest.NewClient(client).SetRoot(apiBaseURL)
|
f.rest = rest.NewClient(client).SetRoot(apiBaseURL)
|
||||||
|
|
||||||
@@ -204,6 +203,8 @@ func NewFs(ctx context.Context, name string, root string, config configmap.Mappe
|
|||||||
|
|
||||||
f.dirCache = dircache.New(root, rootID, f)
|
f.dirCache = dircache.New(root, rootID, f)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
// Find the current root
|
// Find the current root
|
||||||
err = f.dirCache.FindRoot(ctx, false)
|
err = f.dirCache.FindRoot(ctx, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -226,7 +227,7 @@ func NewFs(ctx context.Context, name string, root string, config configmap.Mappe
|
|||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
f.features.Fill(ctx, &tempF)
|
f.features.Fill(&tempF)
|
||||||
// XXX: update the old f here instead of returning tempF, since
|
// XXX: update the old f here instead of returning tempF, since
|
||||||
// `features` were already filled with functions having *f as a receiver.
|
// `features` were already filled with functions having *f as a receiver.
|
||||||
// See https://github.com/rclone/rclone/issues/2182
|
// See https://github.com/rclone/rclone/issues/2182
|
||||||
@@ -305,10 +306,10 @@ func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) {
|
|||||||
// will return the object and the error, otherwise will return
|
// will return the object and the error, otherwise will return
|
||||||
// nil and the error
|
// nil and the error
|
||||||
func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
|
func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
|
||||||
existingObj, err := f.NewObject(ctx, src.Remote())
|
exisitingObj, err := f.NewObject(ctx, src.Remote())
|
||||||
switch err {
|
switch err {
|
||||||
case nil:
|
case nil:
|
||||||
return existingObj, existingObj.Update(ctx, in, src, options...)
|
return exisitingObj, exisitingObj.Update(ctx, in, src, options...)
|
||||||
case fs.ErrorObjectNotFound:
|
case fs.ErrorObjectNotFound:
|
||||||
// Not found so create it
|
// Not found so create it
|
||||||
return f.PutUnchecked(ctx, in, src, options...)
|
return f.PutUnchecked(ctx, in, src, options...)
|
||||||
@@ -348,10 +349,8 @@ func (f *Fs) putUnchecked(ctx context.Context, in io.Reader, remote string, size
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(fileUploadResponse.Links) == 0 {
|
if len(fileUploadResponse.Links) != 1 {
|
||||||
return nil, errors.New("upload response not found")
|
return nil, errors.New("unexpected amount of files")
|
||||||
} else if len(fileUploadResponse.Links) > 1 {
|
|
||||||
fs.Debugf(remote, "Multiple upload responses found, using the first")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
link := fileUploadResponse.Links[0]
|
link := fileUploadResponse.Links[0]
|
||||||
@@ -365,6 +364,7 @@ func (f *Fs) putUnchecked(ctx context.Context, in io.Reader, remote string, size
|
|||||||
fs: f,
|
fs: f,
|
||||||
remote: remote,
|
remote: remote,
|
||||||
file: File{
|
file: File{
|
||||||
|
ACL: 0,
|
||||||
CDN: 0,
|
CDN: 0,
|
||||||
Checksum: link.Whirlpool,
|
Checksum: link.Whirlpool,
|
||||||
ContentType: "",
|
ContentType: "",
|
||||||
@@ -417,89 +417,9 @@ func (f *Fs) Rmdir(ctx context.Context, dir string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move src to this remote using server side move operations.
|
|
||||||
func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object, error) {
|
|
||||||
srcObj, ok := src.(*Object)
|
|
||||||
if !ok {
|
|
||||||
fs.Debugf(src, "Can't move - not same remote type")
|
|
||||||
return nil, fs.ErrorCantMove
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create temporary object
|
|
||||||
dstObj, leaf, directoryID, err := f.createObject(ctx, remote)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
folderID, err := strconv.Atoi(directoryID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
resp, err := f.moveFile(ctx, srcObj.file.URL, folderID, leaf)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "couldn't move file")
|
|
||||||
}
|
|
||||||
if resp.Status != "OK" {
|
|
||||||
return nil, errors.New("couldn't move file")
|
|
||||||
}
|
|
||||||
|
|
||||||
file, err := f.readFileInfo(ctx, resp.URLs[0])
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("couldn't read file data")
|
|
||||||
}
|
|
||||||
dstObj.setMetaData(*file)
|
|
||||||
return dstObj, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy src to this remote using server side move operations.
|
|
||||||
func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object, error) {
|
|
||||||
srcObj, ok := src.(*Object)
|
|
||||||
if !ok {
|
|
||||||
fs.Debugf(src, "Can't move - not same remote type")
|
|
||||||
return nil, fs.ErrorCantMove
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create temporary object
|
|
||||||
dstObj, leaf, directoryID, err := f.createObject(ctx, remote)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
folderID, err := strconv.Atoi(directoryID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
resp, err := f.copyFile(ctx, srcObj.file.URL, folderID, leaf)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "couldn't move file")
|
|
||||||
}
|
|
||||||
if resp.Status != "OK" {
|
|
||||||
return nil, errors.New("couldn't move file")
|
|
||||||
}
|
|
||||||
|
|
||||||
file, err := f.readFileInfo(ctx, resp.URLs[0].ToURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("couldn't read file data")
|
|
||||||
}
|
|
||||||
dstObj.setMetaData(*file)
|
|
||||||
return dstObj, 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)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return o.(*Object).file.URL, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the interfaces are satisfied
|
// Check the interfaces are satisfied
|
||||||
var (
|
var (
|
||||||
_ fs.Fs = (*Fs)(nil)
|
_ fs.Fs = (*Fs)(nil)
|
||||||
_ fs.Mover = (*Fs)(nil)
|
|
||||||
_ fs.Copier = (*Fs)(nil)
|
|
||||||
_ fs.PublicLinker = (*Fs)(nil)
|
|
||||||
_ fs.PutUncheckeder = (*Fs)(nil)
|
_ fs.PutUncheckeder = (*Fs)(nil)
|
||||||
_ dircache.DirCacher = (*Fs)(nil)
|
_ dircache.DirCacher = (*Fs)(nil)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ package fichier
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/rclone/rclone/fs"
|
||||||
"github.com/rclone/rclone/fstest/fstests"
|
"github.com/rclone/rclone/fstest/fstests"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestIntegration runs integration tests against the remote
|
// TestIntegration runs integration tests against the remote
|
||||||
func TestIntegration(t *testing.T) {
|
func TestIntegration(t *testing.T) {
|
||||||
|
fs.Config.LogLevel = fs.LogLevelDebug
|
||||||
fstests.Run(t, &fstests.Opt{
|
fstests.Run(t, &fstests.Opt{
|
||||||
RemoteName: "TestFichier:",
|
RemoteName: "TestFichier:",
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -72,10 +72,6 @@ func (o *Object) SetModTime(context.Context, time.Time) error {
|
|||||||
//return errors.New("setting modtime is not supported for 1fichier remotes")
|
//return errors.New("setting modtime is not supported for 1fichier remotes")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *Object) setMetaData(file File) {
|
|
||||||
o.file = file
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open opens the file for read. Call Close() on the returned io.ReadCloser
|
// Open opens the file for read. Call Close() on the returned io.ReadCloser
|
||||||
func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (io.ReadCloser, error) {
|
func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (io.ReadCloser, error) {
|
||||||
fs.FixRangeOption(options, o.file.Size)
|
fs.FixRangeOption(options, o.file.Size)
|
||||||
@@ -94,7 +90,7 @@ func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (io.ReadClo
|
|||||||
|
|
||||||
err = o.fs.pacer.Call(func() (bool, error) {
|
err = o.fs.pacer.Call(func() (bool, error) {
|
||||||
resp, err = o.fs.rest.Call(ctx, &opts)
|
resp, err = o.fs.rest.Call(ctx, &opts)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
package fichier
|
package fichier
|
||||||
|
|
||||||
// FileInfoRequest is the request structure of the corresponding request
|
|
||||||
type FileInfoRequest struct {
|
|
||||||
URL string `json:"url"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListFolderRequest is the request structure of the corresponding request
|
// ListFolderRequest is the request structure of the corresponding request
|
||||||
type ListFolderRequest struct {
|
type ListFolderRequest struct {
|
||||||
FolderID int `json:"folder_id"`
|
FolderID int `json:"folder_id"`
|
||||||
@@ -54,39 +49,6 @@ type MakeFolderResponse struct {
|
|||||||
FolderID int `json:"folder_id"`
|
FolderID int `json:"folder_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// MoveFileRequest is the request structure of the corresponding request
|
|
||||||
type MoveFileRequest struct {
|
|
||||||
URLs []string `json:"urls"`
|
|
||||||
FolderID int `json:"destination_folder_id"`
|
|
||||||
Rename string `json:"rename,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// MoveFileResponse is the response structure of the corresponding request
|
|
||||||
type MoveFileResponse struct {
|
|
||||||
Status string `json:"status"`
|
|
||||||
URLs []string `json:"urls"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// CopyFileRequest is the request structure of the corresponding request
|
|
||||||
type CopyFileRequest struct {
|
|
||||||
URLs []string `json:"urls"`
|
|
||||||
FolderID int `json:"folder_id"`
|
|
||||||
Rename string `json:"rename,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// CopyFileResponse is the response structure of the corresponding request
|
|
||||||
type CopyFileResponse struct {
|
|
||||||
Status string `json:"status"`
|
|
||||||
Copied int `json:"copied"`
|
|
||||||
URLs []FileCopy `json:"urls"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// FileCopy is used in the the CopyFileResponse
|
|
||||||
type FileCopy struct {
|
|
||||||
FromURL string `json:"from_url"`
|
|
||||||
ToURL string `json:"to_url"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetUploadNodeResponse is the response structure of the corresponding request
|
// GetUploadNodeResponse is the response structure of the corresponding request
|
||||||
type GetUploadNodeResponse struct {
|
type GetUploadNodeResponse struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
@@ -124,6 +86,7 @@ type EndFileUploadResponse struct {
|
|||||||
|
|
||||||
// File is the structure how 1Fichier returns a File
|
// File is the structure how 1Fichier returns a File
|
||||||
type File struct {
|
type File struct {
|
||||||
|
ACL int `json:"acl"`
|
||||||
CDN int `json:"cdn"`
|
CDN int `json:"cdn"`
|
||||||
Checksum string `json:"checksum"`
|
Checksum string `json:"checksum"`
|
||||||
ContentType string `json:"content-type"`
|
ContentType string `json:"content-type"`
|
||||||
|
|||||||
@@ -1,391 +0,0 @@
|
|||||||
// Package api has type definitions for filefabric
|
|
||||||
//
|
|
||||||
// Converted from the API responses with help from https://mholt.github.io/json-to-go/
|
|
||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// TimeFormat for parameters (UTC)
|
|
||||||
timeFormatParameters = `2006-01-02 15:04:05`
|
|
||||||
// "2020-08-11 10:10:04" for JSON parsing
|
|
||||||
timeFormatJSON = `"` + timeFormatParameters + `"`
|
|
||||||
)
|
|
||||||
|
|
||||||
// Time represents represents date and time information for the
|
|
||||||
// filefabric API
|
|
||||||
type Time time.Time
|
|
||||||
|
|
||||||
// MarshalJSON turns a Time into JSON (in UTC)
|
|
||||||
func (t *Time) MarshalJSON() (out []byte, err error) {
|
|
||||||
timeString := (*time.Time)(t).UTC().Format(timeFormatJSON)
|
|
||||||
return []byte(timeString), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var zeroTime = []byte(`"0000-00-00 00:00:00"`)
|
|
||||||
|
|
||||||
// UnmarshalJSON turns JSON into a Time (in UTC)
|
|
||||||
func (t *Time) UnmarshalJSON(data []byte) error {
|
|
||||||
// Set a Zero time.Time if we receive a zero time input
|
|
||||||
if bytes.Equal(data, zeroTime) {
|
|
||||||
*t = Time(time.Time{})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
newT, err := time.Parse(timeFormatJSON, string(data))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
*t = Time(newT)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// String turns a Time into a string in UTC suitable for the API
|
|
||||||
// parameters
|
|
||||||
func (t Time) String() string {
|
|
||||||
return time.Time(t).UTC().Format(timeFormatParameters)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Status return returned in all status responses
|
|
||||||
type Status struct {
|
|
||||||
Code string `json:"status"`
|
|
||||||
Message string `json:"statusmessage"`
|
|
||||||
TaskID string `json:"taskid"`
|
|
||||||
// Warning string `json:"warning"` // obsolete
|
|
||||||
}
|
|
||||||
|
|
||||||
// Status statisfies the error interface
|
|
||||||
func (e *Status) Error() string {
|
|
||||||
return fmt.Sprintf("%s (%s)", e.Message, e.Code)
|
|
||||||
}
|
|
||||||
|
|
||||||
// OK returns true if the status is all good
|
|
||||||
func (e *Status) OK() bool {
|
|
||||||
return e.Code == "ok"
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCode returns the status code if any
|
|
||||||
func (e *Status) GetCode() string {
|
|
||||||
return e.Code
|
|
||||||
}
|
|
||||||
|
|
||||||
// OKError defines an interface for items which can be OK or be an error
|
|
||||||
type OKError interface {
|
|
||||||
error
|
|
||||||
OK() bool
|
|
||||||
GetCode() string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check Status satisfies the OKError interface
|
|
||||||
var _ OKError = (*Status)(nil)
|
|
||||||
|
|
||||||
// EmptyResponse is response which just returns the error condition
|
|
||||||
type EmptyResponse struct {
|
|
||||||
Status
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTokenByAuthTokenResponse is the response to getTokenByAuthToken
|
|
||||||
type GetTokenByAuthTokenResponse struct {
|
|
||||||
Status
|
|
||||||
Token string `json:"token"`
|
|
||||||
UserID string `json:"userid"`
|
|
||||||
AllowLoginRemember string `json:"allowloginremember"`
|
|
||||||
LastLogin Time `json:"lastlogin"`
|
|
||||||
AutoLoginCode string `json:"autologincode"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ApplianceInfo is the response to getApplianceInfo
|
|
||||||
type ApplianceInfo struct {
|
|
||||||
Status
|
|
||||||
Sitetitle string `json:"sitetitle"`
|
|
||||||
OauthLoginSupport string `json:"oauthloginsupport"`
|
|
||||||
IsAppliance string `json:"isappliance"`
|
|
||||||
SoftwareVersion string `json:"softwareversion"`
|
|
||||||
SoftwareVersionLabel string `json:"softwareversionlabel"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFolderContentsResponse is returned from getFolderContents
|
|
||||||
type GetFolderContentsResponse struct {
|
|
||||||
Status
|
|
||||||
Total int `json:"total,string"`
|
|
||||||
Items []Item `json:"filelist"`
|
|
||||||
Folder Item `json:"folder"`
|
|
||||||
From int `json:"from,string"`
|
|
||||||
//Count int `json:"count"`
|
|
||||||
Pid string `json:"pid"`
|
|
||||||
RefreshResult Status `json:"refreshresult"`
|
|
||||||
// Curfolder Item `json:"curfolder"` - sometimes returned as "ROOT"?
|
|
||||||
Parents []Item `json:"parents"`
|
|
||||||
CustomPermissions CustomPermissions `json:"custompermissions"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ItemType determine whether it is a file or a folder
|
|
||||||
type ItemType uint8
|
|
||||||
|
|
||||||
// Types of things in Item
|
|
||||||
const (
|
|
||||||
ItemTypeFile ItemType = 0
|
|
||||||
ItemTypeFolder ItemType = 1
|
|
||||||
)
|
|
||||||
|
|
||||||
// Item ia a File or a Folder
|
|
||||||
type Item struct {
|
|
||||||
ID string `json:"fi_id"`
|
|
||||||
PID string `json:"fi_pid"`
|
|
||||||
// UID string `json:"fi_uid"`
|
|
||||||
Name string `json:"fi_name"`
|
|
||||||
// S3Name string `json:"fi_s3name"`
|
|
||||||
// Extension string `json:"fi_extension"`
|
|
||||||
// Description string `json:"fi_description"`
|
|
||||||
Type ItemType `json:"fi_type,string"`
|
|
||||||
// Created Time `json:"fi_created"`
|
|
||||||
Size int64 `json:"fi_size,string"`
|
|
||||||
ContentType string `json:"fi_contenttype"`
|
|
||||||
// Tags string `json:"fi_tags"`
|
|
||||||
// MainCode string `json:"fi_maincode"`
|
|
||||||
// Public int `json:"fi_public,string"`
|
|
||||||
// Provider string `json:"fi_provider"`
|
|
||||||
// ProviderFolder string `json:"fi_providerfolder"` // folder
|
|
||||||
// Encrypted int `json:"fi_encrypted,string"`
|
|
||||||
// StructType string `json:"fi_structtype"`
|
|
||||||
// Bname string `json:"fi_bname"` // folder
|
|
||||||
// OrgID string `json:"fi_orgid"`
|
|
||||||
// Favorite int `json:"fi_favorite,string"`
|
|
||||||
// IspartOf string `json:"fi_ispartof"` // folder
|
|
||||||
Modified Time `json:"fi_modified"`
|
|
||||||
// LastAccessed Time `json:"fi_lastaccessed"`
|
|
||||||
// Hits int64 `json:"fi_hits,string"`
|
|
||||||
// IP string `json:"fi_ip"` // folder
|
|
||||||
// BigDescription string `json:"fi_bigdescription"`
|
|
||||||
LocalTime Time `json:"fi_localtime"`
|
|
||||||
// OrgfolderID string `json:"fi_orgfolderid"`
|
|
||||||
// StorageIP string `json:"fi_storageip"` // folder
|
|
||||||
// RemoteTime Time `json:"fi_remotetime"`
|
|
||||||
// ProviderOptions string `json:"fi_provideroptions"`
|
|
||||||
// Access string `json:"fi_access"`
|
|
||||||
// Hidden string `json:"fi_hidden"` // folder
|
|
||||||
// VersionOf string `json:"fi_versionof"`
|
|
||||||
Trash bool `json:"trash"`
|
|
||||||
// Isbucket string `json:"isbucket"` // filelist
|
|
||||||
SubFolders int64 `json:"subfolders"` // folder
|
|
||||||
}
|
|
||||||
|
|
||||||
// ItemFields is a | separated list of fields in Item
|
|
||||||
var ItemFields = mustFields(Item{})
|
|
||||||
|
|
||||||
// fields returns the JSON fields in use by opt as a | separated
|
|
||||||
// string.
|
|
||||||
func fields(opt interface{}) (pipeTags string, err error) {
|
|
||||||
var tags []string
|
|
||||||
def := reflect.ValueOf(opt)
|
|
||||||
defType := def.Type()
|
|
||||||
for i := 0; i < def.NumField(); i++ {
|
|
||||||
field := defType.Field(i)
|
|
||||||
tag, ok := field.Tag.Lookup("json")
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if comma := strings.IndexRune(tag, ','); comma >= 0 {
|
|
||||||
tag = tag[:comma]
|
|
||||||
}
|
|
||||||
if tag == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
tags = append(tags, tag)
|
|
||||||
}
|
|
||||||
return strings.Join(tags, "|"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// mustFields returns the JSON fields in use by opt as a | separated
|
|
||||||
// string. It panics on failure.
|
|
||||||
func mustFields(opt interface{}) string {
|
|
||||||
tags, err := fields(opt)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return tags
|
|
||||||
}
|
|
||||||
|
|
||||||
// CustomPermissions is returned as part of GetFolderContentsResponse
|
|
||||||
type CustomPermissions struct {
|
|
||||||
Upload string `json:"upload"`
|
|
||||||
CreateSubFolder string `json:"createsubfolder"`
|
|
||||||
Rename string `json:"rename"`
|
|
||||||
Delete string `json:"delete"`
|
|
||||||
Move string `json:"move"`
|
|
||||||
ManagePermissions string `json:"managepermissions"`
|
|
||||||
ListOnly string `json:"listonly"`
|
|
||||||
VisibleInTrash string `json:"visibleintrash"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// DoCreateNewFolderResponse is response from foCreateNewFolder
|
|
||||||
type DoCreateNewFolderResponse struct {
|
|
||||||
Status
|
|
||||||
Item Item `json:"file"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// DoInitUploadResponse is response from doInitUpload
|
|
||||||
type DoInitUploadResponse struct {
|
|
||||||
Status
|
|
||||||
ProviderID string `json:"providerid"`
|
|
||||||
UploadCode string `json:"uploadcode"`
|
|
||||||
FileType string `json:"filetype"`
|
|
||||||
DirectUploadSupport string `json:"directuploadsupport"`
|
|
||||||
ResumeAllowed string `json:"resumeallowed"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// UploaderResponse is returned from /cgi-bin/uploader/uploader1.cgi
|
|
||||||
//
|
|
||||||
// Sometimes the response is returned as XML and sometimes as JSON
|
|
||||||
type UploaderResponse struct {
|
|
||||||
FileSize int64 `xml:"filesize" json:"filesize,string"`
|
|
||||||
MD5 string `xml:"md5" json:"md5"`
|
|
||||||
Success string `xml:"success" json:"success"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// UploadStatus is returned from getUploadStatus
|
|
||||||
type UploadStatus struct {
|
|
||||||
Status
|
|
||||||
UploadCode string `json:"uploadcode"`
|
|
||||||
Metafile string `json:"metafile"`
|
|
||||||
Percent int `json:"percent,string"`
|
|
||||||
Uploaded int64 `json:"uploaded,string"`
|
|
||||||
Size int64 `json:"size,string"`
|
|
||||||
Filename string `json:"filename"`
|
|
||||||
Nofile string `json:"nofile"`
|
|
||||||
Completed string `json:"completed"`
|
|
||||||
Completsuccess string `json:"completsuccess"`
|
|
||||||
Completerror string `json:"completerror"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// DoCompleteUploadResponse is the response to doCompleteUpload
|
|
||||||
type DoCompleteUploadResponse struct {
|
|
||||||
Status
|
|
||||||
UploadedSize int64 `json:"uploadedsize,string"`
|
|
||||||
StorageIP string `json:"storageip"`
|
|
||||||
UploadedName string `json:"uploadedname"`
|
|
||||||
// Versioned []interface{} `json:"versioned"`
|
|
||||||
// VersionedID int `json:"versionedid"`
|
|
||||||
// Comment interface{} `json:"comment"`
|
|
||||||
File Item `json:"file"`
|
|
||||||
// UsSize string `json:"us_size"`
|
|
||||||
// PaSize string `json:"pa_size"`
|
|
||||||
// SpaceInfo SpaceInfo `json:"spaceinfo"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Providers is returned as part of UploadResponse
|
|
||||||
type Providers struct {
|
|
||||||
Max string `json:"max"`
|
|
||||||
Used string `json:"used"`
|
|
||||||
ID string `json:"id"`
|
|
||||||
Private string `json:"private"`
|
|
||||||
Limit string `json:"limit"`
|
|
||||||
Percent int `json:"percent"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Total is returned as part of UploadResponse
|
|
||||||
type Total struct {
|
|
||||||
Max string `json:"max"`
|
|
||||||
Used string `json:"used"`
|
|
||||||
ID string `json:"id"`
|
|
||||||
Priused string `json:"priused"`
|
|
||||||
Primax string `json:"primax"`
|
|
||||||
Limit string `json:"limit"`
|
|
||||||
Percent int `json:"percent"`
|
|
||||||
Pripercent int `json:"pripercent"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// UploadResponse is returned as part of SpaceInfo
|
|
||||||
type UploadResponse struct {
|
|
||||||
Providers []Providers `json:"providers"`
|
|
||||||
Total Total `json:"total"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// SpaceInfo is returned as part of DoCompleteUploadResponse
|
|
||||||
type SpaceInfo struct {
|
|
||||||
Response UploadResponse `json:"response"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteResponse is returned from doDeleteFile
|
|
||||||
type DeleteResponse struct {
|
|
||||||
Status
|
|
||||||
Deleted []string `json:"deleted"`
|
|
||||||
Errors []interface{} `json:"errors"`
|
|
||||||
ID string `json:"fi_id"`
|
|
||||||
BackgroundTask int `json:"backgroundtask"`
|
|
||||||
UsSize string `json:"us_size"`
|
|
||||||
PaSize string `json:"pa_size"`
|
|
||||||
//SpaceInfo SpaceInfo `json:"spaceinfo"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// FileResponse is returned from doRenameFile
|
|
||||||
type FileResponse struct {
|
|
||||||
Status
|
|
||||||
Item Item `json:"file"`
|
|
||||||
Exists string `json:"exists"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// MoveFilesResponse is returned from doMoveFiles
|
|
||||||
type MoveFilesResponse struct {
|
|
||||||
Status
|
|
||||||
Filesleft string `json:"filesleft"`
|
|
||||||
Addedtobackground string `json:"addedtobackground"`
|
|
||||||
Moved string `json:"moved"`
|
|
||||||
Item Item `json:"file"`
|
|
||||||
IDs []string `json:"fi_ids"`
|
|
||||||
Length int `json:"length"`
|
|
||||||
DirID string `json:"dir_id"`
|
|
||||||
MovedObjects []Item `json:"movedobjects"`
|
|
||||||
// FolderTasks []interface{} `json:"foldertasks"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// TasksResponse is the response to getUserBackgroundTasks
|
|
||||||
type TasksResponse struct {
|
|
||||||
Status
|
|
||||||
Tasks []Task `json:"tasks"`
|
|
||||||
Total string `json:"total"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// BtData is part of TasksResponse
|
|
||||||
type BtData struct {
|
|
||||||
Callback string `json:"callback"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Task describes a task returned in TasksResponse
|
|
||||||
type Task struct {
|
|
||||||
BtID string `json:"bt_id"`
|
|
||||||
UsID string `json:"us_id"`
|
|
||||||
BtType string `json:"bt_type"`
|
|
||||||
BtData BtData `json:"bt_data"`
|
|
||||||
BtStatustext string `json:"bt_statustext"`
|
|
||||||
BtStatusdata string `json:"bt_statusdata"`
|
|
||||||
BtMessage string `json:"bt_message"`
|
|
||||||
BtProcent string `json:"bt_procent"`
|
|
||||||
BtAdded string `json:"bt_added"`
|
|
||||||
BtStatus string `json:"bt_status"`
|
|
||||||
BtCompleted string `json:"bt_completed"`
|
|
||||||
BtTitle string `json:"bt_title"`
|
|
||||||
BtCredentials string `json:"bt_credentials"`
|
|
||||||
BtHidden string `json:"bt_hidden"`
|
|
||||||
BtAutoremove string `json:"bt_autoremove"`
|
|
||||||
BtDevsite string `json:"bt_devsite"`
|
|
||||||
BtPriority string `json:"bt_priority"`
|
|
||||||
BtReport string `json:"bt_report"`
|
|
||||||
BtSitemarker string `json:"bt_sitemarker"`
|
|
||||||
BtExecuteafter string `json:"bt_executeafter"`
|
|
||||||
BtCompletestatus string `json:"bt_completestatus"`
|
|
||||||
BtSubtype string `json:"bt_subtype"`
|
|
||||||
BtCanceled string `json:"bt_canceled"`
|
|
||||||
Callback string `json:"callback"`
|
|
||||||
CanBeCanceled bool `json:"canbecanceled"`
|
|
||||||
CanBeRestarted bool `json:"canberestarted"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
Settings string `json:"settings"`
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,17 +0,0 @@
|
|||||||
// Test filefabric filesystem interface
|
|
||||||
package filefabric_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/rclone/rclone/backend/filefabric"
|
|
||||||
"github.com/rclone/rclone/fstest/fstests"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestIntegration runs integration tests against the remote
|
|
||||||
func TestIntegration(t *testing.T) {
|
|
||||||
fstests.Run(t, &fstests.Opt{
|
|
||||||
RemoteName: "TestFileFabric:",
|
|
||||||
NilObject: (*filefabric.Object)(nil),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -5,8 +5,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
|
||||||
"net/textproto"
|
"net/textproto"
|
||||||
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -16,30 +16,16 @@ import (
|
|||||||
"github.com/jlaffaye/ftp"
|
"github.com/jlaffaye/ftp"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/rclone/rclone/fs"
|
"github.com/rclone/rclone/fs"
|
||||||
"github.com/rclone/rclone/fs/accounting"
|
|
||||||
"github.com/rclone/rclone/fs/config"
|
"github.com/rclone/rclone/fs/config"
|
||||||
"github.com/rclone/rclone/fs/config/configmap"
|
"github.com/rclone/rclone/fs/config/configmap"
|
||||||
"github.com/rclone/rclone/fs/config/configstruct"
|
"github.com/rclone/rclone/fs/config/configstruct"
|
||||||
"github.com/rclone/rclone/fs/config/obscure"
|
"github.com/rclone/rclone/fs/config/obscure"
|
||||||
"github.com/rclone/rclone/fs/fserrors"
|
|
||||||
"github.com/rclone/rclone/fs/fshttp"
|
|
||||||
"github.com/rclone/rclone/fs/hash"
|
"github.com/rclone/rclone/fs/hash"
|
||||||
"github.com/rclone/rclone/lib/encoder"
|
"github.com/rclone/rclone/lib/encoder"
|
||||||
"github.com/rclone/rclone/lib/env"
|
|
||||||
"github.com/rclone/rclone/lib/pacer"
|
"github.com/rclone/rclone/lib/pacer"
|
||||||
"github.com/rclone/rclone/lib/readers"
|
"github.com/rclone/rclone/lib/readers"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
currentUser = env.CurrentUser()
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
minSleep = 10 * time.Millisecond
|
|
||||||
maxSleep = 2 * time.Second
|
|
||||||
decayConstant = 2 // bigger for slower decay, exponential
|
|
||||||
)
|
|
||||||
|
|
||||||
// Register with Fs
|
// Register with Fs
|
||||||
func init() {
|
func init() {
|
||||||
fs.Register(&fs.RegInfo{
|
fs.Register(&fs.RegInfo{
|
||||||
@@ -56,7 +42,7 @@ func init() {
|
|||||||
}},
|
}},
|
||||||
}, {
|
}, {
|
||||||
Name: "user",
|
Name: "user",
|
||||||
Help: "FTP username, leave blank for current username, " + currentUser,
|
Help: "FTP username, leave blank for current username, " + os.Getenv("USER"),
|
||||||
}, {
|
}, {
|
||||||
Name: "port",
|
Name: "port",
|
||||||
Help: "FTP port, leave blank to use default (21)",
|
Help: "FTP port, leave blank to use default (21)",
|
||||||
@@ -67,16 +53,16 @@ func init() {
|
|||||||
Required: true,
|
Required: true,
|
||||||
}, {
|
}, {
|
||||||
Name: "tls",
|
Name: "tls",
|
||||||
Help: `Use Implicit FTPS (FTP over TLS)
|
Help: `Use FTPS over TLS (Implicit)
|
||||||
When using implicit FTP over TLS the client connects using TLS
|
When using implicit FTP over TLS the client will connect using TLS
|
||||||
right from the start which breaks compatibility with
|
right from the start, which in turn breaks the compatibility with
|
||||||
non-TLS-aware servers. This is usually served over port 990 rather
|
non-TLS-aware servers. This is usually served over port 990 rather
|
||||||
than port 21. Cannot be used in combination with explicit FTP.`,
|
than port 21. Cannot be used in combination with explicit FTP.`,
|
||||||
Default: false,
|
Default: false,
|
||||||
}, {
|
}, {
|
||||||
Name: "explicit_tls",
|
Name: "explicit_tls",
|
||||||
Help: `Use Explicit FTPS (FTP over TLS)
|
Help: `Use FTP over TLS (Explicit)
|
||||||
When using explicit FTP over TLS the client explicitly requests
|
When using explicit FTP over TLS the client explicitly request
|
||||||
security from the server in order to upgrade a plain text connection
|
security from the server in order to upgrade a plain text connection
|
||||||
to an encrypted one. Cannot be used in combination with implicit FTP.`,
|
to an encrypted one. Cannot be used in combination with implicit FTP.`,
|
||||||
Default: false,
|
Default: false,
|
||||||
@@ -95,27 +81,6 @@ to an encrypted one. Cannot be used in combination with implicit FTP.`,
|
|||||||
Help: "Disable using EPSV even if server advertises support",
|
Help: "Disable using EPSV even if server advertises support",
|
||||||
Default: false,
|
Default: false,
|
||||||
Advanced: true,
|
Advanced: true,
|
||||||
}, {
|
|
||||||
Name: "disable_mlsd",
|
|
||||||
Help: "Disable using MLSD even if server advertises support",
|
|
||||||
Default: false,
|
|
||||||
Advanced: true,
|
|
||||||
}, {
|
|
||||||
Name: "idle_timeout",
|
|
||||||
Default: fs.Duration(60 * time.Second),
|
|
||||||
Help: `Max time before closing idle connections
|
|
||||||
|
|
||||||
If no connections have been returned to the connection pool in the time
|
|
||||||
given, rclone will empty the connection pool.
|
|
||||||
|
|
||||||
Set to 0 to keep connections indefinitely.
|
|
||||||
`,
|
|
||||||
Advanced: true,
|
|
||||||
}, {
|
|
||||||
Name: "close_timeout",
|
|
||||||
Help: "Maximum time to wait for a response to close.",
|
|
||||||
Default: fs.Duration(60 * time.Second),
|
|
||||||
Advanced: true,
|
|
||||||
}, {
|
}, {
|
||||||
Name: config.ConfigEncoding,
|
Name: config.ConfigEncoding,
|
||||||
Help: config.ConfigEncodingHelp,
|
Help: config.ConfigEncodingHelp,
|
||||||
@@ -142,9 +107,6 @@ type Options struct {
|
|||||||
Concurrency int `config:"concurrency"`
|
Concurrency int `config:"concurrency"`
|
||||||
SkipVerifyTLSCert bool `config:"no_check_certificate"`
|
SkipVerifyTLSCert bool `config:"no_check_certificate"`
|
||||||
DisableEPSV bool `config:"disable_epsv"`
|
DisableEPSV bool `config:"disable_epsv"`
|
||||||
DisableMLSD bool `config:"disable_mlsd"`
|
|
||||||
IdleTimeout fs.Duration `config:"idle_timeout"`
|
|
||||||
CloseTimeout fs.Duration `config:"close_timeout"`
|
|
||||||
Enc encoder.MultiEncoder `config:"encoding"`
|
Enc encoder.MultiEncoder `config:"encoding"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,7 +115,6 @@ type Fs struct {
|
|||||||
name string // name of this remote
|
name string // name of this remote
|
||||||
root string // the path we are working on if any
|
root string // the path we are working on if any
|
||||||
opt Options // parsed options
|
opt Options // parsed options
|
||||||
ci *fs.ConfigInfo // global config
|
|
||||||
features *fs.Features // optional features
|
features *fs.Features // optional features
|
||||||
url string
|
url string
|
||||||
user string
|
user string
|
||||||
@@ -161,10 +122,7 @@ type Fs struct {
|
|||||||
dialAddr string
|
dialAddr string
|
||||||
poolMu sync.Mutex
|
poolMu sync.Mutex
|
||||||
pool []*ftp.ServerConn
|
pool []*ftp.ServerConn
|
||||||
drain *time.Timer // used to drain the pool when we stop using the connections
|
|
||||||
tokens *pacer.TokenDispenser
|
tokens *pacer.TokenDispenser
|
||||||
tlsConf *tls.Config
|
|
||||||
pacer *fs.Pacer // pacer for FTP connections
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Object describes an FTP file
|
// Object describes an FTP file
|
||||||
@@ -241,86 +199,51 @@ func (dl *debugLog) Write(p []byte) (n int, err error) {
|
|||||||
return len(p), nil
|
return len(p), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type dialCtx struct {
|
|
||||||
f *Fs
|
|
||||||
ctx context.Context
|
|
||||||
}
|
|
||||||
|
|
||||||
// dial a new connection with fshttp dialer
|
|
||||||
func (d *dialCtx) dial(network, address string) (net.Conn, error) {
|
|
||||||
conn, err := fshttp.NewDialer(d.ctx).Dial(network, address)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if d.f.tlsConf != nil {
|
|
||||||
conn = tls.Client(conn, d.f.tlsConf)
|
|
||||||
}
|
|
||||||
return conn, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// shouldRetry returns a boolean as to whether this err deserve to be
|
|
||||||
// retried. It returns the err as a convenience
|
|
||||||
func shouldRetry(ctx context.Context, err error) (bool, error) {
|
|
||||||
if fserrors.ContextError(ctx, &err) {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
switch errX := err.(type) {
|
|
||||||
case *textproto.Error:
|
|
||||||
switch errX.Code {
|
|
||||||
case ftp.StatusNotAvailable:
|
|
||||||
return true, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fserrors.ShouldRetry(err), err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open a new connection to the FTP server.
|
// Open a new connection to the FTP server.
|
||||||
func (f *Fs) ftpConnection(ctx context.Context) (c *ftp.ServerConn, err error) {
|
func (f *Fs) ftpConnection() (*ftp.ServerConn, error) {
|
||||||
fs.Debugf(f, "Connecting to FTP server")
|
fs.Debugf(f, "Connecting to FTP server")
|
||||||
dCtx := dialCtx{f, ctx}
|
ftpConfig := []ftp.DialOption{ftp.DialWithTimeout(fs.Config.ConnectTimeout)}
|
||||||
ftpConfig := []ftp.DialOption{ftp.DialWithDialFunc(dCtx.dial)}
|
if f.opt.TLS && f.opt.ExplicitTLS {
|
||||||
if f.opt.ExplicitTLS {
|
fs.Errorf(f, "Implicit TLS and explicit TLS are mutually incompatible. Please revise your config")
|
||||||
ftpConfig = append(ftpConfig, ftp.DialWithExplicitTLS(f.tlsConf))
|
return nil, errors.New("Implicit TLS and explicit TLS are mutually incompatible. Please revise your config")
|
||||||
// Initial connection needs to be cleartext for explicit TLS
|
} else if f.opt.TLS {
|
||||||
conn, err := fshttp.NewDialer(ctx).Dial("tcp", f.dialAddr)
|
tlsConfig := &tls.Config{
|
||||||
if err != nil {
|
ServerName: f.opt.Host,
|
||||||
return nil, err
|
InsecureSkipVerify: f.opt.SkipVerifyTLSCert,
|
||||||
}
|
}
|
||||||
ftpConfig = append(ftpConfig, ftp.DialWithNetConn(conn))
|
ftpConfig = append(ftpConfig, ftp.DialWithTLS(tlsConfig))
|
||||||
|
} else if f.opt.ExplicitTLS {
|
||||||
|
tlsConfig := &tls.Config{
|
||||||
|
ServerName: f.opt.Host,
|
||||||
|
InsecureSkipVerify: f.opt.SkipVerifyTLSCert,
|
||||||
|
}
|
||||||
|
ftpConfig = append(ftpConfig, ftp.DialWithExplicitTLS(tlsConfig))
|
||||||
}
|
}
|
||||||
if f.opt.DisableEPSV {
|
if f.opt.DisableEPSV {
|
||||||
ftpConfig = append(ftpConfig, ftp.DialWithDisabledEPSV(true))
|
ftpConfig = append(ftpConfig, ftp.DialWithDisabledEPSV(true))
|
||||||
}
|
}
|
||||||
if f.opt.DisableMLSD {
|
if fs.Config.Dump&(fs.DumpHeaders|fs.DumpBodies|fs.DumpRequests|fs.DumpResponses) != 0 {
|
||||||
ftpConfig = append(ftpConfig, ftp.DialWithDisabledMLSD(true))
|
ftpConfig = append(ftpConfig, ftp.DialWithDebugOutput(&debugLog{auth: fs.Config.Dump&fs.DumpAuth != 0}))
|
||||||
}
|
}
|
||||||
if f.ci.Dump&(fs.DumpHeaders|fs.DumpBodies|fs.DumpRequests|fs.DumpResponses) != 0 {
|
c, err := ftp.Dial(f.dialAddr, ftpConfig...)
|
||||||
ftpConfig = append(ftpConfig, ftp.DialWithDebugOutput(&debugLog{auth: f.ci.Dump&fs.DumpAuth != 0}))
|
|
||||||
}
|
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
|
||||||
c, err = ftp.Dial(f.dialAddr, ftpConfig...)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return shouldRetry(ctx, err)
|
fs.Errorf(f, "Error while Dialing %s: %s", f.dialAddr, err)
|
||||||
|
return nil, errors.Wrap(err, "ftpConnection Dial")
|
||||||
}
|
}
|
||||||
err = c.Login(f.user, f.pass)
|
err = c.Login(f.user, f.pass)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = c.Quit()
|
_ = c.Quit()
|
||||||
return shouldRetry(ctx, err)
|
fs.Errorf(f, "Error while Logging in into %s: %s", f.dialAddr, err)
|
||||||
|
return nil, errors.Wrap(err, "ftpConnection Login")
|
||||||
}
|
}
|
||||||
return false, nil
|
return c, nil
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
err = errors.Wrapf(err, "failed to make FTP connection to %q", f.dialAddr)
|
|
||||||
}
|
|
||||||
return c, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get an FTP connection from the pool, or open a new one
|
// Get an FTP connection from the pool, or open a new one
|
||||||
func (f *Fs) getFtpConnection(ctx context.Context) (c *ftp.ServerConn, err error) {
|
func (f *Fs) getFtpConnection() (c *ftp.ServerConn, err error) {
|
||||||
if f.opt.Concurrency > 0 {
|
if f.opt.Concurrency > 0 {
|
||||||
f.tokens.Get()
|
f.tokens.Get()
|
||||||
}
|
}
|
||||||
accounting.LimitTPS(ctx)
|
|
||||||
f.poolMu.Lock()
|
f.poolMu.Lock()
|
||||||
if len(f.pool) > 0 {
|
if len(f.pool) > 0 {
|
||||||
c = f.pool[0]
|
c = f.pool[0]
|
||||||
@@ -330,7 +253,7 @@ func (f *Fs) getFtpConnection(ctx context.Context) (c *ftp.ServerConn, err error
|
|||||||
if c != nil {
|
if c != nil {
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
c, err = f.ftpConnection(ctx)
|
c, err = f.ftpConnection()
|
||||||
if err != nil && f.opt.Concurrency > 0 {
|
if err != nil && f.opt.Concurrency > 0 {
|
||||||
f.tokens.Put()
|
f.tokens.Put()
|
||||||
}
|
}
|
||||||
@@ -369,34 +292,12 @@ func (f *Fs) putFtpConnection(pc **ftp.ServerConn, err error) {
|
|||||||
}
|
}
|
||||||
f.poolMu.Lock()
|
f.poolMu.Lock()
|
||||||
f.pool = append(f.pool, c)
|
f.pool = append(f.pool, c)
|
||||||
if f.opt.IdleTimeout > 0 {
|
|
||||||
f.drain.Reset(time.Duration(f.opt.IdleTimeout)) // nudge on the pool emptying timer
|
|
||||||
}
|
|
||||||
f.poolMu.Unlock()
|
f.poolMu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drain the pool of any connections
|
|
||||||
func (f *Fs) drainPool(ctx context.Context) (err error) {
|
|
||||||
f.poolMu.Lock()
|
|
||||||
defer f.poolMu.Unlock()
|
|
||||||
if f.opt.IdleTimeout > 0 {
|
|
||||||
f.drain.Stop()
|
|
||||||
}
|
|
||||||
if len(f.pool) != 0 {
|
|
||||||
fs.Debugf(f, "closing %d unused connections", len(f.pool))
|
|
||||||
}
|
|
||||||
for i, c := range f.pool {
|
|
||||||
if cErr := c.Quit(); cErr != nil {
|
|
||||||
err = cErr
|
|
||||||
}
|
|
||||||
f.pool[i] = nil
|
|
||||||
}
|
|
||||||
f.pool = nil
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewFs constructs an Fs from the path, container:path
|
// NewFs constructs an Fs from the path, container:path
|
||||||
func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (ff fs.Fs, err error) {
|
func NewFs(name, root string, m configmap.Mapper) (ff fs.Fs, err error) {
|
||||||
|
ctx := context.Background()
|
||||||
// defer fs.Trace(nil, "name=%q, root=%q", name, root)("fs=%v, err=%v", &ff, &err)
|
// defer fs.Trace(nil, "name=%q, root=%q", name, root)("fs=%v, err=%v", &ff, &err)
|
||||||
// Parse config into Options struct
|
// Parse config into Options struct
|
||||||
opt := new(Options)
|
opt := new(Options)
|
||||||
@@ -410,7 +311,7 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (ff fs.Fs
|
|||||||
}
|
}
|
||||||
user := opt.User
|
user := opt.User
|
||||||
if user == "" {
|
if user == "" {
|
||||||
user = currentUser
|
user = os.Getenv("USER")
|
||||||
}
|
}
|
||||||
port := opt.Port
|
port := opt.Port
|
||||||
if port == "" {
|
if port == "" {
|
||||||
@@ -422,40 +323,22 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (ff fs.Fs
|
|||||||
if opt.TLS {
|
if opt.TLS {
|
||||||
protocol = "ftps://"
|
protocol = "ftps://"
|
||||||
}
|
}
|
||||||
if opt.TLS && opt.ExplicitTLS {
|
|
||||||
return nil, errors.New("Implicit TLS and explicit TLS are mutually incompatible. Please revise your config")
|
|
||||||
}
|
|
||||||
var tlsConfig *tls.Config
|
|
||||||
if opt.TLS || opt.ExplicitTLS {
|
|
||||||
tlsConfig = &tls.Config{
|
|
||||||
ServerName: opt.Host,
|
|
||||||
InsecureSkipVerify: opt.SkipVerifyTLSCert,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
u := protocol + path.Join(dialAddr+"/", root)
|
u := protocol + path.Join(dialAddr+"/", root)
|
||||||
ci := fs.GetConfig(ctx)
|
|
||||||
f := &Fs{
|
f := &Fs{
|
||||||
name: name,
|
name: name,
|
||||||
root: root,
|
root: root,
|
||||||
opt: *opt,
|
opt: *opt,
|
||||||
ci: ci,
|
|
||||||
url: u,
|
url: u,
|
||||||
user: user,
|
user: user,
|
||||||
pass: pass,
|
pass: pass,
|
||||||
dialAddr: dialAddr,
|
dialAddr: dialAddr,
|
||||||
tokens: pacer.NewTokenDispenser(opt.Concurrency),
|
tokens: pacer.NewTokenDispenser(opt.Concurrency),
|
||||||
tlsConf: tlsConfig,
|
|
||||||
pacer: fs.NewPacer(ctx, pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))),
|
|
||||||
}
|
}
|
||||||
f.features = (&fs.Features{
|
f.features = (&fs.Features{
|
||||||
CanHaveEmptyDirectories: true,
|
CanHaveEmptyDirectories: true,
|
||||||
}).Fill(ctx, f)
|
}).Fill(f)
|
||||||
// set the pool drainer timer going
|
|
||||||
if f.opt.IdleTimeout > 0 {
|
|
||||||
f.drain = time.AfterFunc(time.Duration(opt.IdleTimeout), func() { _ = f.drainPool(ctx) })
|
|
||||||
}
|
|
||||||
// Make a connection and pool it to return errors early
|
// Make a connection and pool it to return errors early
|
||||||
c, err := f.getFtpConnection(ctx)
|
c, err := f.getFtpConnection()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "NewFs")
|
return nil, errors.Wrap(err, "NewFs")
|
||||||
}
|
}
|
||||||
@@ -482,12 +365,6 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (ff fs.Fs
|
|||||||
return f, err
|
return f, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shutdown the backend, closing any background tasks and any
|
|
||||||
// cached connections.
|
|
||||||
func (f *Fs) Shutdown(ctx context.Context) error {
|
|
||||||
return f.drainPool(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// translateErrorFile turns FTP errors into rclone errors if possible for a file
|
// translateErrorFile turns FTP errors into rclone errors if possible for a file
|
||||||
func translateErrorFile(err error) error {
|
func translateErrorFile(err error) error {
|
||||||
switch errX := err.(type) {
|
switch errX := err.(type) {
|
||||||
@@ -532,7 +409,7 @@ func (f *Fs) dirFromStandardPath(dir string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// findItem finds a directory entry for the name in its parent directory
|
// findItem finds a directory entry for the name in its parent directory
|
||||||
func (f *Fs) findItem(ctx context.Context, remote string) (entry *ftp.Entry, err error) {
|
func (f *Fs) findItem(remote string) (entry *ftp.Entry, err error) {
|
||||||
// defer fs.Trace(remote, "")("o=%v, err=%v", &o, &err)
|
// defer fs.Trace(remote, "")("o=%v, err=%v", &o, &err)
|
||||||
fullPath := path.Join(f.root, remote)
|
fullPath := path.Join(f.root, remote)
|
||||||
if fullPath == "" || fullPath == "." || fullPath == "/" {
|
if fullPath == "" || fullPath == "." || fullPath == "/" {
|
||||||
@@ -546,7 +423,7 @@ func (f *Fs) findItem(ctx context.Context, remote string) (entry *ftp.Entry, err
|
|||||||
dir := path.Dir(fullPath)
|
dir := path.Dir(fullPath)
|
||||||
base := path.Base(fullPath)
|
base := path.Base(fullPath)
|
||||||
|
|
||||||
c, err := f.getFtpConnection(ctx)
|
c, err := f.getFtpConnection()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "findItem")
|
return nil, errors.Wrap(err, "findItem")
|
||||||
}
|
}
|
||||||
@@ -568,7 +445,7 @@ func (f *Fs) findItem(ctx context.Context, remote string) (entry *ftp.Entry, err
|
|||||||
// it returns the error fs.ErrorObjectNotFound.
|
// it returns the error fs.ErrorObjectNotFound.
|
||||||
func (f *Fs) NewObject(ctx context.Context, remote string) (o fs.Object, err error) {
|
func (f *Fs) NewObject(ctx context.Context, remote string) (o fs.Object, err error) {
|
||||||
// defer fs.Trace(remote, "")("o=%v, err=%v", &o, &err)
|
// defer fs.Trace(remote, "")("o=%v, err=%v", &o, &err)
|
||||||
entry, err := f.findItem(ctx, remote)
|
entry, err := f.findItem(remote)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -590,8 +467,8 @@ func (f *Fs) NewObject(ctx context.Context, remote string) (o fs.Object, err err
|
|||||||
}
|
}
|
||||||
|
|
||||||
// dirExists checks the directory pointed to by remote exists or not
|
// dirExists checks the directory pointed to by remote exists or not
|
||||||
func (f *Fs) dirExists(ctx context.Context, remote string) (exists bool, err error) {
|
func (f *Fs) dirExists(remote string) (exists bool, err error) {
|
||||||
entry, err := f.findItem(ctx, remote)
|
entry, err := f.findItem(remote)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, errors.Wrap(err, "dirExists")
|
return false, errors.Wrap(err, "dirExists")
|
||||||
}
|
}
|
||||||
@@ -612,7 +489,7 @@ func (f *Fs) dirExists(ctx context.Context, remote string) (exists bool, err err
|
|||||||
// found.
|
// found.
|
||||||
func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) {
|
func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) {
|
||||||
// defer log.Trace(dir, "dir=%q", dir)("entries=%v, err=%v", &entries, &err)
|
// defer log.Trace(dir, "dir=%q", dir)("entries=%v, err=%v", &entries, &err)
|
||||||
c, err := f.getFtpConnection(ctx)
|
c, err := f.getFtpConnection()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "list")
|
return nil, errors.Wrap(err, "list")
|
||||||
}
|
}
|
||||||
@@ -633,7 +510,7 @@ func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err e
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
// Wait for List for up to Timeout seconds
|
// Wait for List for up to Timeout seconds
|
||||||
timer := time.NewTimer(f.ci.TimeoutOrInfinite())
|
timer := time.NewTimer(fs.Config.Timeout)
|
||||||
select {
|
select {
|
||||||
case listErr = <-errchan:
|
case listErr = <-errchan:
|
||||||
timer.Stop()
|
timer.Stop()
|
||||||
@@ -650,7 +527,7 @@ func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err e
|
|||||||
// doesn't exist, so check it really doesn't exist if no
|
// doesn't exist, so check it really doesn't exist if no
|
||||||
// entries found.
|
// entries found.
|
||||||
if len(files) == 0 {
|
if len(files) == 0 {
|
||||||
exists, err := f.dirExists(ctx, dir)
|
exists, err := f.dirExists(dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "list")
|
return nil, errors.Wrap(err, "list")
|
||||||
}
|
}
|
||||||
@@ -703,7 +580,7 @@ func (f *Fs) Precision() time.Duration {
|
|||||||
// nil and the error
|
// nil and the error
|
||||||
func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
|
func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
|
||||||
// fs.Debugf(f, "Trying to put file %s", src.Remote())
|
// fs.Debugf(f, "Trying to put file %s", src.Remote())
|
||||||
err := f.mkParentDir(ctx, src.Remote())
|
err := f.mkParentDir(src.Remote())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "Put mkParentDir failed")
|
return nil, errors.Wrap(err, "Put mkParentDir failed")
|
||||||
}
|
}
|
||||||
@@ -721,12 +598,12 @@ func (f *Fs) PutStream(ctx context.Context, in io.Reader, src fs.ObjectInfo, opt
|
|||||||
}
|
}
|
||||||
|
|
||||||
// getInfo reads the FileInfo for a path
|
// getInfo reads the FileInfo for a path
|
||||||
func (f *Fs) getInfo(ctx context.Context, remote string) (fi *FileInfo, err error) {
|
func (f *Fs) getInfo(remote string) (fi *FileInfo, err error) {
|
||||||
// defer fs.Trace(remote, "")("fi=%v, err=%v", &fi, &err)
|
// defer fs.Trace(remote, "")("fi=%v, err=%v", &fi, &err)
|
||||||
dir := path.Dir(remote)
|
dir := path.Dir(remote)
|
||||||
base := path.Base(remote)
|
base := path.Base(remote)
|
||||||
|
|
||||||
c, err := f.getFtpConnection(ctx)
|
c, err := f.getFtpConnection()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "getInfo")
|
return nil, errors.Wrap(err, "getInfo")
|
||||||
}
|
}
|
||||||
@@ -753,12 +630,12 @@ func (f *Fs) getInfo(ctx context.Context, remote string) (fi *FileInfo, err erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
// mkdir makes the directory and parents using unrooted paths
|
// mkdir makes the directory and parents using unrooted paths
|
||||||
func (f *Fs) mkdir(ctx context.Context, abspath string) error {
|
func (f *Fs) mkdir(abspath string) error {
|
||||||
abspath = path.Clean(abspath)
|
abspath = path.Clean(abspath)
|
||||||
if abspath == "." || abspath == "/" {
|
if abspath == "." || abspath == "/" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
fi, err := f.getInfo(ctx, abspath)
|
fi, err := f.getInfo(abspath)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if fi.IsDir {
|
if fi.IsDir {
|
||||||
return nil
|
return nil
|
||||||
@@ -768,11 +645,11 @@ func (f *Fs) mkdir(ctx context.Context, abspath string) error {
|
|||||||
return errors.Wrapf(err, "mkdir %q failed", abspath)
|
return errors.Wrapf(err, "mkdir %q failed", abspath)
|
||||||
}
|
}
|
||||||
parent := path.Dir(abspath)
|
parent := path.Dir(abspath)
|
||||||
err = f.mkdir(ctx, parent)
|
err = f.mkdir(parent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
c, connErr := f.getFtpConnection(ctx)
|
c, connErr := f.getFtpConnection()
|
||||||
if connErr != nil {
|
if connErr != nil {
|
||||||
return errors.Wrap(connErr, "mkdir")
|
return errors.Wrap(connErr, "mkdir")
|
||||||
}
|
}
|
||||||
@@ -792,23 +669,23 @@ func (f *Fs) mkdir(ctx context.Context, abspath string) error {
|
|||||||
|
|
||||||
// mkParentDir makes the parent of remote if necessary and any
|
// mkParentDir makes the parent of remote if necessary and any
|
||||||
// directories above that
|
// directories above that
|
||||||
func (f *Fs) mkParentDir(ctx context.Context, remote string) error {
|
func (f *Fs) mkParentDir(remote string) error {
|
||||||
parent := path.Dir(remote)
|
parent := path.Dir(remote)
|
||||||
return f.mkdir(ctx, path.Join(f.root, parent))
|
return f.mkdir(path.Join(f.root, parent))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mkdir creates the directory if it doesn't exist
|
// Mkdir creates the directory if it doesn't exist
|
||||||
func (f *Fs) Mkdir(ctx context.Context, dir string) (err error) {
|
func (f *Fs) Mkdir(ctx context.Context, dir string) (err error) {
|
||||||
// defer fs.Trace(dir, "")("err=%v", &err)
|
// defer fs.Trace(dir, "")("err=%v", &err)
|
||||||
root := path.Join(f.root, dir)
|
root := path.Join(f.root, dir)
|
||||||
return f.mkdir(ctx, root)
|
return f.mkdir(root)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rmdir removes the directory (container, bucket) if empty
|
// Rmdir removes the directory (container, bucket) if empty
|
||||||
//
|
//
|
||||||
// Return an error if it doesn't exist or isn't empty
|
// Return an error if it doesn't exist or isn't empty
|
||||||
func (f *Fs) Rmdir(ctx context.Context, dir string) error {
|
func (f *Fs) Rmdir(ctx context.Context, dir string) error {
|
||||||
c, err := f.getFtpConnection(ctx)
|
c, err := f.getFtpConnection()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(translateErrorFile(err), "Rmdir")
|
return errors.Wrap(translateErrorFile(err), "Rmdir")
|
||||||
}
|
}
|
||||||
@@ -824,11 +701,11 @@ func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object,
|
|||||||
fs.Debugf(src, "Can't move - not same remote type")
|
fs.Debugf(src, "Can't move - not same remote type")
|
||||||
return nil, fs.ErrorCantMove
|
return nil, fs.ErrorCantMove
|
||||||
}
|
}
|
||||||
err := f.mkParentDir(ctx, remote)
|
err := f.mkParentDir(remote)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "Move mkParentDir failed")
|
return nil, errors.Wrap(err, "Move mkParentDir failed")
|
||||||
}
|
}
|
||||||
c, err := f.getFtpConnection(ctx)
|
c, err := f.getFtpConnection()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "Move")
|
return nil, errors.Wrap(err, "Move")
|
||||||
}
|
}
|
||||||
@@ -848,7 +725,7 @@ func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DirMove moves src, srcRemote to this remote at dstRemote
|
// DirMove moves src, srcRemote to this remote at dstRemote
|
||||||
// using server-side move operations.
|
// using server side move operations.
|
||||||
//
|
//
|
||||||
// Will only be called if src.Fs().Name() == f.Name()
|
// Will only be called if src.Fs().Name() == f.Name()
|
||||||
//
|
//
|
||||||
@@ -865,7 +742,7 @@ func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string
|
|||||||
dstPath := path.Join(f.root, dstRemote)
|
dstPath := path.Join(f.root, dstRemote)
|
||||||
|
|
||||||
// Check if destination exists
|
// Check if destination exists
|
||||||
fi, err := f.getInfo(ctx, dstPath)
|
fi, err := f.getInfo(dstPath)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if fi.IsDir {
|
if fi.IsDir {
|
||||||
return fs.ErrorDirExists
|
return fs.ErrorDirExists
|
||||||
@@ -876,13 +753,13 @@ func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Make sure the parent directory exists
|
// Make sure the parent directory exists
|
||||||
err = f.mkdir(ctx, path.Dir(dstPath))
|
err = f.mkdir(path.Dir(dstPath))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "DirMove mkParentDir dst failed")
|
return errors.Wrap(err, "DirMove mkParentDir dst failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do the move
|
// Do the move
|
||||||
c, err := f.getFtpConnection(ctx)
|
c, err := f.getFtpConnection()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "DirMove")
|
return errors.Wrap(err, "DirMove")
|
||||||
}
|
}
|
||||||
@@ -966,8 +843,8 @@ func (f *ftpReadCloser) Close() error {
|
|||||||
go func() {
|
go func() {
|
||||||
errchan <- f.rc.Close()
|
errchan <- f.rc.Close()
|
||||||
}()
|
}()
|
||||||
// Wait for Close for up to 60 seconds by default
|
// Wait for Close for up to 60 seconds
|
||||||
timer := time.NewTimer(time.Duration(f.f.opt.CloseTimeout))
|
timer := time.NewTimer(60 * time.Second)
|
||||||
select {
|
select {
|
||||||
case err = <-errchan:
|
case err = <-errchan:
|
||||||
timer.Stop()
|
timer.Stop()
|
||||||
@@ -1014,7 +891,7 @@ func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (rc io.Read
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c, err := o.fs.getFtpConnection(ctx)
|
c, err := o.fs.getFtpConnection()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "open")
|
return nil, errors.Wrap(err, "open")
|
||||||
}
|
}
|
||||||
@@ -1049,7 +926,7 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
|
|||||||
fs.Debugf(o, "Removed after failed upload: %v", err)
|
fs.Debugf(o, "Removed after failed upload: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c, err := o.fs.getFtpConnection(ctx)
|
c, err := o.fs.getFtpConnection()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "Update")
|
return errors.Wrap(err, "Update")
|
||||||
}
|
}
|
||||||
@@ -1061,7 +938,7 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
|
|||||||
return errors.Wrap(err, "update stor")
|
return errors.Wrap(err, "update stor")
|
||||||
}
|
}
|
||||||
o.fs.putFtpConnection(&c, nil)
|
o.fs.putFtpConnection(&c, nil)
|
||||||
o.info, err = o.fs.getInfo(ctx, path)
|
o.info, err = o.fs.getInfo(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "update getinfo")
|
return errors.Wrap(err, "update getinfo")
|
||||||
}
|
}
|
||||||
@@ -1073,14 +950,14 @@ func (o *Object) Remove(ctx context.Context) (err error) {
|
|||||||
// defer fs.Trace(o, "")("err=%v", &err)
|
// defer fs.Trace(o, "")("err=%v", &err)
|
||||||
path := path.Join(o.fs.root, o.remote)
|
path := path.Join(o.fs.root, o.remote)
|
||||||
// Check if it's a directory or a file
|
// Check if it's a directory or a file
|
||||||
info, err := o.fs.getInfo(ctx, path)
|
info, err := o.fs.getInfo(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if info.IsDir {
|
if info.IsDir {
|
||||||
err = o.fs.Rmdir(ctx, o.remote)
|
err = o.fs.Rmdir(ctx, o.remote)
|
||||||
} else {
|
} else {
|
||||||
c, err := o.fs.getFtpConnection(ctx)
|
c, err := o.fs.getFtpConnection()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "Remove")
|
return errors.Wrap(err, "Remove")
|
||||||
}
|
}
|
||||||
@@ -1096,6 +973,5 @@ var (
|
|||||||
_ fs.Mover = &Fs{}
|
_ fs.Mover = &Fs{}
|
||||||
_ fs.DirMover = &Fs{}
|
_ fs.DirMover = &Fs{}
|
||||||
_ fs.PutStreamer = &Fs{}
|
_ fs.PutStreamer = &Fs{}
|
||||||
_ fs.Shutdowner = &Fs{}
|
|
||||||
_ fs.Object = &Object{}
|
_ fs.Object = &Object{}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -76,14 +76,14 @@ func init() {
|
|||||||
Prefix: "gcs",
|
Prefix: "gcs",
|
||||||
Description: "Google Cloud Storage (this is not Google Drive)",
|
Description: "Google Cloud Storage (this is not Google Drive)",
|
||||||
NewFs: NewFs,
|
NewFs: NewFs,
|
||||||
Config: func(ctx context.Context, name string, m configmap.Mapper) {
|
Config: func(name string, m configmap.Mapper) {
|
||||||
saFile, _ := m.Get("service_account_file")
|
saFile, _ := m.Get("service_account_file")
|
||||||
saCreds, _ := m.Get("service_account_credentials")
|
saCreds, _ := m.Get("service_account_credentials")
|
||||||
anonymous, _ := m.Get("anonymous")
|
anonymous, _ := m.Get("anonymous")
|
||||||
if saFile != "" || saCreds != "" || anonymous == "true" {
|
if saFile != "" || saCreds != "" || anonymous == "true" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err := oauthutil.Config(ctx, "google cloud storage", name, m, storageConfig, nil)
|
err := oauthutil.Config("google cloud storage", name, m, storageConfig, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to configure token: %v", err)
|
log.Fatalf("Failed to configure token: %v", err)
|
||||||
}
|
}
|
||||||
@@ -329,10 +329,7 @@ func (f *Fs) Features() *fs.Features {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// shouldRetry determines whether a given err rates being retried
|
// shouldRetry determines whether a given err rates being retried
|
||||||
func shouldRetry(ctx context.Context, err error) (again bool, errOut error) {
|
func shouldRetry(err error) (again bool, errOut error) {
|
||||||
if fserrors.ContextError(ctx, &err) {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
again = false
|
again = false
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if fserrors.ShouldRetry(err) {
|
if fserrors.ShouldRetry(err) {
|
||||||
@@ -373,12 +370,12 @@ func (o *Object) split() (bucket, bucketPath string) {
|
|||||||
return o.fs.split(o.remote)
|
return o.fs.split(o.remote)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getServiceAccountClient(ctx context.Context, credentialsData []byte) (*http.Client, error) {
|
func getServiceAccountClient(credentialsData []byte) (*http.Client, error) {
|
||||||
conf, err := google.JWTConfigFromJSON(credentialsData, storageConfig.Scopes...)
|
conf, err := google.JWTConfigFromJSON(credentialsData, storageConfig.Scopes...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "error processing credentials")
|
return nil, errors.Wrap(err, "error processing credentials")
|
||||||
}
|
}
|
||||||
ctxWithSpecialClient := oauthutil.Context(ctx, fshttp.NewClient(ctx))
|
ctxWithSpecialClient := oauthutil.Context(fshttp.NewClient(fs.Config))
|
||||||
return oauth2.NewClient(ctxWithSpecialClient, conf.TokenSource(ctxWithSpecialClient)), nil
|
return oauth2.NewClient(ctxWithSpecialClient, conf.TokenSource(ctxWithSpecialClient)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -389,7 +386,8 @@ func (f *Fs) setRoot(root string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewFs constructs an Fs from the path, bucket:path
|
// NewFs constructs an Fs from the path, bucket:path
|
||||||
func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, error) {
|
func NewFs(name, root string, m configmap.Mapper) (fs.Fs, error) {
|
||||||
|
ctx := context.TODO()
|
||||||
var oAuthClient *http.Client
|
var oAuthClient *http.Client
|
||||||
|
|
||||||
// Parse config into Options struct
|
// Parse config into Options struct
|
||||||
@@ -414,14 +412,14 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
opt.ServiceAccountCredentials = string(loadedCreds)
|
opt.ServiceAccountCredentials = string(loadedCreds)
|
||||||
}
|
}
|
||||||
if opt.Anonymous {
|
if opt.Anonymous {
|
||||||
oAuthClient = fshttp.NewClient(ctx)
|
oAuthClient = &http.Client{}
|
||||||
} else if opt.ServiceAccountCredentials != "" {
|
} else if opt.ServiceAccountCredentials != "" {
|
||||||
oAuthClient, err = getServiceAccountClient(ctx, []byte(opt.ServiceAccountCredentials))
|
oAuthClient, err = getServiceAccountClient([]byte(opt.ServiceAccountCredentials))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed configuring Google Cloud Storage Service Account")
|
return nil, errors.Wrap(err, "failed configuring Google Cloud Storage Service Account")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
oAuthClient, _, err = oauthutil.NewClient(ctx, name, m, storageConfig)
|
oAuthClient, _, err = oauthutil.NewClient(name, m, storageConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
oAuthClient, err = google.DefaultClient(ctx, storage.DevstorageFullControlScope)
|
oAuthClient, err = google.DefaultClient(ctx, storage.DevstorageFullControlScope)
|
||||||
@@ -435,7 +433,7 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
name: name,
|
name: name,
|
||||||
root: root,
|
root: root,
|
||||||
opt: *opt,
|
opt: *opt,
|
||||||
pacer: fs.NewPacer(ctx, pacer.NewGoogleDrive(pacer.MinSleep(minSleep))),
|
pacer: fs.NewPacer(pacer.NewGoogleDrive(pacer.MinSleep(minSleep))),
|
||||||
cache: bucket.NewCache(),
|
cache: bucket.NewCache(),
|
||||||
}
|
}
|
||||||
f.setRoot(root)
|
f.setRoot(root)
|
||||||
@@ -444,7 +442,7 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
WriteMimeType: true,
|
WriteMimeType: true,
|
||||||
BucketBased: true,
|
BucketBased: true,
|
||||||
BucketBasedRootOK: true,
|
BucketBasedRootOK: true,
|
||||||
}).Fill(ctx, f)
|
}).Fill(f)
|
||||||
|
|
||||||
// Create a new authorized Drive client.
|
// Create a new authorized Drive client.
|
||||||
f.client = oAuthClient
|
f.client = oAuthClient
|
||||||
@@ -458,7 +456,7 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
encodedDirectory := f.opt.Enc.FromStandardPath(f.rootDirectory)
|
encodedDirectory := f.opt.Enc.FromStandardPath(f.rootDirectory)
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
_, err = f.svc.Objects.Get(f.rootBucket, encodedDirectory).Context(ctx).Do()
|
_, err = f.svc.Objects.Get(f.rootBucket, encodedDirectory).Context(ctx).Do()
|
||||||
return shouldRetry(ctx, err)
|
return shouldRetry(err)
|
||||||
})
|
})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
newRoot := path.Dir(f.root)
|
newRoot := path.Dir(f.root)
|
||||||
@@ -524,7 +522,7 @@ func (f *Fs) list(ctx context.Context, bucket, directory, prefix string, addBuck
|
|||||||
var objects *storage.Objects
|
var objects *storage.Objects
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
objects, err = list.Context(ctx).Do()
|
objects, err = list.Context(ctx).Do()
|
||||||
return shouldRetry(ctx, err)
|
return shouldRetry(err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if gErr, ok := err.(*googleapi.Error); ok {
|
if gErr, ok := err.(*googleapi.Error); ok {
|
||||||
@@ -567,7 +565,7 @@ func (f *Fs) list(ctx context.Context, bucket, directory, prefix string, addBuck
|
|||||||
remote = path.Join(bucket, remote)
|
remote = path.Join(bucket, remote)
|
||||||
}
|
}
|
||||||
// is this a directory marker?
|
// is this a directory marker?
|
||||||
if isDirectory {
|
if isDirectory && object.Size == 0 {
|
||||||
continue // skip directory marker
|
continue // skip directory marker
|
||||||
}
|
}
|
||||||
err = fn(remote, object, false)
|
err = fn(remote, object, false)
|
||||||
@@ -627,7 +625,7 @@ func (f *Fs) listBuckets(ctx context.Context) (entries fs.DirEntries, err error)
|
|||||||
var buckets *storage.Buckets
|
var buckets *storage.Buckets
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
buckets, err = listBuckets.Context(ctx).Do()
|
buckets, err = listBuckets.Context(ctx).Do()
|
||||||
return shouldRetry(ctx, err)
|
return shouldRetry(err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -753,7 +751,7 @@ func (f *Fs) makeBucket(ctx context.Context, bucket string) (err error) {
|
|||||||
// service account that only has the "Storage Object Admin" role. See #2193 for details.
|
// service account that only has the "Storage Object Admin" role. See #2193 for details.
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
_, err = f.svc.Objects.List(bucket).MaxResults(1).Context(ctx).Do()
|
_, err = f.svc.Objects.List(bucket).MaxResults(1).Context(ctx).Do()
|
||||||
return shouldRetry(ctx, err)
|
return shouldRetry(err)
|
||||||
})
|
})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// Bucket already exists
|
// Bucket already exists
|
||||||
@@ -788,7 +786,7 @@ func (f *Fs) makeBucket(ctx context.Context, bucket string) (err error) {
|
|||||||
insertBucket.PredefinedAcl(f.opt.BucketACL)
|
insertBucket.PredefinedAcl(f.opt.BucketACL)
|
||||||
}
|
}
|
||||||
_, err = insertBucket.Context(ctx).Do()
|
_, err = insertBucket.Context(ctx).Do()
|
||||||
return shouldRetry(ctx, err)
|
return shouldRetry(err)
|
||||||
})
|
})
|
||||||
}, nil)
|
}, nil)
|
||||||
}
|
}
|
||||||
@@ -805,7 +803,7 @@ func (f *Fs) Rmdir(ctx context.Context, dir string) (err error) {
|
|||||||
return f.cache.Remove(bucket, func() error {
|
return f.cache.Remove(bucket, func() error {
|
||||||
return f.pacer.Call(func() (bool, error) {
|
return f.pacer.Call(func() (bool, error) {
|
||||||
err = f.svc.Buckets.Delete(bucket).Context(ctx).Do()
|
err = f.svc.Buckets.Delete(bucket).Context(ctx).Do()
|
||||||
return shouldRetry(ctx, err)
|
return shouldRetry(err)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -815,7 +813,7 @@ func (f *Fs) Precision() time.Duration {
|
|||||||
return time.Nanosecond
|
return time.Nanosecond
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy src to this remote using server-side copy operations.
|
// Copy src to this remote using server side copy operations.
|
||||||
//
|
//
|
||||||
// This is stored with the remote path given
|
// This is stored with the remote path given
|
||||||
//
|
//
|
||||||
@@ -851,7 +849,7 @@ func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object,
|
|||||||
for {
|
for {
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
rewriteResponse, err = rewriteRequest.Context(ctx).Do()
|
rewriteResponse, err = rewriteRequest.Context(ctx).Do()
|
||||||
return shouldRetry(ctx, err)
|
return shouldRetry(err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -944,7 +942,7 @@ func (o *Object) readObjectInfo(ctx context.Context) (object *storage.Object, er
|
|||||||
bucket, bucketPath := o.split()
|
bucket, bucketPath := o.split()
|
||||||
err = o.fs.pacer.Call(func() (bool, error) {
|
err = o.fs.pacer.Call(func() (bool, error) {
|
||||||
object, err = o.fs.svc.Objects.Get(bucket, bucketPath).Context(ctx).Do()
|
object, err = o.fs.svc.Objects.Get(bucket, bucketPath).Context(ctx).Do()
|
||||||
return shouldRetry(ctx, err)
|
return shouldRetry(err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if gErr, ok := err.(*googleapi.Error); ok {
|
if gErr, ok := err.(*googleapi.Error); ok {
|
||||||
@@ -1015,7 +1013,7 @@ func (o *Object) SetModTime(ctx context.Context, modTime time.Time) (err error)
|
|||||||
copyObject.DestinationPredefinedAcl(o.fs.opt.ObjectACL)
|
copyObject.DestinationPredefinedAcl(o.fs.opt.ObjectACL)
|
||||||
}
|
}
|
||||||
newObject, err = copyObject.Context(ctx).Do()
|
newObject, err = copyObject.Context(ctx).Do()
|
||||||
return shouldRetry(ctx, err)
|
return shouldRetry(err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1031,10 +1029,11 @@ func (o *Object) Storable() bool {
|
|||||||
|
|
||||||
// Open an object for read
|
// Open an object for read
|
||||||
func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.ReadCloser, err error) {
|
func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.ReadCloser, err error) {
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", o.url, nil)
|
req, err := http.NewRequest("GET", o.url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
req = req.WithContext(ctx) // go1.13 can use NewRequestWithContext
|
||||||
fs.FixRangeOption(options, o.bytes)
|
fs.FixRangeOption(options, o.bytes)
|
||||||
fs.OpenOptionAddHTTPHeaders(req.Header, options)
|
fs.OpenOptionAddHTTPHeaders(req.Header, options)
|
||||||
var res *http.Response
|
var res *http.Response
|
||||||
@@ -1046,7 +1045,7 @@ func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.Read
|
|||||||
_ = res.Body.Close() // ignore error
|
_ = res.Body.Close() // ignore error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return shouldRetry(ctx, err)
|
return shouldRetry(err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -1093,8 +1092,6 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
|
|||||||
object.ContentLanguage = value
|
object.ContentLanguage = value
|
||||||
case "content-type":
|
case "content-type":
|
||||||
object.ContentType = value
|
object.ContentType = value
|
||||||
case "x-goog-storage-class":
|
|
||||||
object.StorageClass = value
|
|
||||||
default:
|
default:
|
||||||
const googMetaPrefix = "x-goog-meta-"
|
const googMetaPrefix = "x-goog-meta-"
|
||||||
if strings.HasPrefix(lowerKey, googMetaPrefix) {
|
if strings.HasPrefix(lowerKey, googMetaPrefix) {
|
||||||
@@ -1112,7 +1109,7 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
|
|||||||
insertObject.PredefinedAcl(o.fs.opt.ObjectACL)
|
insertObject.PredefinedAcl(o.fs.opt.ObjectACL)
|
||||||
}
|
}
|
||||||
newObject, err = insertObject.Context(ctx).Do()
|
newObject, err = insertObject.Context(ctx).Do()
|
||||||
return shouldRetry(ctx, err)
|
return shouldRetry(err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1127,7 +1124,7 @@ func (o *Object) Remove(ctx context.Context) (err error) {
|
|||||||
bucket, bucketPath := o.split()
|
bucket, bucketPath := o.split()
|
||||||
err = o.fs.pacer.Call(func() (bool, error) {
|
err = o.fs.pacer.Call(func() (bool, error) {
|
||||||
err = o.fs.svc.Objects.Delete(bucket, bucketPath).Context(ctx).Do()
|
err = o.fs.svc.Objects.Delete(bucket, bucketPath).Context(ctx).Do()
|
||||||
return shouldRetry(ctx, err)
|
return shouldRetry(err)
|
||||||
})
|
})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ func init() {
|
|||||||
Prefix: "gphotos",
|
Prefix: "gphotos",
|
||||||
Description: "Google Photos",
|
Description: "Google Photos",
|
||||||
NewFs: NewFs,
|
NewFs: NewFs,
|
||||||
Config: func(ctx context.Context, name string, m configmap.Mapper) {
|
Config: func(name string, m configmap.Mapper) {
|
||||||
// Parse config into Options struct
|
// Parse config into Options struct
|
||||||
opt := new(Options)
|
opt := new(Options)
|
||||||
err := configstruct.Set(m, opt)
|
err := configstruct.Set(m, opt)
|
||||||
@@ -95,7 +95,7 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Do the oauth
|
// Do the oauth
|
||||||
err = oauthutil.Config(ctx, "google photos", name, m, oauthConfig, nil)
|
err = oauthutil.Config("google photos", name, m, oauthConfig, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
golog.Fatalf("Failed to configure token: %v", err)
|
golog.Fatalf("Failed to configure token: %v", err)
|
||||||
}
|
}
|
||||||
@@ -132,23 +132,6 @@ you want to read the media.`,
|
|||||||
Default: 2000,
|
Default: 2000,
|
||||||
Help: `Year limits the photos to be downloaded to those which are uploaded after the given year`,
|
Help: `Year limits the photos to be downloaded to those which are uploaded after the given year`,
|
||||||
Advanced: true,
|
Advanced: true,
|
||||||
}, {
|
|
||||||
Name: "include_archived",
|
|
||||||
Default: false,
|
|
||||||
Help: `Also view and download archived media.
|
|
||||||
|
|
||||||
By default rclone does not request archived media. Thus, when syncing,
|
|
||||||
archived media is not visible in directory listings or transferred.
|
|
||||||
|
|
||||||
Note that media in albums is always visible and synced, no matter
|
|
||||||
their archive status.
|
|
||||||
|
|
||||||
With this flag, archived media are always visible in directory
|
|
||||||
listings and transferred.
|
|
||||||
|
|
||||||
Without this flag, archived media will not be visible in directory
|
|
||||||
listings and won't be transferred.`,
|
|
||||||
Advanced: true,
|
|
||||||
}}...),
|
}}...),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -158,7 +141,6 @@ type Options struct {
|
|||||||
ReadOnly bool `config:"read_only"`
|
ReadOnly bool `config:"read_only"`
|
||||||
ReadSize bool `config:"read_size"`
|
ReadSize bool `config:"read_size"`
|
||||||
StartYear int `config:"start_year"`
|
StartYear int `config:"start_year"`
|
||||||
IncludeArchived bool `config:"include_archived"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fs represents a remote storage server
|
// Fs represents a remote storage server
|
||||||
@@ -224,10 +206,6 @@ func (f *Fs) startYear() int {
|
|||||||
return f.opt.StartYear
|
return f.opt.StartYear
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fs) includeArchived() bool {
|
|
||||||
return f.opt.IncludeArchived
|
|
||||||
}
|
|
||||||
|
|
||||||
// retryErrorCodes is a slice of error codes that we will retry
|
// retryErrorCodes is a slice of error codes that we will retry
|
||||||
var retryErrorCodes = []int{
|
var retryErrorCodes = []int{
|
||||||
429, // Too Many Requests.
|
429, // Too Many Requests.
|
||||||
@@ -240,10 +218,7 @@ var retryErrorCodes = []int{
|
|||||||
|
|
||||||
// shouldRetry returns a boolean as to whether this resp and err
|
// shouldRetry returns a boolean as to whether this resp and err
|
||||||
// deserve to be retried. It returns the err as a convenience
|
// deserve to be retried. It returns the err as a convenience
|
||||||
func shouldRetry(ctx context.Context, resp *http.Response, err error) (bool, error) {
|
func shouldRetry(resp *http.Response, err error) (bool, error) {
|
||||||
if fserrors.ContextError(ctx, &err) {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
return fserrors.ShouldRetry(err) || fserrors.ShouldRetryHTTP(resp, retryErrorCodes), err
|
return fserrors.ShouldRetry(err) || fserrors.ShouldRetryHTTP(resp, retryErrorCodes), err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,7 +246,7 @@ func errorHandler(resp *http.Response) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewFs constructs an Fs from the path, bucket:path
|
// NewFs constructs an Fs from the path, bucket:path
|
||||||
func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, error) {
|
func NewFs(name, root string, m configmap.Mapper) (fs.Fs, error) {
|
||||||
// Parse config into Options struct
|
// Parse config into Options struct
|
||||||
opt := new(Options)
|
opt := new(Options)
|
||||||
err := configstruct.Set(m, opt)
|
err := configstruct.Set(m, opt)
|
||||||
@@ -279,8 +254,8 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
baseClient := fshttp.NewClient(ctx)
|
baseClient := fshttp.NewClient(fs.Config)
|
||||||
oAuthClient, ts, err := oauthutil.NewClientWithBaseClient(ctx, name, m, oauthConfig, baseClient)
|
oAuthClient, ts, err := oauthutil.NewClientWithBaseClient(name, m, oauthConfig, baseClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to configure Box")
|
return nil, errors.Wrap(err, "failed to configure Box")
|
||||||
}
|
}
|
||||||
@@ -297,14 +272,14 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
unAuth: rest.NewClient(baseClient),
|
unAuth: rest.NewClient(baseClient),
|
||||||
srv: rest.NewClient(oAuthClient).SetRoot(rootURL),
|
srv: rest.NewClient(oAuthClient).SetRoot(rootURL),
|
||||||
ts: ts,
|
ts: ts,
|
||||||
pacer: fs.NewPacer(ctx, pacer.NewGoogleDrive(pacer.MinSleep(minSleep))),
|
pacer: fs.NewPacer(pacer.NewGoogleDrive(pacer.MinSleep(minSleep))),
|
||||||
startTime: time.Now(),
|
startTime: time.Now(),
|
||||||
albums: map[bool]*albums{},
|
albums: map[bool]*albums{},
|
||||||
uploaded: dirtree.New(),
|
uploaded: dirtree.New(),
|
||||||
}
|
}
|
||||||
f.features = (&fs.Features{
|
f.features = (&fs.Features{
|
||||||
ReadMimeType: true,
|
ReadMimeType: true,
|
||||||
}).Fill(ctx, f)
|
}).Fill(f)
|
||||||
f.srv.SetErrorHandler(errorHandler)
|
f.srv.SetErrorHandler(errorHandler)
|
||||||
|
|
||||||
_, _, pattern := patterns.match(f.root, "", true)
|
_, _, pattern := patterns.match(f.root, "", true)
|
||||||
@@ -313,7 +288,7 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
var leaf string
|
var leaf string
|
||||||
f.root, leaf = path.Split(f.root)
|
f.root, leaf = path.Split(f.root)
|
||||||
f.root = strings.TrimRight(f.root, "/")
|
f.root = strings.TrimRight(f.root, "/")
|
||||||
_, err := f.NewObject(ctx, leaf)
|
_, err := f.NewObject(context.TODO(), leaf)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return f, fs.ErrorIsFile
|
return f, fs.ErrorIsFile
|
||||||
}
|
}
|
||||||
@@ -332,7 +307,7 @@ func (f *Fs) fetchEndpoint(ctx context.Context, name string) (endpoint string, e
|
|||||||
var openIDconfig map[string]interface{}
|
var openIDconfig map[string]interface{}
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err := f.unAuth.CallJSON(ctx, &opts, nil, &openIDconfig)
|
resp, err := f.unAuth.CallJSON(ctx, &opts, nil, &openIDconfig)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.Wrap(err, "couldn't read openID config")
|
return "", errors.Wrap(err, "couldn't read openID config")
|
||||||
@@ -361,7 +336,7 @@ func (f *Fs) UserInfo(ctx context.Context) (userInfo map[string]string, err erro
|
|||||||
}
|
}
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err := f.srv.CallJSON(ctx, &opts, nil, &userInfo)
|
resp, err := f.srv.CallJSON(ctx, &opts, nil, &userInfo)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "couldn't read user info")
|
return nil, errors.Wrap(err, "couldn't read user info")
|
||||||
@@ -392,7 +367,7 @@ func (f *Fs) Disconnect(ctx context.Context) (err error) {
|
|||||||
var res interface{}
|
var res interface{}
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err := f.srv.CallJSON(ctx, &opts, nil, &res)
|
resp, err := f.srv.CallJSON(ctx, &opts, nil, &res)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "couldn't revoke token")
|
return errors.Wrap(err, "couldn't revoke token")
|
||||||
@@ -479,7 +454,7 @@ func (f *Fs) listAlbums(ctx context.Context, shared bool) (all *albums, err erro
|
|||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "couldn't list albums")
|
return nil, errors.Wrap(err, "couldn't list albums")
|
||||||
@@ -522,19 +497,13 @@ func (f *Fs) list(ctx context.Context, filter api.SearchFilter, fn listFn) (err
|
|||||||
}
|
}
|
||||||
filter.PageSize = listChunks
|
filter.PageSize = listChunks
|
||||||
filter.PageToken = ""
|
filter.PageToken = ""
|
||||||
if filter.AlbumID == "" { // album ID and filters cannot be set together, else error 400 INVALID_ARGUMENT
|
|
||||||
if filter.Filters == nil {
|
|
||||||
filter.Filters = &api.Filters{}
|
|
||||||
}
|
|
||||||
filter.Filters.IncludeArchivedMedia = &f.opt.IncludeArchived
|
|
||||||
}
|
|
||||||
lastID := ""
|
lastID := ""
|
||||||
for {
|
for {
|
||||||
var result api.MediaItems
|
var result api.MediaItems
|
||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, &filter, &result)
|
resp, err = f.srv.CallJSON(ctx, &opts, &filter, &result)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "couldn't list files")
|
return errors.Wrap(err, "couldn't list files")
|
||||||
@@ -678,7 +647,7 @@ func (f *Fs) createAlbum(ctx context.Context, albumTitle string) (album *api.Alb
|
|||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, request, &result)
|
resp, err = f.srv.CallJSON(ctx, &opts, request, &result)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "couldn't create album")
|
return nil, errors.Wrap(err, "couldn't create album")
|
||||||
@@ -813,7 +782,7 @@ func (o *Object) Size() int64 {
|
|||||||
}
|
}
|
||||||
err = o.fs.pacer.Call(func() (bool, error) {
|
err = o.fs.pacer.Call(func() (bool, error) {
|
||||||
resp, err = o.fs.srv.Call(ctx, &opts)
|
resp, err = o.fs.srv.Call(ctx, &opts)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fs.Debugf(o, "Reading size failed: %v", err)
|
fs.Debugf(o, "Reading size failed: %v", err)
|
||||||
@@ -864,7 +833,7 @@ func (o *Object) readMetaData(ctx context.Context) (err error) {
|
|||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
err = o.fs.pacer.Call(func() (bool, error) {
|
err = o.fs.pacer.Call(func() (bool, error) {
|
||||||
resp, err = o.fs.srv.CallJSON(ctx, &opts, nil, &item)
|
resp, err = o.fs.srv.CallJSON(ctx, &opts, nil, &item)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "couldn't get media item")
|
return errors.Wrap(err, "couldn't get media item")
|
||||||
@@ -941,7 +910,7 @@ func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.Read
|
|||||||
}
|
}
|
||||||
err = o.fs.pacer.Call(func() (bool, error) {
|
err = o.fs.pacer.Call(func() (bool, error) {
|
||||||
resp, err = o.fs.srv.Call(ctx, &opts)
|
resp, err = o.fs.srv.Call(ctx, &opts)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -996,10 +965,10 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
|
|||||||
err = o.fs.pacer.CallNoRetry(func() (bool, error) {
|
err = o.fs.pacer.CallNoRetry(func() (bool, error) {
|
||||||
resp, err = o.fs.srv.Call(ctx, &opts)
|
resp, err = o.fs.srv.Call(ctx, &opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
}
|
}
|
||||||
token, err = rest.ReadBody(resp)
|
token, err = rest.ReadBody(resp)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "couldn't upload file")
|
return errors.Wrap(err, "couldn't upload file")
|
||||||
@@ -1027,7 +996,7 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
|
|||||||
var result api.BatchCreateResponse
|
var result api.BatchCreateResponse
|
||||||
err = o.fs.pacer.Call(func() (bool, error) {
|
err = o.fs.pacer.Call(func() (bool, error) {
|
||||||
resp, err = o.fs.srv.CallJSON(ctx, &opts, request, &result)
|
resp, err = o.fs.srv.CallJSON(ctx, &opts, request, &result)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to create media item")
|
return errors.Wrap(err, "failed to create media item")
|
||||||
@@ -1072,7 +1041,7 @@ func (o *Object) Remove(ctx context.Context) (err error) {
|
|||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
err = o.fs.pacer.Call(func() (bool, error) {
|
err = o.fs.pacer.Call(func() (bool, error) {
|
||||||
resp, err = o.fs.srv.CallJSON(ctx, &opts, &request, nil)
|
resp, err = o.fs.srv.CallJSON(ctx, &opts, &request, nil)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "couldn't delete item from album")
|
return errors.Wrap(err, "couldn't delete item from album")
|
||||||
|
|||||||
@@ -35,14 +35,14 @@ func TestIntegration(t *testing.T) {
|
|||||||
if *fstest.RemoteName == "" {
|
if *fstest.RemoteName == "" {
|
||||||
*fstest.RemoteName = "TestGooglePhotos:"
|
*fstest.RemoteName = "TestGooglePhotos:"
|
||||||
}
|
}
|
||||||
f, err := fs.NewFs(ctx, *fstest.RemoteName)
|
f, err := fs.NewFs(*fstest.RemoteName)
|
||||||
if err == fs.ErrorNotFoundInConfigFile {
|
if err == fs.ErrorNotFoundInConfigFile {
|
||||||
t.Skip(fmt.Sprintf("Couldn't create google photos backend - skipping tests: %v", err))
|
t.Skip(fmt.Sprintf("Couldn't create google photos backend - skipping tests: %v", err))
|
||||||
}
|
}
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Create local Fs pointing at testfiles
|
// Create local Fs pointing at testfiles
|
||||||
localFs, err := fs.NewFs(ctx, "testfiles")
|
localFs, err := fs.NewFs("testfiles")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
t.Run("CreateAlbum", func(t *testing.T) {
|
t.Run("CreateAlbum", func(t *testing.T) {
|
||||||
@@ -115,7 +115,7 @@ func TestIntegration(t *testing.T) {
|
|||||||
assert.Equal(t, "2013-07-26 08:57:21 +0000 UTC", entries[0].ModTime(ctx).String())
|
assert.Equal(t, "2013-07-26 08:57:21 +0000 UTC", entries[0].ModTime(ctx).String())
|
||||||
})
|
})
|
||||||
|
|
||||||
// Check it is there in the date/month/year hierarchy
|
// Check it is there in the date/month/year heirachy
|
||||||
// 2013-07-13 is the creation date of the folder
|
// 2013-07-13 is the creation date of the folder
|
||||||
checkPresent := func(t *testing.T, objPath string) {
|
checkPresent := func(t *testing.T, objPath string) {
|
||||||
entries, err := f.List(ctx, objPath)
|
entries, err := f.List(ctx, objPath)
|
||||||
@@ -155,7 +155,7 @@ func TestIntegration(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("NewFsIsFile", func(t *testing.T) {
|
t.Run("NewFsIsFile", func(t *testing.T) {
|
||||||
fNew, err := fs.NewFs(ctx, *fstest.RemoteName+remote)
|
fNew, err := fs.NewFs(*fstest.RemoteName + remote)
|
||||||
assert.Equal(t, fs.ErrorIsFile, err)
|
assert.Equal(t, fs.ErrorIsFile, err)
|
||||||
leaf := path.Base(remote)
|
leaf := path.Base(remote)
|
||||||
o, err := fNew.NewObject(ctx, leaf)
|
o, err := fNew.NewObject(ctx, leaf)
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ type lister interface {
|
|||||||
listUploads(ctx context.Context, dir string) (entries fs.DirEntries, err error)
|
listUploads(ctx context.Context, dir string) (entries fs.DirEntries, err error)
|
||||||
dirTime() time.Time
|
dirTime() time.Time
|
||||||
startYear() int
|
startYear() int
|
||||||
includeArchived() bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// dirPattern describes a single directory pattern
|
// dirPattern describes a single directory pattern
|
||||||
|
|||||||
@@ -64,11 +64,6 @@ func (f *testLister) startYear() int {
|
|||||||
return 2000
|
return 2000
|
||||||
}
|
}
|
||||||
|
|
||||||
// mock includeArchived for testing
|
|
||||||
func (f *testLister) includeArchived() bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPatternMatch(t *testing.T) {
|
func TestPatternMatch(t *testing.T) {
|
||||||
for testNumber, test := range []struct {
|
for testNumber, test := range []struct {
|
||||||
// input
|
// input
|
||||||
|
|||||||
@@ -1,320 +0,0 @@
|
|||||||
// +build !plan9
|
|
||||||
|
|
||||||
package hdfs
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"os/user"
|
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/colinmarc/hdfs/v2"
|
|
||||||
krb "github.com/jcmturner/gokrb5/v8/client"
|
|
||||||
"github.com/jcmturner/gokrb5/v8/config"
|
|
||||||
"github.com/jcmturner/gokrb5/v8/credentials"
|
|
||||||
"github.com/rclone/rclone/fs"
|
|
||||||
"github.com/rclone/rclone/fs/config/configmap"
|
|
||||||
"github.com/rclone/rclone/fs/config/configstruct"
|
|
||||||
"github.com/rclone/rclone/fs/hash"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Fs represents a HDFS server
|
|
||||||
type Fs struct {
|
|
||||||
name string
|
|
||||||
root string
|
|
||||||
features *fs.Features // optional features
|
|
||||||
opt Options // options for this backend
|
|
||||||
ci *fs.ConfigInfo // global config
|
|
||||||
client *hdfs.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
// copy-paste from https://github.com/colinmarc/hdfs/blob/master/cmd/hdfs/kerberos.go
|
|
||||||
func getKerberosClient() (*krb.Client, error) {
|
|
||||||
configPath := os.Getenv("KRB5_CONFIG")
|
|
||||||
if configPath == "" {
|
|
||||||
configPath = "/etc/krb5.conf"
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg, err := config.Load(configPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine the ccache location from the environment, falling back to the
|
|
||||||
// default location.
|
|
||||||
ccachePath := os.Getenv("KRB5CCNAME")
|
|
||||||
if strings.Contains(ccachePath, ":") {
|
|
||||||
if strings.HasPrefix(ccachePath, "FILE:") {
|
|
||||||
ccachePath = strings.SplitN(ccachePath, ":", 2)[1]
|
|
||||||
} else {
|
|
||||||
return nil, fmt.Errorf("unusable ccache: %s", ccachePath)
|
|
||||||
}
|
|
||||||
} else if ccachePath == "" {
|
|
||||||
u, err := user.Current()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
ccachePath = fmt.Sprintf("/tmp/krb5cc_%s", u.Uid)
|
|
||||||
}
|
|
||||||
|
|
||||||
ccache, err := credentials.LoadCCache(ccachePath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
client, err := krb.NewFromCCache(ccache, cfg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return client, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewFs constructs an Fs from the path
|
|
||||||
func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, error) {
|
|
||||||
opt := new(Options)
|
|
||||||
err := configstruct.Set(m, opt)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
options := hdfs.ClientOptions{
|
|
||||||
Addresses: []string{opt.Namenode},
|
|
||||||
UseDatanodeHostname: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
if opt.ServicePrincipalName != "" {
|
|
||||||
options.KerberosClient, err = getKerberosClient()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Problem with kerberos authentication: %s", err)
|
|
||||||
}
|
|
||||||
options.KerberosServicePrincipleName = opt.ServicePrincipalName
|
|
||||||
|
|
||||||
if opt.DataTransferProtection != "" {
|
|
||||||
options.DataTransferProtection = opt.DataTransferProtection
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
options.User = opt.Username
|
|
||||||
}
|
|
||||||
|
|
||||||
client, err := hdfs.NewClient(options)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
f := &Fs{
|
|
||||||
name: name,
|
|
||||||
root: root,
|
|
||||||
opt: *opt,
|
|
||||||
ci: fs.GetConfig(ctx),
|
|
||||||
client: client,
|
|
||||||
}
|
|
||||||
|
|
||||||
f.features = (&fs.Features{
|
|
||||||
CanHaveEmptyDirectories: true,
|
|
||||||
}).Fill(ctx, f)
|
|
||||||
|
|
||||||
info, err := f.client.Stat(f.realpath(""))
|
|
||||||
if err == nil && !info.IsDir() {
|
|
||||||
f.root = path.Dir(f.root)
|
|
||||||
return f, fs.ErrorIsFile
|
|
||||||
}
|
|
||||||
|
|
||||||
return f, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Name of this fs
|
|
||||||
func (f *Fs) Name() string {
|
|
||||||
return f.name
|
|
||||||
}
|
|
||||||
|
|
||||||
// Root of the remote (as passed into NewFs)
|
|
||||||
func (f *Fs) Root() string {
|
|
||||||
return f.root
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns a description of the FS
|
|
||||||
func (f *Fs) String() string {
|
|
||||||
return fmt.Sprintf("hdfs://%s", f.opt.Namenode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Features returns the optional features of this Fs
|
|
||||||
func (f *Fs) Features() *fs.Features {
|
|
||||||
return f.features
|
|
||||||
}
|
|
||||||
|
|
||||||
// Precision return the precision of this Fs
|
|
||||||
func (f *Fs) Precision() time.Duration {
|
|
||||||
return time.Second
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hashes are not supported
|
|
||||||
func (f *Fs) Hashes() hash.Set {
|
|
||||||
return hash.Set(hash.None)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewObject finds file at remote or return fs.ErrorObjectNotFound
|
|
||||||
func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) {
|
|
||||||
realpath := f.realpath(remote)
|
|
||||||
fs.Debugf(f, "new [%s]", realpath)
|
|
||||||
|
|
||||||
info, err := f.ensureFile(realpath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Object{
|
|
||||||
fs: f,
|
|
||||||
remote: remote,
|
|
||||||
size: info.Size(),
|
|
||||||
modTime: info.ModTime(),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// List the objects and directories in dir into entries.
|
|
||||||
func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) {
|
|
||||||
realpath := f.realpath(dir)
|
|
||||||
fs.Debugf(f, "list [%s]", realpath)
|
|
||||||
|
|
||||||
err = f.ensureDirectory(realpath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
list, err := f.client.ReadDir(realpath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
for _, x := range list {
|
|
||||||
stdName := f.opt.Enc.ToStandardName(x.Name())
|
|
||||||
remote := path.Join(dir, stdName)
|
|
||||||
if x.IsDir() {
|
|
||||||
entries = append(entries, fs.NewDir(remote, x.ModTime()))
|
|
||||||
} else {
|
|
||||||
entries = append(entries, &Object{
|
|
||||||
fs: f,
|
|
||||||
remote: remote,
|
|
||||||
size: x.Size(),
|
|
||||||
modTime: x.ModTime()})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return entries, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Put the object
|
|
||||||
func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
|
|
||||||
o := &Object{
|
|
||||||
fs: f,
|
|
||||||
remote: src.Remote(),
|
|
||||||
}
|
|
||||||
err := o.Update(ctx, in, src, options...)
|
|
||||||
return o, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// PutStream uploads to the remote path with the modTime given of indeterminate size
|
|
||||||
func (f *Fs) PutStream(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
|
|
||||||
return f.Put(ctx, in, src, options...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mkdir makes a directory
|
|
||||||
func (f *Fs) Mkdir(ctx context.Context, dir string) error {
|
|
||||||
fs.Debugf(f, "mkdir [%s]", f.realpath(dir))
|
|
||||||
return f.client.MkdirAll(f.realpath(dir), 0755)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rmdir deletes the directory
|
|
||||||
func (f *Fs) Rmdir(ctx context.Context, dir string) error {
|
|
||||||
realpath := f.realpath(dir)
|
|
||||||
fs.Debugf(f, "rmdir [%s]", realpath)
|
|
||||||
|
|
||||||
err := f.ensureDirectory(realpath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// do not remove empty directory
|
|
||||||
list, err := f.client.ReadDir(realpath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if len(list) > 0 {
|
|
||||||
return fs.ErrorDirectoryNotEmpty
|
|
||||||
}
|
|
||||||
|
|
||||||
return f.client.Remove(realpath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Purge deletes all the files in the directory
|
|
||||||
func (f *Fs) Purge(ctx context.Context, dir string) error {
|
|
||||||
realpath := f.realpath(dir)
|
|
||||||
fs.Debugf(f, "purge [%s]", realpath)
|
|
||||||
|
|
||||||
err := f.ensureDirectory(realpath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return f.client.RemoveAll(realpath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// About gets quota information from the Fs
|
|
||||||
func (f *Fs) About(ctx context.Context) (*fs.Usage, error) {
|
|
||||||
info, err := f.client.StatFs()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &fs.Usage{
|
|
||||||
Total: fs.NewUsageValue(int64(info.Capacity)),
|
|
||||||
Used: fs.NewUsageValue(int64(info.Used)),
|
|
||||||
Free: fs.NewUsageValue(int64(info.Remaining)),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Fs) ensureDirectory(realpath string) error {
|
|
||||||
info, err := f.client.Stat(realpath)
|
|
||||||
|
|
||||||
if e, ok := err.(*os.PathError); ok && e.Err == os.ErrNotExist {
|
|
||||||
return fs.ErrorDirNotFound
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !info.IsDir() {
|
|
||||||
return fs.ErrorDirNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Fs) ensureFile(realpath string) (os.FileInfo, error) {
|
|
||||||
info, err := f.client.Stat(realpath)
|
|
||||||
|
|
||||||
if e, ok := err.(*os.PathError); ok && e.Err == os.ErrNotExist {
|
|
||||||
return nil, fs.ErrorObjectNotFound
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if info.IsDir() {
|
|
||||||
return nil, fs.ErrorObjectNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
return info, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Fs) realpath(dir string) string {
|
|
||||||
return f.opt.Enc.FromStandardPath(xPath(f.Root(), dir))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the interfaces are satisfied
|
|
||||||
var (
|
|
||||||
_ fs.Fs = (*Fs)(nil)
|
|
||||||
_ fs.Purger = (*Fs)(nil)
|
|
||||||
_ fs.PutStreamer = (*Fs)(nil)
|
|
||||||
_ fs.Abouter = (*Fs)(nil)
|
|
||||||
)
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
// +build !plan9
|
|
||||||
|
|
||||||
package hdfs
|
|
||||||
|
|
||||||
import (
|
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/rclone/rclone/fs"
|
|
||||||
"github.com/rclone/rclone/fs/config"
|
|
||||||
"github.com/rclone/rclone/lib/encoder"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
fsi := &fs.RegInfo{
|
|
||||||
Name: "hdfs",
|
|
||||||
Description: "Hadoop distributed file system",
|
|
||||||
NewFs: NewFs,
|
|
||||||
Options: []fs.Option{{
|
|
||||||
Name: "namenode",
|
|
||||||
Help: "hadoop name node and port",
|
|
||||||
Required: true,
|
|
||||||
Examples: []fs.OptionExample{{
|
|
||||||
Value: "namenode:8020",
|
|
||||||
Help: "Connect to host namenode at port 8020",
|
|
||||||
}},
|
|
||||||
}, {
|
|
||||||
Name: "username",
|
|
||||||
Help: "hadoop user name",
|
|
||||||
Required: false,
|
|
||||||
Examples: []fs.OptionExample{{
|
|
||||||
Value: "root",
|
|
||||||
Help: "Connect to hdfs as root",
|
|
||||||
}},
|
|
||||||
}, {
|
|
||||||
Name: "service_principal_name",
|
|
||||||
Help: `Kerberos service principal name for the namenode
|
|
||||||
|
|
||||||
Enables KERBEROS authentication. Specifies the Service Principal Name
|
|
||||||
(<SERVICE>/<FQDN>) for the namenode.`,
|
|
||||||
Required: false,
|
|
||||||
Examples: []fs.OptionExample{{
|
|
||||||
Value: "hdfs/namenode.hadoop.docker",
|
|
||||||
Help: "Namenode running as service 'hdfs' with FQDN 'namenode.hadoop.docker'.",
|
|
||||||
}},
|
|
||||||
Advanced: true,
|
|
||||||
}, {
|
|
||||||
Name: "data_transfer_protection",
|
|
||||||
Help: `Kerberos data transfer protection: authentication|integrity|privacy
|
|
||||||
|
|
||||||
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.",
|
|
||||||
}},
|
|
||||||
Advanced: true,
|
|
||||||
}, {
|
|
||||||
Name: config.ConfigEncoding,
|
|
||||||
Help: config.ConfigEncodingHelp,
|
|
||||||
Advanced: true,
|
|
||||||
Default: (encoder.Display | encoder.EncodeInvalidUtf8 | encoder.EncodeColon),
|
|
||||||
}},
|
|
||||||
}
|
|
||||||
fs.Register(fsi)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Options for this backend
|
|
||||||
type Options struct {
|
|
||||||
Namenode string `config:"namenode"`
|
|
||||||
Username string `config:"username"`
|
|
||||||
ServicePrincipalName string `config:"service_principal_name"`
|
|
||||||
DataTransferProtection string `config:"data_transfer_protection"`
|
|
||||||
Enc encoder.MultiEncoder `config:"encoding"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// xPath make correct file path with leading '/'
|
|
||||||
func xPath(root string, tail string) string {
|
|
||||||
if !strings.HasPrefix(root, "/") {
|
|
||||||
root = "/" + root
|
|
||||||
}
|
|
||||||
return path.Join(root, tail)
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
// Test HDFS filesystem interface
|
|
||||||
|
|
||||||
// +build !plan9
|
|
||||||
|
|
||||||
package hdfs_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/rclone/rclone/backend/hdfs"
|
|
||||||
"github.com/rclone/rclone/fstest/fstests"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestIntegration runs integration tests against the remote
|
|
||||||
func TestIntegration(t *testing.T) {
|
|
||||||
fstests.Run(t, &fstests.Opt{
|
|
||||||
RemoteName: "TestHdfs:",
|
|
||||||
NilObject: (*hdfs.Object)(nil),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
// Build for hdfs for unsupported platforms to stop go complaining
|
|
||||||
// about "no buildable Go source files "
|
|
||||||
|
|
||||||
// +build plan9
|
|
||||||
|
|
||||||
package hdfs
|
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
// +build !plan9
|
|
||||||
|
|
||||||
package hdfs
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"io"
|
|
||||||
"path"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/rclone/rclone/fs"
|
|
||||||
"github.com/rclone/rclone/fs/hash"
|
|
||||||
"github.com/rclone/rclone/lib/readers"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Object describes an HDFS file
|
|
||||||
type Object struct {
|
|
||||||
fs *Fs
|
|
||||||
remote string
|
|
||||||
size int64
|
|
||||||
modTime time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fs returns the parent Fs
|
|
||||||
func (o *Object) Fs() fs.Info {
|
|
||||||
return o.fs
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remote returns the remote path
|
|
||||||
func (o *Object) Remote() string {
|
|
||||||
return o.remote
|
|
||||||
}
|
|
||||||
|
|
||||||
// Size returns the size of an object in bytes
|
|
||||||
func (o *Object) Size() int64 {
|
|
||||||
return o.size
|
|
||||||
}
|
|
||||||
|
|
||||||
// ModTime returns the modification time of the object
|
|
||||||
func (o *Object) ModTime(ctx context.Context) time.Time {
|
|
||||||
return o.modTime
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetModTime sets the modification time of the local fs object
|
|
||||||
func (o *Object) SetModTime(ctx context.Context, modTime time.Time) error {
|
|
||||||
realpath := o.fs.realpath(o.Remote())
|
|
||||||
err := o.fs.client.Chtimes(realpath, modTime, modTime)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
o.modTime = modTime
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Storable returns whether this object is storable
|
|
||||||
func (o *Object) Storable() bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return a string version
|
|
||||||
func (o *Object) String() string {
|
|
||||||
if o == nil {
|
|
||||||
return "<nil>"
|
|
||||||
}
|
|
||||||
return o.Remote()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hash is not supported
|
|
||||||
func (o *Object) Hash(ctx context.Context, r hash.Type) (string, error) {
|
|
||||||
return "", hash.ErrUnsupported
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open an object for read
|
|
||||||
func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.ReadCloser, err error) {
|
|
||||||
realpath := o.realpath()
|
|
||||||
fs.Debugf(o.fs, "open [%s]", realpath)
|
|
||||||
f, err := o.fs.client.Open(realpath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var offset, limit int64 = 0, -1
|
|
||||||
for _, option := range options {
|
|
||||||
switch x := option.(type) {
|
|
||||||
case *fs.SeekOption:
|
|
||||||
offset = x.Offset
|
|
||||||
case *fs.RangeOption:
|
|
||||||
offset, limit = x.Decode(o.Size())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = f.Seek(offset, io.SeekStart)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if limit != -1 {
|
|
||||||
in = readers.NewLimitedReadCloser(f, limit)
|
|
||||||
} else {
|
|
||||||
in = f
|
|
||||||
}
|
|
||||||
|
|
||||||
return in, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update object
|
|
||||||
func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) error {
|
|
||||||
realpath := o.fs.realpath(src.Remote())
|
|
||||||
dirname := path.Dir(realpath)
|
|
||||||
fs.Debugf(o.fs, "update [%s]", realpath)
|
|
||||||
|
|
||||||
err := o.fs.client.MkdirAll(dirname, 0755)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
info, err := o.fs.client.Stat(realpath)
|
|
||||||
if err == nil {
|
|
||||||
err = o.fs.client.Remove(realpath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
out, err := o.fs.client.Create(realpath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup := func() {
|
|
||||||
rerr := o.fs.client.Remove(realpath)
|
|
||||||
if rerr != nil {
|
|
||||||
fs.Errorf(o.fs, "failed to remove [%v]: %v", realpath, rerr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = io.Copy(out, in)
|
|
||||||
if err != nil {
|
|
||||||
cleanup()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = out.Close()
|
|
||||||
if err != nil {
|
|
||||||
cleanup()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
info, err = o.fs.client.Stat(realpath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = o.SetModTime(ctx, src.ModTime(ctx))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
o.size = info.Size()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove an object
|
|
||||||
func (o *Object) Remove(ctx context.Context) error {
|
|
||||||
realpath := o.fs.realpath(o.remote)
|
|
||||||
fs.Debugf(o.fs, "remove [%s]", realpath)
|
|
||||||
return o.fs.client.Remove(realpath)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *Object) realpath() string {
|
|
||||||
return o.fs.opt.Enc.FromStandardPath(xPath(o.Fs().Root(), o.remote))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the interfaces are satisfied
|
|
||||||
var (
|
|
||||||
_ fs.Object = (*Object)(nil)
|
|
||||||
)
|
|
||||||
@@ -58,7 +58,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"'.
|
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, eg '"Cookie","name=value","Authorization","xxx"'.
|
||||||
`,
|
`,
|
||||||
Default: fs.CommaSepList{},
|
Default: fs.CommaSepList{},
|
||||||
Advanced: true,
|
Advanced: true,
|
||||||
@@ -117,7 +117,6 @@ type Fs struct {
|
|||||||
root string
|
root string
|
||||||
features *fs.Features // optional features
|
features *fs.Features // optional features
|
||||||
opt Options // options for this backend
|
opt Options // options for this backend
|
||||||
ci *fs.ConfigInfo // global config
|
|
||||||
endpoint *url.URL
|
endpoint *url.URL
|
||||||
endpointURL string // endpoint as a string
|
endpointURL string // endpoint as a string
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
@@ -146,7 +145,8 @@ func statusError(res *http.Response, err error) error {
|
|||||||
|
|
||||||
// NewFs creates a new Fs object from the name and root. It connects to
|
// NewFs creates a new Fs object from the name and root. It connects to
|
||||||
// the host specified in the config file.
|
// the host specified in the config file.
|
||||||
func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, error) {
|
func NewFs(name, root string, m configmap.Mapper) (fs.Fs, error) {
|
||||||
|
ctx := context.TODO()
|
||||||
// Parse config into Options struct
|
// Parse config into Options struct
|
||||||
opt := new(Options)
|
opt := new(Options)
|
||||||
err := configstruct.Set(m, opt)
|
err := configstruct.Set(m, opt)
|
||||||
@@ -172,7 +172,7 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
client := fshttp.NewClient(ctx)
|
client := fshttp.NewClient(fs.Config)
|
||||||
|
|
||||||
var isFile = false
|
var isFile = false
|
||||||
if !strings.HasSuffix(u.String(), "/") {
|
if !strings.HasSuffix(u.String(), "/") {
|
||||||
@@ -183,8 +183,9 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
return http.ErrUseLastResponse
|
return http.ErrUseLastResponse
|
||||||
}
|
}
|
||||||
// check to see if points to a file
|
// check to see if points to a file
|
||||||
req, err := http.NewRequestWithContext(ctx, "HEAD", u.String(), nil)
|
req, err := http.NewRequest("HEAD", u.String(), nil)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
req = req.WithContext(ctx) // go1.13 can use NewRequestWithContext
|
||||||
addHeaders(req, opt)
|
addHeaders(req, opt)
|
||||||
res, err := noRedir.Do(req)
|
res, err := noRedir.Do(req)
|
||||||
err = statusError(res, err)
|
err = statusError(res, err)
|
||||||
@@ -209,19 +210,17 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
ci := fs.GetConfig(ctx)
|
|
||||||
f := &Fs{
|
f := &Fs{
|
||||||
name: name,
|
name: name,
|
||||||
root: root,
|
root: root,
|
||||||
opt: *opt,
|
opt: *opt,
|
||||||
ci: ci,
|
|
||||||
httpClient: client,
|
httpClient: client,
|
||||||
endpoint: u,
|
endpoint: u,
|
||||||
endpointURL: u.String(),
|
endpointURL: u.String(),
|
||||||
}
|
}
|
||||||
f.features = (&fs.Features{
|
f.features = (&fs.Features{
|
||||||
CanHaveEmptyDirectories: true,
|
CanHaveEmptyDirectories: true,
|
||||||
}).Fill(ctx, f)
|
}).Fill(f)
|
||||||
if isFile {
|
if isFile {
|
||||||
return f, fs.ErrorIsFile
|
return f, fs.ErrorIsFile
|
||||||
}
|
}
|
||||||
@@ -390,10 +389,11 @@ func (f *Fs) readDir(ctx context.Context, dir string) (names []string, err error
|
|||||||
return nil, errors.Errorf("internal error: readDir URL %q didn't end in /", URL)
|
return nil, errors.Errorf("internal error: readDir URL %q didn't end in /", URL)
|
||||||
}
|
}
|
||||||
// Do the request
|
// Do the request
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", URL, nil)
|
req, err := http.NewRequest("GET", URL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "readDir failed")
|
return nil, errors.Wrap(err, "readDir failed")
|
||||||
}
|
}
|
||||||
|
req = req.WithContext(ctx) // go1.13 can use NewRequestWithContext
|
||||||
f.addHeaders(req)
|
f.addHeaders(req)
|
||||||
res, err := f.httpClient.Do(req)
|
res, err := f.httpClient.Do(req)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -440,15 +440,14 @@ func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err e
|
|||||||
var (
|
var (
|
||||||
entriesMu sync.Mutex // to protect entries
|
entriesMu sync.Mutex // to protect entries
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
checkers = f.ci.Checkers
|
in = make(chan string, fs.Config.Checkers)
|
||||||
in = make(chan string, checkers)
|
|
||||||
)
|
)
|
||||||
add := func(entry fs.DirEntry) {
|
add := func(entry fs.DirEntry) {
|
||||||
entriesMu.Lock()
|
entriesMu.Lock()
|
||||||
entries = append(entries, entry)
|
entries = append(entries, entry)
|
||||||
entriesMu.Unlock()
|
entriesMu.Unlock()
|
||||||
}
|
}
|
||||||
for i := 0; i < checkers; i++ {
|
for i := 0; i < fs.Config.Checkers; i++ {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
@@ -545,10 +544,11 @@ func (o *Object) stat(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
url := o.url()
|
url := o.url()
|
||||||
req, err := http.NewRequestWithContext(ctx, "HEAD", url, nil)
|
req, err := http.NewRequest("HEAD", url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "stat failed")
|
return errors.Wrap(err, "stat failed")
|
||||||
}
|
}
|
||||||
|
req = req.WithContext(ctx) // go1.13 can use NewRequestWithContext
|
||||||
o.fs.addHeaders(req)
|
o.fs.addHeaders(req)
|
||||||
res, err := o.fs.httpClient.Do(req)
|
res, err := o.fs.httpClient.Do(req)
|
||||||
if err == nil && res.StatusCode == http.StatusNotFound {
|
if err == nil && res.StatusCode == http.StatusNotFound {
|
||||||
@@ -585,7 +585,7 @@ func (o *Object) SetModTime(ctx context.Context, modTime time.Time) error {
|
|||||||
return errorReadOnly
|
return errorReadOnly
|
||||||
}
|
}
|
||||||
|
|
||||||
// Storable returns whether the remote http file is a regular file (not a directory, symbolic link, block device, character device, named pipe, etc.)
|
// Storable returns whether the remote http file is a regular file (not a directory, symbolic link, block device, character device, named pipe, etc)
|
||||||
func (o *Object) Storable() bool {
|
func (o *Object) Storable() bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -593,10 +593,11 @@ func (o *Object) Storable() bool {
|
|||||||
// Open a remote http file object for reading. Seek is supported
|
// Open a remote http file object for reading. Seek is supported
|
||||||
func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.ReadCloser, err error) {
|
func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.ReadCloser, err error) {
|
||||||
url := o.url()
|
url := o.url()
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "Open failed")
|
return nil, errors.Wrap(err, "Open failed")
|
||||||
}
|
}
|
||||||
|
req = req.WithContext(ctx) // go1.13 can use NewRequestWithContext
|
||||||
|
|
||||||
// Add optional headers
|
// Add optional headers
|
||||||
for k, v := range fs.OpenOptionHeaders(options) {
|
for k, v := range fs.OpenOptionHeaders(options) {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/rclone/rclone/fs"
|
"github.com/rclone/rclone/fs"
|
||||||
"github.com/rclone/rclone/fs/config/configfile"
|
"github.com/rclone/rclone/fs/config"
|
||||||
"github.com/rclone/rclone/fs/config/configmap"
|
"github.com/rclone/rclone/fs/config/configmap"
|
||||||
"github.com/rclone/rclone/fstest"
|
"github.com/rclone/rclone/fstest"
|
||||||
"github.com/rclone/rclone/lib/rest"
|
"github.com/rclone/rclone/lib/rest"
|
||||||
@@ -47,7 +47,7 @@ func prepareServer(t *testing.T) (configmap.Simple, func()) {
|
|||||||
ts := httptest.NewServer(handler)
|
ts := httptest.NewServer(handler)
|
||||||
|
|
||||||
// Configure the remote
|
// Configure the remote
|
||||||
configfile.LoadConfig(context.Background())
|
config.LoadConfig()
|
||||||
// fs.Config.LogLevel = fs.LogLevelDebug
|
// fs.Config.LogLevel = fs.LogLevelDebug
|
||||||
// fs.Config.DumpHeaders = true
|
// fs.Config.DumpHeaders = true
|
||||||
// fs.Config.DumpBodies = true
|
// fs.Config.DumpBodies = true
|
||||||
@@ -69,7 +69,7 @@ func prepare(t *testing.T) (fs.Fs, func()) {
|
|||||||
m, tidy := prepareServer(t)
|
m, tidy := prepareServer(t)
|
||||||
|
|
||||||
// Instantiate it
|
// Instantiate it
|
||||||
f, err := NewFs(context.Background(), remoteName, "", m)
|
f, err := NewFs(remoteName, "", m)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
return f, tidy
|
return f, tidy
|
||||||
@@ -214,7 +214,7 @@ func TestIsAFileRoot(t *testing.T) {
|
|||||||
m, tidy := prepareServer(t)
|
m, tidy := prepareServer(t)
|
||||||
defer tidy()
|
defer tidy()
|
||||||
|
|
||||||
f, err := NewFs(context.Background(), remoteName, "one%.txt", m)
|
f, err := NewFs(remoteName, "one%.txt", m)
|
||||||
assert.Equal(t, err, fs.ErrorIsFile)
|
assert.Equal(t, err, fs.ErrorIsFile)
|
||||||
|
|
||||||
testListRoot(t, f, false)
|
testListRoot(t, f, false)
|
||||||
@@ -224,7 +224,7 @@ func TestIsAFileSubDir(t *testing.T) {
|
|||||||
m, tidy := prepareServer(t)
|
m, tidy := prepareServer(t)
|
||||||
defer tidy()
|
defer tidy()
|
||||||
|
|
||||||
f, err := NewFs(context.Background(), remoteName, "three/underthree.txt", m)
|
f, err := NewFs(remoteName, "three/underthree.txt", m)
|
||||||
assert.Equal(t, err, fs.ErrorIsFile)
|
assert.Equal(t, err, fs.ErrorIsFile)
|
||||||
|
|
||||||
entries, err := f.List(context.Background(), "")
|
entries, err := f.List(context.Background(), "")
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ncw/swift/v2"
|
"github.com/ncw/swift"
|
||||||
"github.com/rclone/rclone/fs"
|
"github.com/rclone/rclone/fs"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ func newAuth(f *Fs) *auth {
|
|||||||
// Request constructs an http.Request for authentication
|
// Request constructs an http.Request for authentication
|
||||||
//
|
//
|
||||||
// returns nil for not needed
|
// returns nil for not needed
|
||||||
func (a *auth) Request(ctx context.Context, c *swift.Connection) (r *http.Request, err error) {
|
func (a *auth) Request(*swift.Connection) (r *http.Request, err error) {
|
||||||
const retries = 10
|
const retries = 10
|
||||||
for try := 1; try <= retries; try++ {
|
for try := 1; try <= retries; try++ {
|
||||||
err = a.f.getCredentials(context.TODO())
|
err = a.f.getCredentials(context.TODO())
|
||||||
@@ -38,7 +38,7 @@ func (a *auth) Request(ctx context.Context, c *swift.Connection) (r *http.Reques
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Response parses the result of an http request
|
// Response parses the result of an http request
|
||||||
func (a *auth) Response(ctx context.Context, resp *http.Response) error {
|
func (a *auth) Response(resp *http.Response) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ package hubic
|
|||||||
|
|
||||||
// This uses the normal swift mechanism to update the credentials and
|
// This uses the normal swift mechanism to update the credentials and
|
||||||
// ignores the expires field returned by the Hubic API. This may need
|
// ignores the expires field returned by the Hubic API. This may need
|
||||||
// to be revisited after some actual experience.
|
// to be revisted after some actual experience.
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -16,7 +16,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
swiftLib "github.com/ncw/swift/v2"
|
swiftLib "github.com/ncw/swift"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/rclone/rclone/backend/swift"
|
"github.com/rclone/rclone/backend/swift"
|
||||||
"github.com/rclone/rclone/fs"
|
"github.com/rclone/rclone/fs"
|
||||||
@@ -56,8 +56,8 @@ func init() {
|
|||||||
Name: "hubic",
|
Name: "hubic",
|
||||||
Description: "Hubic",
|
Description: "Hubic",
|
||||||
NewFs: NewFs,
|
NewFs: NewFs,
|
||||||
Config: func(ctx context.Context, name string, m configmap.Mapper) {
|
Config: func(name string, m configmap.Mapper) {
|
||||||
err := oauthutil.Config(ctx, "hubic", name, m, oauthConfig, nil)
|
err := oauthutil.Config("hubic", name, m, oauthConfig, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to configure token: %v", err)
|
log.Fatalf("Failed to configure token: %v", err)
|
||||||
}
|
}
|
||||||
@@ -71,7 +71,7 @@ func init() {
|
|||||||
type credentials struct {
|
type credentials struct {
|
||||||
Token string `json:"token"` // OpenStack token
|
Token string `json:"token"` // OpenStack token
|
||||||
Endpoint string `json:"endpoint"` // OpenStack endpoint
|
Endpoint string `json:"endpoint"` // OpenStack endpoint
|
||||||
Expires string `json:"expires"` // Expires date - e.g. "2015-11-09T14:24:56+01:00"
|
Expires string `json:"expires"` // Expires date - eg "2015-11-09T14:24:56+01:00"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fs represents a remote hubic
|
// Fs represents a remote hubic
|
||||||
@@ -110,10 +110,11 @@ func (f *Fs) String() string {
|
|||||||
//
|
//
|
||||||
// The credentials are read into the Fs
|
// The credentials are read into the Fs
|
||||||
func (f *Fs) getCredentials(ctx context.Context) (err error) {
|
func (f *Fs) getCredentials(ctx context.Context) (err error) {
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.hubic.com/1.0/account/credentials", nil)
|
req, err := http.NewRequest("GET", "https://api.hubic.com/1.0/account/credentials", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
req = req.WithContext(ctx) // go1.13 can use NewRequestWithContext
|
||||||
resp, err := f.client.Do(req)
|
resp, err := f.client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -145,8 +146,8 @@ func (f *Fs) getCredentials(ctx context.Context) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewFs constructs an Fs from the path, container:path
|
// NewFs constructs an Fs from the path, container:path
|
||||||
func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, error) {
|
func NewFs(name, root string, m configmap.Mapper) (fs.Fs, error) {
|
||||||
client, _, err := oauthutil.NewClient(ctx, name, m, oauthConfig)
|
client, _, err := oauthutil.NewClient(name, m, oauthConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to configure Hubic")
|
return nil, errors.Wrap(err, "failed to configure Hubic")
|
||||||
}
|
}
|
||||||
@@ -156,14 +157,13 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Make the swift Connection
|
// Make the swift Connection
|
||||||
ci := fs.GetConfig(ctx)
|
|
||||||
c := &swiftLib.Connection{
|
c := &swiftLib.Connection{
|
||||||
Auth: newAuth(f),
|
Auth: newAuth(f),
|
||||||
ConnectTimeout: 10 * ci.ConnectTimeout, // Use the timeouts in the transport
|
ConnectTimeout: 10 * fs.Config.ConnectTimeout, // Use the timeouts in the transport
|
||||||
Timeout: 10 * ci.Timeout, // Use the timeouts in the transport
|
Timeout: 10 * fs.Config.Timeout, // Use the timeouts in the transport
|
||||||
Transport: fshttp.NewTransport(ctx),
|
Transport: fshttp.NewTransport(fs.Config),
|
||||||
}
|
}
|
||||||
err = c.Authenticate(ctx)
|
err = c.Authenticate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "error authenticating swift connection")
|
return nil, errors.Wrap(err, "error authenticating swift connection")
|
||||||
}
|
}
|
||||||
@@ -176,7 +176,7 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Make inner swift Fs from the connection
|
// Make inner swift Fs from the connection
|
||||||
swiftFs, err := swift.NewFsWithConnection(ctx, opt, name, root, c, true)
|
swiftFs, err := swift.NewFsWithConnection(opt, name, root, c, true)
|
||||||
if err != nil && err != fs.ErrorIsFile {
|
if err != nil && err != fs.ErrorIsFile {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,9 +153,9 @@ type CustomerInfo struct {
|
|||||||
AccountType string `json:"account_type"`
|
AccountType string `json:"account_type"`
|
||||||
SubscriptionType string `json:"subscription_type"`
|
SubscriptionType string `json:"subscription_type"`
|
||||||
Usage int64 `json:"usage"`
|
Usage int64 `json:"usage"`
|
||||||
Quota int64 `json:"quota"`
|
Qouta int64 `json:"quota"`
|
||||||
BusinessUsage int64 `json:"business_usage"`
|
BusinessUsage int64 `json:"business_usage"`
|
||||||
BusinessQuota int64 `json:"business_quota"`
|
BusinessQouta int64 `json:"business_quota"`
|
||||||
WriteLocked bool `json:"write_locked"`
|
WriteLocked bool `json:"write_locked"`
|
||||||
ReadLocked bool `json:"read_locked"`
|
ReadLocked bool `json:"read_locked"`
|
||||||
LockedCause interface{} `json:"locked_cause"`
|
LockedCause interface{} `json:"locked_cause"`
|
||||||
@@ -386,7 +386,7 @@ type Error struct {
|
|||||||
Cause string `xml:"cause"`
|
Cause string `xml:"cause"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error returns a string for the error and satisfies the error interface
|
// Error returns a string for the error and statistifes the error interface
|
||||||
func (e *Error) Error() string {
|
func (e *Error) Error() string {
|
||||||
out := fmt.Sprintf("error %d", e.StatusCode)
|
out := fmt.Sprintf("error %d", e.StatusCode)
|
||||||
if e.Message != "" {
|
if e.Message != "" {
|
||||||
|
|||||||
@@ -63,10 +63,6 @@ const (
|
|||||||
v1ClientID = "nibfk8biu12ju7hpqomr8b1e40"
|
v1ClientID = "nibfk8biu12ju7hpqomr8b1e40"
|
||||||
v1EncryptedClientSecret = "Vp8eAv7eVElMnQwN-kgU9cbhgApNDaMqWdlDi5qFydlQoji4JBxrGMF2"
|
v1EncryptedClientSecret = "Vp8eAv7eVElMnQwN-kgU9cbhgApNDaMqWdlDi5qFydlQoji4JBxrGMF2"
|
||||||
v1configVersion = 0
|
v1configVersion = 0
|
||||||
|
|
||||||
teliaCloudTokenURL = "https://cloud-auth.telia.se/auth/realms/telia_se/protocol/openid-connect/token"
|
|
||||||
teliaCloudAuthURL = "https://cloud-auth.telia.se/auth/realms/telia_se/protocol/openid-connect/auth"
|
|
||||||
teliaCloudClientID = "desktop"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -87,7 +83,9 @@ func init() {
|
|||||||
Name: "jottacloud",
|
Name: "jottacloud",
|
||||||
Description: "Jottacloud",
|
Description: "Jottacloud",
|
||||||
NewFs: NewFs,
|
NewFs: NewFs,
|
||||||
Config: func(ctx context.Context, name string, m configmap.Mapper) {
|
Config: func(name string, m configmap.Mapper) {
|
||||||
|
ctx := context.TODO()
|
||||||
|
|
||||||
refresh := false
|
refresh := false
|
||||||
if version, ok := m.Get("configVersion"); ok {
|
if version, ok := m.Get("configVersion"); ok {
|
||||||
ver, err := strconv.Atoi(version)
|
ver, err := strconv.Atoi(version)
|
||||||
@@ -109,18 +107,11 @@ func init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Choose authentication type:\n" +
|
fmt.Printf("Use legacy authentification?.\nThis is only required for certain whitelabel versions of Jottacloud and not recommended for normal users.\n")
|
||||||
"1: Standard authentication - use this if you're a normal Jottacloud user.\n" +
|
if config.Confirm(false) {
|
||||||
"2: Legacy authentication - this is only required for certain whitelabel versions of Jottacloud and not recommended for normal users.\n" +
|
|
||||||
"3: Telia Cloud authentication - use this if you are using Telia Cloud.\n")
|
|
||||||
|
|
||||||
switch config.ChooseNumber("Your choice", 1, 3) {
|
|
||||||
case 1:
|
|
||||||
v2config(ctx, name, m)
|
|
||||||
case 2:
|
|
||||||
v1config(ctx, name, m)
|
v1config(ctx, name, m)
|
||||||
case 3:
|
} else {
|
||||||
teliaCloudConfig(ctx, name, m)
|
v2config(ctx, name, m)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Options: []fs.Option{{
|
Options: []fs.Option{{
|
||||||
@@ -235,56 +226,13 @@ var retryErrorCodes = []int{
|
|||||||
|
|
||||||
// shouldRetry returns a boolean as to whether this resp and err
|
// shouldRetry returns a boolean as to whether this resp and err
|
||||||
// deserve to be retried. It returns the err as a convenience
|
// deserve to be retried. It returns the err as a convenience
|
||||||
func shouldRetry(ctx context.Context, resp *http.Response, err error) (bool, error) {
|
func shouldRetry(resp *http.Response, err error) (bool, error) {
|
||||||
if fserrors.ContextError(ctx, &err) {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
return fserrors.ShouldRetry(err) || fserrors.ShouldRetryHTTP(resp, retryErrorCodes), err
|
return fserrors.ShouldRetry(err) || fserrors.ShouldRetryHTTP(resp, retryErrorCodes), err
|
||||||
}
|
}
|
||||||
|
|
||||||
func teliaCloudConfig(ctx context.Context, name string, m configmap.Mapper) {
|
// v1config configure a jottacloud backend using legacy authentification
|
||||||
teliaCloudOauthConfig := &oauth2.Config{
|
|
||||||
Endpoint: oauth2.Endpoint{
|
|
||||||
AuthURL: teliaCloudAuthURL,
|
|
||||||
TokenURL: teliaCloudTokenURL,
|
|
||||||
},
|
|
||||||
ClientID: teliaCloudClientID,
|
|
||||||
Scopes: []string{"openid", "jotta-default", "offline_access"},
|
|
||||||
RedirectURL: oauthutil.RedirectLocalhostURL,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := oauthutil.Config(ctx, "jottacloud", name, m, teliaCloudOauthConfig, nil)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to configure token: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("\nDo you want to use a non standard device/mountpoint e.g. for accessing files uploaded using the official Jottacloud client?\n\n")
|
|
||||||
if config.Confirm(false) {
|
|
||||||
oAuthClient, _, err := oauthutil.NewClient(ctx, name, m, teliaCloudOauthConfig)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to load oAuthClient: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
srv := rest.NewClient(oAuthClient).SetRoot(rootURL)
|
|
||||||
apiSrv := rest.NewClient(oAuthClient).SetRoot(apiURL)
|
|
||||||
|
|
||||||
device, mountpoint, err := setupMountpoint(ctx, srv, apiSrv)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to setup mountpoint: %s", err)
|
|
||||||
}
|
|
||||||
m.Set(configDevice, device)
|
|
||||||
m.Set(configMountpoint, mountpoint)
|
|
||||||
}
|
|
||||||
|
|
||||||
m.Set("configVersion", strconv.Itoa(configVersion))
|
|
||||||
m.Set(configClientID, teliaCloudClientID)
|
|
||||||
m.Set(configTokenURL, teliaCloudTokenURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
// v1config configure a jottacloud backend using legacy authentication
|
|
||||||
func v1config(ctx context.Context, name string, m configmap.Mapper) {
|
func v1config(ctx context.Context, name string, m configmap.Mapper) {
|
||||||
srv := rest.NewClient(fshttp.NewClient(ctx))
|
srv := rest.NewClient(fshttp.NewClient(fs.Config))
|
||||||
|
|
||||||
fmt.Printf("\nDo you want to create a machine specific API key?\n\nRclone has it's own Jottacloud API KEY which works fine as long as one only uses rclone on a single machine. When you want to use rclone with this account on more than one machine it's recommended to create a machine specific API key. These keys can NOT be shared between machines.\n\n")
|
fmt.Printf("\nDo you want to create a machine specific API key?\n\nRclone has it's own Jottacloud API KEY which works fine as long as one only uses rclone on a single machine. When you want to use rclone with this account on more than one machine it's recommended to create a machine specific API key. These keys can NOT be shared between machines.\n\n")
|
||||||
if config.Confirm(false) {
|
if config.Confirm(false) {
|
||||||
@@ -327,7 +275,7 @@ func v1config(ctx context.Context, name string, m configmap.Mapper) {
|
|||||||
|
|
||||||
fmt.Printf("\nDo you want to use a non standard device/mountpoint e.g. for accessing files uploaded using the official Jottacloud client?\n\n")
|
fmt.Printf("\nDo you want to use a non standard device/mountpoint e.g. for accessing files uploaded using the official Jottacloud client?\n\n")
|
||||||
if config.Confirm(false) {
|
if config.Confirm(false) {
|
||||||
oAuthClient, _, err := oauthutil.NewClient(ctx, name, m, oauthConfig)
|
oAuthClient, _, err := oauthutil.NewClient(name, m, oauthConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to load oAuthClient: %s", err)
|
log.Fatalf("Failed to load oAuthClient: %s", err)
|
||||||
}
|
}
|
||||||
@@ -375,7 +323,7 @@ func registerDevice(ctx context.Context, srv *rest.Client) (reg *api.DeviceRegis
|
|||||||
return deviceRegistration, err
|
return deviceRegistration, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// doAuthV1 runs the actual token request for V1 authentication
|
// doAuthV1 runs the actual token request for V1 authentification
|
||||||
func doAuthV1(ctx context.Context, srv *rest.Client, username, password string) (token oauth2.Token, err error) {
|
func doAuthV1(ctx context.Context, srv *rest.Client, username, password string) (token oauth2.Token, err error) {
|
||||||
// prepare out token request with username and password
|
// prepare out token request with username and password
|
||||||
values := url.Values{}
|
values := url.Values{}
|
||||||
@@ -417,9 +365,9 @@ func doAuthV1(ctx context.Context, srv *rest.Client, username, password string)
|
|||||||
return token, err
|
return token, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// v2config configure a jottacloud backend using the modern JottaCli token based authentication
|
// v2config configure a jottacloud backend using the modern JottaCli token based authentification
|
||||||
func v2config(ctx context.Context, name string, m configmap.Mapper) {
|
func v2config(ctx context.Context, name string, m configmap.Mapper) {
|
||||||
srv := rest.NewClient(fshttp.NewClient(ctx))
|
srv := rest.NewClient(fshttp.NewClient(fs.Config))
|
||||||
|
|
||||||
fmt.Printf("Generate a personal login token here: https://www.jottacloud.com/web/secure\n")
|
fmt.Printf("Generate a personal login token here: https://www.jottacloud.com/web/secure\n")
|
||||||
fmt.Printf("Login Token> ")
|
fmt.Printf("Login Token> ")
|
||||||
@@ -439,7 +387,7 @@ func v2config(ctx context.Context, name string, m configmap.Mapper) {
|
|||||||
|
|
||||||
fmt.Printf("\nDo you want to use a non standard device/mountpoint e.g. for accessing files uploaded using the official Jottacloud client?\n\n")
|
fmt.Printf("\nDo you want to use a non standard device/mountpoint e.g. for accessing files uploaded using the official Jottacloud client?\n\n")
|
||||||
if config.Confirm(false) {
|
if config.Confirm(false) {
|
||||||
oAuthClient, _, err := oauthutil.NewClient(ctx, name, m, oauthConfig)
|
oAuthClient, _, err := oauthutil.NewClient(name, m, oauthConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to load oAuthClient: %s", err)
|
log.Fatalf("Failed to load oAuthClient: %s", err)
|
||||||
}
|
}
|
||||||
@@ -457,7 +405,7 @@ func v2config(ctx context.Context, name string, m configmap.Mapper) {
|
|||||||
m.Set("configVersion", strconv.Itoa(configVersion))
|
m.Set("configVersion", strconv.Itoa(configVersion))
|
||||||
}
|
}
|
||||||
|
|
||||||
// doAuthV2 runs the actual token request for V2 authentication
|
// doAuthV2 runs the actual token request for V2 authentification
|
||||||
func doAuthV2(ctx context.Context, srv *rest.Client, loginTokenBase64 string, m configmap.Mapper) (token oauth2.Token, err error) {
|
func doAuthV2(ctx context.Context, srv *rest.Client, loginTokenBase64 string, m configmap.Mapper) (token oauth2.Token, err error) {
|
||||||
loginTokenBytes, err := base64.RawURLEncoding.DecodeString(loginTokenBase64)
|
loginTokenBytes, err := base64.RawURLEncoding.DecodeString(loginTokenBase64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -618,7 +566,7 @@ func (f *Fs) readMetaDataForPath(ctx context.Context, path string) (info *api.Jo
|
|||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallXML(ctx, &opts, nil, &result)
|
resp, err = f.srv.CallXML(ctx, &opts, nil, &result)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
if apiErr, ok := err.(*api.Error); ok {
|
if apiErr, ok := err.(*api.Error); ok {
|
||||||
@@ -693,7 +641,8 @@ func grantTypeFilter(req *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewFs constructs an Fs from the path, container:path
|
// NewFs constructs an Fs from the path, container:path
|
||||||
func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, error) {
|
func NewFs(name, root string, m configmap.Mapper) (fs.Fs, error) {
|
||||||
|
ctx := context.TODO()
|
||||||
// Parse config into Options struct
|
// Parse config into Options struct
|
||||||
opt := new(Options)
|
opt := new(Options)
|
||||||
err := configstruct.Set(m, opt)
|
err := configstruct.Set(m, opt)
|
||||||
@@ -715,7 +664,7 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
return nil, errors.New("Outdated config - please reconfigure this backend")
|
return nil, errors.New("Outdated config - please reconfigure this backend")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseClient := fshttp.NewClient(ctx)
|
baseClient := fshttp.NewClient(fs.Config)
|
||||||
|
|
||||||
if ver == configVersion {
|
if ver == configVersion {
|
||||||
oauthConfig.ClientID = "jottacli"
|
oauthConfig.ClientID = "jottacli"
|
||||||
@@ -751,7 +700,7 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create OAuth Client
|
// Create OAuth Client
|
||||||
oAuthClient, ts, err := oauthutil.NewClientWithBaseClient(ctx, name, m, oauthConfig, baseClient)
|
oAuthClient, ts, err := oauthutil.NewClientWithBaseClient(name, m, oauthConfig, baseClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "Failed to configure Jottacloud oauth client")
|
return nil, errors.Wrap(err, "Failed to configure Jottacloud oauth client")
|
||||||
}
|
}
|
||||||
@@ -765,14 +714,14 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
opt: *opt,
|
opt: *opt,
|
||||||
srv: rest.NewClient(oAuthClient).SetRoot(rootURL),
|
srv: rest.NewClient(oAuthClient).SetRoot(rootURL),
|
||||||
apiSrv: rest.NewClient(oAuthClient).SetRoot(apiURL),
|
apiSrv: rest.NewClient(oAuthClient).SetRoot(apiURL),
|
||||||
pacer: fs.NewPacer(ctx, pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))),
|
pacer: fs.NewPacer(pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))),
|
||||||
}
|
}
|
||||||
f.features = (&fs.Features{
|
f.features = (&fs.Features{
|
||||||
CaseInsensitive: true,
|
CaseInsensitive: true,
|
||||||
CanHaveEmptyDirectories: true,
|
CanHaveEmptyDirectories: true,
|
||||||
ReadMimeType: true,
|
ReadMimeType: true,
|
||||||
WriteMimeType: false,
|
WriteMimeType: true,
|
||||||
}).Fill(ctx, f)
|
}).Fill(f)
|
||||||
f.srv.SetErrorHandler(errorHandler)
|
f.srv.SetErrorHandler(errorHandler)
|
||||||
if opt.TrashedOnly { // we cannot support showing Trashed Files when using ListR right now
|
if opt.TrashedOnly { // we cannot support showing Trashed Files when using ListR right now
|
||||||
f.features.ListR = nil
|
f.features.ListR = nil
|
||||||
@@ -857,7 +806,7 @@ func (f *Fs) CreateDir(ctx context.Context, path string) (jf *api.JottaFolder, e
|
|||||||
|
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallXML(ctx, &opts, nil, &jf)
|
resp, err = f.srv.CallXML(ctx, &opts, nil, &jf)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
//fmt.Printf("...Error %v\n", err)
|
//fmt.Printf("...Error %v\n", err)
|
||||||
@@ -886,7 +835,7 @@ func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err e
|
|||||||
var result api.JottaFolder
|
var result api.JottaFolder
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallXML(ctx, &opts, nil, &result)
|
resp, err = f.srv.CallXML(ctx, &opts, nil, &result)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -998,7 +947,7 @@ func (f *Fs) ListR(ctx context.Context, dir string, callback fs.ListRCallback) (
|
|||||||
var result api.JottaFolder // Could be JottaFileDirList, but JottaFolder is close enough
|
var result api.JottaFolder // Could be JottaFileDirList, but JottaFolder is close enough
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallXML(ctx, &opts, nil, &result)
|
resp, err = f.srv.CallXML(ctx, &opts, nil, &result)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if apiErr, ok := err.(*api.Error); ok {
|
if apiErr, ok := err.(*api.Error); ok {
|
||||||
@@ -1104,7 +1053,7 @@ func (f *Fs) purgeCheck(ctx context.Context, dir string, check bool) (err error)
|
|||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.Call(ctx, &opts)
|
resp, err = f.srv.Call(ctx, &opts)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "couldn't purge directory")
|
return errors.Wrap(err, "couldn't purge directory")
|
||||||
@@ -1143,7 +1092,7 @@ func (f *Fs) copyOrMove(ctx context.Context, method, src, dest string) (info *ap
|
|||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallXML(ctx, &opts, nil, &info)
|
resp, err = f.srv.CallXML(ctx, &opts, nil, &info)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -1151,7 +1100,7 @@ func (f *Fs) copyOrMove(ctx context.Context, method, src, dest string) (info *ap
|
|||||||
return info, nil
|
return info, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy src to this remote using server-side copy operations.
|
// Copy src to this remote using server side copy operations.
|
||||||
//
|
//
|
||||||
// This is stored with the remote path given
|
// This is stored with the remote path given
|
||||||
//
|
//
|
||||||
@@ -1181,7 +1130,7 @@ func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object,
|
|||||||
//return f.newObjectWithInfo(remote, &result)
|
//return f.newObjectWithInfo(remote, &result)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move src to this remote using server-side move operations.
|
// Move src to this remote using server side move operations.
|
||||||
//
|
//
|
||||||
// This is stored with the remote path given
|
// This is stored with the remote path given
|
||||||
//
|
//
|
||||||
@@ -1212,7 +1161,7 @@ func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DirMove moves src, srcRemote to this remote at dstRemote
|
// DirMove moves src, srcRemote to this remote at dstRemote
|
||||||
// using server-side move operations.
|
// using server side move operations.
|
||||||
//
|
//
|
||||||
// Will only be called if src.Fs().Name() == f.Name()
|
// Will only be called if src.Fs().Name() == f.Name()
|
||||||
//
|
//
|
||||||
@@ -1271,7 +1220,7 @@ func (f *Fs) PublicLink(ctx context.Context, remote string, expire fs.Duration,
|
|||||||
var result api.JottaFile
|
var result api.JottaFile
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallXML(ctx, &opts, nil, &result)
|
resp, err = f.srv.CallXML(ctx, &opts, nil, &result)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
if apiErr, ok := err.(*api.Error); ok {
|
if apiErr, ok := err.(*api.Error); ok {
|
||||||
@@ -1449,7 +1398,7 @@ func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.Read
|
|||||||
|
|
||||||
err = o.fs.pacer.Call(func() (bool, error) {
|
err = o.fs.pacer.Call(func() (bool, error) {
|
||||||
resp, err = o.fs.srv.Call(ctx, &opts)
|
resp, err = o.fs.srv.Call(ctx, &opts)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -1562,13 +1511,13 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
|
|||||||
var response api.AllocateFileResponse
|
var response api.AllocateFileResponse
|
||||||
err = o.fs.pacer.CallNoRetry(func() (bool, error) {
|
err = o.fs.pacer.CallNoRetry(func() (bool, error) {
|
||||||
resp, err = o.fs.apiSrv.CallJSON(ctx, &opts, &request, &response)
|
resp, err = o.fs.apiSrv.CallJSON(ctx, &opts, &request, &response)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the file state is INCOMPLETE and CORRUPT, try to upload a then
|
// If the file state is INCOMPLETE and CORRPUT, try to upload a then
|
||||||
if response.State != "COMPLETED" {
|
if response.State != "COMPLETED" {
|
||||||
// how much do we still have to upload?
|
// how much do we still have to upload?
|
||||||
remainingBytes := size - response.ResumePos
|
remainingBytes := size - response.ResumePos
|
||||||
@@ -1627,7 +1576,7 @@ func (o *Object) Remove(ctx context.Context) error {
|
|||||||
|
|
||||||
return o.fs.pacer.Call(func() (bool, error) {
|
return o.fs.pacer.Call(func() (bool, error) {
|
||||||
resp, err := o.fs.srv.CallXML(ctx, &opts, nil, nil)
|
resp, err := o.fs.srv.CallXML(ctx, &opts, nil, nil)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -256,7 +256,7 @@ func (f *Fs) fullPath(part string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewFs constructs a new filesystem given a root path and configuration options
|
// NewFs constructs a new filesystem given a root path and configuration options
|
||||||
func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (ff fs.Fs, err error) {
|
func NewFs(name, root string, m configmap.Mapper) (ff fs.Fs, err error) {
|
||||||
opt := new(Options)
|
opt := new(Options)
|
||||||
err = configstruct.Set(m, opt)
|
err = configstruct.Set(m, opt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -267,7 +267,7 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (ff fs.Fs
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
httpClient := httpclient.New()
|
httpClient := httpclient.New()
|
||||||
httpClient.Client = fshttp.NewClient(ctx)
|
httpClient.Client = fshttp.NewClient(fs.Config)
|
||||||
client := koofrclient.NewKoofrClientWithHTTPClient(opt.Endpoint, httpClient)
|
client := koofrclient.NewKoofrClientWithHTTPClient(opt.Endpoint, httpClient)
|
||||||
basicAuth := fmt.Sprintf("Basic %s",
|
basicAuth := fmt.Sprintf("Basic %s",
|
||||||
base64.StdEncoding.EncodeToString([]byte(opt.User+":"+pass)))
|
base64.StdEncoding.EncodeToString([]byte(opt.User+":"+pass)))
|
||||||
@@ -287,7 +287,7 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (ff fs.Fs
|
|||||||
DuplicateFiles: false,
|
DuplicateFiles: false,
|
||||||
BucketBased: false,
|
BucketBased: false,
|
||||||
CanHaveEmptyDirectories: true,
|
CanHaveEmptyDirectories: true,
|
||||||
}).Fill(ctx, f)
|
}).Fill(f)
|
||||||
for _, m := range mounts {
|
for _, m := range mounts {
|
||||||
if opt.MountID != "" {
|
if opt.MountID != "" {
|
||||||
if m.Id == opt.MountID {
|
if m.Id == opt.MountID {
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ func init() {
|
|||||||
Options: []fs.Option{{
|
Options: []fs.Option{{
|
||||||
Name: "nounc",
|
Name: "nounc",
|
||||||
Help: "Disable UNC (long path names) conversion on Windows",
|
Help: "Disable UNC (long path names) conversion on Windows",
|
||||||
Advanced: runtime.GOOS != "windows",
|
|
||||||
Examples: []fs.OptionExample{{
|
Examples: []fs.OptionExample{{
|
||||||
Value: "true",
|
Value: "true",
|
||||||
Help: "Disables long file names",
|
Help: "Disables long file names",
|
||||||
@@ -71,20 +70,6 @@ points, as you explicitly acknowledge that they should be skipped.`,
|
|||||||
Default: false,
|
Default: false,
|
||||||
NoPrefix: true,
|
NoPrefix: true,
|
||||||
Advanced: true,
|
Advanced: true,
|
||||||
}, {
|
|
||||||
Name: "zero_size_links",
|
|
||||||
Help: `Assume the Stat size of links is zero (and read them instead)
|
|
||||||
|
|
||||||
On some virtual filesystems (such ash LucidLink), reading a link size via a Stat call always returns 0.
|
|
||||||
However, on unix it reads as the length of the text in the link. This may cause errors like this when
|
|
||||||
syncing:
|
|
||||||
|
|
||||||
Failed to copy: corrupted on transfer: sizes differ 0 vs 13
|
|
||||||
|
|
||||||
Setting this flag causes rclone to read the link and use that as the size of the link
|
|
||||||
instead of 0 which in most cases fixes the problem.`,
|
|
||||||
Default: false,
|
|
||||||
Advanced: true,
|
|
||||||
}, {
|
}, {
|
||||||
Name: "no_unicode_normalization",
|
Name: "no_unicode_normalization",
|
||||||
Help: `Don't apply unicode normalization to paths and filenames (Deprecated)
|
Help: `Don't apply unicode normalization to paths and filenames (Deprecated)
|
||||||
@@ -102,13 +87,13 @@ Normally rclone checks the size and modification time of files as they
|
|||||||
are being uploaded and aborts with a message which starts "can't copy
|
are being uploaded and aborts with a message which starts "can't copy
|
||||||
- source file is being updated" if the file changes during upload.
|
- source file is being updated" if the file changes during upload.
|
||||||
|
|
||||||
However on some file systems this modification time check may fail (e.g.
|
However on some file systems this modification time check may fail (eg
|
||||||
[Glusterfs #2206](https://github.com/rclone/rclone/issues/2206)) so this
|
[Glusterfs #2206](https://github.com/rclone/rclone/issues/2206)) so this
|
||||||
check can be disabled with this flag.
|
check can be disabled with this flag.
|
||||||
|
|
||||||
If this flag is set, rclone will use its best efforts to transfer a
|
If this flag is set, rclone will use its best efforts to transfer a
|
||||||
file which is being updated. If the file is only having things
|
file which is being updated. If the file is only having things
|
||||||
appended to it (e.g. a log) then rclone will transfer the log file with
|
appended to it (eg a log) then rclone will transfer the log file with
|
||||||
the size it had the first time rclone saw it.
|
the size it had the first time rclone saw it.
|
||||||
|
|
||||||
If the file is being modified throughout (not just appended to) then
|
If the file is being modified throughout (not just appended to) then
|
||||||
@@ -149,17 +134,6 @@ Windows/macOS and case sensitive for everything else. Use this flag
|
|||||||
to override the default choice.`,
|
to override the default choice.`,
|
||||||
Default: false,
|
Default: false,
|
||||||
Advanced: true,
|
Advanced: true,
|
||||||
}, {
|
|
||||||
Name: "no_preallocate",
|
|
||||||
Help: `Disable preallocation of disk space for transferred files
|
|
||||||
|
|
||||||
Preallocation of disk space helps prevent filesystem fragmentation.
|
|
||||||
However, some virtual filesystem layers (such as Google Drive File
|
|
||||||
Stream) may incorrectly set the actual file size equal to the
|
|
||||||
preallocated space, causing checksum and file size checks to fail.
|
|
||||||
Use this flag to disable preallocation.`,
|
|
||||||
Default: false,
|
|
||||||
Advanced: true,
|
|
||||||
}, {
|
}, {
|
||||||
Name: "no_sparse",
|
Name: "no_sparse",
|
||||||
Help: `Disable sparse files for multi-thread downloads
|
Help: `Disable sparse files for multi-thread downloads
|
||||||
@@ -196,14 +170,12 @@ type Options struct {
|
|||||||
FollowSymlinks bool `config:"copy_links"`
|
FollowSymlinks bool `config:"copy_links"`
|
||||||
TranslateSymlinks bool `config:"links"`
|
TranslateSymlinks bool `config:"links"`
|
||||||
SkipSymlinks bool `config:"skip_links"`
|
SkipSymlinks bool `config:"skip_links"`
|
||||||
ZeroSizeLinks bool `config:"zero_size_links"`
|
|
||||||
NoUTFNorm bool `config:"no_unicode_normalization"`
|
NoUTFNorm bool `config:"no_unicode_normalization"`
|
||||||
NoCheckUpdated bool `config:"no_check_updated"`
|
NoCheckUpdated bool `config:"no_check_updated"`
|
||||||
NoUNC bool `config:"nounc"`
|
NoUNC bool `config:"nounc"`
|
||||||
OneFileSystem bool `config:"one_file_system"`
|
OneFileSystem bool `config:"one_file_system"`
|
||||||
CaseSensitive bool `config:"case_sensitive"`
|
CaseSensitive bool `config:"case_sensitive"`
|
||||||
CaseInsensitive bool `config:"case_insensitive"`
|
CaseInsensitive bool `config:"case_insensitive"`
|
||||||
NoPreAllocate bool `config:"no_preallocate"`
|
|
||||||
NoSparse bool `config:"no_sparse"`
|
NoSparse bool `config:"no_sparse"`
|
||||||
NoSetModTime bool `config:"no_set_modtime"`
|
NoSetModTime bool `config:"no_set_modtime"`
|
||||||
Enc encoder.MultiEncoder `config:"encoding"`
|
Enc encoder.MultiEncoder `config:"encoding"`
|
||||||
@@ -245,7 +217,7 @@ type Object struct {
|
|||||||
var errLinksAndCopyLinks = errors.New("can't use -l/--links with -L/--copy-links")
|
var errLinksAndCopyLinks = errors.New("can't use -l/--links with -L/--copy-links")
|
||||||
|
|
||||||
// NewFs constructs an Fs from the path
|
// NewFs constructs an Fs from the path
|
||||||
func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, error) {
|
func NewFs(name, root string, m configmap.Mapper) (fs.Fs, error) {
|
||||||
// Parse config into Options struct
|
// Parse config into Options struct
|
||||||
opt := new(Options)
|
opt := new(Options)
|
||||||
err := configstruct.Set(m, opt)
|
err := configstruct.Set(m, opt)
|
||||||
@@ -273,7 +245,7 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
CanHaveEmptyDirectories: true,
|
CanHaveEmptyDirectories: true,
|
||||||
IsLocal: true,
|
IsLocal: true,
|
||||||
SlowHash: true,
|
SlowHash: true,
|
||||||
}).Fill(ctx, f)
|
}).Fill(f)
|
||||||
if opt.FollowSymlinks {
|
if opt.FollowSymlinks {
|
||||||
f.lstat = os.Stat
|
f.lstat = os.Stat
|
||||||
}
|
}
|
||||||
@@ -484,8 +456,8 @@ func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err e
|
|||||||
if f.opt.FollowSymlinks && (mode&os.ModeSymlink) != 0 {
|
if f.opt.FollowSymlinks && (mode&os.ModeSymlink) != 0 {
|
||||||
localPath := filepath.Join(fsDirPath, name)
|
localPath := filepath.Join(fsDirPath, name)
|
||||||
fi, err = os.Stat(localPath)
|
fi, err = os.Stat(localPath)
|
||||||
if os.IsNotExist(err) || isCircularSymlinkError(err) {
|
if os.IsNotExist(err) {
|
||||||
// Skip bad symlinks and circular symlinks
|
// Skip bad symlinks
|
||||||
err = fserrors.NoRetryError(errors.Wrap(err, "symlink"))
|
err = fserrors.NoRetryError(errors.Wrap(err, "symlink"))
|
||||||
fs.Errorf(newRemote, "Listing error: %v", err)
|
fs.Errorf(newRemote, "Listing error: %v", err)
|
||||||
err = accounting.Stats(ctx).Error(err)
|
err = accounting.Stats(ctx).Error(err)
|
||||||
@@ -665,7 +637,7 @@ func (f *Fs) Purge(ctx context.Context, dir string) error {
|
|||||||
return os.RemoveAll(dir)
|
return os.RemoveAll(dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move src to this remote using server-side move operations.
|
// Move src to this remote using server side move operations.
|
||||||
//
|
//
|
||||||
// This is stored with the remote path given
|
// This is stored with the remote path given
|
||||||
//
|
//
|
||||||
@@ -729,7 +701,7 @@ func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DirMove moves src, srcRemote to this remote at dstRemote
|
// DirMove moves src, srcRemote to this remote at dstRemote
|
||||||
// using server-side move operations.
|
// using server side move operations.
|
||||||
//
|
//
|
||||||
// Will only be called if src.Fs().Name() == f.Name()
|
// Will only be called if src.Fs().Name() == f.Name()
|
||||||
//
|
//
|
||||||
@@ -1140,16 +1112,10 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !o.fs.opt.NoPreAllocate {
|
|
||||||
// Pre-allocate the file for performance reasons
|
// Pre-allocate the file for performance reasons
|
||||||
err = file.PreAllocate(src.Size(), f)
|
err = file.PreAllocate(src.Size(), f)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fs.Debugf(o, "Failed to pre-allocate: %v", err)
|
fs.Debugf(o, "Failed to pre-allocate: %v", err)
|
||||||
if err == file.ErrDiskFull {
|
|
||||||
_ = f.Close()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
out = f
|
out = f
|
||||||
} else {
|
} else {
|
||||||
@@ -1236,12 +1202,10 @@ func (f *Fs) OpenWriterAt(ctx context.Context, remote string, size int64) (fs.Wr
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// Pre-allocate the file for performance reasons
|
// Pre-allocate the file for performance reasons
|
||||||
if !f.opt.NoPreAllocate {
|
|
||||||
err = file.PreAllocate(size, out)
|
err = file.PreAllocate(size, out)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fs.Debugf(o, "Failed to pre-allocate: %v", err)
|
fs.Debugf(o, "Failed to pre-allocate: %v", err)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if !f.opt.NoSparse && file.SetSparseImplemented {
|
if !f.opt.NoSparse && file.SetSparseImplemented {
|
||||||
sparseWarning.Do(func() {
|
sparseWarning.Do(func() {
|
||||||
fs.Infof(nil, "Writing sparse files: use --local-no-sparse or --multi-thread-streams 0 to disable")
|
fs.Infof(nil, "Writing sparse files: use --local-no-sparse or --multi-thread-streams 0 to disable")
|
||||||
@@ -1268,8 +1232,7 @@ func (o *Object) setMetadata(info os.FileInfo) {
|
|||||||
o.mode = info.Mode()
|
o.mode = info.Mode()
|
||||||
o.fs.objectMetaMu.Unlock()
|
o.fs.objectMetaMu.Unlock()
|
||||||
// On Windows links read as 0 size so set the correct size here
|
// On Windows links read as 0 size so set the correct size here
|
||||||
// Optionally, users can turn this feature on with the zero_size_links flag
|
if runtime.GOOS == "windows" && o.translatedLink {
|
||||||
if (runtime.GOOS == "windows" || o.fs.opt.ZeroSizeLinks) && o.translatedLink {
|
|
||||||
linkdst, err := os.Readlink(o.path)
|
linkdst, err := os.Readlink(o.path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fs.Errorf(o, "Failed to read link size: %v", err)
|
fs.Errorf(o, "Failed to read link size: %v", err)
|
||||||
|
|||||||
@@ -163,6 +163,6 @@ func TestSymlinkError(t *testing.T) {
|
|||||||
"links": "true",
|
"links": "true",
|
||||||
"copy_links": "true",
|
"copy_links": "true",
|
||||||
}
|
}
|
||||||
_, err := NewFs(context.Background(), "local", "/", m)
|
_, err := NewFs("local", "/", m)
|
||||||
assert.Equal(t, errLinksAndCopyLinks, err)
|
assert.Equal(t, errLinksAndCopyLinks, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
// +build !windows,!plan9,!js
|
|
||||||
|
|
||||||
package local
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"syscall"
|
|
||||||
)
|
|
||||||
|
|
||||||
// isCircularSymlinkError checks if the current error code is because of a circular symlink
|
|
||||||
func isCircularSymlinkError(err error) bool {
|
|
||||||
if err != nil {
|
|
||||||
if newerr, ok := err.(*os.PathError); ok {
|
|
||||||
if errcode, ok := newerr.Err.(syscall.Errno); ok {
|
|
||||||
if errcode == syscall.ELOOP {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
// +build windows plan9 js
|
|
||||||
|
|
||||||
package local
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// isCircularSymlinkError checks if the current error code is because of a circular symlink
|
|
||||||
func isCircularSymlinkError(err error) bool {
|
|
||||||
if err != nil {
|
|
||||||
if strings.Contains(err.Error(), "The name of the file cannot be resolved by the system") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
@@ -102,7 +102,6 @@ func init() {
|
|||||||
This feature is called "speedup" or "put by hash". It is especially efficient
|
This feature is called "speedup" or "put by hash". It is especially efficient
|
||||||
in case of generally available files like popular books, video or audio clips,
|
in case of generally available files like popular books, video or audio clips,
|
||||||
because files are searched by hash in all accounts of all mailru users.
|
because files are searched by hash in all accounts of all mailru users.
|
||||||
It is meaningless and ineffective if source file is unique or encrypted.
|
|
||||||
Please note that rclone may need local memory and disk space to calculate
|
Please note that rclone may need local memory and disk space to calculate
|
||||||
content hash in advance and decide whether full upload is required.
|
content hash in advance and decide whether full upload is required.
|
||||||
Also, if rclone does not know file size in advance (e.g. in case of
|
Also, if rclone does not know file size in advance (e.g. in case of
|
||||||
@@ -193,7 +192,7 @@ This option must not be used by an ordinary user. It is intended only to
|
|||||||
facilitate remote troubleshooting of backend issues. Strict meaning of
|
facilitate remote troubleshooting of backend issues. Strict meaning of
|
||||||
flags is not documented and not guaranteed to persist between releases.
|
flags is not documented and not guaranteed to persist between releases.
|
||||||
Quirks will be removed when the backend grows stable.
|
Quirks will be removed when the backend grows stable.
|
||||||
Supported quirks: atomicmkdir binlist unknowndirs`,
|
Supported quirks: atomicmkdir binlist gzip insecure retry400`,
|
||||||
}, {
|
}, {
|
||||||
Name: config.ConfigEncoding,
|
Name: config.ConfigEncoding,
|
||||||
Help: config.ConfigEncodingHelp,
|
Help: config.ConfigEncodingHelp,
|
||||||
@@ -234,14 +233,14 @@ var retryErrorCodes = []int{
|
|||||||
// shouldRetry returns a boolean as to whether this response and err
|
// shouldRetry returns a boolean as to whether this response and err
|
||||||
// deserve to be retried. It returns the err as a convenience.
|
// deserve to be retried. It returns the err as a convenience.
|
||||||
// Retries password authorization (once) in a special case of access denied.
|
// Retries password authorization (once) in a special case of access denied.
|
||||||
func shouldRetry(ctx context.Context, res *http.Response, err error, f *Fs, opts *rest.Opts) (bool, error) {
|
func shouldRetry(res *http.Response, err error, f *Fs, opts *rest.Opts) (bool, error) {
|
||||||
if fserrors.ContextError(ctx, &err) {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
if res != nil && res.StatusCode == 403 && f.opt.Password != "" && !f.passFailed {
|
if res != nil && res.StatusCode == 403 && f.opt.Password != "" && !f.passFailed {
|
||||||
reAuthErr := f.reAuthorize(opts, err)
|
reAuthErr := f.reAuthorize(opts, err)
|
||||||
return reAuthErr == nil, err // return an original error
|
return reAuthErr == nil, err // return an original error
|
||||||
}
|
}
|
||||||
|
if res != nil && res.StatusCode == 400 && f.quirks.retry400 {
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
return fserrors.ShouldRetry(err) || fserrors.ShouldRetryHTTP(res, retryErrorCodes), err
|
return fserrors.ShouldRetry(err) || fserrors.ShouldRetryHTTP(res, retryErrorCodes), err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,9 +275,8 @@ type Fs struct {
|
|||||||
name string
|
name string
|
||||||
root string // root path
|
root string // root path
|
||||||
opt Options // parsed options
|
opt Options // parsed options
|
||||||
ci *fs.ConfigInfo // global config
|
|
||||||
speedupGlobs []string // list of file name patterns eligible for speedup
|
speedupGlobs []string // list of file name patterns eligible for speedup
|
||||||
speedupAny bool // true if all file names are eligible for speedup
|
speedupAny bool // true if all file names are aligible for speedup
|
||||||
features *fs.Features // optional features
|
features *fs.Features // optional features
|
||||||
srv *rest.Client // REST API client
|
srv *rest.Client // REST API client
|
||||||
cli *http.Client // underlying HTTP client (for authorize)
|
cli *http.Client // underlying HTTP client (for authorize)
|
||||||
@@ -298,8 +296,9 @@ type Fs struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewFs constructs an Fs from the path, container:path
|
// NewFs constructs an Fs from the path, container:path
|
||||||
func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, error) {
|
func NewFs(name, root string, m configmap.Mapper) (fs.Fs, error) {
|
||||||
// fs.Debugf(nil, ">>> NewFs %q %q", name, root)
|
// fs.Debugf(nil, ">>> NewFs %q %q", name, root)
|
||||||
|
ctx := context.Background() // Note: NewFs does not pass context!
|
||||||
|
|
||||||
// Parse config into Options struct
|
// Parse config into Options struct
|
||||||
opt := new(Options)
|
opt := new(Options)
|
||||||
@@ -316,12 +315,10 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
// However the f.root string should not have leading or trailing slashes
|
// However the f.root string should not have leading or trailing slashes
|
||||||
root = strings.Trim(root, "/")
|
root = strings.Trim(root, "/")
|
||||||
|
|
||||||
ci := fs.GetConfig(ctx)
|
|
||||||
f := &Fs{
|
f := &Fs{
|
||||||
name: name,
|
name: name,
|
||||||
root: root,
|
root: root,
|
||||||
opt: *opt,
|
opt: *opt,
|
||||||
ci: ci,
|
|
||||||
m: m,
|
m: m,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,7 +327,7 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
}
|
}
|
||||||
f.quirks.parseQuirks(opt.Quirks)
|
f.quirks.parseQuirks(opt.Quirks)
|
||||||
|
|
||||||
f.pacer = fs.NewPacer(ctx, pacer.NewDefault(pacer.MinSleep(minSleepPacer), pacer.MaxSleep(maxSleepPacer), pacer.DecayConstant(decayConstPacer)))
|
f.pacer = fs.NewPacer(pacer.NewDefault(pacer.MinSleep(minSleepPacer), pacer.MaxSleep(maxSleepPacer), pacer.DecayConstant(decayConstPacer)))
|
||||||
|
|
||||||
f.features = (&fs.Features{
|
f.features = (&fs.Features{
|
||||||
CaseInsensitive: true,
|
CaseInsensitive: true,
|
||||||
@@ -338,21 +335,27 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
// Can copy/move across mailru configs (almost, thus true here), but
|
// Can copy/move across mailru configs (almost, thus true here), but
|
||||||
// only when they share common account (this is checked in Copy/Move).
|
// only when they share common account (this is checked in Copy/Move).
|
||||||
ServerSideAcrossConfigs: true,
|
ServerSideAcrossConfigs: true,
|
||||||
}).Fill(ctx, f)
|
}).Fill(f)
|
||||||
|
|
||||||
// Override few config settings and create a client
|
// Override few config settings and create a client
|
||||||
newCtx, clientConfig := fs.AddConfig(ctx)
|
clientConfig := *fs.Config
|
||||||
if opt.UserAgent != "" {
|
if opt.UserAgent != "" {
|
||||||
clientConfig.UserAgent = opt.UserAgent
|
clientConfig.UserAgent = opt.UserAgent
|
||||||
}
|
}
|
||||||
clientConfig.NoGzip = true // Mimic official client, skip sending "Accept-Encoding: gzip"
|
clientConfig.NoGzip = !f.quirks.gzip // Send not "Accept-Encoding: gzip" like official client
|
||||||
f.cli = fshttp.NewClient(newCtx)
|
f.cli = fshttp.NewClient(&clientConfig)
|
||||||
|
|
||||||
f.srv = rest.NewClient(f.cli)
|
f.srv = rest.NewClient(f.cli)
|
||||||
f.srv.SetRoot(api.APIServerURL)
|
f.srv.SetRoot(api.APIServerURL)
|
||||||
f.srv.SetHeader("Accept", "*/*") // Send "Accept: */*" with every request like official client
|
f.srv.SetHeader("Accept", "*/*") // Send "Accept: */*" with every request like official client
|
||||||
f.srv.SetErrorHandler(errorHandler)
|
f.srv.SetErrorHandler(errorHandler)
|
||||||
|
|
||||||
|
if f.quirks.insecure {
|
||||||
|
transport := f.cli.Transport.(*fshttp.Transport).Transport
|
||||||
|
transport.TLSClientConfig.InsecureSkipVerify = true
|
||||||
|
transport.ProxyConnectHeader = http.Header{"User-Agent": {clientConfig.UserAgent}}
|
||||||
|
}
|
||||||
|
|
||||||
if err = f.authorize(ctx, false); err != nil {
|
if err = f.authorize(ctx, false); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -385,14 +388,30 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
// Internal maintenance flags (to be removed when the backend matures).
|
// Internal maintenance flags (to be removed when the backend matures).
|
||||||
// Primarily intended to facilitate remote support and troubleshooting.
|
// Primarily intended to facilitate remote support and troubleshooting.
|
||||||
type quirks struct {
|
type quirks struct {
|
||||||
|
gzip bool
|
||||||
|
insecure bool
|
||||||
binlist bool
|
binlist bool
|
||||||
atomicmkdir bool
|
atomicmkdir bool
|
||||||
unknowndirs bool
|
retry400 bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *quirks) parseQuirks(option string) {
|
func (q *quirks) parseQuirks(option string) {
|
||||||
for _, flag := range strings.Split(option, ",") {
|
for _, flag := range strings.Split(option, ",") {
|
||||||
switch strings.ToLower(strings.TrimSpace(flag)) {
|
switch strings.ToLower(strings.TrimSpace(flag)) {
|
||||||
|
case "gzip":
|
||||||
|
// This backend mimics the official client which never sends the
|
||||||
|
// "Accept-Encoding: gzip" header. However, enabling compression
|
||||||
|
// might be good for performance.
|
||||||
|
// Use this quirk to investigate the performance impact.
|
||||||
|
// Remove this quirk if performance does not improve.
|
||||||
|
q.gzip = true
|
||||||
|
case "insecure":
|
||||||
|
// The mailru disk-o protocol is not documented. To compare HTTP
|
||||||
|
// stream against the official client one can use Telerik Fiddler,
|
||||||
|
// which introduces a self-signed certificate. This quirk forces
|
||||||
|
// the Go http layer to accept it.
|
||||||
|
// Remove this quirk when the backend reaches maturity.
|
||||||
|
q.insecure = true
|
||||||
case "binlist":
|
case "binlist":
|
||||||
// The official client sometimes uses a so called "bin" protocol,
|
// The official client sometimes uses a so called "bin" protocol,
|
||||||
// implemented in the listBin file system method below. This method
|
// implemented in the listBin file system method below. This method
|
||||||
@@ -405,14 +424,18 @@ func (q *quirks) parseQuirks(option string) {
|
|||||||
case "atomicmkdir":
|
case "atomicmkdir":
|
||||||
// At the moment rclone requires Mkdir to return success if the
|
// At the moment rclone requires Mkdir to return success if the
|
||||||
// directory already exists. However, such programs as borgbackup
|
// directory already exists. However, such programs as borgbackup
|
||||||
// use mkdir as a locking primitive and depend on its atomicity.
|
// or restic use mkdir as a locking primitive and depend on its
|
||||||
// Remove this quirk when the above issue is investigated.
|
// atomicity. This quirk is a workaround. It can be removed
|
||||||
|
// when the above issue is investigated.
|
||||||
q.atomicmkdir = true
|
q.atomicmkdir = true
|
||||||
case "unknowndirs":
|
case "retry400":
|
||||||
// Accepts unknown resource types as folders.
|
// This quirk will help in troubleshooting a very rare "Error 400"
|
||||||
q.unknowndirs = true
|
// issue. It can be removed if the problem does not show up
|
||||||
|
// for a year or so. See the below issue:
|
||||||
|
// https://github.com/ivandeex/rclone/issues/14
|
||||||
|
q.retry400 = true
|
||||||
default:
|
default:
|
||||||
// Ignore unknown flags
|
// Just ignore all unknown flags
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -426,7 +449,7 @@ func (f *Fs) authorize(ctx context.Context, force bool) (err error) {
|
|||||||
|
|
||||||
if err != nil || !tokenIsValid(t) {
|
if err != nil || !tokenIsValid(t) {
|
||||||
fs.Infof(f, "Valid token not found, authorizing.")
|
fs.Infof(f, "Valid token not found, authorizing.")
|
||||||
ctx := oauthutil.Context(ctx, f.cli)
|
ctx := oauthutil.Context(f.cli)
|
||||||
t, err = oauthConfig.PasswordCredentialsToken(ctx, f.opt.Username, f.opt.Password)
|
t, err = oauthConfig.PasswordCredentialsToken(ctx, f.opt.Username, f.opt.Password)
|
||||||
}
|
}
|
||||||
if err == nil && !tokenIsValid(t) {
|
if err == nil && !tokenIsValid(t) {
|
||||||
@@ -449,7 +472,7 @@ func (f *Fs) authorize(ctx context.Context, force bool) (err error) {
|
|||||||
// crashing with panic `comparing uncomparable type map[string]interface{}`
|
// crashing with panic `comparing uncomparable type map[string]interface{}`
|
||||||
// As a workaround, mimic oauth2.NewClient() wrapping token source in
|
// As a workaround, mimic oauth2.NewClient() wrapping token source in
|
||||||
// oauth2.ReuseTokenSource
|
// oauth2.ReuseTokenSource
|
||||||
_, ts, err := oauthutil.NewClientWithBaseClient(ctx, f.name, f.m, oauthConfig, f.cli)
|
_, ts, err := oauthutil.NewClientWithBaseClient(f.name, f.m, oauthConfig, f.cli)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
f.source = oauth2.ReuseTokenSource(nil, ts)
|
f.source = oauth2.ReuseTokenSource(nil, ts)
|
||||||
}
|
}
|
||||||
@@ -528,7 +551,7 @@ func (f *Fs) relPath(absPath string) (string, error) {
|
|||||||
return "", fmt.Errorf("path %q should be under %q", absPath, f.root)
|
return "", fmt.Errorf("path %q should be under %q", absPath, f.root)
|
||||||
}
|
}
|
||||||
|
|
||||||
// metaServer returns URL of current meta server
|
// metaServer ...
|
||||||
func (f *Fs) metaServer(ctx context.Context) (string, error) {
|
func (f *Fs) metaServer(ctx context.Context) (string, error) {
|
||||||
f.metaMu.Lock()
|
f.metaMu.Lock()
|
||||||
defer f.metaMu.Unlock()
|
defer f.metaMu.Unlock()
|
||||||
@@ -603,7 +626,7 @@ func (f *Fs) readItemMetaData(ctx context.Context, path string) (entry fs.DirEnt
|
|||||||
var info api.ItemInfoResponse
|
var info api.ItemInfoResponse
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
res, err := f.srv.CallJSON(ctx, &opts, nil, &info)
|
res, err := f.srv.CallJSON(ctx, &opts, nil, &info)
|
||||||
return shouldRetry(ctx, res, err, f, &opts)
|
return shouldRetry(res, err, f, &opts)
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -633,23 +656,17 @@ func (f *Fs) itemToDirEntry(ctx context.Context, item *api.ListItem) (entry fs.D
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, -1, err
|
return nil, -1, err
|
||||||
}
|
}
|
||||||
|
|
||||||
mTime := int64(item.Mtime)
|
mTime := int64(item.Mtime)
|
||||||
if mTime < 0 {
|
if mTime < 0 {
|
||||||
fs.Debugf(f, "Fixing invalid timestamp %d on mailru file %q", mTime, remote)
|
fs.Debugf(f, "Fixing invalid timestamp %d on mailru file %q", mTime, remote)
|
||||||
mTime = 0
|
mTime = 0
|
||||||
}
|
}
|
||||||
modTime := time.Unix(mTime, 0)
|
switch item.Kind {
|
||||||
|
case "folder":
|
||||||
isDir, err := f.isDir(item.Kind, remote)
|
dir := fs.NewDir(remote, time.Unix(mTime, 0)).SetSize(item.Size)
|
||||||
if err != nil {
|
dirSize := item.Count.Files + item.Count.Folders
|
||||||
return nil, -1, err
|
return dir, dirSize, nil
|
||||||
}
|
case "file":
|
||||||
if isDir {
|
|
||||||
dir := fs.NewDir(remote, modTime).SetSize(item.Size)
|
|
||||||
return dir, item.Count.Files + item.Count.Folders, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
binHash, err := mrhash.DecodeString(item.Hash)
|
binHash, err := mrhash.DecodeString(item.Hash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, -1, err
|
return nil, -1, err
|
||||||
@@ -660,29 +677,12 @@ func (f *Fs) itemToDirEntry(ctx context.Context, item *api.ListItem) (entry fs.D
|
|||||||
hasMetaData: true,
|
hasMetaData: true,
|
||||||
size: item.Size,
|
size: item.Size,
|
||||||
mrHash: binHash,
|
mrHash: binHash,
|
||||||
modTime: modTime,
|
modTime: time.Unix(mTime, 0),
|
||||||
}
|
}
|
||||||
return file, -1, nil
|
return file, -1, nil
|
||||||
}
|
|
||||||
|
|
||||||
// isDir returns true for directories, false for files
|
|
||||||
func (f *Fs) isDir(kind, path string) (bool, error) {
|
|
||||||
switch kind {
|
|
||||||
case "":
|
|
||||||
return false, errors.New("empty resource type")
|
|
||||||
case "file":
|
|
||||||
return false, nil
|
|
||||||
case "folder":
|
|
||||||
// fall thru
|
|
||||||
case "camera-upload", "mounted", "shared":
|
|
||||||
fs.Debugf(f, "[%s]: folder has type %q", path, kind)
|
|
||||||
default:
|
default:
|
||||||
if !f.quirks.unknowndirs {
|
return nil, -1, fmt.Errorf("Unknown resource type %q", item.Kind)
|
||||||
return false, fmt.Errorf("unknown resource type %q", kind)
|
|
||||||
}
|
}
|
||||||
fs.Errorf(f, "[%s]: folder has unknown type %q", path, kind)
|
|
||||||
}
|
|
||||||
return true, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// List the objects and directories in dir into entries.
|
// List the objects and directories in dir into entries.
|
||||||
@@ -698,7 +698,7 @@ func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err e
|
|||||||
entries, err = f.listM1(ctx, f.absPath(dir), 0, maxInt32)
|
entries, err = f.listM1(ctx, f.absPath(dir), 0, maxInt32)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err == nil && f.ci.LogLevel >= fs.LogLevelDebug {
|
if err == nil && fs.Config.LogLevel >= fs.LogLevelDebug {
|
||||||
names := []string{}
|
names := []string{}
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
names = append(names, entry.Remote())
|
names = append(names, entry.Remote())
|
||||||
@@ -739,7 +739,7 @@ func (f *Fs) listM1(ctx context.Context, dirPath string, offset int, limit int)
|
|||||||
)
|
)
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
res, err = f.srv.CallJSON(ctx, &opts, nil, &info)
|
res, err = f.srv.CallJSON(ctx, &opts, nil, &info)
|
||||||
return shouldRetry(ctx, res, err, f, &opts)
|
return shouldRetry(res, err, f, &opts)
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -750,11 +750,7 @@ func (f *Fs) listM1(ctx context.Context, dirPath string, offset int, limit int)
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
isDir, err := f.isDir(info.Body.Kind, dirPath)
|
if info.Body.Kind != "folder" {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !isDir {
|
|
||||||
return nil, fs.ErrorIsFile
|
return nil, fs.ErrorIsFile
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -803,7 +799,7 @@ func (f *Fs) listBin(ctx context.Context, dirPath string, depth int) (entries fs
|
|||||||
var res *http.Response
|
var res *http.Response
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
res, err = f.srv.Call(ctx, &opts)
|
res, err = f.srv.Call(ctx, &opts)
|
||||||
return shouldRetry(ctx, res, err, f, &opts)
|
return shouldRetry(res, err, f, &opts)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
closeBody(res)
|
closeBody(res)
|
||||||
@@ -962,7 +958,7 @@ func (t *treeState) NextRecord() (fs.DirEntry, error) {
|
|||||||
return nil, r.Error()
|
return nil, r.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
if t.f.ci.LogLevel >= fs.LogLevelDebug {
|
if fs.Config.LogLevel >= fs.LogLevelDebug {
|
||||||
ctime, _ := modTime.MarshalJSON()
|
ctime, _ := modTime.MarshalJSON()
|
||||||
fs.Debugf(t.f, "binDir %d.%d %q %q (%d) %s", t.level, itemType, t.currDir, name, size, ctime)
|
fs.Debugf(t.f, "binDir %d.%d %q %q (%d) %s", t.level, itemType, t.currDir, name, size, ctime)
|
||||||
}
|
}
|
||||||
@@ -1076,7 +1072,7 @@ func (f *Fs) CreateDir(ctx context.Context, path string) error {
|
|||||||
var res *http.Response
|
var res *http.Response
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
res, err = f.srv.Call(ctx, &opts)
|
res, err = f.srv.Call(ctx, &opts)
|
||||||
return shouldRetry(ctx, res, err, f, &opts)
|
return shouldRetry(res, err, f, &opts)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
closeBody(res)
|
closeBody(res)
|
||||||
@@ -1219,7 +1215,7 @@ func (f *Fs) delete(ctx context.Context, path string, hardDelete bool) error {
|
|||||||
var response api.GenericResponse
|
var response api.GenericResponse
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
res, err := f.srv.CallJSON(ctx, &opts, nil, &response)
|
res, err := f.srv.CallJSON(ctx, &opts, nil, &response)
|
||||||
return shouldRetry(ctx, res, err, f, &opts)
|
return shouldRetry(res, err, f, &opts)
|
||||||
})
|
})
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
@@ -1232,7 +1228,7 @@ func (f *Fs) delete(ctx context.Context, path string, hardDelete bool) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy src to this remote using server-side copy operations.
|
// Copy src to this remote using server side copy operations.
|
||||||
// This is stored with the remote path given.
|
// This is stored with the remote path given.
|
||||||
// It returns the destination Object and a possible error.
|
// It returns the destination Object and a possible error.
|
||||||
// Will only be called if src.Fs().Name() == f.Name()
|
// Will only be called if src.Fs().Name() == f.Name()
|
||||||
@@ -1291,7 +1287,7 @@ func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object,
|
|||||||
var response api.GenericBodyResponse
|
var response api.GenericBodyResponse
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
res, err := f.srv.CallJSON(ctx, &opts, nil, &response)
|
res, err := f.srv.CallJSON(ctx, &opts, nil, &response)
|
||||||
return shouldRetry(ctx, res, err, f, &opts)
|
return shouldRetry(res, err, f, &opts)
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1327,7 +1323,7 @@ func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object,
|
|||||||
return dstObj, err
|
return dstObj, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move src to this remote using server-side move operations.
|
// Move src to this remote using server side move operations.
|
||||||
// This is stored with the remote path given.
|
// This is stored with the remote path given.
|
||||||
// It returns the destination Object and a possible error.
|
// It returns the destination Object and a possible error.
|
||||||
// Will only be called if src.Fs().Name() == f.Name()
|
// Will only be called if src.Fs().Name() == f.Name()
|
||||||
@@ -1395,7 +1391,7 @@ func (f *Fs) moveItemBin(ctx context.Context, srcPath, dstPath, opName string) e
|
|||||||
var res *http.Response
|
var res *http.Response
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
res, err = f.srv.Call(ctx, &opts)
|
res, err = f.srv.Call(ctx, &opts)
|
||||||
return shouldRetry(ctx, res, err, f, &opts)
|
return shouldRetry(res, err, f, &opts)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
closeBody(res)
|
closeBody(res)
|
||||||
@@ -1414,7 +1410,7 @@ func (f *Fs) moveItemBin(ctx context.Context, srcPath, dstPath, opName string) e
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DirMove moves src, srcRemote to this remote at dstRemote
|
// DirMove moves src, srcRemote to this remote at dstRemote
|
||||||
// using server-side move operations.
|
// using server side move operations.
|
||||||
// Will only be called if src.Fs().Name() == f.Name()
|
// Will only be called if src.Fs().Name() == f.Name()
|
||||||
// If it isn't possible then return fs.ErrorCantDirMove
|
// If it isn't possible then return fs.ErrorCantDirMove
|
||||||
// If destination exists then return fs.ErrorDirExists
|
// If destination exists then return fs.ErrorDirExists
|
||||||
@@ -1486,7 +1482,7 @@ func (f *Fs) PublicLink(ctx context.Context, remote string, expire fs.Duration,
|
|||||||
var response api.GenericBodyResponse
|
var response api.GenericBodyResponse
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
res, err := f.srv.CallJSON(ctx, &opts, nil, &response)
|
res, err := f.srv.CallJSON(ctx, &opts, nil, &response)
|
||||||
return shouldRetry(ctx, res, err, f, &opts)
|
return shouldRetry(res, err, f, &opts)
|
||||||
})
|
})
|
||||||
|
|
||||||
if err == nil && response.Body != "" {
|
if err == nil && response.Body != "" {
|
||||||
@@ -1527,7 +1523,7 @@ func (f *Fs) CleanUp(ctx context.Context) error {
|
|||||||
var response api.CleanupResponse
|
var response api.CleanupResponse
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
res, err := f.srv.CallJSON(ctx, &opts, nil, &response)
|
res, err := f.srv.CallJSON(ctx, &opts, nil, &response)
|
||||||
return shouldRetry(ctx, res, err, f, &opts)
|
return shouldRetry(res, err, f, &opts)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1560,7 +1556,7 @@ func (f *Fs) About(ctx context.Context) (*fs.Usage, error) {
|
|||||||
var info api.UserInfoResponse
|
var info api.UserInfoResponse
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
res, err := f.srv.CallJSON(ctx, &opts, nil, &info)
|
res, err := f.srv.CallJSON(ctx, &opts, nil, &info)
|
||||||
return shouldRetry(ctx, res, err, f, &opts)
|
return shouldRetry(res, err, f, &opts)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -1610,25 +1606,20 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
|
|||||||
fileBuf []byte
|
fileBuf []byte
|
||||||
fileHash []byte
|
fileHash []byte
|
||||||
newHash []byte
|
newHash []byte
|
||||||
slowHash bool
|
trySpeedup bool
|
||||||
localSrc bool
|
|
||||||
)
|
)
|
||||||
if srcObj := fs.UnWrapObjectInfo(src); srcObj != nil {
|
|
||||||
srcFeatures := srcObj.Fs().Features()
|
|
||||||
slowHash = srcFeatures.SlowHash
|
|
||||||
localSrc = srcFeatures.IsLocal
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try speedup if it's globally enabled but skip extra post
|
// Don't disturb the source if file fits in hash.
|
||||||
// request if file is small and fits in the metadata request
|
// Skip an extra speedup request if file fits in hash.
|
||||||
trySpeedup := o.fs.opt.SpeedupEnable && size > mrhash.Size
|
if size > mrhash.Size {
|
||||||
|
// Request hash from source.
|
||||||
// Try to get the hash if it's instant
|
|
||||||
if trySpeedup && !slowHash {
|
|
||||||
if srcHash, err := src.Hash(ctx, MrHashType); err == nil && srcHash != "" {
|
if srcHash, err := src.Hash(ctx, MrHashType); err == nil && srcHash != "" {
|
||||||
fileHash, _ = mrhash.DecodeString(srcHash)
|
fileHash, _ = mrhash.DecodeString(srcHash)
|
||||||
}
|
}
|
||||||
if fileHash != nil {
|
|
||||||
|
// Try speedup if it's globally enabled and source hash is available.
|
||||||
|
trySpeedup = o.fs.opt.SpeedupEnable
|
||||||
|
if trySpeedup && fileHash != nil {
|
||||||
if o.putByHash(ctx, fileHash, src, "source") {
|
if o.putByHash(ctx, fileHash, src, "source") {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -1637,22 +1628,13 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Need to calculate hash, check whether file is still eligible for speedup
|
// Need to calculate hash, check whether file is still eligible for speedup
|
||||||
trySpeedup = trySpeedup && o.fs.eligibleForSpeedup(o.Remote(), size, options...)
|
if trySpeedup {
|
||||||
|
trySpeedup = o.fs.eligibleForSpeedup(o.Remote(), size, options...)
|
||||||
// Attempt to put by hash if file is local and eligible
|
|
||||||
if trySpeedup && localSrc {
|
|
||||||
if srcHash, err := src.Hash(ctx, MrHashType); err == nil && srcHash != "" {
|
|
||||||
fileHash, _ = mrhash.DecodeString(srcHash)
|
|
||||||
}
|
|
||||||
if fileHash != nil && o.putByHash(ctx, fileHash, src, "localfs") {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// If local file hashing has failed, it's pointless to try anymore
|
|
||||||
trySpeedup = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempt to put by calculating hash in memory
|
// Attempt to put by calculating hash in memory
|
||||||
if trySpeedup && size <= int64(o.fs.opt.SpeedupMaxMem) {
|
if trySpeedup && size <= int64(o.fs.opt.SpeedupMaxMem) {
|
||||||
|
//fs.Debugf(o, "attempt to put by hash from memory")
|
||||||
fileBuf, err = ioutil.ReadAll(in)
|
fileBuf, err = ioutil.ReadAll(in)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1667,7 +1649,7 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
|
|||||||
|
|
||||||
// Attempt to put by hash using a spool file
|
// Attempt to put by hash using a spool file
|
||||||
if trySpeedup {
|
if trySpeedup {
|
||||||
tmpFs, err := fs.TemporaryLocalFs(ctx)
|
tmpFs, err := fs.TemporaryLocalFs()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fs.Infof(tmpFs, "Failed to create spool FS: %v", err)
|
fs.Infof(tmpFs, "Failed to create spool FS: %v", err)
|
||||||
} else {
|
} else {
|
||||||
@@ -1782,7 +1764,6 @@ func (f *Fs) parseSpeedupPatterns(patternString string) (err error) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// putByHash is a thin wrapper around addFileMetaData
|
|
||||||
func (o *Object) putByHash(ctx context.Context, mrHash []byte, info fs.ObjectInfo, method string) bool {
|
func (o *Object) putByHash(ctx context.Context, mrHash []byte, info fs.ObjectInfo, method string) bool {
|
||||||
oNew := new(Object)
|
oNew := new(Object)
|
||||||
*oNew = *o
|
*oNew = *o
|
||||||
@@ -2079,7 +2060,7 @@ func (o *Object) addFileMetaData(ctx context.Context, overwrite bool) error {
|
|||||||
var res *http.Response
|
var res *http.Response
|
||||||
err = o.fs.pacer.Call(func() (bool, error) {
|
err = o.fs.pacer.Call(func() (bool, error) {
|
||||||
res, err = o.fs.srv.Call(ctx, &opts)
|
res, err = o.fs.srv.Call(ctx, &opts)
|
||||||
return shouldRetry(ctx, res, err, o.fs, &opts)
|
return shouldRetry(res, err, o.fs, &opts)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
closeBody(res)
|
closeBody(res)
|
||||||
@@ -2175,7 +2156,7 @@ func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.Read
|
|||||||
}
|
}
|
||||||
opts.RootURL = server
|
opts.RootURL = server
|
||||||
res, err = o.fs.srv.Call(ctx, &opts)
|
res, err = o.fs.srv.Call(ctx, &opts)
|
||||||
return shouldRetry(ctx, res, err, o.fs, &opts)
|
return shouldRetry(res, err, o.fs, &opts)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if res != nil && res.Body != nil {
|
if res != nil && res.Body != nil {
|
||||||
@@ -2209,7 +2190,6 @@ func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.Read
|
|||||||
// Discard the beginning of the data
|
// Discard the beginning of the data
|
||||||
_, err = io.CopyN(ioutil.Discard, wrapStream, start)
|
_, err = io.CopyN(ioutil.Discard, wrapStream, start)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
closeBody(res)
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2267,7 +2247,7 @@ func (e *endHandler) handle(err error) error {
|
|||||||
return io.EOF
|
return io.EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
// serverPool backs server dispatcher
|
// serverPool backs server dispacher
|
||||||
type serverPool struct {
|
type serverPool struct {
|
||||||
pool pendingServerMap
|
pool pendingServerMap
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
@@ -2382,7 +2362,7 @@ func (p *serverPool) addServer(url string, now time.Time) {
|
|||||||
expiry := now.Add(p.expirySec * time.Second)
|
expiry := now.Add(p.expirySec * time.Second)
|
||||||
|
|
||||||
expiryStr := []byte("-")
|
expiryStr := []byte("-")
|
||||||
if p.fs.ci.LogLevel >= fs.LogLevelInfo {
|
if fs.Config.LogLevel >= fs.LogLevelInfo {
|
||||||
expiryStr, _ = expiry.MarshalJSON()
|
expiryStr, _ = expiry.MarshalJSON()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ Improvements:
|
|||||||
* Uploads could be done in parallel
|
* Uploads could be done in parallel
|
||||||
* Downloads would be more efficient done in one go
|
* Downloads would be more efficient done in one go
|
||||||
* Uploads would be more efficient with bigger chunks
|
* Uploads would be more efficient with bigger chunks
|
||||||
* Looks like mega can support server-side copy, but it isn't implemented in go-mega
|
* Looks like mega can support server side copy, but it isn't implemented in go-mega
|
||||||
* Upload can set modtime... - set as int64_t - can set ctime and mtime?
|
* Upload can set modtime... - set as int64_t - can set ctime and mtime?
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -30,7 +30,6 @@ import (
|
|||||||
"github.com/rclone/rclone/fs/config/configmap"
|
"github.com/rclone/rclone/fs/config/configmap"
|
||||||
"github.com/rclone/rclone/fs/config/configstruct"
|
"github.com/rclone/rclone/fs/config/configstruct"
|
||||||
"github.com/rclone/rclone/fs/config/obscure"
|
"github.com/rclone/rclone/fs/config/obscure"
|
||||||
"github.com/rclone/rclone/fs/fserrors"
|
|
||||||
"github.com/rclone/rclone/fs/fshttp"
|
"github.com/rclone/rclone/fs/fshttp"
|
||||||
"github.com/rclone/rclone/fs/hash"
|
"github.com/rclone/rclone/fs/hash"
|
||||||
"github.com/rclone/rclone/lib/encoder"
|
"github.com/rclone/rclone/lib/encoder"
|
||||||
@@ -159,10 +158,7 @@ func parsePath(path string) (root string) {
|
|||||||
|
|
||||||
// shouldRetry returns a boolean as to whether this err deserves to be
|
// shouldRetry returns a boolean as to whether this err deserves to be
|
||||||
// retried. It returns the err as a convenience
|
// retried. It returns the err as a convenience
|
||||||
func shouldRetry(ctx context.Context, err error) (bool, error) {
|
func shouldRetry(err error) (bool, error) {
|
||||||
if fserrors.ContextError(ctx, &err) {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
// Let the mega library handle the low level retries
|
// Let the mega library handle the low level retries
|
||||||
return false, err
|
return false, err
|
||||||
/*
|
/*
|
||||||
@@ -175,8 +171,8 @@ func shouldRetry(ctx context.Context, err error) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// readMetaDataForPath reads the metadata from the path
|
// readMetaDataForPath reads the metadata from the path
|
||||||
func (f *Fs) readMetaDataForPath(ctx context.Context, remote string) (info *mega.Node, err error) {
|
func (f *Fs) readMetaDataForPath(remote string) (info *mega.Node, err error) {
|
||||||
rootNode, err := f.findRoot(ctx, false)
|
rootNode, err := f.findRoot(false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -184,7 +180,7 @@ func (f *Fs) readMetaDataForPath(ctx context.Context, remote string) (info *mega
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewFs constructs an Fs from the path, container:path
|
// NewFs constructs an Fs from the path, container:path
|
||||||
func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, error) {
|
func NewFs(name, root string, m configmap.Mapper) (fs.Fs, error) {
|
||||||
// Parse config into Options struct
|
// Parse config into Options struct
|
||||||
opt := new(Options)
|
opt := new(Options)
|
||||||
err := configstruct.Set(m, opt)
|
err := configstruct.Set(m, opt)
|
||||||
@@ -198,7 +194,6 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
return nil, errors.Wrap(err, "couldn't decrypt password")
|
return nil, errors.Wrap(err, "couldn't decrypt password")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ci := fs.GetConfig(ctx)
|
|
||||||
|
|
||||||
// cache *mega.Mega on username so we can re-use and share
|
// cache *mega.Mega on username so we can re-use and share
|
||||||
// them between remotes. They are expensive to make as they
|
// them between remotes. They are expensive to make as they
|
||||||
@@ -209,8 +204,8 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
defer megaCacheMu.Unlock()
|
defer megaCacheMu.Unlock()
|
||||||
srv := megaCache[opt.User]
|
srv := megaCache[opt.User]
|
||||||
if srv == nil {
|
if srv == nil {
|
||||||
srv = mega.New().SetClient(fshttp.NewClient(ctx))
|
srv = mega.New().SetClient(fshttp.NewClient(fs.Config))
|
||||||
srv.SetRetries(ci.LowLevelRetries) // let mega do the low level retries
|
srv.SetRetries(fs.Config.LowLevelRetries) // let mega do the low level retries
|
||||||
srv.SetLogger(func(format string, v ...interface{}) {
|
srv.SetLogger(func(format string, v ...interface{}) {
|
||||||
fs.Infof("*go-mega*", format, v...)
|
fs.Infof("*go-mega*", format, v...)
|
||||||
})
|
})
|
||||||
@@ -233,15 +228,15 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
root: root,
|
root: root,
|
||||||
opt: *opt,
|
opt: *opt,
|
||||||
srv: srv,
|
srv: srv,
|
||||||
pacer: fs.NewPacer(ctx, pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))),
|
pacer: fs.NewPacer(pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))),
|
||||||
}
|
}
|
||||||
f.features = (&fs.Features{
|
f.features = (&fs.Features{
|
||||||
DuplicateFiles: true,
|
DuplicateFiles: true,
|
||||||
CanHaveEmptyDirectories: true,
|
CanHaveEmptyDirectories: true,
|
||||||
}).Fill(ctx, f)
|
}).Fill(f)
|
||||||
|
|
||||||
// Find the root node and check if it is a file or not
|
// Find the root node and check if it is a file or not
|
||||||
_, err = f.findRoot(ctx, false)
|
_, err = f.findRoot(false)
|
||||||
switch err {
|
switch err {
|
||||||
case nil:
|
case nil:
|
||||||
// root node found and is a directory
|
// root node found and is a directory
|
||||||
@@ -311,8 +306,8 @@ func (f *Fs) findObject(rootNode *mega.Node, file string) (node *mega.Node, err
|
|||||||
// lookupDir looks up the node for the directory of the name given
|
// lookupDir looks up the node for the directory of the name given
|
||||||
//
|
//
|
||||||
// if create is true it tries to create the root directory if not found
|
// if create is true it tries to create the root directory if not found
|
||||||
func (f *Fs) lookupDir(ctx context.Context, dir string) (*mega.Node, error) {
|
func (f *Fs) lookupDir(dir string) (*mega.Node, error) {
|
||||||
rootNode, err := f.findRoot(ctx, false)
|
rootNode, err := f.findRoot(false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -320,15 +315,15 @@ func (f *Fs) lookupDir(ctx context.Context, dir string) (*mega.Node, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// lookupParentDir finds the parent node for the remote passed in
|
// lookupParentDir finds the parent node for the remote passed in
|
||||||
func (f *Fs) lookupParentDir(ctx context.Context, remote string) (dirNode *mega.Node, leaf string, err error) {
|
func (f *Fs) lookupParentDir(remote string) (dirNode *mega.Node, leaf string, err error) {
|
||||||
parent, leaf := path.Split(remote)
|
parent, leaf := path.Split(remote)
|
||||||
dirNode, err = f.lookupDir(ctx, parent)
|
dirNode, err = f.lookupDir(parent)
|
||||||
return dirNode, leaf, err
|
return dirNode, leaf, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// mkdir makes the directory and any parent directories for the
|
// mkdir makes the directory and any parent directories for the
|
||||||
// directory of the name given
|
// directory of the name given
|
||||||
func (f *Fs) mkdir(ctx context.Context, rootNode *mega.Node, dir string) (node *mega.Node, err error) {
|
func (f *Fs) mkdir(rootNode *mega.Node, dir string) (node *mega.Node, err error) {
|
||||||
f.mkdirMu.Lock()
|
f.mkdirMu.Lock()
|
||||||
defer f.mkdirMu.Unlock()
|
defer f.mkdirMu.Unlock()
|
||||||
|
|
||||||
@@ -362,7 +357,7 @@ func (f *Fs) mkdir(ctx context.Context, rootNode *mega.Node, dir string) (node *
|
|||||||
// create directory called name in node
|
// create directory called name in node
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
node, err = f.srv.CreateDir(name, node)
|
node, err = f.srv.CreateDir(name, node)
|
||||||
return shouldRetry(ctx, err)
|
return shouldRetry(err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "mkdir create node failed")
|
return nil, errors.Wrap(err, "mkdir create node failed")
|
||||||
@@ -372,20 +367,20 @@ func (f *Fs) mkdir(ctx context.Context, rootNode *mega.Node, dir string) (node *
|
|||||||
}
|
}
|
||||||
|
|
||||||
// mkdirParent creates the parent directory of remote
|
// mkdirParent creates the parent directory of remote
|
||||||
func (f *Fs) mkdirParent(ctx context.Context, remote string) (dirNode *mega.Node, leaf string, err error) {
|
func (f *Fs) mkdirParent(remote string) (dirNode *mega.Node, leaf string, err error) {
|
||||||
rootNode, err := f.findRoot(ctx, true)
|
rootNode, err := f.findRoot(true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
parent, leaf := path.Split(remote)
|
parent, leaf := path.Split(remote)
|
||||||
dirNode, err = f.mkdir(ctx, rootNode, parent)
|
dirNode, err = f.mkdir(rootNode, parent)
|
||||||
return dirNode, leaf, err
|
return dirNode, leaf, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// findRoot looks up the root directory node and returns it.
|
// findRoot looks up the root directory node and returns it.
|
||||||
//
|
//
|
||||||
// if create is true it tries to create the root directory if not found
|
// if create is true it tries to create the root directory if not found
|
||||||
func (f *Fs) findRoot(ctx context.Context, create bool) (*mega.Node, error) {
|
func (f *Fs) findRoot(create bool) (*mega.Node, error) {
|
||||||
f.rootNodeMu.Lock()
|
f.rootNodeMu.Lock()
|
||||||
defer f.rootNodeMu.Unlock()
|
defer f.rootNodeMu.Unlock()
|
||||||
|
|
||||||
@@ -407,7 +402,7 @@ func (f *Fs) findRoot(ctx context.Context, create bool) (*mega.Node, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//..not found so create the root directory
|
//..not found so create the root directory
|
||||||
f._rootNode, err = f.mkdir(ctx, absRoot, f.root)
|
f._rootNode, err = f.mkdir(absRoot, f.root)
|
||||||
return f._rootNode, err
|
return f._rootNode, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -437,7 +432,7 @@ func (f *Fs) CleanUp(ctx context.Context) (err error) {
|
|||||||
fs.Debugf(f, "Deleting trash %q", f.opt.Enc.ToStandardName(item.GetName()))
|
fs.Debugf(f, "Deleting trash %q", f.opt.Enc.ToStandardName(item.GetName()))
|
||||||
deleteErr := f.pacer.Call(func() (bool, error) {
|
deleteErr := f.pacer.Call(func() (bool, error) {
|
||||||
err := f.srv.Delete(item, true)
|
err := f.srv.Delete(item, true)
|
||||||
return shouldRetry(ctx, err)
|
return shouldRetry(err)
|
||||||
})
|
})
|
||||||
if deleteErr != nil {
|
if deleteErr != nil {
|
||||||
err = deleteErr
|
err = deleteErr
|
||||||
@@ -451,7 +446,7 @@ func (f *Fs) CleanUp(ctx context.Context) (err error) {
|
|||||||
// Return an Object from a path
|
// Return an Object from a path
|
||||||
//
|
//
|
||||||
// If it can't be found it returns the error fs.ErrorObjectNotFound.
|
// If it can't be found it returns the error fs.ErrorObjectNotFound.
|
||||||
func (f *Fs) newObjectWithInfo(ctx context.Context, remote string, info *mega.Node) (fs.Object, error) {
|
func (f *Fs) newObjectWithInfo(remote string, info *mega.Node) (fs.Object, error) {
|
||||||
o := &Object{
|
o := &Object{
|
||||||
fs: f,
|
fs: f,
|
||||||
remote: remote,
|
remote: remote,
|
||||||
@@ -461,7 +456,7 @@ func (f *Fs) newObjectWithInfo(ctx context.Context, remote string, info *mega.No
|
|||||||
// Set info
|
// Set info
|
||||||
err = o.setMetaData(info)
|
err = o.setMetaData(info)
|
||||||
} else {
|
} else {
|
||||||
err = o.readMetaData(ctx) // reads info and meta, returning an error
|
err = o.readMetaData() // reads info and meta, returning an error
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -472,7 +467,7 @@ func (f *Fs) newObjectWithInfo(ctx context.Context, remote string, info *mega.No
|
|||||||
// NewObject finds the Object at remote. If it can't be found
|
// NewObject finds the Object at remote. If it can't be found
|
||||||
// it returns the error fs.ErrorObjectNotFound.
|
// it returns the error fs.ErrorObjectNotFound.
|
||||||
func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) {
|
func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) {
|
||||||
return f.newObjectWithInfo(ctx, remote, nil)
|
return f.newObjectWithInfo(remote, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// list the objects into the function supplied
|
// list the objects into the function supplied
|
||||||
@@ -510,7 +505,7 @@ func (f *Fs) list(ctx context.Context, dir *mega.Node, fn listFn) (found bool, e
|
|||||||
// This should return ErrDirNotFound if the directory isn't
|
// This should return ErrDirNotFound if the directory isn't
|
||||||
// found.
|
// found.
|
||||||
func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) {
|
func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) {
|
||||||
dirNode, err := f.lookupDir(ctx, dir)
|
dirNode, err := f.lookupDir(dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -522,7 +517,7 @@ func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err e
|
|||||||
d := fs.NewDir(remote, info.GetTimeStamp()).SetID(info.GetHash())
|
d := fs.NewDir(remote, info.GetTimeStamp()).SetID(info.GetHash())
|
||||||
entries = append(entries, d)
|
entries = append(entries, d)
|
||||||
case mega.FILE:
|
case mega.FILE:
|
||||||
o, err := f.newObjectWithInfo(ctx, remote, info)
|
o, err := f.newObjectWithInfo(remote, info)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
iErr = err
|
iErr = err
|
||||||
return true
|
return true
|
||||||
@@ -546,8 +541,8 @@ func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err e
|
|||||||
// Returns the dirNode, object, leaf and error
|
// Returns the dirNode, object, leaf and error
|
||||||
//
|
//
|
||||||
// Used to create new objects
|
// Used to create new objects
|
||||||
func (f *Fs) createObject(ctx context.Context, remote string, modTime time.Time, size int64) (o *Object, dirNode *mega.Node, leaf string, err error) {
|
func (f *Fs) createObject(remote string, modTime time.Time, size int64) (o *Object, dirNode *mega.Node, leaf string, err error) {
|
||||||
dirNode, leaf, err = f.mkdirParent(ctx, remote)
|
dirNode, leaf, err = f.mkdirParent(remote)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, leaf, err
|
return nil, nil, leaf, err
|
||||||
}
|
}
|
||||||
@@ -569,7 +564,7 @@ func (f *Fs) createObject(ctx context.Context, remote string, modTime time.Time,
|
|||||||
// This will create a duplicate if we upload a new file without
|
// This will create a duplicate if we upload a new file without
|
||||||
// checking to see if there is one already - use Put() for that.
|
// checking to see if there is one already - use Put() for that.
|
||||||
func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
|
func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
|
||||||
existingObj, err := f.newObjectWithInfo(ctx, src.Remote(), nil)
|
existingObj, err := f.newObjectWithInfo(src.Remote(), nil)
|
||||||
switch err {
|
switch err {
|
||||||
case nil:
|
case nil:
|
||||||
return existingObj, existingObj.Update(ctx, in, src, options...)
|
return existingObj, existingObj.Update(ctx, in, src, options...)
|
||||||
@@ -595,7 +590,7 @@ func (f *Fs) PutUnchecked(ctx context.Context, in io.Reader, src fs.ObjectInfo,
|
|||||||
size := src.Size()
|
size := src.Size()
|
||||||
modTime := src.ModTime(ctx)
|
modTime := src.ModTime(ctx)
|
||||||
|
|
||||||
o, _, _, err := f.createObject(ctx, remote, modTime, size)
|
o, _, _, err := f.createObject(remote, modTime, size)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -604,30 +599,30 @@ func (f *Fs) PutUnchecked(ctx context.Context, in io.Reader, src fs.ObjectInfo,
|
|||||||
|
|
||||||
// Mkdir creates the directory if it doesn't exist
|
// Mkdir creates the directory if it doesn't exist
|
||||||
func (f *Fs) Mkdir(ctx context.Context, dir string) error {
|
func (f *Fs) Mkdir(ctx context.Context, dir string) error {
|
||||||
rootNode, err := f.findRoot(ctx, true)
|
rootNode, err := f.findRoot(true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err = f.mkdir(ctx, rootNode, dir)
|
_, err = f.mkdir(rootNode, dir)
|
||||||
return errors.Wrap(err, "Mkdir failed")
|
return errors.Wrap(err, "Mkdir failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
// deleteNode removes a file or directory, observing useTrash
|
// deleteNode removes a file or directory, observing useTrash
|
||||||
func (f *Fs) deleteNode(ctx context.Context, node *mega.Node) (err error) {
|
func (f *Fs) deleteNode(node *mega.Node) (err error) {
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
err = f.srv.Delete(node, f.opt.HardDelete)
|
err = f.srv.Delete(node, f.opt.HardDelete)
|
||||||
return shouldRetry(ctx, err)
|
return shouldRetry(err)
|
||||||
})
|
})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// purgeCheck removes the directory dir, if check is set then it
|
// purgeCheck removes the directory dir, if check is set then it
|
||||||
// refuses to do so if it has anything in
|
// refuses to do so if it has anything in
|
||||||
func (f *Fs) purgeCheck(ctx context.Context, dir string, check bool) error {
|
func (f *Fs) purgeCheck(dir string, check bool) error {
|
||||||
f.mkdirMu.Lock()
|
f.mkdirMu.Lock()
|
||||||
defer f.mkdirMu.Unlock()
|
defer f.mkdirMu.Unlock()
|
||||||
|
|
||||||
rootNode, err := f.findRoot(ctx, false)
|
rootNode, err := f.findRoot(false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -648,7 +643,7 @@ func (f *Fs) purgeCheck(ctx context.Context, dir string, check bool) error {
|
|||||||
|
|
||||||
waitEvent := f.srv.WaitEventsStart()
|
waitEvent := f.srv.WaitEventsStart()
|
||||||
|
|
||||||
err = f.deleteNode(ctx, dirNode)
|
err = f.deleteNode(dirNode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "delete directory node failed")
|
return errors.Wrap(err, "delete directory node failed")
|
||||||
}
|
}
|
||||||
@@ -666,7 +661,7 @@ func (f *Fs) purgeCheck(ctx context.Context, dir string, check bool) error {
|
|||||||
//
|
//
|
||||||
// Returns an error if it isn't empty
|
// Returns an error if it isn't empty
|
||||||
func (f *Fs) Rmdir(ctx context.Context, dir string) error {
|
func (f *Fs) Rmdir(ctx context.Context, dir string) error {
|
||||||
return f.purgeCheck(ctx, dir, true)
|
return f.purgeCheck(dir, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Precision return the precision of this Fs
|
// Precision return the precision of this Fs
|
||||||
@@ -680,13 +675,13 @@ func (f *Fs) Precision() time.Duration {
|
|||||||
// deleting all the files quicker than just running Remove() on the
|
// deleting all the files quicker than just running Remove() on the
|
||||||
// result of List()
|
// result of List()
|
||||||
func (f *Fs) Purge(ctx context.Context, dir string) error {
|
func (f *Fs) Purge(ctx context.Context, dir string) error {
|
||||||
return f.purgeCheck(ctx, dir, false)
|
return f.purgeCheck(dir, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// move a file or folder (srcFs, srcRemote, info) to (f, dstRemote)
|
// move a file or folder (srcFs, srcRemote, info) to (f, dstRemote)
|
||||||
//
|
//
|
||||||
// info will be updates
|
// info will be updates
|
||||||
func (f *Fs) move(ctx context.Context, dstRemote string, srcFs *Fs, srcRemote string, info *mega.Node) (err error) {
|
func (f *Fs) move(dstRemote string, srcFs *Fs, srcRemote string, info *mega.Node) (err error) {
|
||||||
var (
|
var (
|
||||||
dstFs = f
|
dstFs = f
|
||||||
srcDirNode, dstDirNode *mega.Node
|
srcDirNode, dstDirNode *mega.Node
|
||||||
@@ -696,20 +691,20 @@ func (f *Fs) move(ctx context.Context, dstRemote string, srcFs *Fs, srcRemote st
|
|||||||
|
|
||||||
if dstRemote != "" {
|
if dstRemote != "" {
|
||||||
// lookup or create the destination parent directory
|
// lookup or create the destination parent directory
|
||||||
dstDirNode, dstLeaf, err = dstFs.mkdirParent(ctx, dstRemote)
|
dstDirNode, dstLeaf, err = dstFs.mkdirParent(dstRemote)
|
||||||
} else {
|
} else {
|
||||||
// find or create the parent of the root directory
|
// find or create the parent of the root directory
|
||||||
absRoot := dstFs.srv.FS.GetRoot()
|
absRoot := dstFs.srv.FS.GetRoot()
|
||||||
dstParent, dstLeaf = path.Split(dstFs.root)
|
dstParent, dstLeaf = path.Split(dstFs.root)
|
||||||
dstDirNode, err = dstFs.mkdir(ctx, absRoot, dstParent)
|
dstDirNode, err = dstFs.mkdir(absRoot, dstParent)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "server-side move failed to make dst parent dir")
|
return errors.Wrap(err, "server side move failed to make dst parent dir")
|
||||||
}
|
}
|
||||||
|
|
||||||
if srcRemote != "" {
|
if srcRemote != "" {
|
||||||
// lookup the existing parent directory
|
// lookup the existing parent directory
|
||||||
srcDirNode, srcLeaf, err = srcFs.lookupParentDir(ctx, srcRemote)
|
srcDirNode, srcLeaf, err = srcFs.lookupParentDir(srcRemote)
|
||||||
} else {
|
} else {
|
||||||
// lookup the existing root parent
|
// lookup the existing root parent
|
||||||
absRoot := srcFs.srv.FS.GetRoot()
|
absRoot := srcFs.srv.FS.GetRoot()
|
||||||
@@ -717,7 +712,7 @@ func (f *Fs) move(ctx context.Context, dstRemote string, srcFs *Fs, srcRemote st
|
|||||||
srcDirNode, err = f.findDir(absRoot, srcParent)
|
srcDirNode, err = f.findDir(absRoot, srcParent)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "server-side move failed to lookup src parent dir")
|
return errors.Wrap(err, "server side move failed to lookup src parent dir")
|
||||||
}
|
}
|
||||||
|
|
||||||
// move the object into its new directory if required
|
// move the object into its new directory if required
|
||||||
@@ -725,10 +720,10 @@ func (f *Fs) move(ctx context.Context, dstRemote string, srcFs *Fs, srcRemote st
|
|||||||
//log.Printf("move src %p %q dst %p %q", srcDirNode, srcDirNode.GetName(), dstDirNode, dstDirNode.GetName())
|
//log.Printf("move src %p %q dst %p %q", srcDirNode, srcDirNode.GetName(), dstDirNode, dstDirNode.GetName())
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
err = f.srv.Move(info, dstDirNode)
|
err = f.srv.Move(info, dstDirNode)
|
||||||
return shouldRetry(ctx, err)
|
return shouldRetry(err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "server-side move failed")
|
return errors.Wrap(err, "server side move failed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -739,10 +734,10 @@ func (f *Fs) move(ctx context.Context, dstRemote string, srcFs *Fs, srcRemote st
|
|||||||
//log.Printf("rename %q to %q", srcLeaf, dstLeaf)
|
//log.Printf("rename %q to %q", srcLeaf, dstLeaf)
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
err = f.srv.Rename(info, f.opt.Enc.FromStandardName(dstLeaf))
|
err = f.srv.Rename(info, f.opt.Enc.FromStandardName(dstLeaf))
|
||||||
return shouldRetry(ctx, err)
|
return shouldRetry(err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "server-side rename failed")
|
return errors.Wrap(err, "server side rename failed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -751,7 +746,7 @@ func (f *Fs) move(ctx context.Context, dstRemote string, srcFs *Fs, srcRemote st
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move src to this remote using server-side move operations.
|
// Move src to this remote using server side move operations.
|
||||||
//
|
//
|
||||||
// This is stored with the remote path given
|
// This is stored with the remote path given
|
||||||
//
|
//
|
||||||
@@ -771,7 +766,7 @@ func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Do the move
|
// Do the move
|
||||||
err := f.move(ctx, remote, srcObj.fs, srcObj.remote, srcObj.info)
|
err := f.move(remote, srcObj.fs, srcObj.remote, srcObj.info)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -786,7 +781,7 @@ func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DirMove moves src, srcRemote to this remote at dstRemote
|
// DirMove moves src, srcRemote to this remote at dstRemote
|
||||||
// using server-side move operations.
|
// using server side move operations.
|
||||||
//
|
//
|
||||||
// Will only be called if src.Fs().Name() == f.Name()
|
// Will only be called if src.Fs().Name() == f.Name()
|
||||||
//
|
//
|
||||||
@@ -802,13 +797,13 @@ func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string
|
|||||||
}
|
}
|
||||||
|
|
||||||
// find the source
|
// find the source
|
||||||
info, err := srcFs.lookupDir(ctx, srcRemote)
|
info, err := srcFs.lookupDir(srcRemote)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// check the destination doesn't exist
|
// check the destination doesn't exist
|
||||||
_, err = dstFs.lookupDir(ctx, dstRemote)
|
_, err = dstFs.lookupDir(dstRemote)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return fs.ErrorDirExists
|
return fs.ErrorDirExists
|
||||||
} else if err != fs.ErrorDirNotFound {
|
} else if err != fs.ErrorDirNotFound {
|
||||||
@@ -816,7 +811,7 @@ func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Do the move
|
// Do the move
|
||||||
err = f.move(ctx, dstRemote, srcFs, srcRemote, info)
|
err = f.move(dstRemote, srcFs, srcRemote, info)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -842,7 +837,7 @@ func (f *Fs) Hashes() hash.Set {
|
|||||||
|
|
||||||
// PublicLink generates a public link to the remote path (usually readable by anyone)
|
// PublicLink generates a public link to the remote path (usually readable by anyone)
|
||||||
func (f *Fs) PublicLink(ctx context.Context, remote string, expire fs.Duration, unlink bool) (link string, err error) {
|
func (f *Fs) PublicLink(ctx context.Context, remote string, expire fs.Duration, unlink bool) (link string, err error) {
|
||||||
root, err := f.findRoot(ctx, false)
|
root, err := f.findRoot(false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.Wrap(err, "PublicLink failed to find root node")
|
return "", errors.Wrap(err, "PublicLink failed to find root node")
|
||||||
}
|
}
|
||||||
@@ -890,7 +885,7 @@ func (f *Fs) MergeDirs(ctx context.Context, dirs []fs.Directory) error {
|
|||||||
fs.Infof(srcDir, "merging %q", f.opt.Enc.ToStandardName(info.GetName()))
|
fs.Infof(srcDir, "merging %q", f.opt.Enc.ToStandardName(info.GetName()))
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
err = f.srv.Move(info, dstDirNode)
|
err = f.srv.Move(info, dstDirNode)
|
||||||
return shouldRetry(ctx, err)
|
return shouldRetry(err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(err, "MergeDirs move failed on %q in %v", f.opt.Enc.ToStandardName(info.GetName()), srcDir)
|
return errors.Wrapf(err, "MergeDirs move failed on %q in %v", f.opt.Enc.ToStandardName(info.GetName()), srcDir)
|
||||||
@@ -898,7 +893,7 @@ func (f *Fs) MergeDirs(ctx context.Context, dirs []fs.Directory) error {
|
|||||||
}
|
}
|
||||||
// rmdir (into trash) the now empty source directory
|
// rmdir (into trash) the now empty source directory
|
||||||
fs.Infof(srcDir, "removing empty directory")
|
fs.Infof(srcDir, "removing empty directory")
|
||||||
err = f.deleteNode(ctx, srcDirNode)
|
err = f.deleteNode(srcDirNode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(err, "MergeDirs move failed to rmdir %q", srcDir)
|
return errors.Wrapf(err, "MergeDirs move failed to rmdir %q", srcDir)
|
||||||
}
|
}
|
||||||
@@ -912,7 +907,7 @@ func (f *Fs) About(ctx context.Context) (*fs.Usage, error) {
|
|||||||
var err error
|
var err error
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
q, err = f.srv.GetQuota()
|
q, err = f.srv.GetQuota()
|
||||||
return shouldRetry(ctx, err)
|
return shouldRetry(err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to get Mega Quota")
|
return nil, errors.Wrap(err, "failed to get Mega Quota")
|
||||||
@@ -967,11 +962,11 @@ func (o *Object) setMetaData(info *mega.Node) (err error) {
|
|||||||
// readMetaData gets the metadata if it hasn't already been fetched
|
// readMetaData gets the metadata if it hasn't already been fetched
|
||||||
//
|
//
|
||||||
// it also sets the info
|
// it also sets the info
|
||||||
func (o *Object) readMetaData(ctx context.Context) (err error) {
|
func (o *Object) readMetaData() (err error) {
|
||||||
if o.info != nil {
|
if o.info != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
info, err := o.fs.readMetaDataForPath(ctx, o.remote)
|
info, err := o.fs.readMetaDataForPath(o.remote)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == fs.ErrorDirNotFound {
|
if err == fs.ErrorDirNotFound {
|
||||||
err = fs.ErrorObjectNotFound
|
err = fs.ErrorObjectNotFound
|
||||||
@@ -1002,7 +997,6 @@ func (o *Object) Storable() bool {
|
|||||||
|
|
||||||
// openObject represents a download in progress
|
// openObject represents a download in progress
|
||||||
type openObject struct {
|
type openObject struct {
|
||||||
ctx context.Context
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
o *Object
|
o *Object
|
||||||
d *mega.Download
|
d *mega.Download
|
||||||
@@ -1013,14 +1007,14 @@ type openObject struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// get the next chunk
|
// get the next chunk
|
||||||
func (oo *openObject) getChunk(ctx context.Context) (err error) {
|
func (oo *openObject) getChunk() (err error) {
|
||||||
if oo.id >= oo.d.Chunks() {
|
if oo.id >= oo.d.Chunks() {
|
||||||
return io.EOF
|
return io.EOF
|
||||||
}
|
}
|
||||||
var chunk []byte
|
var chunk []byte
|
||||||
err = oo.o.fs.pacer.Call(func() (bool, error) {
|
err = oo.o.fs.pacer.Call(func() (bool, error) {
|
||||||
chunk, err = oo.d.DownloadChunk(oo.id)
|
chunk, err = oo.d.DownloadChunk(oo.id)
|
||||||
return shouldRetry(ctx, err)
|
return shouldRetry(err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1050,7 +1044,7 @@ func (oo *openObject) Read(p []byte) (n int, err error) {
|
|||||||
oo.skip -= int64(size)
|
oo.skip -= int64(size)
|
||||||
}
|
}
|
||||||
if len(oo.chunk) == 0 {
|
if len(oo.chunk) == 0 {
|
||||||
err = oo.getChunk(oo.ctx)
|
err = oo.getChunk()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
@@ -1073,7 +1067,7 @@ func (oo *openObject) Close() (err error) {
|
|||||||
}
|
}
|
||||||
err = oo.o.fs.pacer.Call(func() (bool, error) {
|
err = oo.o.fs.pacer.Call(func() (bool, error) {
|
||||||
err = oo.d.Finish()
|
err = oo.d.Finish()
|
||||||
return shouldRetry(oo.ctx, err)
|
return shouldRetry(err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to finish download")
|
return errors.Wrap(err, "failed to finish download")
|
||||||
@@ -1101,14 +1095,13 @@ func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.Read
|
|||||||
var d *mega.Download
|
var d *mega.Download
|
||||||
err = o.fs.pacer.Call(func() (bool, error) {
|
err = o.fs.pacer.Call(func() (bool, error) {
|
||||||
d, err = o.fs.srv.NewDownload(o.info)
|
d, err = o.fs.srv.NewDownload(o.info)
|
||||||
return shouldRetry(ctx, err)
|
return shouldRetry(err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "open download file failed")
|
return nil, errors.Wrap(err, "open download file failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
oo := &openObject{
|
oo := &openObject{
|
||||||
ctx: ctx,
|
|
||||||
o: o,
|
o: o,
|
||||||
d: d,
|
d: d,
|
||||||
skip: offset,
|
skip: offset,
|
||||||
@@ -1131,7 +1124,7 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
|
|||||||
remote := o.Remote()
|
remote := o.Remote()
|
||||||
|
|
||||||
// Create the parent directory
|
// Create the parent directory
|
||||||
dirNode, leaf, err := o.fs.mkdirParent(ctx, remote)
|
dirNode, leaf, err := o.fs.mkdirParent(remote)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "update make parent dir failed")
|
return errors.Wrap(err, "update make parent dir failed")
|
||||||
}
|
}
|
||||||
@@ -1139,7 +1132,7 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
|
|||||||
var u *mega.Upload
|
var u *mega.Upload
|
||||||
err = o.fs.pacer.Call(func() (bool, error) {
|
err = o.fs.pacer.Call(func() (bool, error) {
|
||||||
u, err = o.fs.srv.NewUpload(dirNode, o.fs.opt.Enc.FromStandardName(leaf), size)
|
u, err = o.fs.srv.NewUpload(dirNode, o.fs.opt.Enc.FromStandardName(leaf), size)
|
||||||
return shouldRetry(ctx, err)
|
return shouldRetry(err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "upload file failed to create session")
|
return errors.Wrap(err, "upload file failed to create session")
|
||||||
@@ -1160,7 +1153,7 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
|
|||||||
|
|
||||||
err = o.fs.pacer.Call(func() (bool, error) {
|
err = o.fs.pacer.Call(func() (bool, error) {
|
||||||
err = u.UploadChunk(id, chunk)
|
err = u.UploadChunk(id, chunk)
|
||||||
return shouldRetry(ctx, err)
|
return shouldRetry(err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "upload file failed to upload chunk")
|
return errors.Wrap(err, "upload file failed to upload chunk")
|
||||||
@@ -1171,7 +1164,7 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
|
|||||||
var info *mega.Node
|
var info *mega.Node
|
||||||
err = o.fs.pacer.Call(func() (bool, error) {
|
err = o.fs.pacer.Call(func() (bool, error) {
|
||||||
info, err = u.Finish()
|
info, err = u.Finish()
|
||||||
return shouldRetry(ctx, err)
|
return shouldRetry(err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to finish upload")
|
return errors.Wrap(err, "failed to finish upload")
|
||||||
@@ -1179,7 +1172,7 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
|
|||||||
|
|
||||||
// If the upload succeeded and the original object existed, then delete it
|
// If the upload succeeded and the original object existed, then delete it
|
||||||
if o.info != nil {
|
if o.info != nil {
|
||||||
err = o.fs.deleteNode(ctx, o.info)
|
err = o.fs.deleteNode(o.info)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "upload failed to remove old version")
|
return errors.Wrap(err, "upload failed to remove old version")
|
||||||
}
|
}
|
||||||
@@ -1191,7 +1184,7 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
|
|||||||
|
|
||||||
// Remove an object
|
// Remove an object
|
||||||
func (o *Object) Remove(ctx context.Context) error {
|
func (o *Object) Remove(ctx context.Context) error {
|
||||||
err := o.fs.deleteNode(ctx, o.info)
|
err := o.fs.deleteNode(o.info)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "Remove object failed")
|
return errors.Wrap(err, "Remove object failed")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -221,8 +221,8 @@ func (f *Fs) setRoot(root string) {
|
|||||||
f.rootBucket, f.rootDirectory = bucket.Split(f.root)
|
f.rootBucket, f.rootDirectory = bucket.Split(f.root)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewFs constructs an Fs from the path, bucket:path
|
// NewFs contstructs an Fs from the path, bucket:path
|
||||||
func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, error) {
|
func NewFs(name, root string, m configmap.Mapper) (fs.Fs, error) {
|
||||||
// Parse config into Options struct
|
// Parse config into Options struct
|
||||||
opt := new(Options)
|
opt := new(Options)
|
||||||
err := configstruct.Set(m, opt)
|
err := configstruct.Set(m, opt)
|
||||||
@@ -241,7 +241,7 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
WriteMimeType: true,
|
WriteMimeType: true,
|
||||||
BucketBased: true,
|
BucketBased: true,
|
||||||
BucketBasedRootOK: true,
|
BucketBasedRootOK: true,
|
||||||
}).Fill(ctx, f)
|
}).Fill(f)
|
||||||
if f.rootBucket != "" && f.rootDirectory != "" {
|
if f.rootBucket != "" && f.rootDirectory != "" {
|
||||||
od := buckets.getObjectData(f.rootBucket, f.rootDirectory)
|
od := buckets.getObjectData(f.rootBucket, f.rootDirectory)
|
||||||
if od != nil {
|
if od != nil {
|
||||||
@@ -462,7 +462,7 @@ func (f *Fs) Precision() time.Duration {
|
|||||||
return time.Nanosecond
|
return time.Nanosecond
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy src to this remote using server-side copy operations.
|
// Copy src to this remote using server side copy operations.
|
||||||
//
|
//
|
||||||
// This is stored with the remote path given
|
// This is stored with the remote path given
|
||||||
//
|
//
|
||||||
@@ -592,7 +592,7 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
|
|||||||
data: data,
|
data: data,
|
||||||
hash: "",
|
hash: "",
|
||||||
modTime: src.ModTime(ctx),
|
modTime: src.ModTime(ctx),
|
||||||
mimeType: fs.MimeType(ctx, src),
|
mimeType: fs.MimeType(ctx, o),
|
||||||
}
|
}
|
||||||
buckets.updateObjectData(bucket, bucketPath, o.od)
|
buckets.updateObjectData(bucket, bucketPath, o.od)
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -254,9 +254,7 @@ type MoveItemRequest struct {
|
|||||||
//Always Type:view and Scope:anonymous for public sharing
|
//Always Type:view and Scope:anonymous for public sharing
|
||||||
type CreateShareLinkRequest struct {
|
type CreateShareLinkRequest struct {
|
||||||
Type string `json:"type"` //Link type in View, Edit or Embed
|
Type string `json:"type"` //Link type in View, Edit or Embed
|
||||||
Scope string `json:"scope,omitempty"` // Scope in anonymous, organization
|
Scope string `json:"scope,omitempty"` //Optional. Scope in anonymousi, organization
|
||||||
Password string `json:"password,omitempty"` // The password of the sharing link that is set by the creator. Optional and OneDrive Personal only.
|
|
||||||
Expiry *time.Time `json:"expirationDateTime,omitempty"` // A String with format of yyyy-MM-ddTHH:mm:ssZ of DateTime indicates the expiration time of the permission.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//CreateShareLinkResponse is the response from CreateShareLinkRequest
|
//CreateShareLinkResponse is the response from CreateShareLinkRequest
|
||||||
@@ -283,7 +281,6 @@ type CreateShareLinkResponse struct {
|
|||||||
type AsyncOperationStatus struct {
|
type AsyncOperationStatus struct {
|
||||||
PercentageComplete float64 `json:"percentageComplete"` // A float value between 0 and 100 that indicates the percentage complete.
|
PercentageComplete float64 `json:"percentageComplete"` // A float value between 0 and 100 that indicates the percentage complete.
|
||||||
Status string `json:"status"` // A string value that maps to an enumeration of possible values about the status of the job. "notStarted | inProgress | completed | updating | failed | deletePending | deleteFailed | waiting"
|
Status string `json:"status"` // A string value that maps to an enumeration of possible values about the status of the job. "notStarted | inProgress | completed | updating | failed | deletePending | deleteFailed | waiting"
|
||||||
ErrorCode string `json:"errorCode"` // Not officially documented :(
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetID returns a normalized ID of the item
|
// GetID returns a normalized ID of the item
|
||||||
|
|||||||
@@ -11,9 +11,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"path"
|
"path"
|
||||||
"regexp"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -47,6 +45,7 @@ const (
|
|||||||
minSleep = 10 * time.Millisecond
|
minSleep = 10 * time.Millisecond
|
||||||
maxSleep = 2 * time.Second
|
maxSleep = 2 * time.Second
|
||||||
decayConstant = 2 // bigger for slower decay, exponential
|
decayConstant = 2 // bigger for slower decay, exponential
|
||||||
|
graphURL = "https://graph.microsoft.com/v1.0"
|
||||||
configDriveID = "drive_id"
|
configDriveID = "drive_id"
|
||||||
configDriveType = "drive_type"
|
configDriveType = "drive_type"
|
||||||
driveTypePersonal = "personal"
|
driveTypePersonal = "personal"
|
||||||
@@ -54,40 +53,22 @@ const (
|
|||||||
driveTypeSharepoint = "documentLibrary"
|
driveTypeSharepoint = "documentLibrary"
|
||||||
defaultChunkSize = 10 * fs.MebiByte
|
defaultChunkSize = 10 * fs.MebiByte
|
||||||
chunkSizeMultiple = 320 * fs.KibiByte
|
chunkSizeMultiple = 320 * fs.KibiByte
|
||||||
|
|
||||||
regionGlobal = "global"
|
|
||||||
regionUS = "us"
|
|
||||||
regionDE = "de"
|
|
||||||
regionCN = "cn"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Globals
|
// Globals
|
||||||
var (
|
var (
|
||||||
authPath = "/common/oauth2/v2.0/authorize"
|
|
||||||
tokenPath = "/common/oauth2/v2.0/token"
|
|
||||||
|
|
||||||
// Description of how to auth for this app for a business account
|
// Description of how to auth for this app for a business account
|
||||||
oauthConfig = &oauth2.Config{
|
oauthConfig = &oauth2.Config{
|
||||||
|
Endpoint: oauth2.Endpoint{
|
||||||
|
AuthURL: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
|
||||||
|
TokenURL: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
|
||||||
|
},
|
||||||
Scopes: []string{"Files.Read", "Files.ReadWrite", "Files.Read.All", "Files.ReadWrite.All", "offline_access", "Sites.Read.All"},
|
Scopes: []string{"Files.Read", "Files.ReadWrite", "Files.Read.All", "Files.ReadWrite.All", "offline_access", "Sites.Read.All"},
|
||||||
ClientID: rcloneClientID,
|
ClientID: rcloneClientID,
|
||||||
ClientSecret: obscure.MustReveal(rcloneEncryptedClientSecret),
|
ClientSecret: obscure.MustReveal(rcloneEncryptedClientSecret),
|
||||||
RedirectURL: oauthutil.RedirectLocalhostURL,
|
RedirectURL: oauthutil.RedirectLocalhostURL,
|
||||||
}
|
}
|
||||||
|
|
||||||
graphAPIEndpoint = map[string]string{
|
|
||||||
"global": "https://graph.microsoft.com",
|
|
||||||
"us": "https://graph.microsoft.us",
|
|
||||||
"de": "https://graph.microsoft.de",
|
|
||||||
"cn": "https://microsoftgraph.chinacloudapi.cn",
|
|
||||||
}
|
|
||||||
|
|
||||||
authEndpoint = map[string]string{
|
|
||||||
"global": "https://login.microsoftonline.com",
|
|
||||||
"us": "https://login.microsoftonline.us",
|
|
||||||
"de": "https://login.microsoftonline.de",
|
|
||||||
"cn": "https://login.chinacloudapi.cn",
|
|
||||||
}
|
|
||||||
|
|
||||||
// QuickXorHashType is the hash.Type for OneDrive
|
// QuickXorHashType is the hash.Type for OneDrive
|
||||||
QuickXorHashType hash.Type
|
QuickXorHashType hash.Type
|
||||||
)
|
)
|
||||||
@@ -99,22 +80,16 @@ func init() {
|
|||||||
Name: "onedrive",
|
Name: "onedrive",
|
||||||
Description: "Microsoft OneDrive",
|
Description: "Microsoft OneDrive",
|
||||||
NewFs: NewFs,
|
NewFs: NewFs,
|
||||||
Config: func(ctx context.Context, name string, m configmap.Mapper) {
|
Config: func(name string, m configmap.Mapper) {
|
||||||
region, _ := m.Get("region")
|
ctx := context.TODO()
|
||||||
graphURL := graphAPIEndpoint[region] + "/v1.0"
|
err := oauthutil.Config("onedrive", name, m, oauthConfig, nil)
|
||||||
oauthConfig.Endpoint = oauth2.Endpoint{
|
|
||||||
AuthURL: authEndpoint[region] + authPath,
|
|
||||||
TokenURL: authEndpoint[region] + tokenPath,
|
|
||||||
}
|
|
||||||
ci := fs.GetConfig(ctx)
|
|
||||||
err := oauthutil.Config(ctx, "onedrive", name, m, oauthConfig, nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to configure token: %v", err)
|
log.Fatalf("Failed to configure token: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop if we are running non-interactive config
|
// Stop if we are running non-interactive config
|
||||||
if ci.AutoConfirm {
|
if fs.Config.AutoConfirm {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +111,7 @@ func init() {
|
|||||||
Sites []siteResource `json:"value"`
|
Sites []siteResource `json:"value"`
|
||||||
}
|
}
|
||||||
|
|
||||||
oAuthClient, _, err := oauthutil.NewClient(ctx, name, m, oauthConfig)
|
oAuthClient, _, err := oauthutil.NewClient(name, m, oauthConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to configure OneDrive: %v", err)
|
log.Fatalf("Failed to configure OneDrive: %v", err)
|
||||||
}
|
}
|
||||||
@@ -145,18 +120,9 @@ func init() {
|
|||||||
var opts rest.Opts
|
var opts rest.Opts
|
||||||
var finalDriveID string
|
var finalDriveID string
|
||||||
var siteID string
|
var siteID string
|
||||||
var relativePath string
|
|
||||||
switch config.Choose("Your choice",
|
switch config.Choose("Your choice",
|
||||||
[]string{"onedrive", "sharepoint", "url", "search", "driveid", "siteid", "path"},
|
[]string{"onedrive", "sharepoint", "driveid", "siteid", "search"},
|
||||||
[]string{
|
[]string{"OneDrive Personal or Business", "Root Sharepoint site", "Type in driveID", "Type in SiteID", "Search a Sharepoint site"},
|
||||||
"OneDrive Personal or Business",
|
|
||||||
"Root Sharepoint site",
|
|
||||||
"Sharepoint site name or URL (e.g. mysite or https://contoso.sharepoint.com/sites/mysite)",
|
|
||||||
"Search for a Sharepoint site",
|
|
||||||
"Type in driveID (advanced)",
|
|
||||||
"Type in SiteID (advanced)",
|
|
||||||
"Sharepoint server-relative path (advanced, e.g. /teams/hr)",
|
|
||||||
},
|
|
||||||
false) {
|
false) {
|
||||||
|
|
||||||
case "onedrive":
|
case "onedrive":
|
||||||
@@ -177,20 +143,6 @@ func init() {
|
|||||||
case "siteid":
|
case "siteid":
|
||||||
fmt.Printf("Paste your Site ID here> ")
|
fmt.Printf("Paste your Site ID here> ")
|
||||||
siteID = config.ReadLine()
|
siteID = config.ReadLine()
|
||||||
case "url":
|
|
||||||
fmt.Println("Example: \"https://contoso.sharepoint.com/sites/mysite\" or \"mysite\"")
|
|
||||||
fmt.Printf("Paste your Site URL here> ")
|
|
||||||
siteURL := config.ReadLine()
|
|
||||||
re := regexp.MustCompile(`https://.*\.sharepoint.com/sites/(.*)`)
|
|
||||||
match := re.FindStringSubmatch(siteURL)
|
|
||||||
if len(match) == 2 {
|
|
||||||
relativePath = "/sites/" + match[1]
|
|
||||||
} else {
|
|
||||||
relativePath = "/sites/" + siteURL
|
|
||||||
}
|
|
||||||
case "path":
|
|
||||||
fmt.Printf("Enter server-relative URL here> ")
|
|
||||||
relativePath = config.ReadLine()
|
|
||||||
case "search":
|
case "search":
|
||||||
fmt.Printf("What to search for> ")
|
fmt.Printf("What to search for> ")
|
||||||
searchTerm := config.ReadLine()
|
searchTerm := config.ReadLine()
|
||||||
@@ -217,21 +169,6 @@ func init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if we use server-relative URL for finding the drive
|
|
||||||
if relativePath != "" {
|
|
||||||
opts = rest.Opts{
|
|
||||||
Method: "GET",
|
|
||||||
RootURL: graphURL,
|
|
||||||
Path: "/sites/root:" + relativePath,
|
|
||||||
}
|
|
||||||
site := siteResource{}
|
|
||||||
_, err := srv.CallJSON(ctx, &opts, nil, &site)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to query available site by relative path: %v", err)
|
|
||||||
}
|
|
||||||
siteID = site.SiteID
|
|
||||||
}
|
|
||||||
|
|
||||||
// if we have a siteID we need to ask for the drives
|
// if we have a siteID we need to ask for the drives
|
||||||
if siteID != "" {
|
if siteID != "" {
|
||||||
opts = rest.Opts{
|
opts = rest.Opts{
|
||||||
@@ -296,33 +233,15 @@ func init() {
|
|||||||
|
|
||||||
fmt.Printf("Found drive '%s' of type '%s', URL: %s\nIs that okay?\n", rootItem.Name, rootItem.ParentReference.DriveType, rootItem.WebURL)
|
fmt.Printf("Found drive '%s' of type '%s', URL: %s\nIs that okay?\n", rootItem.Name, rootItem.ParentReference.DriveType, rootItem.WebURL)
|
||||||
// This does not work, YET :)
|
// This does not work, YET :)
|
||||||
if !config.ConfirmWithConfig(ctx, m, "config_drive_ok", true) {
|
if !config.ConfirmWithConfig(m, "config_drive_ok", true) {
|
||||||
log.Fatalf("Cancelled by user")
|
log.Fatalf("Cancelled by user")
|
||||||
}
|
}
|
||||||
|
|
||||||
m.Set(configDriveID, finalDriveID)
|
m.Set(configDriveID, finalDriveID)
|
||||||
m.Set(configDriveType, rootItem.ParentReference.DriveType)
|
m.Set(configDriveType, rootItem.ParentReference.DriveType)
|
||||||
|
config.SaveConfig()
|
||||||
},
|
},
|
||||||
Options: append(oauthutil.SharedOptions, []fs.Option{{
|
Options: append(oauthutil.SharedOptions, []fs.Option{{
|
||||||
Name: "region",
|
|
||||||
Help: "Choose national cloud region for OneDrive.",
|
|
||||||
Default: "global",
|
|
||||||
Examples: []fs.OptionExample{
|
|
||||||
{
|
|
||||||
Value: regionGlobal,
|
|
||||||
Help: "Microsoft Cloud Global",
|
|
||||||
}, {
|
|
||||||
Value: regionUS,
|
|
||||||
Help: "Microsoft Cloud for US Government",
|
|
||||||
}, {
|
|
||||||
Value: regionDE,
|
|
||||||
Help: "Microsoft Cloud Germany",
|
|
||||||
}, {
|
|
||||||
Value: regionCN,
|
|
||||||
Help: "Azure and Office 365 operated by 21Vianet in China",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
Name: "chunk_size",
|
Name: "chunk_size",
|
||||||
Help: `Chunk size to upload files with - must be multiple of 320k (327,680 bytes).
|
Help: `Chunk size to upload files with - must be multiple of 320k (327,680 bytes).
|
||||||
|
|
||||||
@@ -355,16 +274,12 @@ listing, set this option.`,
|
|||||||
}, {
|
}, {
|
||||||
Name: "server_side_across_configs",
|
Name: "server_side_across_configs",
|
||||||
Default: false,
|
Default: false,
|
||||||
Help: `Allow server-side operations (e.g. copy) to work across different onedrive configs.
|
Help: `Allow server side operations (eg copy) to work across different onedrive configs.
|
||||||
|
|
||||||
This will only work if you are copying between two OneDrive *Personal* drives AND
|
This can be useful if you wish to do a server side copy between two
|
||||||
the files to copy are already shared between them. In other cases, rclone will
|
different Onedrives. Note that this isn't enabled by default
|
||||||
fall back to normal copy (which will be slightly slower).`,
|
because it isn't easy to tell if it will work between any two
|
||||||
Advanced: true,
|
configurations.`,
|
||||||
}, {
|
|
||||||
Name: "list_chunk",
|
|
||||||
Help: "Size of listing chunk.",
|
|
||||||
Default: 1000,
|
|
||||||
Advanced: true,
|
Advanced: true,
|
||||||
}, {
|
}, {
|
||||||
Name: "no_versions",
|
Name: "no_versions",
|
||||||
@@ -381,41 +296,6 @@ modification time and removes all but the last version.
|
|||||||
|
|
||||||
**NB** Onedrive personal can't currently delete versions so don't use
|
**NB** Onedrive personal can't currently delete versions so don't use
|
||||||
this flag there.
|
this flag there.
|
||||||
`,
|
|
||||||
Advanced: true,
|
|
||||||
}, {
|
|
||||||
Name: "link_scope",
|
|
||||||
Default: "anonymous",
|
|
||||||
Help: `Set the scope of the links created by the link command.`,
|
|
||||||
Advanced: true,
|
|
||||||
Examples: []fs.OptionExample{{
|
|
||||||
Value: "anonymous",
|
|
||||||
Help: "Anyone with the link has access, without needing to sign in. This may include people outside of your organization. Anonymous link support may be disabled by an administrator.",
|
|
||||||
}, {
|
|
||||||
Value: "organization",
|
|
||||||
Help: "Anyone signed into your organization (tenant) can use the link to get access. Only available in OneDrive for Business and SharePoint.",
|
|
||||||
}},
|
|
||||||
}, {
|
|
||||||
Name: "link_type",
|
|
||||||
Default: "view",
|
|
||||||
Help: `Set the type of the links created by the link command.`,
|
|
||||||
Advanced: true,
|
|
||||||
Examples: []fs.OptionExample{{
|
|
||||||
Value: "view",
|
|
||||||
Help: "Creates a read-only link to the item.",
|
|
||||||
}, {
|
|
||||||
Value: "edit",
|
|
||||||
Help: "Creates a read-write link to the item.",
|
|
||||||
}, {
|
|
||||||
Value: "embed",
|
|
||||||
Help: "Creates an embeddable link to the item.",
|
|
||||||
}},
|
|
||||||
}, {
|
|
||||||
Name: "link_password",
|
|
||||||
Default: "",
|
|
||||||
Help: `Set the password for links created by the link command.
|
|
||||||
|
|
||||||
At the time of writing this only works with OneDrive personal paid accounts.
|
|
||||||
`,
|
`,
|
||||||
Advanced: true,
|
Advanced: true,
|
||||||
}, {
|
}, {
|
||||||
@@ -431,6 +311,8 @@ At the time of writing this only works with OneDrive personal paid accounts.
|
|||||||
// | (vertical line) -> '|' // FULLWIDTH VERTICAL LINE
|
// | (vertical line) -> '|' // FULLWIDTH VERTICAL LINE
|
||||||
// ? (question mark) -> '?' // FULLWIDTH QUESTION MARK
|
// ? (question mark) -> '?' // FULLWIDTH QUESTION MARK
|
||||||
// * (asterisk) -> '*' // FULLWIDTH ASTERISK
|
// * (asterisk) -> '*' // FULLWIDTH ASTERISK
|
||||||
|
// # (number sign) -> '#' // FULLWIDTH NUMBER SIGN
|
||||||
|
// % (percent sign) -> '%' // FULLWIDTH PERCENT SIGN
|
||||||
//
|
//
|
||||||
// Folder names cannot begin with a tilde ('~')
|
// Folder names cannot begin with a tilde ('~')
|
||||||
// List of replaced characters:
|
// List of replaced characters:
|
||||||
@@ -455,6 +337,7 @@ At the time of writing this only works with OneDrive personal paid accounts.
|
|||||||
// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/concepts/addressing-driveitems?view=odsp-graph-online#path-encoding
|
// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/concepts/addressing-driveitems?view=odsp-graph-online#path-encoding
|
||||||
Default: (encoder.Display |
|
Default: (encoder.Display |
|
||||||
encoder.EncodeBackSlash |
|
encoder.EncodeBackSlash |
|
||||||
|
encoder.EncodeHashPercent |
|
||||||
encoder.EncodeLeftSpace |
|
encoder.EncodeLeftSpace |
|
||||||
encoder.EncodeLeftTilde |
|
encoder.EncodeLeftTilde |
|
||||||
encoder.EncodeRightPeriod |
|
encoder.EncodeRightPeriod |
|
||||||
@@ -467,17 +350,12 @@ At the time of writing this only works with OneDrive personal paid accounts.
|
|||||||
|
|
||||||
// Options defines the configuration for this backend
|
// Options defines the configuration for this backend
|
||||||
type Options struct {
|
type Options struct {
|
||||||
Region string `config:"region"`
|
|
||||||
ChunkSize fs.SizeSuffix `config:"chunk_size"`
|
ChunkSize fs.SizeSuffix `config:"chunk_size"`
|
||||||
DriveID string `config:"drive_id"`
|
DriveID string `config:"drive_id"`
|
||||||
DriveType string `config:"drive_type"`
|
DriveType string `config:"drive_type"`
|
||||||
ExposeOneNoteFiles bool `config:"expose_onenote_files"`
|
ExposeOneNoteFiles bool `config:"expose_onenote_files"`
|
||||||
ServerSideAcrossConfigs bool `config:"server_side_across_configs"`
|
ServerSideAcrossConfigs bool `config:"server_side_across_configs"`
|
||||||
ListChunk int64 `config:"list_chunk"`
|
|
||||||
NoVersions bool `config:"no_versions"`
|
NoVersions bool `config:"no_versions"`
|
||||||
LinkScope string `config:"link_scope"`
|
|
||||||
LinkType string `config:"link_type"`
|
|
||||||
LinkPassword string `config:"link_password"`
|
|
||||||
Enc encoder.MultiEncoder `config:"encoding"`
|
Enc encoder.MultiEncoder `config:"encoding"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -486,7 +364,6 @@ type Fs struct {
|
|||||||
name string // name of this remote
|
name string // name of this remote
|
||||||
root string // the path we are working on
|
root string // the path we are working on
|
||||||
opt Options // parsed options
|
opt Options // parsed options
|
||||||
ci *fs.ConfigInfo // global config
|
|
||||||
features *fs.Features // optional features
|
features *fs.Features // optional features
|
||||||
srv *rest.Client // the connection to the one drive server
|
srv *rest.Client // the connection to the one drive server
|
||||||
dirCache *dircache.DirCache // Map of directory path to directory id
|
dirCache *dircache.DirCache // Map of directory path to directory id
|
||||||
@@ -550,15 +427,9 @@ var retryErrorCodes = []int{
|
|||||||
509, // Bandwidth Limit Exceeded
|
509, // Bandwidth Limit Exceeded
|
||||||
}
|
}
|
||||||
|
|
||||||
var gatewayTimeoutError sync.Once
|
|
||||||
var errAsyncJobAccessDenied = errors.New("async job failed - access denied")
|
|
||||||
|
|
||||||
// shouldRetry returns a boolean as to whether this resp and err
|
// shouldRetry returns a boolean as to whether this resp and err
|
||||||
// deserve to be retried. It returns the err as a convenience
|
// deserve to be retried. It returns the err as a convenience
|
||||||
func shouldRetry(ctx context.Context, resp *http.Response, err error) (bool, error) {
|
func shouldRetry(resp *http.Response, err error) (bool, error) {
|
||||||
if fserrors.ContextError(ctx, &err) {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
retry := false
|
retry := false
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
switch resp.StatusCode {
|
switch resp.StatusCode {
|
||||||
@@ -580,10 +451,6 @@ func shouldRetry(ctx context.Context, resp *http.Response, err error) (bool, err
|
|||||||
fs.Debugf(nil, "Too many requests. Trying again in %d seconds.", retryAfter)
|
fs.Debugf(nil, "Too many requests. Trying again in %d seconds.", retryAfter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 504: // Gateway timeout
|
|
||||||
gatewayTimeoutError.Do(func() {
|
|
||||||
fs.Errorf(nil, "%v: upload chunks may be taking too long - try reducing --onedrive-chunk-size or decreasing --transfers", err)
|
|
||||||
})
|
|
||||||
case 507: // Insufficient Storage
|
case 507: // Insufficient Storage
|
||||||
return false, fserrors.FatalError(err)
|
return false, fserrors.FatalError(err)
|
||||||
}
|
}
|
||||||
@@ -601,11 +468,13 @@ func shouldRetry(ctx context.Context, resp *http.Response, err error) (bool, err
|
|||||||
//
|
//
|
||||||
// If `relPath` == '', do not append the slash (See #3664)
|
// If `relPath` == '', do not append the slash (See #3664)
|
||||||
func (f *Fs) readMetaDataForPathRelativeToID(ctx context.Context, normalizedID string, relPath string) (info *api.Item, resp *http.Response, err error) {
|
func (f *Fs) readMetaDataForPathRelativeToID(ctx context.Context, normalizedID string, relPath string) (info *api.Item, resp *http.Response, err error) {
|
||||||
opts, _ := f.newOptsCallWithIDPath(normalizedID, relPath, true, "GET", "")
|
if relPath != "" {
|
||||||
|
relPath = "/" + withTrailingColon(rest.URLPathEscape(f.opt.Enc.FromStandardPath(relPath)))
|
||||||
|
}
|
||||||
|
opts := newOptsCall(normalizedID, "GET", ":"+relPath)
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, &info)
|
resp, err = f.srv.CallJSON(ctx, &opts, nil, &info)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
return info, resp, err
|
return info, resp, err
|
||||||
@@ -617,11 +486,20 @@ func (f *Fs) readMetaDataForPath(ctx context.Context, path string) (info *api.It
|
|||||||
|
|
||||||
if f.driveType != driveTypePersonal || firstSlashIndex == -1 {
|
if f.driveType != driveTypePersonal || firstSlashIndex == -1 {
|
||||||
var opts rest.Opts
|
var opts rest.Opts
|
||||||
opts = f.newOptsCallWithPath(ctx, path, "GET", "")
|
if len(path) == 0 {
|
||||||
opts.Path = strings.TrimSuffix(opts.Path, ":")
|
opts = rest.Opts{
|
||||||
|
Method: "GET",
|
||||||
|
Path: "/root",
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
opts = rest.Opts{
|
||||||
|
Method: "GET",
|
||||||
|
Path: "/root:/" + rest.URLPathEscape(f.opt.Enc.FromStandardPath(path)),
|
||||||
|
}
|
||||||
|
}
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, &info)
|
resp, err = f.srv.CallJSON(ctx, &opts, nil, &info)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
return info, resp, err
|
return info, resp, err
|
||||||
}
|
}
|
||||||
@@ -712,7 +590,8 @@ func (f *Fs) setUploadChunkSize(cs fs.SizeSuffix) (old fs.SizeSuffix, err error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewFs constructs an Fs from the path, container:path
|
// NewFs constructs an Fs from the path, container:path
|
||||||
func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, error) {
|
func NewFs(name, root string, m configmap.Mapper) (fs.Fs, error) {
|
||||||
|
ctx := context.Background()
|
||||||
// Parse config into Options struct
|
// Parse config into Options struct
|
||||||
opt := new(Options)
|
opt := new(Options)
|
||||||
err := configstruct.Set(m, opt)
|
err := configstruct.Set(m, opt)
|
||||||
@@ -729,35 +608,27 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
return nil, errors.New("unable to get drive_id and drive_type - if you are upgrading from older versions of rclone, please run `rclone config` and re-configure this backend")
|
return nil, errors.New("unable to get drive_id and drive_type - if you are upgrading from older versions of rclone, please run `rclone config` and re-configure this backend")
|
||||||
}
|
}
|
||||||
|
|
||||||
rootURL := graphAPIEndpoint[opt.Region] + "/v1.0" + "/drives/" + opt.DriveID
|
|
||||||
oauthConfig.Endpoint = oauth2.Endpoint{
|
|
||||||
AuthURL: authEndpoint[opt.Region] + authPath,
|
|
||||||
TokenURL: authEndpoint[opt.Region] + tokenPath,
|
|
||||||
}
|
|
||||||
|
|
||||||
root = parsePath(root)
|
root = parsePath(root)
|
||||||
oAuthClient, ts, err := oauthutil.NewClient(ctx, name, m, oauthConfig)
|
oAuthClient, ts, err := oauthutil.NewClient(name, m, oauthConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to configure OneDrive")
|
return nil, errors.Wrap(err, "failed to configure OneDrive")
|
||||||
}
|
}
|
||||||
|
|
||||||
ci := fs.GetConfig(ctx)
|
|
||||||
f := &Fs{
|
f := &Fs{
|
||||||
name: name,
|
name: name,
|
||||||
root: root,
|
root: root,
|
||||||
opt: *opt,
|
opt: *opt,
|
||||||
ci: ci,
|
|
||||||
driveID: opt.DriveID,
|
driveID: opt.DriveID,
|
||||||
driveType: opt.DriveType,
|
driveType: opt.DriveType,
|
||||||
srv: rest.NewClient(oAuthClient).SetRoot(rootURL),
|
srv: rest.NewClient(oAuthClient).SetRoot(graphURL + "/drives/" + opt.DriveID),
|
||||||
pacer: fs.NewPacer(ctx, pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))),
|
pacer: fs.NewPacer(pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))),
|
||||||
}
|
}
|
||||||
f.features = (&fs.Features{
|
f.features = (&fs.Features{
|
||||||
CaseInsensitive: true,
|
CaseInsensitive: true,
|
||||||
ReadMimeType: true,
|
ReadMimeType: true,
|
||||||
CanHaveEmptyDirectories: true,
|
CanHaveEmptyDirectories: true,
|
||||||
ServerSideAcrossConfigs: opt.ServerSideAcrossConfigs,
|
ServerSideAcrossConfigs: opt.ServerSideAcrossConfigs,
|
||||||
}).Fill(ctx, f)
|
}).Fill(f)
|
||||||
f.srv.SetErrorHandler(errorHandler)
|
f.srv.SetErrorHandler(errorHandler)
|
||||||
|
|
||||||
// Renew the token in the background
|
// Renew the token in the background
|
||||||
@@ -870,14 +741,14 @@ func (f *Fs) CreateDir(ctx context.Context, dirID, leaf string) (newID string, e
|
|||||||
// fs.Debugf(f, "CreateDir(%q, %q)\n", dirID, leaf)
|
// fs.Debugf(f, "CreateDir(%q, %q)\n", dirID, leaf)
|
||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
var info *api.Item
|
var info *api.Item
|
||||||
opts := f.newOptsCall(dirID, "POST", "/children")
|
opts := newOptsCall(dirID, "POST", "/children")
|
||||||
mkdir := api.CreateItemRequest{
|
mkdir := api.CreateItemRequest{
|
||||||
Name: f.opt.Enc.FromStandardName(leaf),
|
Name: f.opt.Enc.FromStandardName(leaf),
|
||||||
ConflictBehavior: "fail",
|
ConflictBehavior: "fail",
|
||||||
}
|
}
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, &mkdir, &info)
|
resp, err = f.srv.CallJSON(ctx, &opts, &mkdir, &info)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
//fmt.Printf("...Error %v\n", err)
|
//fmt.Printf("...Error %v\n", err)
|
||||||
@@ -902,14 +773,14 @@ type listAllFn func(*api.Item) bool
|
|||||||
func (f *Fs) listAll(ctx context.Context, dirID string, directoriesOnly bool, filesOnly bool, fn listAllFn) (found bool, err error) {
|
func (f *Fs) listAll(ctx context.Context, dirID string, directoriesOnly bool, filesOnly bool, fn listAllFn) (found bool, err error) {
|
||||||
// Top parameter asks for bigger pages of data
|
// Top parameter asks for bigger pages of data
|
||||||
// https://dev.onedrive.com/odata/optional-query-parameters.htm
|
// https://dev.onedrive.com/odata/optional-query-parameters.htm
|
||||||
opts := f.newOptsCall(dirID, "GET", fmt.Sprintf("/children?$top=%d", f.opt.ListChunk))
|
opts := newOptsCall(dirID, "GET", "/children?$top=1000")
|
||||||
OUTER:
|
OUTER:
|
||||||
for {
|
for {
|
||||||
var result api.ListChildrenResponse
|
var result api.ListChildrenResponse
|
||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return found, errors.Wrap(err, "couldn't list files")
|
return found, errors.Wrap(err, "couldn't list files")
|
||||||
@@ -1041,12 +912,12 @@ func (f *Fs) Mkdir(ctx context.Context, dir string) error {
|
|||||||
|
|
||||||
// deleteObject removes an object by ID
|
// deleteObject removes an object by ID
|
||||||
func (f *Fs) deleteObject(ctx context.Context, id string) error {
|
func (f *Fs) deleteObject(ctx context.Context, id string) error {
|
||||||
opts := f.newOptsCall(id, "DELETE", "")
|
opts := newOptsCall(id, "DELETE", "")
|
||||||
opts.NoResponse = true
|
opts.NoResponse = true
|
||||||
|
|
||||||
return f.pacer.Call(func() (bool, error) {
|
return f.pacer.Call(func() (bool, error) {
|
||||||
resp, err := f.srv.Call(ctx, &opts)
|
resp, err := f.srv.Call(ctx, &opts)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1096,7 +967,7 @@ func (f *Fs) Precision() time.Duration {
|
|||||||
|
|
||||||
// waitForJob waits for the job with status in url to complete
|
// waitForJob waits for the job with status in url to complete
|
||||||
func (f *Fs) waitForJob(ctx context.Context, location string, o *Object) error {
|
func (f *Fs) waitForJob(ctx context.Context, location string, o *Object) error {
|
||||||
deadline := time.Now().Add(f.ci.TimeoutOrInfinite())
|
deadline := time.Now().Add(fs.Config.Timeout)
|
||||||
for time.Now().Before(deadline) {
|
for time.Now().Before(deadline) {
|
||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
var err error
|
var err error
|
||||||
@@ -1121,12 +992,10 @@ func (f *Fs) waitForJob(ctx context.Context, location string, o *Object) error {
|
|||||||
|
|
||||||
switch status.Status {
|
switch status.Status {
|
||||||
case "failed":
|
case "failed":
|
||||||
if strings.HasPrefix(status.ErrorCode, "AccessDenied_") {
|
|
||||||
return errAsyncJobAccessDenied
|
|
||||||
}
|
|
||||||
fallthrough
|
|
||||||
case "deleteFailed":
|
case "deleteFailed":
|
||||||
|
{
|
||||||
return errors.Errorf("%s: async operation returned %q", o.remote, status.Status)
|
return errors.Errorf("%s: async operation returned %q", o.remote, status.Status)
|
||||||
|
}
|
||||||
case "completed":
|
case "completed":
|
||||||
err = o.readMetaData(ctx)
|
err = o.readMetaData(ctx)
|
||||||
return errors.Wrapf(err, "async operation completed but readMetaData failed")
|
return errors.Wrapf(err, "async operation completed but readMetaData failed")
|
||||||
@@ -1134,10 +1003,10 @@ func (f *Fs) waitForJob(ctx context.Context, location string, o *Object) error {
|
|||||||
|
|
||||||
time.Sleep(1 * time.Second)
|
time.Sleep(1 * time.Second)
|
||||||
}
|
}
|
||||||
return errors.Errorf("async operation didn't complete after %v", f.ci.TimeoutOrInfinite())
|
return errors.Errorf("async operation didn't complete after %v", fs.Config.Timeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy src to this remote using server-side copy operations.
|
// Copy src to this remote using server side copy operations.
|
||||||
//
|
//
|
||||||
// This is stored with the remote path given
|
// This is stored with the remote path given
|
||||||
//
|
//
|
||||||
@@ -1152,17 +1021,6 @@ func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object,
|
|||||||
fs.Debugf(src, "Can't copy - not same remote type")
|
fs.Debugf(src, "Can't copy - not same remote type")
|
||||||
return nil, fs.ErrorCantCopy
|
return nil, fs.ErrorCantCopy
|
||||||
}
|
}
|
||||||
if f.driveType != srcObj.fs.driveType {
|
|
||||||
fs.Debugf(src, "Can't server-side copy - drive types differ")
|
|
||||||
return nil, fs.ErrorCantCopy
|
|
||||||
}
|
|
||||||
|
|
||||||
// For OneDrive Business, this is only supported within the same drive
|
|
||||||
if f.driveType != driveTypePersonal && srcObj.fs.driveID != f.driveID {
|
|
||||||
fs.Debugf(src, "Can't server-side copy - cross-drive but not OneDrive Personal")
|
|
||||||
return nil, fs.ErrorCantCopy
|
|
||||||
}
|
|
||||||
|
|
||||||
err := srcObj.readMetaData(ctx)
|
err := srcObj.readMetaData(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -1184,12 +1042,11 @@ func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Copy the object
|
// Copy the object
|
||||||
// The query param is a workaround for OneDrive Business for #4590
|
opts := newOptsCall(srcObj.id, "POST", "/copy")
|
||||||
opts := f.newOptsCall(srcObj.id, "POST", "/copy?@microsoft.graph.conflictBehavior=replace")
|
|
||||||
opts.ExtraHeaders = map[string]string{"Prefer": "respond-async"}
|
opts.ExtraHeaders = map[string]string{"Prefer": "respond-async"}
|
||||||
opts.NoResponse = true
|
opts.NoResponse = true
|
||||||
|
|
||||||
id, dstDriveID, _ := f.parseNormalizedID(directoryID)
|
id, dstDriveID, _ := parseNormalizedID(directoryID)
|
||||||
|
|
||||||
replacedLeaf := f.opt.Enc.FromStandardName(leaf)
|
replacedLeaf := f.opt.Enc.FromStandardName(leaf)
|
||||||
copyReq := api.CopyItemRequest{
|
copyReq := api.CopyItemRequest{
|
||||||
@@ -1202,7 +1059,7 @@ func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object,
|
|||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, ©Req, nil)
|
resp, err = f.srv.CallJSON(ctx, &opts, ©Req, nil)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -1216,10 +1073,6 @@ func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object,
|
|||||||
|
|
||||||
// Wait for job to finish
|
// Wait for job to finish
|
||||||
err = f.waitForJob(ctx, location, dstObj)
|
err = f.waitForJob(ctx, location, dstObj)
|
||||||
if err == errAsyncJobAccessDenied {
|
|
||||||
fs.Debugf(src, "Server-side copy failed - file not shared between drives")
|
|
||||||
return nil, fs.ErrorCantCopy
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -1244,7 +1097,7 @@ func (f *Fs) Purge(ctx context.Context, dir string) error {
|
|||||||
return f.purgeCheck(ctx, dir, false)
|
return f.purgeCheck(ctx, dir, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move src to this remote using server-side move operations.
|
// Move src to this remote using server side move operations.
|
||||||
//
|
//
|
||||||
// This is stored with the remote path given
|
// This is stored with the remote path given
|
||||||
//
|
//
|
||||||
@@ -1266,8 +1119,8 @@ func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
id, dstDriveID, _ := f.parseNormalizedID(directoryID)
|
id, dstDriveID, _ := parseNormalizedID(directoryID)
|
||||||
_, srcObjDriveID, _ := f.parseNormalizedID(srcObj.id)
|
_, srcObjDriveID, _ := parseNormalizedID(srcObj.id)
|
||||||
|
|
||||||
if f.canonicalDriveID(dstDriveID) != srcObj.fs.canonicalDriveID(srcObjDriveID) {
|
if f.canonicalDriveID(dstDriveID) != srcObj.fs.canonicalDriveID(srcObjDriveID) {
|
||||||
// https://docs.microsoft.com/en-us/graph/api/driveitem-move?view=graph-rest-1.0
|
// https://docs.microsoft.com/en-us/graph/api/driveitem-move?view=graph-rest-1.0
|
||||||
@@ -1277,7 +1130,7 @@ func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Move the object
|
// Move the object
|
||||||
opts := f.newOptsCall(srcObj.id, "PATCH", "")
|
opts := newOptsCall(srcObj.id, "PATCH", "")
|
||||||
|
|
||||||
move := api.MoveItemRequest{
|
move := api.MoveItemRequest{
|
||||||
Name: f.opt.Enc.FromStandardName(leaf),
|
Name: f.opt.Enc.FromStandardName(leaf),
|
||||||
@@ -1295,7 +1148,7 @@ func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object,
|
|||||||
var info api.Item
|
var info api.Item
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, &move, &info)
|
resp, err = f.srv.CallJSON(ctx, &opts, &move, &info)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -1309,7 +1162,7 @@ func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DirMove moves src, srcRemote to this remote at dstRemote
|
// DirMove moves src, srcRemote to this remote at dstRemote
|
||||||
// using server-side move operations.
|
// using server side move operations.
|
||||||
//
|
//
|
||||||
// Will only be called if src.Fs().Name() == f.Name()
|
// Will only be called if src.Fs().Name() == f.Name()
|
||||||
//
|
//
|
||||||
@@ -1328,8 +1181,8 @@ func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
parsedDstDirID, dstDriveID, _ := f.parseNormalizedID(dstDirectoryID)
|
parsedDstDirID, dstDriveID, _ := parseNormalizedID(dstDirectoryID)
|
||||||
_, srcDriveID, _ := f.parseNormalizedID(srcID)
|
_, srcDriveID, _ := parseNormalizedID(srcID)
|
||||||
|
|
||||||
if f.canonicalDriveID(dstDriveID) != srcFs.canonicalDriveID(srcDriveID) {
|
if f.canonicalDriveID(dstDriveID) != srcFs.canonicalDriveID(srcDriveID) {
|
||||||
// https://docs.microsoft.com/en-us/graph/api/driveitem-move?view=graph-rest-1.0
|
// https://docs.microsoft.com/en-us/graph/api/driveitem-move?view=graph-rest-1.0
|
||||||
@@ -1345,7 +1198,7 @@ func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Do the move
|
// Do the move
|
||||||
opts := f.newOptsCall(srcID, "PATCH", "")
|
opts := newOptsCall(srcID, "PATCH", "")
|
||||||
move := api.MoveItemRequest{
|
move := api.MoveItemRequest{
|
||||||
Name: f.opt.Enc.FromStandardName(dstLeaf),
|
Name: f.opt.Enc.FromStandardName(dstLeaf),
|
||||||
ParentReference: &api.ItemReference{
|
ParentReference: &api.ItemReference{
|
||||||
@@ -1362,7 +1215,7 @@ func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string
|
|||||||
var info api.Item
|
var info api.Item
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, &move, &info)
|
resp, err = f.srv.CallJSON(ctx, &opts, &move, &info)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1388,7 +1241,7 @@ func (f *Fs) About(ctx context.Context) (usage *fs.Usage, err error) {
|
|||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, &drive)
|
resp, err = f.srv.CallJSON(ctx, &opts, nil, &drive)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "about failed")
|
return nil, errors.Wrap(err, "about failed")
|
||||||
@@ -1421,24 +1274,18 @@ func (f *Fs) PublicLink(ctx context.Context, remote string, expire fs.Duration,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
opts := f.newOptsCall(info.GetID(), "POST", "/createLink")
|
opts := newOptsCall(info.GetID(), "POST", "/createLink")
|
||||||
|
|
||||||
share := api.CreateShareLinkRequest{
|
share := api.CreateShareLinkRequest{
|
||||||
Type: f.opt.LinkType,
|
Type: "view",
|
||||||
Scope: f.opt.LinkScope,
|
Scope: "anonymous",
|
||||||
Password: f.opt.LinkPassword,
|
|
||||||
}
|
|
||||||
|
|
||||||
if expire < fs.DurationOff {
|
|
||||||
expiry := time.Now().Add(time.Duration(expire))
|
|
||||||
share.Expiry = &expiry
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
var result api.CreateShareLinkResponse
|
var result api.CreateShareLinkResponse
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, &share, &result)
|
resp, err = f.srv.CallJSON(ctx, &opts, &share, &result)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
@@ -1449,7 +1296,7 @@ func (f *Fs) PublicLink(ctx context.Context, remote string, expire fs.Duration,
|
|||||||
|
|
||||||
// CleanUp deletes all the hidden files.
|
// CleanUp deletes all the hidden files.
|
||||||
func (f *Fs) CleanUp(ctx context.Context) error {
|
func (f *Fs) CleanUp(ctx context.Context) error {
|
||||||
token := make(chan struct{}, f.ci.Checkers)
|
token := make(chan struct{}, fs.Config.Checkers)
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
err := walk.Walk(ctx, f, "", true, -1, func(path string, entries fs.DirEntries, err error) error {
|
err := walk.Walk(ctx, f, "", true, -1, func(path string, entries fs.DirEntries, err error) error {
|
||||||
err = entries.ForObjectError(func(obj fs.Object) error {
|
err = entries.ForObjectError(func(obj fs.Object) error {
|
||||||
@@ -1479,11 +1326,11 @@ func (f *Fs) CleanUp(ctx context.Context) error {
|
|||||||
|
|
||||||
// Finds and removes any old versions for o
|
// Finds and removes any old versions for o
|
||||||
func (o *Object) deleteVersions(ctx context.Context) error {
|
func (o *Object) deleteVersions(ctx context.Context) error {
|
||||||
opts := o.fs.newOptsCall(o.id, "GET", "/versions")
|
opts := newOptsCall(o.id, "GET", "/versions")
|
||||||
var versions api.VersionsResponse
|
var versions api.VersionsResponse
|
||||||
err := o.fs.pacer.Call(func() (bool, error) {
|
err := o.fs.pacer.Call(func() (bool, error) {
|
||||||
resp, err := o.fs.srv.CallJSON(ctx, &opts, nil, &versions)
|
resp, err := o.fs.srv.CallJSON(ctx, &opts, nil, &versions)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1506,11 +1353,11 @@ func (o *Object) deleteVersion(ctx context.Context, ID string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
fs.Infof(o, "removing version %q", ID)
|
fs.Infof(o, "removing version %q", ID)
|
||||||
opts := o.fs.newOptsCall(o.id, "DELETE", "/versions/"+ID)
|
opts := newOptsCall(o.id, "DELETE", "/versions/"+ID)
|
||||||
opts.NoResponse = true
|
opts.NoResponse = true
|
||||||
return o.fs.pacer.Call(func() (bool, error) {
|
return o.fs.pacer.Call(func() (bool, error) {
|
||||||
resp, err := o.fs.srv.Call(ctx, &opts)
|
resp, err := o.fs.srv.Call(ctx, &opts)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1651,7 +1498,21 @@ func (o *Object) ModTime(ctx context.Context) time.Time {
|
|||||||
|
|
||||||
// setModTime sets the modification time of the local fs object
|
// setModTime sets the modification time of the local fs object
|
||||||
func (o *Object) setModTime(ctx context.Context, modTime time.Time) (*api.Item, error) {
|
func (o *Object) setModTime(ctx context.Context, modTime time.Time) (*api.Item, error) {
|
||||||
opts := o.fs.newOptsCallWithPath(ctx, o.remote, "PATCH", "")
|
var opts rest.Opts
|
||||||
|
leaf, directoryID, _ := o.fs.dirCache.FindPath(ctx, o.remote, false)
|
||||||
|
trueDirID, drive, rootURL := parseNormalizedID(directoryID)
|
||||||
|
if drive != "" {
|
||||||
|
opts = rest.Opts{
|
||||||
|
Method: "PATCH",
|
||||||
|
RootURL: rootURL,
|
||||||
|
Path: "/" + drive + "/items/" + trueDirID + ":/" + withTrailingColon(rest.URLPathEscape(o.fs.opt.Enc.FromStandardName(leaf))),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
opts = rest.Opts{
|
||||||
|
Method: "PATCH",
|
||||||
|
Path: "/root:/" + withTrailingColon(rest.URLPathEscape(o.srvPath())),
|
||||||
|
}
|
||||||
|
}
|
||||||
update := api.SetFileSystemInfo{
|
update := api.SetFileSystemInfo{
|
||||||
FileSystemInfo: api.FileSystemInfoFacet{
|
FileSystemInfo: api.FileSystemInfoFacet{
|
||||||
CreatedDateTime: api.Timestamp(modTime),
|
CreatedDateTime: api.Timestamp(modTime),
|
||||||
@@ -1661,7 +1522,7 @@ func (o *Object) setModTime(ctx context.Context, modTime time.Time) (*api.Item,
|
|||||||
var info *api.Item
|
var info *api.Item
|
||||||
err := o.fs.pacer.Call(func() (bool, error) {
|
err := o.fs.pacer.Call(func() (bool, error) {
|
||||||
resp, err := o.fs.srv.CallJSON(ctx, &opts, &update, &info)
|
resp, err := o.fs.srv.CallJSON(ctx, &opts, &update, &info)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
// Remove versions if required
|
// Remove versions if required
|
||||||
if o.fs.opt.NoVersions {
|
if o.fs.opt.NoVersions {
|
||||||
@@ -1698,12 +1559,12 @@ func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.Read
|
|||||||
|
|
||||||
fs.FixRangeOption(options, o.size)
|
fs.FixRangeOption(options, o.size)
|
||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
opts := o.fs.newOptsCall(o.id, "GET", "/content")
|
opts := newOptsCall(o.id, "GET", "/content")
|
||||||
opts.Options = options
|
opts.Options = options
|
||||||
|
|
||||||
err = o.fs.pacer.Call(func() (bool, error) {
|
err = o.fs.pacer.Call(func() (bool, error) {
|
||||||
resp, err = o.fs.srv.Call(ctx, &opts)
|
resp, err = o.fs.srv.Call(ctx, &opts)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -1718,7 +1579,22 @@ func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.Read
|
|||||||
|
|
||||||
// createUploadSession creates an upload session for the object
|
// createUploadSession creates an upload session for the object
|
||||||
func (o *Object) createUploadSession(ctx context.Context, modTime time.Time) (response *api.CreateUploadResponse, err error) {
|
func (o *Object) createUploadSession(ctx context.Context, modTime time.Time) (response *api.CreateUploadResponse, err error) {
|
||||||
opts := o.fs.newOptsCallWithPath(ctx, o.remote, "POST", "/createUploadSession")
|
leaf, directoryID, _ := o.fs.dirCache.FindPath(ctx, o.remote, false)
|
||||||
|
id, drive, rootURL := parseNormalizedID(directoryID)
|
||||||
|
var opts rest.Opts
|
||||||
|
if drive != "" {
|
||||||
|
opts = rest.Opts{
|
||||||
|
Method: "POST",
|
||||||
|
RootURL: rootURL,
|
||||||
|
Path: fmt.Sprintf("/%s/items/%s:/%s:/createUploadSession",
|
||||||
|
drive, id, rest.URLPathEscape(o.fs.opt.Enc.FromStandardName(leaf))),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
opts = rest.Opts{
|
||||||
|
Method: "POST",
|
||||||
|
Path: "/root:/" + rest.URLPathEscape(o.srvPath()) + ":/createUploadSession",
|
||||||
|
}
|
||||||
|
}
|
||||||
createRequest := api.CreateUploadRequest{}
|
createRequest := api.CreateUploadRequest{}
|
||||||
createRequest.Item.FileSystemInfo.CreatedDateTime = api.Timestamp(modTime)
|
createRequest.Item.FileSystemInfo.CreatedDateTime = api.Timestamp(modTime)
|
||||||
createRequest.Item.FileSystemInfo.LastModifiedDateTime = api.Timestamp(modTime)
|
createRequest.Item.FileSystemInfo.LastModifiedDateTime = api.Timestamp(modTime)
|
||||||
@@ -1731,7 +1607,7 @@ func (o *Object) createUploadSession(ctx context.Context, modTime time.Time) (re
|
|||||||
err = errors.New(err.Error() + " (is it a OneNote file?)")
|
err = errors.New(err.Error() + " (is it a OneNote file?)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
return response, err
|
return response, err
|
||||||
}
|
}
|
||||||
@@ -1746,7 +1622,7 @@ func (o *Object) getPosition(ctx context.Context, url string) (pos int64, err er
|
|||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
err = o.fs.pacer.Call(func() (bool, error) {
|
err = o.fs.pacer.Call(func() (bool, error) {
|
||||||
resp, err = o.fs.srv.CallJSON(ctx, &opts, nil, &info)
|
resp, err = o.fs.srv.CallJSON(ctx, &opts, nil, &info)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
@@ -1806,11 +1682,11 @@ func (o *Object) uploadFragment(ctx context.Context, url string, start int64, to
|
|||||||
return true, errors.Wrapf(err, "retry this chunk skipping %d bytes", skip)
|
return true, errors.Wrapf(err, "retry this chunk skipping %d bytes", skip)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
}
|
}
|
||||||
body, err = rest.ReadBody(resp)
|
body, err = rest.ReadBody(resp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
}
|
}
|
||||||
if resp.StatusCode == 200 || resp.StatusCode == 201 {
|
if resp.StatusCode == 200 || resp.StatusCode == 201 {
|
||||||
// we are done :)
|
// we are done :)
|
||||||
@@ -1833,7 +1709,7 @@ func (o *Object) cancelUploadSession(ctx context.Context, url string) (err error
|
|||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
err = o.fs.pacer.Call(func() (bool, error) {
|
err = o.fs.pacer.Call(func() (bool, error) {
|
||||||
resp, err = o.fs.srv.Call(ctx, &opts)
|
resp, err = o.fs.srv.Call(ctx, &opts)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1891,10 +1767,27 @@ func (o *Object) uploadSinglepart(ctx context.Context, in io.Reader, size int64,
|
|||||||
|
|
||||||
fs.Debugf(o, "Starting singlepart upload")
|
fs.Debugf(o, "Starting singlepart upload")
|
||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
opts := o.fs.newOptsCallWithPath(ctx, o.remote, "PUT", "/content")
|
var opts rest.Opts
|
||||||
opts.ContentLength = &size
|
leaf, directoryID, _ := o.fs.dirCache.FindPath(ctx, o.remote, false)
|
||||||
opts.Body = in
|
trueDirID, drive, rootURL := parseNormalizedID(directoryID)
|
||||||
opts.Options = options
|
if drive != "" {
|
||||||
|
opts = rest.Opts{
|
||||||
|
Method: "PUT",
|
||||||
|
RootURL: rootURL,
|
||||||
|
Path: "/" + drive + "/items/" + trueDirID + ":/" + rest.URLPathEscape(o.fs.opt.Enc.FromStandardName(leaf)) + ":/content",
|
||||||
|
ContentLength: &size,
|
||||||
|
Body: in,
|
||||||
|
Options: options,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
opts = rest.Opts{
|
||||||
|
Method: "PUT",
|
||||||
|
Path: "/root:/" + rest.URLPathEscape(o.srvPath()) + ":/content",
|
||||||
|
ContentLength: &size,
|
||||||
|
Body: in,
|
||||||
|
Options: options,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
err = o.fs.pacer.Call(func() (bool, error) {
|
err = o.fs.pacer.Call(func() (bool, error) {
|
||||||
resp, err = o.fs.srv.CallJSON(ctx, &opts, nil, &info)
|
resp, err = o.fs.srv.CallJSON(ctx, &opts, nil, &info)
|
||||||
@@ -1904,7 +1797,7 @@ func (o *Object) uploadSinglepart(ctx context.Context, in io.Reader, size int64,
|
|||||||
err = errors.New(err.Error() + " (is it a OneNote file?)")
|
err = errors.New(err.Error() + " (is it a OneNote file?)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -1970,42 +1863,8 @@ func (o *Object) ID() string {
|
|||||||
return o.id
|
return o.id
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
func newOptsCall(normalizedID string, method string, route string) (opts rest.Opts) {
|
||||||
* URL Build routine area start
|
id, drive, rootURL := parseNormalizedID(normalizedID)
|
||||||
* 1. In this area, region-related URL rewrites are applied. As the API is blackbox,
|
|
||||||
* we cannot thoroughly test this part. Please be extremely careful while changing them.
|
|
||||||
* 2. If possible, please don't introduce region related code in other region, but patch these helper functions.
|
|
||||||
* 3. To avoid region-related issues, please don't manually build rest.Opts from scratch.
|
|
||||||
* Instead, use these helper function, and customize the URL afterwards if needed.
|
|
||||||
*
|
|
||||||
* currently, the 21ViaNet's API differs in the following places:
|
|
||||||
* - https://{Endpoint}/drives/{driveID}/items/{leaf}:/{route}
|
|
||||||
* - this API doesn't work (gives invalid request)
|
|
||||||
* - can be replaced with the following API:
|
|
||||||
* - https://{Endpoint}/drives/{driveID}/items/children('{leaf}')/{route}
|
|
||||||
* - however, this API does NOT support multi-level leaf like a/b/c
|
|
||||||
* - https://{Endpoint}/drives/{driveID}/items/children('@a1')/{route}?@a1=URLEncode("'{leaf}'")
|
|
||||||
* - this API does support multi-level leaf like a/b/c
|
|
||||||
* - https://{Endpoint}/drives/{driveID}/root/children('@a1')/{route}?@a1=URLEncode({path})
|
|
||||||
* - Same as above
|
|
||||||
*/
|
|
||||||
|
|
||||||
// parseNormalizedID parses a normalized ID (may be in the form `driveID#itemID` or just `itemID`)
|
|
||||||
// and returns itemID, driveID, rootURL.
|
|
||||||
// Such a normalized ID can come from (*Item).GetID()
|
|
||||||
func (f *Fs) parseNormalizedID(ID string) (string, string, string) {
|
|
||||||
rootURL := graphAPIEndpoint[f.opt.Region] + "/v1.0/drives"
|
|
||||||
if strings.Index(ID, "#") >= 0 {
|
|
||||||
s := strings.Split(ID, "#")
|
|
||||||
return s[1], s[0], rootURL
|
|
||||||
}
|
|
||||||
return ID, "", ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// newOptsCall build the rest.Opts structure with *a normalizedID(driveID#fileID, or simply fileID)*
|
|
||||||
// using url template https://{Endpoint}/drives/{driveID}/items/{itemID}/{route}
|
|
||||||
func (f *Fs) newOptsCall(normalizedID string, method string, route string) (opts rest.Opts) {
|
|
||||||
id, drive, rootURL := f.parseNormalizedID(normalizedID)
|
|
||||||
|
|
||||||
if drive != "" {
|
if drive != "" {
|
||||||
return rest.Opts{
|
return rest.Opts{
|
||||||
@@ -2020,90 +1879,16 @@ func (f *Fs) newOptsCall(normalizedID string, method string, route string) (opts
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func escapeSingleQuote(str string) string {
|
// parseNormalizedID parses a normalized ID (may be in the form `driveID#itemID` or just `itemID`)
|
||||||
return strings.ReplaceAll(str, "'", "''")
|
// and returns itemID, driveID, rootURL.
|
||||||
|
// Such a normalized ID can come from (*Item).GetID()
|
||||||
|
func parseNormalizedID(ID string) (string, string, string) {
|
||||||
|
if strings.Index(ID, "#") >= 0 {
|
||||||
|
s := strings.Split(ID, "#")
|
||||||
|
return s[1], s[0], graphURL + "/drives"
|
||||||
}
|
}
|
||||||
|
return ID, "", ""
|
||||||
// newOptsCallWithIDPath build the rest.Opts structure with *a normalizedID (driveID#fileID, or simply fileID) and leaf*
|
|
||||||
// using url template https://{Endpoint}/drives/{driveID}/items/{leaf}:/{route} (for international OneDrive)
|
|
||||||
// or https://{Endpoint}/drives/{driveID}/items/children('{leaf}')/{route}
|
|
||||||
// and https://{Endpoint}/drives/{driveID}/items/children('@a1')/{route}?@a1=URLEncode("'{leaf}'") (for 21ViaNet)
|
|
||||||
// if isPath is false, this function will only work when the leaf is "" or a child name (i.e. it doesn't accept multi-level leaf)
|
|
||||||
// if isPath is true, multi-level leaf like a/b/c can be passed
|
|
||||||
func (f *Fs) newOptsCallWithIDPath(normalizedID string, leaf string, isPath bool, method string, route string) (opts rest.Opts, ok bool) {
|
|
||||||
encoder := f.opt.Enc.FromStandardName
|
|
||||||
if isPath {
|
|
||||||
encoder = f.opt.Enc.FromStandardPath
|
|
||||||
}
|
}
|
||||||
trueDirID, drive, rootURL := f.parseNormalizedID(normalizedID)
|
|
||||||
if drive == "" {
|
|
||||||
trueDirID = normalizedID
|
|
||||||
}
|
|
||||||
entity := "/items/" + trueDirID + ":/" + withTrailingColon(rest.URLPathEscape(encoder(leaf))) + route
|
|
||||||
if f.opt.Region == regionCN {
|
|
||||||
if isPath {
|
|
||||||
entity = "/items/" + trueDirID + "/children('@a1')" + route + "?@a1=" + url.QueryEscape("'"+encoder(escapeSingleQuote(leaf))+"'")
|
|
||||||
} else {
|
|
||||||
entity = "/items/" + trueDirID + "/children('" + rest.URLPathEscape(encoder(escapeSingleQuote(leaf))) + "')" + route
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if drive == "" {
|
|
||||||
ok = false
|
|
||||||
opts = rest.Opts{
|
|
||||||
Method: method,
|
|
||||||
Path: entity,
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ok = true
|
|
||||||
opts = rest.Opts{
|
|
||||||
Method: method,
|
|
||||||
RootURL: rootURL,
|
|
||||||
Path: "/" + drive + entity,
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// newOptsCallWithIDPath build the rest.Opts structure with an *absolute path start from root*
|
|
||||||
// using url template https://{Endpoint}/drives/{driveID}/root:/{path}:/{route}
|
|
||||||
// or https://{Endpoint}/drives/{driveID}/root/children('@a1')/{route}?@a1=URLEncode({path})
|
|
||||||
func (f *Fs) newOptsCallWithRootPath(path string, method string, route string) (opts rest.Opts) {
|
|
||||||
path = strings.TrimSuffix(path, "/")
|
|
||||||
newURL := "/root:/" + withTrailingColon(rest.URLPathEscape(f.opt.Enc.FromStandardPath(path))) + route
|
|
||||||
if f.opt.Region == regionCN {
|
|
||||||
newURL = "/root/children('@a1')" + route + "?@a1=" + url.QueryEscape("'"+escapeSingleQuote(f.opt.Enc.FromStandardPath(path))+"'")
|
|
||||||
}
|
|
||||||
return rest.Opts{
|
|
||||||
Method: method,
|
|
||||||
Path: newURL,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// newOptsCallWithPath build the rest.Opt intelligently.
|
|
||||||
// It will first try to resolve the path using dircache, which enables support for "Share with me" files.
|
|
||||||
// If present in cache, then use ID + Path variant, else fallback into RootPath variant
|
|
||||||
func (f *Fs) newOptsCallWithPath(ctx context.Context, path string, method string, route string) (opts rest.Opts) {
|
|
||||||
if path == "" {
|
|
||||||
url := "/root" + route
|
|
||||||
return rest.Opts{
|
|
||||||
Method: method,
|
|
||||||
Path: url,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// find dircache
|
|
||||||
leaf, directoryID, _ := f.dirCache.FindPath(ctx, path, false)
|
|
||||||
// try to use IDPath variant first
|
|
||||||
if opts, ok := f.newOptsCallWithIDPath(directoryID, leaf, false, method, route); ok {
|
|
||||||
return opts
|
|
||||||
}
|
|
||||||
// fallback to use RootPath variant first
|
|
||||||
return f.newOptsCallWithRootPath(path, method, route)
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* URL Build routine area end
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Returns the canonical form of the driveID
|
// Returns the canonical form of the driveID
|
||||||
func (f *Fs) canonicalDriveID(driveID string) (canonicalDriveID string) {
|
func (f *Fs) canonicalDriveID(driveID string) (canonicalDriveID string) {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/rclone/rclone/fs"
|
"github.com/rclone/rclone/fs"
|
||||||
"github.com/rclone/rclone/fstest"
|
|
||||||
"github.com/rclone/rclone/fstest/fstests"
|
"github.com/rclone/rclone/fstest/fstests"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -20,20 +19,6 @@ func TestIntegration(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestIntegrationCn runs integration tests against the remote
|
|
||||||
func TestIntegrationCn(t *testing.T) {
|
|
||||||
if *fstest.RemoteName != "" {
|
|
||||||
t.Skip("skipping as -remote is set")
|
|
||||||
}
|
|
||||||
fstests.Run(t, &fstests.Opt{
|
|
||||||
RemoteName: "TestOneDriveCn:",
|
|
||||||
NilObject: (*Object)(nil),
|
|
||||||
ChunkedUpload: fstests.ChunkedUploadConfig{
|
|
||||||
CeilChunkSize: fstests.NextMultipleOf(chunkSizeMultiple),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Fs) SetUploadChunkSize(cs fs.SizeSuffix) (fs.SizeSuffix, error) {
|
func (f *Fs) SetUploadChunkSize(cs fs.SizeSuffix) (fs.SizeSuffix, error) {
|
||||||
return f.setUploadChunkSize(cs)
|
return f.setUploadChunkSize(cs)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,7 +119,6 @@ type Object struct {
|
|||||||
fs *Fs // what this object is part of
|
fs *Fs // what this object is part of
|
||||||
remote string // The remote path
|
remote string // The remote path
|
||||||
id string // ID of the file
|
id string // ID of the file
|
||||||
parent string // ID of the parent directory
|
|
||||||
modTime time.Time // The modified time of the object if known
|
modTime time.Time // The modified time of the object if known
|
||||||
md5 string // MD5 hash if known
|
md5 string // MD5 hash if known
|
||||||
size int64 // Size of the object
|
size int64 // Size of the object
|
||||||
@@ -165,7 +164,8 @@ func (f *Fs) DirCacheFlush() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewFs constructs an Fs from the path, bucket:path
|
// NewFs constructs an Fs from the path, bucket:path
|
||||||
func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, error) {
|
func NewFs(name, root string, m configmap.Mapper) (fs.Fs, error) {
|
||||||
|
ctx := context.Background()
|
||||||
// Parse config into Options struct
|
// Parse config into Options struct
|
||||||
opt := new(Options)
|
opt := new(Options)
|
||||||
err := configstruct.Set(m, opt)
|
err := configstruct.Set(m, opt)
|
||||||
@@ -188,8 +188,8 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
name: name,
|
name: name,
|
||||||
root: root,
|
root: root,
|
||||||
opt: *opt,
|
opt: *opt,
|
||||||
srv: rest.NewClient(fshttp.NewClient(ctx)).SetErrorHandler(errorHandler),
|
srv: rest.NewClient(fshttp.NewClient(fs.Config)).SetErrorHandler(errorHandler),
|
||||||
pacer: fs.NewPacer(ctx, pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))),
|
pacer: fs.NewPacer(pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))),
|
||||||
}
|
}
|
||||||
|
|
||||||
f.dirCache = dircache.New(root, "0", f)
|
f.dirCache = dircache.New(root, "0", f)
|
||||||
@@ -207,7 +207,7 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
Path: "/session/login.json",
|
Path: "/session/login.json",
|
||||||
}
|
}
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, &account, &f.session)
|
resp, err = f.srv.CallJSON(ctx, &opts, &account, &f.session)
|
||||||
return f.shouldRetry(ctx, resp, err)
|
return f.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to create session")
|
return nil, errors.Wrap(err, "failed to create session")
|
||||||
@@ -217,7 +217,7 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
f.features = (&fs.Features{
|
f.features = (&fs.Features{
|
||||||
CaseInsensitive: true,
|
CaseInsensitive: true,
|
||||||
CanHaveEmptyDirectories: true,
|
CanHaveEmptyDirectories: true,
|
||||||
}).Fill(ctx, f)
|
}).Fill(f)
|
||||||
|
|
||||||
// Find the current root
|
// Find the current root
|
||||||
err = f.dirCache.FindRoot(ctx, false)
|
err = f.dirCache.FindRoot(ctx, false)
|
||||||
@@ -234,7 +234,7 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
// No root so return old f
|
// No root so return old f
|
||||||
return f, nil
|
return f, nil
|
||||||
}
|
}
|
||||||
_, err := tempF.newObjectWithInfo(ctx, remote, nil, "")
|
_, err := tempF.newObjectWithInfo(ctx, remote, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == fs.ErrorObjectNotFound {
|
if err == fs.ErrorObjectNotFound {
|
||||||
// File doesn't exist so return old f
|
// File doesn't exist so return old f
|
||||||
@@ -294,7 +294,7 @@ func (f *Fs) deleteObject(ctx context.Context, id string) error {
|
|||||||
Path: "/folder/remove.json",
|
Path: "/folder/remove.json",
|
||||||
}
|
}
|
||||||
resp, err := f.srv.CallJSON(ctx, &opts, &removeDirData, nil)
|
resp, err := f.srv.CallJSON(ctx, &opts, &removeDirData, nil)
|
||||||
return f.shouldRetry(ctx, resp, err)
|
return f.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,7 +338,7 @@ func (f *Fs) Precision() time.Duration {
|
|||||||
return time.Second
|
return time.Second
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy src to this remote using server-side copy operations.
|
// Copy src to this remote using server side copy operations.
|
||||||
//
|
//
|
||||||
// This is stored with the remote path given
|
// This is stored with the remote path given
|
||||||
//
|
//
|
||||||
@@ -389,7 +389,7 @@ func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object,
|
|||||||
Path: "/file/move_copy.json",
|
Path: "/file/move_copy.json",
|
||||||
}
|
}
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, ©FileData, &response)
|
resp, err = f.srv.CallJSON(ctx, &opts, ©FileData, &response)
|
||||||
return f.shouldRetry(ctx, resp, err)
|
return f.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -402,7 +402,7 @@ func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object,
|
|||||||
return dstObj, nil
|
return dstObj, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move src to this remote using server-side move operations.
|
// Move src to this remote using server side move operations.
|
||||||
//
|
//
|
||||||
// This is stored with the remote path given
|
// This is stored with the remote path given
|
||||||
//
|
//
|
||||||
@@ -446,7 +446,7 @@ func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object,
|
|||||||
Path: "/file/move_copy.json",
|
Path: "/file/move_copy.json",
|
||||||
}
|
}
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, ©FileData, &response)
|
resp, err = f.srv.CallJSON(ctx, &opts, ©FileData, &response)
|
||||||
return f.shouldRetry(ctx, resp, err)
|
return f.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -460,7 +460,7 @@ func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DirMove moves src, srcRemote to this remote at dstRemote
|
// DirMove moves src, srcRemote to this remote at dstRemote
|
||||||
// using server-side move operations.
|
// using server side move operations.
|
||||||
//
|
//
|
||||||
// Will only be called if src.Fs().Name() == f.Name()
|
// Will only be called if src.Fs().Name() == f.Name()
|
||||||
//
|
//
|
||||||
@@ -495,7 +495,7 @@ func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string
|
|||||||
Path: "/folder/move_copy.json",
|
Path: "/folder/move_copy.json",
|
||||||
}
|
}
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, &moveFolderData, &response)
|
resp, err = f.srv.CallJSON(ctx, &opts, &moveFolderData, &response)
|
||||||
return f.shouldRetry(ctx, resp, err)
|
return f.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fs.Debugf(src, "DirMove error %v", err)
|
fs.Debugf(src, "DirMove error %v", err)
|
||||||
@@ -518,7 +518,7 @@ func (f *Fs) Purge(ctx context.Context, dir string) error {
|
|||||||
// Return an Object from a path
|
// Return an Object from a path
|
||||||
//
|
//
|
||||||
// If it can't be found it returns the error fs.ErrorObjectNotFound.
|
// If it can't be found it returns the error fs.ErrorObjectNotFound.
|
||||||
func (f *Fs) newObjectWithInfo(ctx context.Context, remote string, file *File, parent string) (fs.Object, error) {
|
func (f *Fs) newObjectWithInfo(ctx context.Context, remote string, file *File) (fs.Object, error) {
|
||||||
// fs.Debugf(nil, "newObjectWithInfo(%s, %v)", remote, file)
|
// fs.Debugf(nil, "newObjectWithInfo(%s, %v)", remote, file)
|
||||||
|
|
||||||
var o *Object
|
var o *Object
|
||||||
@@ -527,7 +527,6 @@ func (f *Fs) newObjectWithInfo(ctx context.Context, remote string, file *File, p
|
|||||||
fs: f,
|
fs: f,
|
||||||
remote: remote,
|
remote: remote,
|
||||||
id: file.FileID,
|
id: file.FileID,
|
||||||
parent: parent,
|
|
||||||
modTime: time.Unix(file.DateModified, 0),
|
modTime: time.Unix(file.DateModified, 0),
|
||||||
size: file.Size,
|
size: file.Size,
|
||||||
md5: file.FileHash,
|
md5: file.FileHash,
|
||||||
@@ -550,7 +549,7 @@ func (f *Fs) newObjectWithInfo(ctx context.Context, remote string, file *File, p
|
|||||||
// it returns the error fs.ErrorObjectNotFound.
|
// it returns the error fs.ErrorObjectNotFound.
|
||||||
func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) {
|
func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) {
|
||||||
// fs.Debugf(nil, "NewObject(\"%s\")", remote)
|
// fs.Debugf(nil, "NewObject(\"%s\")", remote)
|
||||||
return f.newObjectWithInfo(ctx, remote, nil, "")
|
return f.newObjectWithInfo(ctx, remote, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates from the parameters passed in a half finished Object which
|
// Creates from the parameters passed in a half finished Object which
|
||||||
@@ -583,7 +582,7 @@ func (f *Fs) readMetaDataForFolderID(ctx context.Context, id string) (info *Fold
|
|||||||
}
|
}
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, &info)
|
resp, err = f.srv.CallJSON(ctx, &opts, nil, &info)
|
||||||
return f.shouldRetry(ctx, resp, err)
|
return f.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -633,7 +632,7 @@ func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options .
|
|||||||
Path: "/upload/create_file.json",
|
Path: "/upload/create_file.json",
|
||||||
}
|
}
|
||||||
resp, err = o.fs.srv.CallJSON(ctx, &opts, &createFileData, &response)
|
resp, err = o.fs.srv.CallJSON(ctx, &opts, &createFileData, &response)
|
||||||
return o.fs.shouldRetry(ctx, resp, err)
|
return o.fs.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to create file")
|
return nil, errors.Wrap(err, "failed to create file")
|
||||||
@@ -659,10 +658,7 @@ var retryErrorCodes = []int{
|
|||||||
|
|
||||||
// shouldRetry returns a boolean as to whether this resp and err
|
// shouldRetry returns a boolean as to whether this resp and err
|
||||||
// deserve to be retried. It returns the err as a convenience
|
// deserve to be retried. It returns the err as a convenience
|
||||||
func (f *Fs) shouldRetry(ctx context.Context, resp *http.Response, err error) (bool, error) {
|
func (f *Fs) shouldRetry(resp *http.Response, err error) (bool, error) {
|
||||||
if fserrors.ContextError(ctx, &err) {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
return fserrors.ShouldRetry(err) || fserrors.ShouldRetryHTTP(resp, retryErrorCodes), err
|
return fserrors.ShouldRetry(err) || fserrors.ShouldRetryHTTP(resp, retryErrorCodes), err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -688,7 +684,7 @@ func (f *Fs) CreateDir(ctx context.Context, pathID, leaf string) (newID string,
|
|||||||
Path: "/folder.json",
|
Path: "/folder.json",
|
||||||
}
|
}
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, &createDirData, &response)
|
resp, err = f.srv.CallJSON(ctx, &opts, &createDirData, &response)
|
||||||
return f.shouldRetry(ctx, resp, err)
|
return f.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -716,7 +712,7 @@ func (f *Fs) FindLeaf(ctx context.Context, pathID, leaf string) (pathIDOut strin
|
|||||||
Path: "/folder/list.json/" + f.session.SessionID + "/" + pathID,
|
Path: "/folder/list.json/" + f.session.SessionID + "/" + pathID,
|
||||||
}
|
}
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, &folderList)
|
resp, err = f.srv.CallJSON(ctx, &opts, nil, &folderList)
|
||||||
return f.shouldRetry(ctx, resp, err)
|
return f.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", false, errors.Wrap(err, "failed to get folder list")
|
return "", false, errors.Wrap(err, "failed to get folder list")
|
||||||
@@ -726,7 +722,7 @@ func (f *Fs) FindLeaf(ctx context.Context, pathID, leaf string) (pathIDOut strin
|
|||||||
for _, folder := range folderList.Folders {
|
for _, folder := range folderList.Folders {
|
||||||
// fs.Debugf(nil, "Folder: %s (%s)", folder.Name, folder.FolderID)
|
// fs.Debugf(nil, "Folder: %s (%s)", folder.Name, folder.FolderID)
|
||||||
|
|
||||||
if strings.EqualFold(leaf, folder.Name) {
|
if leaf == folder.Name {
|
||||||
// found
|
// found
|
||||||
return folder.FolderID, true, nil
|
return folder.FolderID, true, nil
|
||||||
}
|
}
|
||||||
@@ -759,7 +755,7 @@ func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err e
|
|||||||
folderList := FolderList{}
|
folderList := FolderList{}
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, &folderList)
|
resp, err = f.srv.CallJSON(ctx, &opts, nil, &folderList)
|
||||||
return f.shouldRetry(ctx, resp, err)
|
return f.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to get folder list")
|
return nil, errors.Wrap(err, "failed to get folder list")
|
||||||
@@ -773,7 +769,6 @@ func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err e
|
|||||||
f.dirCache.Put(remote, folder.FolderID)
|
f.dirCache.Put(remote, folder.FolderID)
|
||||||
d := fs.NewDir(remote, time.Unix(folder.DateModified, 0)).SetID(folder.FolderID)
|
d := fs.NewDir(remote, time.Unix(folder.DateModified, 0)).SetID(folder.FolderID)
|
||||||
d.SetItems(int64(folder.ChildFolders))
|
d.SetItems(int64(folder.ChildFolders))
|
||||||
d.SetParentID(directoryID)
|
|
||||||
entries = append(entries, d)
|
entries = append(entries, d)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -781,7 +776,7 @@ func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err e
|
|||||||
file.Name = f.opt.Enc.ToStandardName(file.Name)
|
file.Name = f.opt.Enc.ToStandardName(file.Name)
|
||||||
// fs.Debugf(nil, "File: %s (%s)", file.Name, file.FileID)
|
// fs.Debugf(nil, "File: %s (%s)", file.Name, file.FileID)
|
||||||
remote := path.Join(dir, file.Name)
|
remote := path.Join(dir, file.Name)
|
||||||
o, err := f.newObjectWithInfo(ctx, remote, &file, directoryID)
|
o, err := f.newObjectWithInfo(ctx, remote, &file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -848,7 +843,7 @@ func (o *Object) SetModTime(ctx context.Context, modTime time.Time) error {
|
|||||||
}
|
}
|
||||||
err := o.fs.pacer.Call(func() (bool, error) {
|
err := o.fs.pacer.Call(func() (bool, error) {
|
||||||
resp, err := o.fs.srv.CallJSON(ctx, &opts, &update, nil)
|
resp, err := o.fs.srv.CallJSON(ctx, &opts, &update, nil)
|
||||||
return o.fs.shouldRetry(ctx, resp, err)
|
return o.fs.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
o.modTime = modTime
|
o.modTime = modTime
|
||||||
@@ -868,7 +863,7 @@ func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.Read
|
|||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
err = o.fs.pacer.Call(func() (bool, error) {
|
err = o.fs.pacer.Call(func() (bool, error) {
|
||||||
resp, err = o.fs.srv.Call(ctx, &opts)
|
resp, err = o.fs.srv.Call(ctx, &opts)
|
||||||
return o.fs.shouldRetry(ctx, resp, err)
|
return o.fs.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to open file)")
|
return nil, errors.Wrap(err, "failed to open file)")
|
||||||
@@ -887,7 +882,7 @@ func (o *Object) Remove(ctx context.Context) error {
|
|||||||
Path: "/file.json/" + o.fs.session.SessionID + "/" + o.id,
|
Path: "/file.json/" + o.fs.session.SessionID + "/" + o.id,
|
||||||
}
|
}
|
||||||
resp, err := o.fs.srv.Call(ctx, &opts)
|
resp, err := o.fs.srv.Call(ctx, &opts)
|
||||||
return o.fs.shouldRetry(ctx, resp, err)
|
return o.fs.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -916,7 +911,7 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
|
|||||||
Path: "/upload/open_file_upload.json",
|
Path: "/upload/open_file_upload.json",
|
||||||
}
|
}
|
||||||
resp, err := o.fs.srv.CallJSON(ctx, &opts, &openUploadData, &openResponse)
|
resp, err := o.fs.srv.CallJSON(ctx, &opts, &openUploadData, &openResponse)
|
||||||
return o.fs.shouldRetry(ctx, resp, err)
|
return o.fs.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to create file")
|
return errors.Wrap(err, "failed to create file")
|
||||||
@@ -960,7 +955,7 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
|
|||||||
|
|
||||||
}
|
}
|
||||||
resp, err = o.fs.srv.CallJSON(ctx, &opts, nil, &reply)
|
resp, err = o.fs.srv.CallJSON(ctx, &opts, nil, &reply)
|
||||||
return o.fs.shouldRetry(ctx, resp, err)
|
return o.fs.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to create file")
|
return errors.Wrap(err, "failed to create file")
|
||||||
@@ -983,7 +978,7 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
|
|||||||
Path: "/upload/close_file_upload.json",
|
Path: "/upload/close_file_upload.json",
|
||||||
}
|
}
|
||||||
resp, err = o.fs.srv.CallJSON(ctx, &opts, &closeUploadData, &closeResponse)
|
resp, err = o.fs.srv.CallJSON(ctx, &opts, &closeUploadData, &closeResponse)
|
||||||
return o.fs.shouldRetry(ctx, resp, err)
|
return o.fs.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to create file")
|
return errors.Wrap(err, "failed to create file")
|
||||||
@@ -1009,7 +1004,7 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
|
|||||||
Path: "/file/access.json",
|
Path: "/file/access.json",
|
||||||
}
|
}
|
||||||
resp, err = o.fs.srv.CallJSON(ctx, &opts, &update, nil)
|
resp, err = o.fs.srv.CallJSON(ctx, &opts, &update, nil)
|
||||||
return o.fs.shouldRetry(ctx, resp, err)
|
return o.fs.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1035,7 +1030,7 @@ func (o *Object) readMetaData(ctx context.Context) (err error) {
|
|||||||
o.fs.session.SessionID, directoryID, url.QueryEscape(o.fs.opt.Enc.FromStandardName(leaf))),
|
o.fs.session.SessionID, directoryID, url.QueryEscape(o.fs.opt.Enc.FromStandardName(leaf))),
|
||||||
}
|
}
|
||||||
resp, err = o.fs.srv.CallJSON(ctx, &opts, nil, &folderList)
|
resp, err = o.fs.srv.CallJSON(ctx, &opts, nil, &folderList)
|
||||||
return o.fs.shouldRetry(ctx, resp, err)
|
return o.fs.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to get folder list")
|
return errors.Wrap(err, "failed to get folder list")
|
||||||
@@ -1059,11 +1054,6 @@ func (o *Object) ID() string {
|
|||||||
return o.id
|
return o.id
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParentID returns the ID of the Object parent directory if known, or "" if not
|
|
||||||
func (o *Object) ParentID() string {
|
|
||||||
return o.parent
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the interfaces are satisfied
|
// Check the interfaces are satisfied
|
||||||
var (
|
var (
|
||||||
_ fs.Fs = (*Fs)(nil)
|
_ fs.Fs = (*Fs)(nil)
|
||||||
@@ -1074,5 +1064,4 @@ var (
|
|||||||
_ fs.DirCacheFlusher = (*Fs)(nil)
|
_ fs.DirCacheFlusher = (*Fs)(nil)
|
||||||
_ fs.Object = (*Object)(nil)
|
_ fs.Object = (*Object)(nil)
|
||||||
_ fs.IDer = (*Object)(nil)
|
_ fs.IDer = (*Object)(nil)
|
||||||
_ fs.ParentIDer = (*Object)(nil)
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ func (i *Item) ModTime() (t time.Time) {
|
|||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
|
|
||||||
// ItemResult is returned from the /listfolder, /createfolder, /deletefolder, /deletefile, etc. methods
|
// ItemResult is returned from the /listfolder, /createfolder, /deletefolder, /deletefile etc methods
|
||||||
type ItemResult struct {
|
type ItemResult struct {
|
||||||
Error
|
Error
|
||||||
Metadata Item `json:"metadata"`
|
Metadata Item `json:"metadata"`
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ func init() {
|
|||||||
Name: "pcloud",
|
Name: "pcloud",
|
||||||
Description: "Pcloud",
|
Description: "Pcloud",
|
||||||
NewFs: NewFs,
|
NewFs: NewFs,
|
||||||
Config: func(ctx context.Context, name string, m configmap.Mapper) {
|
Config: func(name string, m configmap.Mapper) {
|
||||||
optc := new(Options)
|
optc := new(Options)
|
||||||
err := configstruct.Set(m, optc)
|
err := configstruct.Set(m, optc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -98,7 +98,7 @@ func init() {
|
|||||||
CheckAuth: checkAuth,
|
CheckAuth: checkAuth,
|
||||||
StateBlankOK: true, // pCloud seems to drop the state parameter now - see #4210
|
StateBlankOK: true, // pCloud seems to drop the state parameter now - see #4210
|
||||||
}
|
}
|
||||||
err = oauthutil.Config(ctx, "pcloud", name, m, oauthConfig, &opt)
|
err = oauthutil.Config("pcloud", name, m, oauthConfig, &opt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to configure token: %v", err)
|
log.Fatalf("Failed to configure token: %v", err)
|
||||||
}
|
}
|
||||||
@@ -213,16 +213,13 @@ var retryErrorCodes = []int{
|
|||||||
|
|
||||||
// shouldRetry returns a boolean as to whether this resp and err
|
// shouldRetry returns a boolean as to whether this resp and err
|
||||||
// deserve to be retried. It returns the err as a convenience
|
// deserve to be retried. It returns the err as a convenience
|
||||||
func shouldRetry(ctx context.Context, resp *http.Response, err error) (bool, error) {
|
func shouldRetry(resp *http.Response, err error) (bool, error) {
|
||||||
if fserrors.ContextError(ctx, &err) {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
doRetry := false
|
doRetry := false
|
||||||
|
|
||||||
// Check if it is an api.Error
|
// Check if it is an api.Error
|
||||||
if apiErr, ok := err.(*api.Error); ok {
|
if apiErr, ok := err.(*api.Error); ok {
|
||||||
// See https://docs.pcloud.com/errors/ for error treatment
|
// See https://docs.pcloud.com/errors/ for error treatment
|
||||||
// Errors are classified as 1xxx, 2xxx, etc.
|
// Errors are classified as 1xxx, 2xxx etc
|
||||||
switch apiErr.Result / 1000 {
|
switch apiErr.Result / 1000 {
|
||||||
case 4: // 4xxx: rate limiting
|
case 4: // 4xxx: rate limiting
|
||||||
doRetry = true
|
doRetry = true
|
||||||
@@ -283,7 +280,8 @@ func errorHandler(resp *http.Response) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewFs constructs an Fs from the path, container:path
|
// NewFs constructs an Fs from the path, container:path
|
||||||
func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, error) {
|
func NewFs(name, root string, m configmap.Mapper) (fs.Fs, error) {
|
||||||
|
ctx := context.Background()
|
||||||
// Parse config into Options struct
|
// Parse config into Options struct
|
||||||
opt := new(Options)
|
opt := new(Options)
|
||||||
err := configstruct.Set(m, opt)
|
err := configstruct.Set(m, opt)
|
||||||
@@ -291,7 +289,7 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
root = parsePath(root)
|
root = parsePath(root)
|
||||||
oAuthClient, ts, err := oauthutil.NewClient(ctx, name, m, oauthConfig)
|
oAuthClient, ts, err := oauthutil.NewClient(name, m, oauthConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to configure Pcloud")
|
return nil, errors.Wrap(err, "failed to configure Pcloud")
|
||||||
}
|
}
|
||||||
@@ -302,12 +300,12 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
root: root,
|
root: root,
|
||||||
opt: *opt,
|
opt: *opt,
|
||||||
srv: rest.NewClient(oAuthClient).SetRoot("https://" + opt.Hostname),
|
srv: rest.NewClient(oAuthClient).SetRoot("https://" + opt.Hostname),
|
||||||
pacer: fs.NewPacer(ctx, pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))),
|
pacer: fs.NewPacer(pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))),
|
||||||
}
|
}
|
||||||
f.features = (&fs.Features{
|
f.features = (&fs.Features{
|
||||||
CaseInsensitive: false,
|
CaseInsensitive: false,
|
||||||
CanHaveEmptyDirectories: true,
|
CanHaveEmptyDirectories: true,
|
||||||
}).Fill(ctx, f)
|
}).Fill(f)
|
||||||
f.srv.SetErrorHandler(errorHandler)
|
f.srv.SetErrorHandler(errorHandler)
|
||||||
|
|
||||||
// Renew the token in the background
|
// Renew the token in the background
|
||||||
@@ -408,7 +406,7 @@ func (f *Fs) CreateDir(ctx context.Context, pathID, leaf string) (newID string,
|
|||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
||||||
err = result.Error.Update(err)
|
err = result.Error.Update(err)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
//fmt.Printf("...Error %v\n", err)
|
//fmt.Printf("...Error %v\n", err)
|
||||||
@@ -463,7 +461,7 @@ func (f *Fs) listAll(ctx context.Context, dirID string, directoriesOnly bool, fi
|
|||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
||||||
err = result.Error.Update(err)
|
err = result.Error.Update(err)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return found, errors.Wrap(err, "couldn't list files")
|
return found, errors.Wrap(err, "couldn't list files")
|
||||||
@@ -600,7 +598,7 @@ func (f *Fs) purgeCheck(ctx context.Context, dir string, check bool) error {
|
|||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
||||||
err = result.Error.Update(err)
|
err = result.Error.Update(err)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "rmdir failed")
|
return errors.Wrap(err, "rmdir failed")
|
||||||
@@ -624,7 +622,7 @@ func (f *Fs) Precision() time.Duration {
|
|||||||
return time.Second
|
return time.Second
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy src to this remote using server-side copy operations.
|
// Copy src to this remote using server side copy operations.
|
||||||
//
|
//
|
||||||
// This is stored with the remote path given
|
// This is stored with the remote path given
|
||||||
//
|
//
|
||||||
@@ -665,7 +663,7 @@ func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object,
|
|||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
||||||
err = result.Error.Update(err)
|
err = result.Error.Update(err)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -703,11 +701,11 @@ func (f *Fs) CleanUp(ctx context.Context) error {
|
|||||||
return f.pacer.Call(func() (bool, error) {
|
return f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
||||||
err = result.Update(err)
|
err = result.Update(err)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move src to this remote using server-side move operations.
|
// Move src to this remote using server side move operations.
|
||||||
//
|
//
|
||||||
// This is stored with the remote path given
|
// This is stored with the remote path given
|
||||||
//
|
//
|
||||||
@@ -743,7 +741,7 @@ func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object,
|
|||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
||||||
err = result.Error.Update(err)
|
err = result.Error.Update(err)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -757,7 +755,7 @@ func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DirMove moves src, srcRemote to this remote at dstRemote
|
// DirMove moves src, srcRemote to this remote at dstRemote
|
||||||
// using server-side move operations.
|
// using server side move operations.
|
||||||
//
|
//
|
||||||
// Will only be called if src.Fs().Name() == f.Name()
|
// Will only be called if src.Fs().Name() == f.Name()
|
||||||
//
|
//
|
||||||
@@ -790,7 +788,7 @@ func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string
|
|||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
||||||
err = result.Error.Update(err)
|
err = result.Error.Update(err)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -817,7 +815,7 @@ func (f *Fs) linkDir(ctx context.Context, dirID string, expire fs.Duration) (str
|
|||||||
err := f.pacer.Call(func() (bool, error) {
|
err := f.pacer.Call(func() (bool, error) {
|
||||||
resp, err := f.srv.CallJSON(ctx, &opts, nil, &result)
|
resp, err := f.srv.CallJSON(ctx, &opts, nil, &result)
|
||||||
err = result.Error.Update(err)
|
err = result.Error.Update(err)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -841,7 +839,7 @@ func (f *Fs) linkFile(ctx context.Context, path string, expire fs.Duration) (str
|
|||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err := f.srv.CallJSON(ctx, &opts, nil, &result)
|
resp, err := f.srv.CallJSON(ctx, &opts, nil, &result)
|
||||||
err = result.Error.Update(err)
|
err = result.Error.Update(err)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -872,7 +870,7 @@ func (f *Fs) About(ctx context.Context) (usage *fs.Usage, err error) {
|
|||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, &q)
|
resp, err = f.srv.CallJSON(ctx, &opts, nil, &q)
|
||||||
err = q.Error.Update(err)
|
err = q.Error.Update(err)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "about failed")
|
return nil, errors.Wrap(err, "about failed")
|
||||||
@@ -930,7 +928,7 @@ func (o *Object) getHashes(ctx context.Context) (err error) {
|
|||||||
err = o.fs.pacer.Call(func() (bool, error) {
|
err = o.fs.pacer.Call(func() (bool, error) {
|
||||||
resp, err = o.fs.srv.CallJSON(ctx, &opts, nil, &result)
|
resp, err = o.fs.srv.CallJSON(ctx, &opts, nil, &result)
|
||||||
err = result.Error.Update(err)
|
err = result.Error.Update(err)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1049,7 +1047,7 @@ func (o *Object) downloadURL(ctx context.Context) (URL string, err error) {
|
|||||||
err = o.fs.pacer.Call(func() (bool, error) {
|
err = o.fs.pacer.Call(func() (bool, error) {
|
||||||
resp, err = o.fs.srv.CallJSON(ctx, &opts, nil, &result)
|
resp, err = o.fs.srv.CallJSON(ctx, &opts, nil, &result)
|
||||||
err = result.Error.Update(err)
|
err = result.Error.Update(err)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -1075,7 +1073,7 @@ func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.Read
|
|||||||
}
|
}
|
||||||
err = o.fs.pacer.Call(func() (bool, error) {
|
err = o.fs.pacer.Call(func() (bool, error) {
|
||||||
resp, err = o.fs.srv.Call(ctx, &opts)
|
resp, err = o.fs.srv.Call(ctx, &opts)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -1120,7 +1118,7 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
|
|||||||
Method: "PUT",
|
Method: "PUT",
|
||||||
Path: "/uploadfile",
|
Path: "/uploadfile",
|
||||||
Body: in,
|
Body: in,
|
||||||
ContentType: fs.MimeType(ctx, src),
|
ContentType: fs.MimeType(ctx, o),
|
||||||
ContentLength: &size,
|
ContentLength: &size,
|
||||||
Parameters: url.Values{},
|
Parameters: url.Values{},
|
||||||
TransferEncoding: []string{"identity"}, // pcloud doesn't like chunked encoding
|
TransferEncoding: []string{"identity"}, // pcloud doesn't like chunked encoding
|
||||||
@@ -1134,10 +1132,10 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
|
|||||||
|
|
||||||
// Special treatment for a 0 length upload. This doesn't work
|
// Special treatment for a 0 length upload. This doesn't work
|
||||||
// with PUT even with Content-Length set (by setting
|
// with PUT even with Content-Length set (by setting
|
||||||
// opts.Body=0), so upload it as a multipart form POST with
|
// opts.Body=0), so upload it as a multpart form POST with
|
||||||
// Content-Length set.
|
// Content-Length set.
|
||||||
if size == 0 {
|
if size == 0 {
|
||||||
formReader, contentType, overhead, err := rest.MultipartUpload(ctx, in, opts.Parameters, "content", leaf)
|
formReader, contentType, overhead, err := rest.MultipartUpload(in, opts.Parameters, "content", leaf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to make multipart upload for 0 length file")
|
return errors.Wrap(err, "failed to make multipart upload for 0 length file")
|
||||||
}
|
}
|
||||||
@@ -1154,7 +1152,7 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
|
|||||||
err = o.fs.pacer.CallNoRetry(func() (bool, error) {
|
err = o.fs.pacer.CallNoRetry(func() (bool, error) {
|
||||||
resp, err = o.fs.srv.CallJSON(ctx, &opts, nil, &result)
|
resp, err = o.fs.srv.CallJSON(ctx, &opts, nil, &result)
|
||||||
err = result.Error.Update(err)
|
err = result.Error.Update(err)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// sometimes pcloud leaves a half complete file on
|
// sometimes pcloud leaves a half complete file on
|
||||||
@@ -1184,7 +1182,7 @@ func (o *Object) Remove(ctx context.Context) error {
|
|||||||
return o.fs.pacer.Call(func() (bool, error) {
|
return o.fs.pacer.Call(func() (bool, error) {
|
||||||
resp, err := o.fs.srv.CallJSON(ctx, &opts, nil, &result)
|
resp, err := o.fs.srv.CallJSON(ctx, &opts, nil, &result)
|
||||||
err = result.Error.Update(err)
|
err = result.Error.Update(err)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -78,8 +78,8 @@ func init() {
|
|||||||
Name: "premiumizeme",
|
Name: "premiumizeme",
|
||||||
Description: "premiumize.me",
|
Description: "premiumize.me",
|
||||||
NewFs: NewFs,
|
NewFs: NewFs,
|
||||||
Config: func(ctx context.Context, name string, m configmap.Mapper) {
|
Config: func(name string, m configmap.Mapper) {
|
||||||
err := oauthutil.Config(ctx, "premiumizeme", name, m, oauthConfig, nil)
|
err := oauthutil.Config("premiumizeme", name, m, oauthConfig, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to configure token: %v", err)
|
log.Fatalf("Failed to configure token: %v", err)
|
||||||
}
|
}
|
||||||
@@ -176,10 +176,7 @@ var retryErrorCodes = []int{
|
|||||||
|
|
||||||
// shouldRetry returns a boolean as to whether this resp and err
|
// shouldRetry returns a boolean as to whether this resp and err
|
||||||
// deserve to be retried. It returns the err as a convenience
|
// deserve to be retried. It returns the err as a convenience
|
||||||
func shouldRetry(ctx context.Context, resp *http.Response, err error) (bool, error) {
|
func shouldRetry(resp *http.Response, err error) (bool, error) {
|
||||||
if fserrors.ContextError(ctx, &err) {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
return fserrors.ShouldRetry(err) || fserrors.ShouldRetryHTTP(resp, retryErrorCodes), err
|
return fserrors.ShouldRetry(err) || fserrors.ShouldRetryHTTP(resp, retryErrorCodes), err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,7 +234,8 @@ func (f *Fs) baseParams() url.Values {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewFs constructs an Fs from the path, container:path
|
// NewFs constructs an Fs from the path, container:path
|
||||||
func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, error) {
|
func NewFs(name, root string, m configmap.Mapper) (fs.Fs, error) {
|
||||||
|
ctx := context.Background()
|
||||||
// Parse config into Options struct
|
// Parse config into Options struct
|
||||||
opt := new(Options)
|
opt := new(Options)
|
||||||
err := configstruct.Set(m, opt)
|
err := configstruct.Set(m, opt)
|
||||||
@@ -250,12 +248,12 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
var client *http.Client
|
var client *http.Client
|
||||||
var ts *oauthutil.TokenSource
|
var ts *oauthutil.TokenSource
|
||||||
if opt.APIKey == "" {
|
if opt.APIKey == "" {
|
||||||
client, ts, err = oauthutil.NewClient(ctx, name, m, oauthConfig)
|
client, ts, err = oauthutil.NewClient(name, m, oauthConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to configure premiumize.me")
|
return nil, errors.Wrap(err, "failed to configure premiumize.me")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
client = fshttp.NewClient(ctx)
|
client = fshttp.NewClient(fs.Config)
|
||||||
}
|
}
|
||||||
|
|
||||||
f := &Fs{
|
f := &Fs{
|
||||||
@@ -263,13 +261,13 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
root: root,
|
root: root,
|
||||||
opt: *opt,
|
opt: *opt,
|
||||||
srv: rest.NewClient(client).SetRoot(rootURL),
|
srv: rest.NewClient(client).SetRoot(rootURL),
|
||||||
pacer: fs.NewPacer(ctx, pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))),
|
pacer: fs.NewPacer(pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))),
|
||||||
}
|
}
|
||||||
f.features = (&fs.Features{
|
f.features = (&fs.Features{
|
||||||
CaseInsensitive: true,
|
CaseInsensitive: true,
|
||||||
CanHaveEmptyDirectories: true,
|
CanHaveEmptyDirectories: true,
|
||||||
ReadMimeType: true,
|
ReadMimeType: true,
|
||||||
}).Fill(ctx, f)
|
}).Fill(f)
|
||||||
f.srv.SetErrorHandler(errorHandler)
|
f.srv.SetErrorHandler(errorHandler)
|
||||||
|
|
||||||
// Renew the token in the background
|
// Renew the token in the background
|
||||||
@@ -305,7 +303,7 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
f.features.Fill(ctx, &tempF)
|
f.features.Fill(&tempF)
|
||||||
// XXX: update the old f here instead of returning tempF, since
|
// XXX: update the old f here instead of returning tempF, since
|
||||||
// `features` were already filled with functions having *f as a receiver.
|
// `features` were already filled with functions having *f as a receiver.
|
||||||
// See https://github.com/rclone/rclone/issues/2182
|
// See https://github.com/rclone/rclone/issues/2182
|
||||||
@@ -348,7 +346,7 @@ func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) {
|
|||||||
func (f *Fs) FindLeaf(ctx context.Context, pathID, leaf string) (pathIDOut string, found bool, err error) {
|
func (f *Fs) FindLeaf(ctx context.Context, pathID, leaf string) (pathIDOut string, found bool, err error) {
|
||||||
// Find the leaf in pathID
|
// 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, func(item *api.Item) bool {
|
||||||
if strings.EqualFold(item.Name, leaf) {
|
if item.Name == leaf {
|
||||||
pathIDOut = item.ID
|
pathIDOut = item.ID
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -373,7 +371,7 @@ func (f *Fs) CreateDir(ctx context.Context, pathID, leaf string) (newID string,
|
|||||||
}
|
}
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, &info)
|
resp, err = f.srv.CallJSON(ctx, &opts, nil, &info)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
//fmt.Printf("...Error %v\n", err)
|
//fmt.Printf("...Error %v\n", err)
|
||||||
@@ -410,7 +408,7 @@ func (f *Fs) listAll(ctx context.Context, dirID string, directoriesOnly bool, fi
|
|||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return found, errors.Wrap(err, "couldn't list files")
|
return found, errors.Wrap(err, "couldn't list files")
|
||||||
@@ -584,7 +582,7 @@ func (f *Fs) purgeCheck(ctx context.Context, dir string, check bool) error {
|
|||||||
var result api.Response
|
var result api.Response
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "rmdir failed")
|
return errors.Wrap(err, "rmdir failed")
|
||||||
@@ -663,7 +661,7 @@ func (f *Fs) move(ctx context.Context, isFile bool, id, oldLeaf, newLeaf, oldDir
|
|||||||
var result api.Response
|
var result api.Response
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "Move http")
|
return errors.Wrap(err, "Move http")
|
||||||
@@ -684,7 +682,7 @@ func (f *Fs) move(ctx context.Context, isFile bool, id, oldLeaf, newLeaf, oldDir
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move src to this remote using server-side move operations.
|
// Move src to this remote using server side move operations.
|
||||||
//
|
//
|
||||||
// This is stored with the remote path given
|
// This is stored with the remote path given
|
||||||
//
|
//
|
||||||
@@ -720,7 +718,7 @@ func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DirMove moves src, srcRemote to this remote at dstRemote
|
// DirMove moves src, srcRemote to this remote at dstRemote
|
||||||
// using server-side move operations.
|
// using server side move operations.
|
||||||
//
|
//
|
||||||
// Will only be called if src.Fs().Name() == f.Name()
|
// Will only be called if src.Fs().Name() == f.Name()
|
||||||
//
|
//
|
||||||
@@ -772,7 +770,7 @@ func (f *Fs) About(ctx context.Context) (usage *fs.Usage, err error) {
|
|||||||
}
|
}
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, &info)
|
resp, err = f.srv.CallJSON(ctx, &opts, nil, &info)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "CreateDir http")
|
return nil, errors.Wrap(err, "CreateDir http")
|
||||||
@@ -899,7 +897,7 @@ func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.Read
|
|||||||
}
|
}
|
||||||
err = o.fs.pacer.Call(func() (bool, error) {
|
err = o.fs.pacer.Call(func() (bool, error) {
|
||||||
resp, err = o.fs.srv.Call(ctx, &opts)
|
resp, err = o.fs.srv.Call(ctx, &opts)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -937,7 +935,7 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
|
|||||||
err = o.fs.pacer.Call(func() (bool, error) {
|
err = o.fs.pacer.Call(func() (bool, error) {
|
||||||
resp, err = o.fs.srv.CallJSON(ctx, &opts, nil, &info)
|
resp, err = o.fs.srv.CallJSON(ctx, &opts, nil, &info)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
}
|
}
|
||||||
// Just check the download URL resolves - sometimes
|
// Just check the download URL resolves - sometimes
|
||||||
// the URLs returned by premiumize.me don't resolve so
|
// the URLs returned by premiumize.me don't resolve so
|
||||||
@@ -996,7 +994,7 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
|
|||||||
var result api.Response
|
var result api.Response
|
||||||
err = o.fs.pacer.CallNoRetry(func() (bool, error) {
|
err = o.fs.pacer.CallNoRetry(func() (bool, error) {
|
||||||
resp, err = o.fs.srv.CallJSON(ctx, &opts, nil, &result)
|
resp, err = o.fs.srv.CallJSON(ctx, &opts, nil, &result)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "upload file http")
|
return errors.Wrap(err, "upload file http")
|
||||||
@@ -1038,7 +1036,7 @@ func (f *Fs) renameLeaf(ctx context.Context, isFile bool, id string, newLeaf str
|
|||||||
var result api.Response
|
var result api.Response
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "rename http")
|
return errors.Wrap(err, "rename http")
|
||||||
@@ -1063,7 +1061,7 @@ func (f *Fs) remove(ctx context.Context, id string) (err error) {
|
|||||||
var result api.Response
|
var result api.Response
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "remove http")
|
return errors.Wrap(err, "remove http")
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package putio
|
package putio
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
@@ -30,10 +29,7 @@ func (e *statusCodeError) Temporary() bool {
|
|||||||
|
|
||||||
// shouldRetry returns a boolean as to whether this err deserves to be
|
// shouldRetry returns a boolean as to whether this err deserves to be
|
||||||
// retried. It returns the err as a convenience
|
// retried. It returns the err as a convenience
|
||||||
func shouldRetry(ctx context.Context, err error) (bool, error) {
|
func shouldRetry(err error) (bool, error) {
|
||||||
if fserrors.ContextError(ctx, &err) {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ func parsePath(path string) (root string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewFs constructs an Fs from the path, container:path
|
// NewFs constructs an Fs from the path, container:path
|
||||||
func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (f fs.Fs, err error) {
|
func NewFs(name, root string, m configmap.Mapper) (f fs.Fs, err error) {
|
||||||
// defer log.Trace(name, "root=%v", root)("f=%+v, err=%v", &f, &err)
|
// defer log.Trace(name, "root=%v", root)("f=%+v, err=%v", &f, &err)
|
||||||
// Parse config into Options struct
|
// Parse config into Options struct
|
||||||
opt := new(Options)
|
opt := new(Options)
|
||||||
@@ -77,8 +77,8 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (f fs.Fs,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
root = parsePath(root)
|
root = parsePath(root)
|
||||||
httpClient := fshttp.NewClient(ctx)
|
httpClient := fshttp.NewClient(fs.Config)
|
||||||
oAuthClient, _, err := oauthutil.NewClientWithBaseClient(ctx, name, m, putioConfig, httpClient)
|
oAuthClient, _, err := oauthutil.NewClientWithBaseClient(name, m, putioConfig, httpClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to configure putio")
|
return nil, errors.Wrap(err, "failed to configure putio")
|
||||||
}
|
}
|
||||||
@@ -86,7 +86,7 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (f fs.Fs,
|
|||||||
name: name,
|
name: name,
|
||||||
root: root,
|
root: root,
|
||||||
opt: *opt,
|
opt: *opt,
|
||||||
pacer: fs.NewPacer(ctx, pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))),
|
pacer: fs.NewPacer(pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))),
|
||||||
client: putio.NewClient(oAuthClient),
|
client: putio.NewClient(oAuthClient),
|
||||||
httpClient: httpClient,
|
httpClient: httpClient,
|
||||||
oAuthClient: oAuthClient,
|
oAuthClient: oAuthClient,
|
||||||
@@ -95,8 +95,9 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (f fs.Fs,
|
|||||||
DuplicateFiles: true,
|
DuplicateFiles: true,
|
||||||
ReadMimeType: true,
|
ReadMimeType: true,
|
||||||
CanHaveEmptyDirectories: true,
|
CanHaveEmptyDirectories: true,
|
||||||
}).Fill(ctx, p)
|
}).Fill(p)
|
||||||
p.dirCache = dircache.New(root, "0", p)
|
p.dirCache = dircache.New(root, "0", p)
|
||||||
|
ctx := context.Background()
|
||||||
// Find the current root
|
// Find the current root
|
||||||
err = p.dirCache.FindRoot(ctx, false)
|
err = p.dirCache.FindRoot(ctx, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -147,7 +148,7 @@ func (f *Fs) CreateDir(ctx context.Context, pathID, leaf string) (newID string,
|
|||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
// fs.Debugf(f, "creating folder. part: %s, parentID: %d", leaf, parentID)
|
// fs.Debugf(f, "creating folder. part: %s, parentID: %d", leaf, parentID)
|
||||||
entry, err = f.client.Files.CreateFolder(ctx, f.opt.Enc.FromStandardName(leaf), parentID)
|
entry, err = f.client.Files.CreateFolder(ctx, f.opt.Enc.FromStandardName(leaf), parentID)
|
||||||
return shouldRetry(ctx, err)
|
return shouldRetry(err)
|
||||||
})
|
})
|
||||||
return itoa(entry.ID), err
|
return itoa(entry.ID), err
|
||||||
}
|
}
|
||||||
@@ -164,7 +165,7 @@ func (f *Fs) FindLeaf(ctx context.Context, pathID, leaf string) (pathIDOut strin
|
|||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
// fs.Debugf(f, "listing file: %d", fileID)
|
// fs.Debugf(f, "listing file: %d", fileID)
|
||||||
children, _, err = f.client.Files.List(ctx, fileID)
|
children, _, err = f.client.Files.List(ctx, fileID)
|
||||||
return shouldRetry(ctx, err)
|
return shouldRetry(err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if perr, ok := err.(*putio.ErrorResponse); ok && perr.Response.StatusCode == 404 {
|
if perr, ok := err.(*putio.ErrorResponse); ok && perr.Response.StatusCode == 404 {
|
||||||
@@ -205,7 +206,7 @@ func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err e
|
|||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
// fs.Debugf(f, "listing files inside List: %d", parentID)
|
// fs.Debugf(f, "listing files inside List: %d", parentID)
|
||||||
children, _, err = f.client.Files.List(ctx, parentID)
|
children, _, err = f.client.Files.List(ctx, parentID)
|
||||||
return shouldRetry(ctx, err)
|
return shouldRetry(err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@@ -235,10 +236,10 @@ func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err e
|
|||||||
// The new object may have been created if an error is returned
|
// The new object may have been created if an error is returned
|
||||||
func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (o fs.Object, err error) {
|
func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (o fs.Object, err error) {
|
||||||
// defer log.Trace(f, "src=%+v", src)("o=%+v, err=%v", &o, &err)
|
// defer log.Trace(f, "src=%+v", src)("o=%+v, err=%v", &o, &err)
|
||||||
existingObj, err := f.NewObject(ctx, src.Remote())
|
exisitingObj, err := f.NewObject(ctx, src.Remote())
|
||||||
switch err {
|
switch err {
|
||||||
case nil:
|
case nil:
|
||||||
return existingObj, existingObj.Update(ctx, in, src, options...)
|
return exisitingObj, exisitingObj.Update(ctx, in, src, options...)
|
||||||
case fs.ErrorObjectNotFound:
|
case fs.ErrorObjectNotFound:
|
||||||
// Not found so create it
|
// Not found so create it
|
||||||
return f.PutUnchecked(ctx, in, src, options...)
|
return f.PutUnchecked(ctx, in, src, options...)
|
||||||
@@ -271,7 +272,7 @@ func (f *Fs) PutUnchecked(ctx context.Context, in io.Reader, src fs.ObjectInfo,
|
|||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
// fs.Debugf(f, "getting file: %d", fileID)
|
// fs.Debugf(f, "getting file: %d", fileID)
|
||||||
entry, err = f.client.Files.Get(ctx, fileID)
|
entry, err = f.client.Files.Get(ctx, fileID)
|
||||||
return shouldRetry(ctx, err)
|
return shouldRetry(err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -282,10 +283,11 @@ func (f *Fs) PutUnchecked(ctx context.Context, in io.Reader, src fs.ObjectInfo,
|
|||||||
func (f *Fs) createUpload(ctx context.Context, name string, size int64, parentID string, modTime time.Time, options []fs.OpenOption) (location string, err error) {
|
func (f *Fs) createUpload(ctx context.Context, name string, size int64, parentID string, modTime time.Time, options []fs.OpenOption) (location string, err error) {
|
||||||
// defer log.Trace(f, "name=%v, size=%v, parentID=%v, modTime=%v", name, size, parentID, modTime.String())("location=%v, err=%v", location, &err)
|
// defer log.Trace(f, "name=%v, size=%v, parentID=%v, modTime=%v", name, size, parentID, modTime.String())("location=%v, err=%v", location, &err)
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
req, err := http.NewRequestWithContext(ctx, "POST", "https://upload.put.io/files/", nil)
|
req, err := http.NewRequest("POST", "https://upload.put.io/files/", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
req = req.WithContext(ctx) // go1.13 can use NewRequestWithContext
|
||||||
req.Header.Set("tus-resumable", "1.0.0")
|
req.Header.Set("tus-resumable", "1.0.0")
|
||||||
req.Header.Set("upload-length", strconv.FormatInt(size, 10))
|
req.Header.Set("upload-length", strconv.FormatInt(size, 10))
|
||||||
b64name := base64.StdEncoding.EncodeToString([]byte(f.opt.Enc.FromStandardName(name)))
|
b64name := base64.StdEncoding.EncodeToString([]byte(f.opt.Enc.FromStandardName(name)))
|
||||||
@@ -295,7 +297,7 @@ func (f *Fs) createUpload(ctx context.Context, name string, size int64, parentID
|
|||||||
req.Header.Set("upload-metadata", fmt.Sprintf("name %s,no-torrent %s,parent_id %s,updated-at %s", b64name, b64true, b64parentID, b64modifiedAt))
|
req.Header.Set("upload-metadata", fmt.Sprintf("name %s,no-torrent %s,parent_id %s,updated-at %s", b64name, b64true, b64parentID, b64modifiedAt))
|
||||||
fs.OpenOptionAddHTTPHeaders(req.Header, options)
|
fs.OpenOptionAddHTTPHeaders(req.Header, options)
|
||||||
resp, err := f.oAuthClient.Do(req)
|
resp, err := f.oAuthClient.Do(req)
|
||||||
retry, err := shouldRetry(ctx, err)
|
retry, err := shouldRetry(err)
|
||||||
if retry {
|
if retry {
|
||||||
return true, err
|
return true, err
|
||||||
}
|
}
|
||||||
@@ -320,7 +322,7 @@ func (f *Fs) sendUpload(ctx context.Context, location string, size int64, in io.
|
|||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
fs.Debugf(f, "Sending zero length chunk")
|
fs.Debugf(f, "Sending zero length chunk")
|
||||||
_, fileID, err = f.transferChunk(ctx, location, 0, bytes.NewReader([]byte{}), 0)
|
_, fileID, err = f.transferChunk(ctx, location, 0, bytes.NewReader([]byte{}), 0)
|
||||||
return shouldRetry(ctx, err)
|
return shouldRetry(err)
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -344,13 +346,13 @@ func (f *Fs) sendUpload(ctx context.Context, location string, size int64, in io.
|
|||||||
// Get file offset and seek to the position
|
// Get file offset and seek to the position
|
||||||
offset, err := f.getServerOffset(ctx, location)
|
offset, err := f.getServerOffset(ctx, location)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return shouldRetry(ctx, err)
|
return shouldRetry(err)
|
||||||
}
|
}
|
||||||
sentBytes := offset - chunkStart
|
sentBytes := offset - chunkStart
|
||||||
fs.Debugf(f, "sentBytes: %d", sentBytes)
|
fs.Debugf(f, "sentBytes: %d", sentBytes)
|
||||||
_, err = chunk.Seek(sentBytes, io.SeekStart)
|
_, err = chunk.Seek(sentBytes, io.SeekStart)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return shouldRetry(ctx, err)
|
return shouldRetry(err)
|
||||||
}
|
}
|
||||||
transferOffset = offset
|
transferOffset = offset
|
||||||
reqSize = chunkSize - sentBytes
|
reqSize = chunkSize - sentBytes
|
||||||
@@ -367,7 +369,7 @@ func (f *Fs) sendUpload(ctx context.Context, location string, size int64, in io.
|
|||||||
offsetMismatch = true
|
offsetMismatch = true
|
||||||
return true, errors.New("connection broken")
|
return true, errors.New("connection broken")
|
||||||
}
|
}
|
||||||
return shouldRetry(ctx, err)
|
return shouldRetry(err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@@ -427,19 +429,21 @@ func (f *Fs) transferChunk(ctx context.Context, location string, start int64, ch
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fs) makeUploadHeadRequest(ctx context.Context, location string) (*http.Request, error) {
|
func (f *Fs) makeUploadHeadRequest(ctx context.Context, location string) (*http.Request, error) {
|
||||||
req, err := http.NewRequestWithContext(ctx, "HEAD", location, nil)
|
req, err := http.NewRequest("HEAD", location, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
req = req.WithContext(ctx) // go1.13 can use NewRequestWithContext
|
||||||
req.Header.Set("tus-resumable", "1.0.0")
|
req.Header.Set("tus-resumable", "1.0.0")
|
||||||
return req, nil
|
return req, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fs) makeUploadPatchRequest(ctx context.Context, location string, in io.Reader, offset, length int64) (*http.Request, error) {
|
func (f *Fs) makeUploadPatchRequest(ctx context.Context, location string, in io.Reader, offset, length int64) (*http.Request, error) {
|
||||||
req, err := http.NewRequestWithContext(ctx, "PATCH", location, in)
|
req, err := http.NewRequest("PATCH", location, in)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
req = req.WithContext(ctx) // go1.13 can use NewRequestWithContext
|
||||||
req.Header.Set("tus-resumable", "1.0.0")
|
req.Header.Set("tus-resumable", "1.0.0")
|
||||||
req.Header.Set("upload-offset", strconv.FormatInt(offset, 10))
|
req.Header.Set("upload-offset", strconv.FormatInt(offset, 10))
|
||||||
req.Header.Set("content-length", strconv.FormatInt(length, 10))
|
req.Header.Set("content-length", strconv.FormatInt(length, 10))
|
||||||
@@ -479,7 +483,7 @@ func (f *Fs) purgeCheck(ctx context.Context, dir string, check bool) (err error)
|
|||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
// fs.Debugf(f, "listing files: %d", dirID)
|
// fs.Debugf(f, "listing files: %d", dirID)
|
||||||
children, _, err = f.client.Files.List(ctx, dirID)
|
children, _, err = f.client.Files.List(ctx, dirID)
|
||||||
return shouldRetry(ctx, err)
|
return shouldRetry(err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "Rmdir")
|
return errors.Wrap(err, "Rmdir")
|
||||||
@@ -493,7 +497,7 @@ func (f *Fs) purgeCheck(ctx context.Context, dir string, check bool) (err error)
|
|||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
// fs.Debugf(f, "deleting file: %d", dirID)
|
// fs.Debugf(f, "deleting file: %d", dirID)
|
||||||
err = f.client.Files.Delete(ctx, dirID)
|
err = f.client.Files.Delete(ctx, dirID)
|
||||||
return shouldRetry(ctx, err)
|
return shouldRetry(err)
|
||||||
})
|
})
|
||||||
f.dirCache.FlushDir(dir)
|
f.dirCache.FlushDir(dir)
|
||||||
return err
|
return err
|
||||||
@@ -521,7 +525,7 @@ func (f *Fs) Purge(ctx context.Context, dir string) (err error) {
|
|||||||
return f.purgeCheck(ctx, dir, false)
|
return f.purgeCheck(ctx, dir, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy src to this remote using server-side copy operations.
|
// Copy src to this remote using server side copy operations.
|
||||||
//
|
//
|
||||||
// This is stored with the remote path given
|
// This is stored with the remote path given
|
||||||
//
|
//
|
||||||
@@ -552,7 +556,7 @@ func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (o fs.Objec
|
|||||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
// fs.Debugf(f, "copying file (%d) to parent_id: %s", srcObj.file.ID, directoryID)
|
// fs.Debugf(f, "copying file (%d) to parent_id: %s", srcObj.file.ID, directoryID)
|
||||||
_, err = f.client.Do(req, nil)
|
_, err = f.client.Do(req, nil)
|
||||||
return shouldRetry(ctx, err)
|
return shouldRetry(err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -560,7 +564,7 @@ func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (o fs.Objec
|
|||||||
return f.NewObject(ctx, remote)
|
return f.NewObject(ctx, remote)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move src to this remote using server-side move operations.
|
// Move src to this remote using server side move operations.
|
||||||
//
|
//
|
||||||
// This is stored with the remote path given
|
// This is stored with the remote path given
|
||||||
//
|
//
|
||||||
@@ -591,7 +595,7 @@ func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (o fs.Objec
|
|||||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
// fs.Debugf(f, "moving file (%d) to parent_id: %s", srcObj.file.ID, directoryID)
|
// fs.Debugf(f, "moving file (%d) to parent_id: %s", srcObj.file.ID, directoryID)
|
||||||
_, err = f.client.Do(req, nil)
|
_, err = f.client.Do(req, nil)
|
||||||
return shouldRetry(ctx, err)
|
return shouldRetry(err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -600,7 +604,7 @@ func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (o fs.Objec
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DirMove moves src, srcRemote to this remote at dstRemote
|
// DirMove moves src, srcRemote to this remote at dstRemote
|
||||||
// using server-side move operations.
|
// using server side move operations.
|
||||||
//
|
//
|
||||||
// Will only be called if src.Fs().Name() == f.Name()
|
// Will only be called if src.Fs().Name() == f.Name()
|
||||||
//
|
//
|
||||||
@@ -631,7 +635,7 @@ func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string
|
|||||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
// fs.Debugf(f, "moving file (%s) to parent_id: %s", srcID, dstDirectoryID)
|
// fs.Debugf(f, "moving file (%s) to parent_id: %s", srcID, dstDirectoryID)
|
||||||
_, err = f.client.Do(req, nil)
|
_, err = f.client.Do(req, nil)
|
||||||
return shouldRetry(ctx, err)
|
return shouldRetry(err)
|
||||||
})
|
})
|
||||||
srcFs.dirCache.FlushDir(srcRemote)
|
srcFs.dirCache.FlushDir(srcRemote)
|
||||||
return err
|
return err
|
||||||
@@ -644,7 +648,7 @@ func (f *Fs) About(ctx context.Context) (usage *fs.Usage, err error) {
|
|||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
// fs.Debugf(f, "getting account info")
|
// fs.Debugf(f, "getting account info")
|
||||||
ai, err = f.client.Account.Info(ctx)
|
ai, err = f.client.Account.Info(ctx)
|
||||||
return shouldRetry(ctx, err)
|
return shouldRetry(err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "about failed")
|
return nil, errors.Wrap(err, "about failed")
|
||||||
@@ -678,6 +682,6 @@ func (f *Fs) CleanUp(ctx context.Context) (err error) {
|
|||||||
}
|
}
|
||||||
// fs.Debugf(f, "emptying trash")
|
// fs.Debugf(f, "emptying trash")
|
||||||
_, err = f.client.Do(req, nil)
|
_, err = f.client.Do(req, nil)
|
||||||
return shouldRetry(ctx, err)
|
return shouldRetry(err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ func (o *Object) MimeType(ctx context.Context) string {
|
|||||||
|
|
||||||
// setMetadataFromEntry sets the fs data from a putio.File
|
// setMetadataFromEntry sets the fs data from a putio.File
|
||||||
//
|
//
|
||||||
// This isn't a complete set of metadata and has an inaccurate date
|
// This isn't a complete set of metadata and has an inacurate date
|
||||||
func (o *Object) setMetadataFromEntry(info putio.File) error {
|
func (o *Object) setMetadataFromEntry(info putio.File) error {
|
||||||
o.file = &info
|
o.file = &info
|
||||||
o.modtime = info.UpdatedAt.Time
|
o.modtime = info.UpdatedAt.Time
|
||||||
@@ -145,7 +145,7 @@ func (o *Object) readEntry(ctx context.Context) (f *putio.File, err error) {
|
|||||||
if perr, ok := err.(*putio.ErrorResponse); ok && perr.Response.StatusCode == 404 {
|
if perr, ok := err.(*putio.ErrorResponse); ok && perr.Response.StatusCode == 404 {
|
||||||
return false, fs.ErrorObjectNotFound
|
return false, fs.ErrorObjectNotFound
|
||||||
}
|
}
|
||||||
return shouldRetry(ctx, err)
|
return shouldRetry(err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -220,7 +220,7 @@ func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.Read
|
|||||||
var storageURL string
|
var storageURL string
|
||||||
err = o.fs.pacer.Call(func() (bool, error) {
|
err = o.fs.pacer.Call(func() (bool, error) {
|
||||||
storageURL, err = o.fs.client.Files.URL(ctx, o.file.ID, true)
|
storageURL, err = o.fs.client.Files.URL(ctx, o.file.ID, true)
|
||||||
return shouldRetry(ctx, err)
|
return shouldRetry(err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@@ -229,10 +229,11 @@ func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.Read
|
|||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
headers := fs.OpenOptionHeaders(options)
|
headers := fs.OpenOptionHeaders(options)
|
||||||
err = o.fs.pacer.Call(func() (bool, error) {
|
err = o.fs.pacer.Call(func() (bool, error) {
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, storageURL, nil)
|
req, err := http.NewRequest(http.MethodGet, storageURL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return shouldRetry(ctx, err)
|
return shouldRetry(err)
|
||||||
}
|
}
|
||||||
|
req = req.WithContext(ctx) // go1.13 can use NewRequestWithContext
|
||||||
req.Header.Set("User-Agent", o.fs.client.UserAgent)
|
req.Header.Set("User-Agent", o.fs.client.UserAgent)
|
||||||
|
|
||||||
// merge headers with extra headers
|
// merge headers with extra headers
|
||||||
@@ -241,7 +242,7 @@ func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.Read
|
|||||||
}
|
}
|
||||||
// fs.Debugf(o, "opening file: id=%d", o.file.ID)
|
// fs.Debugf(o, "opening file: id=%d", o.file.ID)
|
||||||
resp, err = o.fs.httpClient.Do(req)
|
resp, err = o.fs.httpClient.Do(req)
|
||||||
return shouldRetry(ctx, err)
|
return shouldRetry(err)
|
||||||
})
|
})
|
||||||
if perr, ok := err.(*putio.ErrorResponse); ok && perr.Response.StatusCode >= 400 && perr.Response.StatusCode <= 499 {
|
if perr, ok := err.(*putio.ErrorResponse); ok && perr.Response.StatusCode >= 400 && perr.Response.StatusCode <= 499 {
|
||||||
_ = resp.Body.Close()
|
_ = resp.Body.Close()
|
||||||
@@ -283,6 +284,6 @@ func (o *Object) Remove(ctx context.Context) (err error) {
|
|||||||
return o.fs.pacer.Call(func() (bool, error) {
|
return o.fs.pacer.Call(func() (bool, error) {
|
||||||
// fs.Debugf(o, "removing file: id=%d", o.file.ID)
|
// fs.Debugf(o, "removing file: id=%d", o.file.ID)
|
||||||
err = o.fs.client.Files.Delete(ctx, o.file.ID)
|
err = o.fs.client.Files.Delete(ctx, o.file.ID)
|
||||||
return shouldRetry(ctx, err)
|
return shouldRetry(err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package putio
|
package putio
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"log"
|
"log"
|
||||||
"regexp"
|
"regexp"
|
||||||
"time"
|
"time"
|
||||||
@@ -60,11 +59,11 @@ func init() {
|
|||||||
Name: "putio",
|
Name: "putio",
|
||||||
Description: "Put.io",
|
Description: "Put.io",
|
||||||
NewFs: NewFs,
|
NewFs: NewFs,
|
||||||
Config: func(ctx context.Context, name string, m configmap.Mapper) {
|
Config: func(name string, m configmap.Mapper) {
|
||||||
opt := oauthutil.Options{
|
opt := oauthutil.Options{
|
||||||
NoOffline: true,
|
NoOffline: true,
|
||||||
}
|
}
|
||||||
err := oauthutil.Config(ctx, "putio", name, m, putioConfig, &opt)
|
err := oauthutil.Config("putio", name, m, putioConfig, &opt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to configure token: %v", err)
|
log.Fatalf("Failed to configure token: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ as multipart uploads using this chunk size.
|
|||||||
Note that "--qingstor-upload-concurrency" chunks of this size are buffered
|
Note that "--qingstor-upload-concurrency" chunks of this size are buffered
|
||||||
in memory per transfer.
|
in memory per transfer.
|
||||||
|
|
||||||
If you are transferring large files over high-speed links and you have
|
If you are transferring large files over high speed links and you have
|
||||||
enough memory, then increasing this will speed up the transfers.`,
|
enough memory, then increasing this will speed up the transfers.`,
|
||||||
Default: minChunkSize,
|
Default: minChunkSize,
|
||||||
Advanced: true,
|
Advanced: true,
|
||||||
@@ -104,10 +104,10 @@ enough memory, then increasing this will speed up the transfers.`,
|
|||||||
This is the number of chunks of the same file that are uploaded
|
This is the number of chunks of the same file that are uploaded
|
||||||
concurrently.
|
concurrently.
|
||||||
|
|
||||||
NB if you set this to > 1 then the checksums of multipart uploads
|
NB if you set this to > 1 then the checksums of multpart uploads
|
||||||
become corrupted (the uploads themselves are not corrupted though).
|
become corrupted (the uploads themselves are not corrupted though).
|
||||||
|
|
||||||
If you are uploading small numbers of large files over high-speed links
|
If you are uploading small numbers of large file over high speed link
|
||||||
and these uploads do not fully utilize your bandwidth, then increasing
|
and these uploads do not fully utilize your bandwidth, then increasing
|
||||||
this may help to speed up the transfers.`,
|
this may help to speed up the transfers.`,
|
||||||
Default: 1,
|
Default: 1,
|
||||||
@@ -207,7 +207,7 @@ func (o *Object) split() (bucket, bucketPath string) {
|
|||||||
func qsParseEndpoint(endpoint string) (protocol, host, port string, err error) {
|
func qsParseEndpoint(endpoint string) (protocol, host, port string, err error) {
|
||||||
/*
|
/*
|
||||||
Pattern to match an endpoint,
|
Pattern to match an endpoint,
|
||||||
e.g.: "http(s)://qingstor.com:443" --> "http(s)", "qingstor.com", 443
|
eg: "http(s)://qingstor.com:443" --> "http(s)", "qingstor.com", 443
|
||||||
"http(s)//qingstor.com" --> "http(s)", "qingstor.com", ""
|
"http(s)//qingstor.com" --> "http(s)", "qingstor.com", ""
|
||||||
"qingstor.com" --> "", "qingstor.com", ""
|
"qingstor.com" --> "", "qingstor.com", ""
|
||||||
*/
|
*/
|
||||||
@@ -228,7 +228,7 @@ func qsParseEndpoint(endpoint string) (protocol, host, port string, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// qsConnection makes a connection to qingstor
|
// qsConnection makes a connection to qingstor
|
||||||
func qsServiceConnection(ctx context.Context, opt *Options) (*qs.Service, error) {
|
func qsServiceConnection(opt *Options) (*qs.Service, error) {
|
||||||
accessKeyID := opt.AccessKeyID
|
accessKeyID := opt.AccessKeyID
|
||||||
secretAccessKey := opt.SecretAccessKey
|
secretAccessKey := opt.SecretAccessKey
|
||||||
|
|
||||||
@@ -277,7 +277,7 @@ func qsServiceConnection(ctx context.Context, opt *Options) (*qs.Service, error)
|
|||||||
cf.Host = host
|
cf.Host = host
|
||||||
cf.Port = port
|
cf.Port = port
|
||||||
// unsupported in v3.1: cf.ConnectionRetries = opt.ConnectionRetries
|
// unsupported in v3.1: cf.ConnectionRetries = opt.ConnectionRetries
|
||||||
cf.Connection = fshttp.NewClient(ctx)
|
cf.Connection = fshttp.NewClient(fs.Config)
|
||||||
|
|
||||||
return qs.Init(cf)
|
return qs.Init(cf)
|
||||||
}
|
}
|
||||||
@@ -319,7 +319,7 @@ func (f *Fs) setRoot(root string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewFs constructs an Fs from the path, bucket:path
|
// NewFs constructs an Fs from the path, bucket:path
|
||||||
func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, error) {
|
func NewFs(name, root string, m configmap.Mapper) (fs.Fs, error) {
|
||||||
// Parse config into Options struct
|
// Parse config into Options struct
|
||||||
opt := new(Options)
|
opt := new(Options)
|
||||||
err := configstruct.Set(m, opt)
|
err := configstruct.Set(m, opt)
|
||||||
@@ -334,7 +334,7 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "qingstor: upload cutoff")
|
return nil, errors.Wrap(err, "qingstor: upload cutoff")
|
||||||
}
|
}
|
||||||
svc, err := qsServiceConnection(ctx, opt)
|
svc, err := qsServiceConnection(opt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -357,7 +357,7 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
BucketBased: true,
|
BucketBased: true,
|
||||||
BucketBasedRootOK: true,
|
BucketBasedRootOK: true,
|
||||||
SlowModTime: true,
|
SlowModTime: true,
|
||||||
}).Fill(ctx, f)
|
}).Fill(f)
|
||||||
|
|
||||||
if f.rootBucket != "" && f.rootDirectory != "" {
|
if f.rootBucket != "" && f.rootDirectory != "" {
|
||||||
// Check to see if the object exists
|
// Check to see if the object exists
|
||||||
@@ -428,7 +428,7 @@ func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options .
|
|||||||
return fsObj, fsObj.Update(ctx, in, src, options...)
|
return fsObj, fsObj.Update(ctx, in, src, options...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy src to this remote using server-side copy operations.
|
// Copy src to this remote using server side copy operations.
|
||||||
//
|
//
|
||||||
// This is stored with the remote path given
|
// This is stored with the remote path given
|
||||||
//
|
//
|
||||||
@@ -872,12 +872,11 @@ func (f *Fs) cleanUpBucket(ctx context.Context, bucket string) (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// maxLimit := int(listLimitSize)
|
maxLimit := int(listLimitSize)
|
||||||
var marker *string
|
var marker *string
|
||||||
for {
|
for {
|
||||||
req := qs.ListMultipartUploadsInput{
|
req := qs.ListMultipartUploadsInput{
|
||||||
// The default is 200 but this errors if more than 200 is put in so leave at the default
|
Limit: &maxLimit,
|
||||||
// Limit: &maxLimit,
|
|
||||||
KeyMarker: marker,
|
KeyMarker: marker,
|
||||||
}
|
}
|
||||||
var resp *qs.ListMultipartUploadsOutput
|
var resp *qs.ListMultipartUploadsOutput
|
||||||
@@ -928,7 +927,7 @@ func (f *Fs) CleanUp(ctx context.Context) (err error) {
|
|||||||
}
|
}
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
cleanErr := f.cleanUpBucket(ctx, f.opt.Enc.FromStandardName(entry.Remote()))
|
cleanErr := f.cleanUpBucket(ctx, f.opt.Enc.FromStandardName(entry.Remote()))
|
||||||
if cleanErr != nil {
|
if err != nil {
|
||||||
fs.Errorf(f, "Failed to cleanup bucket: %q", cleanErr)
|
fs.Errorf(f, "Failed to cleanup bucket: %q", cleanErr)
|
||||||
err = cleanErr
|
err = cleanErr
|
||||||
}
|
}
|
||||||
|
|||||||
383
backend/s3/s3.go
383
backend/s3/s3.go
@@ -5,7 +5,6 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"crypto/tls"
|
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
@@ -33,7 +32,7 @@ import (
|
|||||||
"github.com/aws/aws-sdk-go/aws/request"
|
"github.com/aws/aws-sdk-go/aws/request"
|
||||||
"github.com/aws/aws-sdk-go/aws/session"
|
"github.com/aws/aws-sdk-go/aws/session"
|
||||||
"github.com/aws/aws-sdk-go/service/s3"
|
"github.com/aws/aws-sdk-go/service/s3"
|
||||||
"github.com/ncw/swift/v2"
|
"github.com/ncw/swift"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/rclone/rclone/fs"
|
"github.com/rclone/rclone/fs"
|
||||||
"github.com/rclone/rclone/fs/config"
|
"github.com/rclone/rclone/fs/config"
|
||||||
@@ -59,7 +58,7 @@ import (
|
|||||||
func init() {
|
func init() {
|
||||||
fs.Register(&fs.RegInfo{
|
fs.Register(&fs.RegInfo{
|
||||||
Name: "s3",
|
Name: "s3",
|
||||||
Description: "Amazon S3 Compliant Storage Providers including AWS, Alibaba, Ceph, Digital Ocean, Dreamhost, IBM COS, Minio, and Tencent COS",
|
Description: "Amazon S3 Compliant Storage Provider (AWS, Alibaba, Ceph, Digital Ocean, Dreamhost, IBM COS, Minio, Tencent COS, etc)",
|
||||||
NewFs: NewFs,
|
NewFs: NewFs,
|
||||||
CommandHelp: commandHelp,
|
CommandHelp: commandHelp,
|
||||||
Options: []fs.Option{{
|
Options: []fs.Option{{
|
||||||
@@ -131,7 +130,7 @@ func init() {
|
|||||||
Provider: "AWS",
|
Provider: "AWS",
|
||||||
Examples: []fs.OptionExample{{
|
Examples: []fs.OptionExample{{
|
||||||
Value: "us-east-1",
|
Value: "us-east-1",
|
||||||
Help: "The default endpoint - a good choice if you are unsure.\nUS Region, Northern Virginia, or Pacific Northwest.\nLeave location constraint empty.",
|
Help: "The default endpoint - a good choice if you are unsure.\nUS Region, Northern Virginia or Pacific Northwest.\nLeave location constraint empty.",
|
||||||
}, {
|
}, {
|
||||||
Value: "us-east-2",
|
Value: "us-east-2",
|
||||||
Help: "US East (Ohio) Region\nNeeds location constraint us-east-2.",
|
Help: "US East (Ohio) Region\nNeeds location constraint us-east-2.",
|
||||||
@@ -225,7 +224,7 @@ func init() {
|
|||||||
Help: "Use this if unsure. Will use v4 signatures and an empty region.",
|
Help: "Use this if unsure. Will use v4 signatures and an empty region.",
|
||||||
}, {
|
}, {
|
||||||
Value: "other-v2-signature",
|
Value: "other-v2-signature",
|
||||||
Help: "Use this only if v4 signatures don't work, e.g. pre Jewel/v10 CEPH.",
|
Help: "Use this only if v4 signatures don't work, eg pre Jewel/v10 CEPH.",
|
||||||
}},
|
}},
|
||||||
}, {
|
}, {
|
||||||
Name: "endpoint",
|
Name: "endpoint",
|
||||||
@@ -612,7 +611,7 @@ func init() {
|
|||||||
Provider: "AWS",
|
Provider: "AWS",
|
||||||
Examples: []fs.OptionExample{{
|
Examples: []fs.OptionExample{{
|
||||||
Value: "",
|
Value: "",
|
||||||
Help: "Empty for US Region, Northern Virginia, or Pacific Northwest.",
|
Help: "Empty for US Region, Northern Virginia or Pacific Northwest.",
|
||||||
}, {
|
}, {
|
||||||
Value: "us-east-2",
|
Value: "us-east-2",
|
||||||
Help: "US East (Ohio) Region.",
|
Help: "US East (Ohio) Region.",
|
||||||
@@ -799,7 +798,7 @@ This ACL is used for creating objects and if bucket_acl isn't set, for creating
|
|||||||
|
|
||||||
For more info visit https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#canned-acl
|
For more info visit https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#canned-acl
|
||||||
|
|
||||||
Note that this ACL is applied when server-side copying objects as S3
|
Note that this ACL is applied when server side copying objects as S3
|
||||||
doesn't copy the ACL from the source but rather writes a fresh one.`,
|
doesn't copy the ACL from the source but rather writes a fresh one.`,
|
||||||
Examples: []fs.OptionExample{{
|
Examples: []fs.OptionExample{{
|
||||||
Value: "default",
|
Value: "default",
|
||||||
@@ -868,12 +867,6 @@ isn't set then "acl" is used instead.`,
|
|||||||
Value: "authenticated-read",
|
Value: "authenticated-read",
|
||||||
Help: "Owner gets FULL_CONTROL. The AuthenticatedUsers group gets READ access.",
|
Help: "Owner gets FULL_CONTROL. The AuthenticatedUsers group gets READ access.",
|
||||||
}},
|
}},
|
||||||
}, {
|
|
||||||
Name: "requester_pays",
|
|
||||||
Help: "Enables requester pays option when interacting with S3 bucket.",
|
|
||||||
Provider: "AWS",
|
|
||||||
Default: false,
|
|
||||||
Advanced: true,
|
|
||||||
}, {
|
}, {
|
||||||
Name: "server_side_encryption",
|
Name: "server_side_encryption",
|
||||||
Help: "The server-side encryption algorithm used when storing this object in S3.",
|
Help: "The server-side encryption algorithm used when storing this object in S3.",
|
||||||
@@ -922,10 +915,7 @@ isn't set then "acl" is used instead.`,
|
|||||||
}},
|
}},
|
||||||
}, {
|
}, {
|
||||||
Name: "sse_customer_key_md5",
|
Name: "sse_customer_key_md5",
|
||||||
Help: `If using SSE-C you may provide the secret encryption key MD5 checksum (optional).
|
Help: "If using SSE-C you must provide the secret encryption key MD5 checksum.",
|
||||||
|
|
||||||
If you leave it blank, this is calculated automatically from the sse_customer_key provided.
|
|
||||||
`,
|
|
||||||
Provider: "AWS,Ceph,Minio",
|
Provider: "AWS,Ceph,Minio",
|
||||||
Advanced: true,
|
Advanced: true,
|
||||||
Examples: []fs.OptionExample{{
|
Examples: []fs.OptionExample{{
|
||||||
@@ -1025,14 +1015,14 @@ The minimum is 0 and the maximum is 5GB.`,
|
|||||||
Help: `Chunk size to use for uploading.
|
Help: `Chunk size to use for uploading.
|
||||||
|
|
||||||
When uploading files larger than upload_cutoff or files with unknown
|
When uploading files larger than upload_cutoff or files with unknown
|
||||||
size (e.g. from "rclone rcat" or uploaded with "rclone mount" or google
|
size (eg from "rclone rcat" or uploaded with "rclone mount" or google
|
||||||
photos or google docs) they will be uploaded as multipart uploads
|
photos or google docs) they will be uploaded as multipart uploads
|
||||||
using this chunk size.
|
using this chunk size.
|
||||||
|
|
||||||
Note that "--s3-upload-concurrency" chunks of this size are buffered
|
Note that "--s3-upload-concurrency" chunks of this size are buffered
|
||||||
in memory per transfer.
|
in memory per transfer.
|
||||||
|
|
||||||
If you are transferring large files over high-speed links and you have
|
If you are transferring large files over high speed links and you have
|
||||||
enough memory, then increasing this will speed up the transfers.
|
enough memory, then increasing this will speed up the transfers.
|
||||||
|
|
||||||
Rclone will automatically increase the chunk size when uploading a
|
Rclone will automatically increase the chunk size when uploading a
|
||||||
@@ -1041,7 +1031,7 @@ large file of known size to stay below the 10,000 chunks limit.
|
|||||||
Files of unknown size are uploaded with the configured
|
Files of unknown size are uploaded with the configured
|
||||||
chunk_size. Since the default chunk size is 5MB and there can be at
|
chunk_size. Since the default chunk size is 5MB and there can be at
|
||||||
most 10,000 chunks, this means that by default the maximum size of
|
most 10,000 chunks, this means that by default the maximum size of
|
||||||
a file you can stream upload is 48GB. If you wish to stream upload
|
file you can stream upload is 48GB. If you wish to stream upload
|
||||||
larger files then you will need to increase chunk_size.`,
|
larger files then you will need to increase chunk_size.`,
|
||||||
Default: minChunkSize,
|
Default: minChunkSize,
|
||||||
Advanced: true,
|
Advanced: true,
|
||||||
@@ -1064,7 +1054,7 @@ large file of a known size to stay below this number of chunks limit.
|
|||||||
Name: "copy_cutoff",
|
Name: "copy_cutoff",
|
||||||
Help: `Cutoff for switching to multipart copy
|
Help: `Cutoff for switching to multipart copy
|
||||||
|
|
||||||
Any files larger than this that need to be server-side copied will be
|
Any files larger than this that need to be server side copied will be
|
||||||
copied in chunks of this size.
|
copied in chunks of this size.
|
||||||
|
|
||||||
The minimum is 0 and the maximum is 5GB.`,
|
The minimum is 0 and the maximum is 5GB.`,
|
||||||
@@ -1116,7 +1106,7 @@ If empty it will default to the environment variable "AWS_PROFILE" or
|
|||||||
This is the number of chunks of the same file that are uploaded
|
This is the number of chunks of the same file that are uploaded
|
||||||
concurrently.
|
concurrently.
|
||||||
|
|
||||||
If you are uploading small numbers of large files over high-speed links
|
If you are uploading small numbers of large file over high speed link
|
||||||
and these uploads do not fully utilize your bandwidth, then increasing
|
and these uploads do not fully utilize your bandwidth, then increasing
|
||||||
this may help to speed up the transfers.`,
|
this may help to speed up the transfers.`,
|
||||||
Default: 4,
|
Default: 4,
|
||||||
@@ -1130,7 +1120,7 @@ if false then rclone will use virtual path style. See [the AWS S3
|
|||||||
docs](https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingBucket.html#access-bucket-intro)
|
docs](https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingBucket.html#access-bucket-intro)
|
||||||
for more info.
|
for more info.
|
||||||
|
|
||||||
Some providers (e.g. AWS, Aliyun OSS, Netease COS, or Tencent COS) require this set to
|
Some providers (eg AWS, Aliyun OSS, Netease COS or Tencent COS) require this set to
|
||||||
false - rclone will do this automatically based on the provider
|
false - rclone will do this automatically based on the provider
|
||||||
setting.`,
|
setting.`,
|
||||||
Default: true,
|
Default: true,
|
||||||
@@ -1142,7 +1132,7 @@ setting.`,
|
|||||||
If this is false (the default) then rclone will use v4 authentication.
|
If this is false (the default) then rclone will use v4 authentication.
|
||||||
If it is set then rclone will use v2 authentication.
|
If it is set then rclone will use v2 authentication.
|
||||||
|
|
||||||
Use this only if v4 signatures don't work, e.g. pre Jewel/v10 CEPH.`,
|
Use this only if v4 signatures don't work, eg pre Jewel/v10 CEPH.`,
|
||||||
Default: false,
|
Default: false,
|
||||||
Advanced: true,
|
Advanced: true,
|
||||||
}, {
|
}, {
|
||||||
@@ -1177,47 +1167,10 @@ In Ceph, this can be increased with the "rgw list buckets max chunk" option.
|
|||||||
Advanced: true,
|
Advanced: true,
|
||||||
}, {
|
}, {
|
||||||
Name: "no_check_bucket",
|
Name: "no_check_bucket",
|
||||||
Help: `If set, don't attempt to check the bucket exists or create it
|
Help: `If set don't attempt to check the bucket exists or create it
|
||||||
|
|
||||||
This can be useful when trying to minimise the number of transactions
|
This can be useful when trying to minimise the number of transactions
|
||||||
rclone does if you know the bucket exists already.
|
rclone does if you know the bucket exists already.
|
||||||
|
|
||||||
It can also be needed if the user you are using does not have bucket
|
|
||||||
creation permissions. Before v1.52.0 this would have passed silently
|
|
||||||
due to a bug.
|
|
||||||
`,
|
|
||||||
Default: false,
|
|
||||||
Advanced: true,
|
|
||||||
}, {
|
|
||||||
Name: "no_head",
|
|
||||||
Help: `If set, don't HEAD uploaded objects to check integrity
|
|
||||||
|
|
||||||
This can be useful when trying to minimise the number of transactions
|
|
||||||
rclone does.
|
|
||||||
|
|
||||||
Setting it means that if rclone receives a 200 OK message after
|
|
||||||
uploading an object with PUT then it will assume that it got uploaded
|
|
||||||
properly.
|
|
||||||
|
|
||||||
In particular it will assume:
|
|
||||||
|
|
||||||
- the metadata, including modtime, storage class and content type was as uploaded
|
|
||||||
- the size was as uploaded
|
|
||||||
|
|
||||||
It reads the following items from the response for a single part PUT:
|
|
||||||
|
|
||||||
- the MD5SUM
|
|
||||||
- The uploaded date
|
|
||||||
|
|
||||||
For multipart uploads these items aren't read.
|
|
||||||
|
|
||||||
If an source object of unknown length is uploaded then rclone **will** do a
|
|
||||||
HEAD request.
|
|
||||||
|
|
||||||
Setting this flag increases the chance for undetected upload failures,
|
|
||||||
in particular an incorrect size, so it isn't recommended for normal
|
|
||||||
operation. In practice the chance of an undetected upload failure is
|
|
||||||
very small even with this flag.
|
|
||||||
`,
|
`,
|
||||||
Default: false,
|
Default: false,
|
||||||
Advanced: true,
|
Advanced: true,
|
||||||
@@ -1250,26 +1203,13 @@ This option controls how often unused buffers will be removed from the pool.`,
|
|||||||
Default: memoryPoolUseMmap,
|
Default: memoryPoolUseMmap,
|
||||||
Advanced: true,
|
Advanced: true,
|
||||||
Help: `Whether to use mmap buffers in internal memory pool.`,
|
Help: `Whether to use mmap buffers in internal memory pool.`,
|
||||||
}, {
|
|
||||||
Name: "disable_http2",
|
|
||||||
Default: false,
|
|
||||||
Advanced: true,
|
|
||||||
Help: `Disable usage of http2 for S3 backends
|
|
||||||
|
|
||||||
There is currently an unsolved issue with the s3 (specifically minio) backend
|
|
||||||
and HTTP/2. HTTP/2 is enabled by default for the s3 backend but can be
|
|
||||||
disabled here. When the issue is solved this flag will be removed.
|
|
||||||
|
|
||||||
See: https://github.com/rclone/rclone/issues/4673, https://github.com/rclone/rclone/issues/3631
|
|
||||||
|
|
||||||
`,
|
|
||||||
},
|
},
|
||||||
}})
|
}})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
const (
|
const (
|
||||||
metaMtime = "Mtime" // the meta key to store mtime in - e.g. X-Amz-Meta-Mtime
|
metaMtime = "Mtime" // the meta key to store mtime in - eg X-Amz-Meta-Mtime
|
||||||
metaMD5Hash = "Md5chksum" // the meta key to store md5hash in
|
metaMD5Hash = "Md5chksum" // the meta key to store md5hash in
|
||||||
// The maximum size of object we can COPY - this should be 5GiB but is < 5GB for b2 compatibility
|
// The maximum size of object we can COPY - this should be 5GiB but is < 5GB for b2 compatibility
|
||||||
// See https://forum.rclone.org/t/copying-files-within-a-b2-bucket/16680/76
|
// See https://forum.rclone.org/t/copying-files-within-a-b2-bucket/16680/76
|
||||||
@@ -1296,7 +1236,6 @@ type Options struct {
|
|||||||
LocationConstraint string `config:"location_constraint"`
|
LocationConstraint string `config:"location_constraint"`
|
||||||
ACL string `config:"acl"`
|
ACL string `config:"acl"`
|
||||||
BucketACL string `config:"bucket_acl"`
|
BucketACL string `config:"bucket_acl"`
|
||||||
RequesterPays bool `config:"requester_pays"`
|
|
||||||
ServerSideEncryption string `config:"server_side_encryption"`
|
ServerSideEncryption string `config:"server_side_encryption"`
|
||||||
SSEKMSKeyID string `config:"sse_kms_key_id"`
|
SSEKMSKeyID string `config:"sse_kms_key_id"`
|
||||||
SSECustomerAlgorithm string `config:"sse_customer_algorithm"`
|
SSECustomerAlgorithm string `config:"sse_customer_algorithm"`
|
||||||
@@ -1318,11 +1257,9 @@ type Options struct {
|
|||||||
LeavePartsOnError bool `config:"leave_parts_on_error"`
|
LeavePartsOnError bool `config:"leave_parts_on_error"`
|
||||||
ListChunk int64 `config:"list_chunk"`
|
ListChunk int64 `config:"list_chunk"`
|
||||||
NoCheckBucket bool `config:"no_check_bucket"`
|
NoCheckBucket bool `config:"no_check_bucket"`
|
||||||
NoHead bool `config:"no_head"`
|
|
||||||
Enc encoder.MultiEncoder `config:"encoding"`
|
Enc encoder.MultiEncoder `config:"encoding"`
|
||||||
MemoryPoolFlushTime fs.Duration `config:"memory_pool_flush_time"`
|
MemoryPoolFlushTime fs.Duration `config:"memory_pool_flush_time"`
|
||||||
MemoryPoolUseMmap bool `config:"memory_pool_use_mmap"`
|
MemoryPoolUseMmap bool `config:"memory_pool_use_mmap"`
|
||||||
DisableHTTP2 bool `config:"disable_http2"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fs represents a remote s3 server
|
// Fs represents a remote s3 server
|
||||||
@@ -1330,8 +1267,6 @@ type Fs struct {
|
|||||||
name string // the name of the remote
|
name string // the name of the remote
|
||||||
root string // root of the bucket - ignore all objects above this
|
root string // root of the bucket - ignore all objects above this
|
||||||
opt Options // parsed options
|
opt Options // parsed options
|
||||||
ci *fs.ConfigInfo // global config
|
|
||||||
ctx context.Context // global context for reading config
|
|
||||||
features *fs.Features // optional features
|
features *fs.Features // optional features
|
||||||
c *s3.S3 // the connection to the s3 server
|
c *s3.S3 // the connection to the s3 server
|
||||||
ses *session.Session // the s3 session
|
ses *session.Session // the s3 session
|
||||||
@@ -1341,7 +1276,6 @@ type Fs struct {
|
|||||||
pacer *fs.Pacer // To pace the API calls
|
pacer *fs.Pacer // To pace the API calls
|
||||||
srv *http.Client // a plain http client
|
srv *http.Client // a plain http client
|
||||||
pool *pool.Pool // memory pool
|
pool *pool.Pool // memory pool
|
||||||
etagIsNotMD5 bool // if set ETags are not MD5s
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Object describes a s3 object
|
// Object describes a s3 object
|
||||||
@@ -1352,12 +1286,12 @@ type Object struct {
|
|||||||
// that in you need to call readMetaData
|
// that in you need to call readMetaData
|
||||||
fs *Fs // what this object is part of
|
fs *Fs // what this object is part of
|
||||||
remote string // The remote path
|
remote string // The remote path
|
||||||
md5 string // md5sum of the object
|
etag string // md5sum of the object
|
||||||
bytes int64 // size of the object
|
bytes int64 // size of the object
|
||||||
lastModified time.Time // Last modified
|
lastModified time.Time // Last modified
|
||||||
meta map[string]*string // The object metadata if known - may be nil
|
meta map[string]*string // The object metadata if known - may be nil
|
||||||
mimeType string // MimeType of object - may be ""
|
mimeType string // MimeType of object - may be ""
|
||||||
storageClass string // e.g. GLACIER
|
storageClass string // eg GLACIER
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------
|
// ------------------------------------------------------------
|
||||||
@@ -1391,7 +1325,6 @@ func (f *Fs) Features() *fs.Features {
|
|||||||
// retryErrorCodes is a slice of error codes that we will retry
|
// retryErrorCodes is a slice of error codes that we will retry
|
||||||
// See: https://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html
|
// See: https://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html
|
||||||
var retryErrorCodes = []int{
|
var retryErrorCodes = []int{
|
||||||
429, // Too Many Requests
|
|
||||||
500, // Internal Server Error - "We encountered an internal error. Please try again."
|
500, // Internal Server Error - "We encountered an internal error. Please try again."
|
||||||
503, // Service Unavailable/Slow Down - "Reduce your request rate"
|
503, // Service Unavailable/Slow Down - "Reduce your request rate"
|
||||||
}
|
}
|
||||||
@@ -1399,10 +1332,7 @@ var retryErrorCodes = []int{
|
|||||||
//S3 is pretty resilient, and the built in retry handling is probably sufficient
|
//S3 is pretty resilient, and the built in retry handling is probably sufficient
|
||||||
// as it should notice closed connections and timeouts which are the most likely
|
// as it should notice closed connections and timeouts which are the most likely
|
||||||
// sort of failure modes
|
// sort of failure modes
|
||||||
func (f *Fs) shouldRetry(ctx context.Context, err error) (bool, error) {
|
func (f *Fs) shouldRetry(err error) (bool, error) {
|
||||||
if fserrors.ContextError(ctx, &err) {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
// If this is an awserr object, try and extract more useful information to determine if we should retry
|
// If this is an awserr object, try and extract more useful information to determine if we should retry
|
||||||
if awsError, ok := err.(awserr.Error); ok {
|
if awsError, ok := err.(awserr.Error); ok {
|
||||||
// Simple case, check the original embedded error in case it's generically retryable
|
// Simple case, check the original embedded error in case it's generically retryable
|
||||||
@@ -1414,7 +1344,7 @@ func (f *Fs) shouldRetry(ctx context.Context, err error) (bool, error) {
|
|||||||
// 301 if wrong region for bucket - can only update if running from a bucket
|
// 301 if wrong region for bucket - can only update if running from a bucket
|
||||||
if f.rootBucket != "" {
|
if f.rootBucket != "" {
|
||||||
if reqErr.StatusCode() == http.StatusMovedPermanently {
|
if reqErr.StatusCode() == http.StatusMovedPermanently {
|
||||||
urfbErr := f.updateRegionForBucket(ctx, f.rootBucket)
|
urfbErr := f.updateRegionForBucket(f.rootBucket)
|
||||||
if urfbErr != nil {
|
if urfbErr != nil {
|
||||||
fs.Errorf(f, "Failed to update region for bucket: %v", urfbErr)
|
fs.Errorf(f, "Failed to update region for bucket: %v", urfbErr)
|
||||||
return false, err
|
return false, err
|
||||||
@@ -1451,21 +1381,8 @@ func (o *Object) split() (bucket, bucketPath string) {
|
|||||||
return o.fs.split(o.remote)
|
return o.fs.split(o.remote)
|
||||||
}
|
}
|
||||||
|
|
||||||
// getClient makes an http client according to the options
|
|
||||||
func getClient(ctx context.Context, opt *Options) *http.Client {
|
|
||||||
// TODO: Do we need cookies too?
|
|
||||||
t := fshttp.NewTransportCustom(ctx, func(t *http.Transport) {
|
|
||||||
if opt.DisableHTTP2 {
|
|
||||||
t.TLSNextProto = map[string]func(string, *tls.Conn) http.RoundTripper{}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return &http.Client{
|
|
||||||
Transport: t,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// s3Connection makes a connection to s3
|
// s3Connection makes a connection to s3
|
||||||
func s3Connection(ctx context.Context, opt *Options, client *http.Client) (*s3.S3, *session.Session, error) {
|
func s3Connection(opt *Options) (*s3.S3, *session.Session, error) {
|
||||||
// Make the auth
|
// Make the auth
|
||||||
v := credentials.Value{
|
v := credentials.Value{
|
||||||
AccessKeyID: opt.AccessKeyID,
|
AccessKeyID: opt.AccessKeyID,
|
||||||
@@ -1474,7 +1391,6 @@ func s3Connection(ctx context.Context, opt *Options, client *http.Client) (*s3.S
|
|||||||
}
|
}
|
||||||
|
|
||||||
lowTimeoutClient := &http.Client{Timeout: 1 * time.Second} // low timeout to ec2 metadata service
|
lowTimeoutClient := &http.Client{Timeout: 1 * time.Second} // low timeout to ec2 metadata service
|
||||||
|
|
||||||
def := defaults.Get()
|
def := defaults.Get()
|
||||||
def.Config.HTTPClient = lowTimeoutClient
|
def.Config.HTTPClient = lowTimeoutClient
|
||||||
|
|
||||||
@@ -1543,7 +1459,7 @@ func s3Connection(ctx context.Context, opt *Options, client *http.Client) (*s3.S
|
|||||||
awsConfig := aws.NewConfig().
|
awsConfig := aws.NewConfig().
|
||||||
WithMaxRetries(0). // Rely on rclone's retry logic
|
WithMaxRetries(0). // Rely on rclone's retry logic
|
||||||
WithCredentials(cred).
|
WithCredentials(cred).
|
||||||
WithHTTPClient(client).
|
WithHTTPClient(fshttp.NewClient(fs.Config)).
|
||||||
WithS3ForcePathStyle(opt.ForcePathStyle).
|
WithS3ForcePathStyle(opt.ForcePathStyle).
|
||||||
WithS3UseAccelerate(opt.UseAccelerateEndpoint).
|
WithS3UseAccelerate(opt.UseAccelerateEndpoint).
|
||||||
WithS3UsEast1RegionalEndpoint(endpoints.RegionalS3UsEast1Endpoint)
|
WithS3UsEast1RegionalEndpoint(endpoints.RegionalS3UsEast1Endpoint)
|
||||||
@@ -1562,8 +1478,9 @@ func s3Connection(ctx context.Context, opt *Options, client *http.Client) (*s3.S
|
|||||||
if opt.EnvAuth && opt.AccessKeyID == "" && opt.SecretAccessKey == "" {
|
if opt.EnvAuth && opt.AccessKeyID == "" && opt.SecretAccessKey == "" {
|
||||||
// Enable loading config options from ~/.aws/config (selected by AWS_PROFILE env)
|
// Enable loading config options from ~/.aws/config (selected by AWS_PROFILE env)
|
||||||
awsSessionOpts.SharedConfigState = session.SharedConfigEnable
|
awsSessionOpts.SharedConfigState = session.SharedConfigEnable
|
||||||
// Set the name of the profile if supplied
|
// The session constructor (aws/session/mergeConfigSrcs) will only use the user's preferred credential source
|
||||||
awsSessionOpts.Profile = opt.Profile
|
// (from the shared config file) if the passed-in Options.Config.Credentials is nil.
|
||||||
|
awsSessionOpts.Config.Credentials = nil
|
||||||
}
|
}
|
||||||
ses, err := session.NewSessionWithOptions(awsSessionOpts)
|
ses, err := session.NewSessionWithOptions(awsSessionOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1623,7 +1540,7 @@ func (f *Fs) setRoot(root string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewFs constructs an Fs from the path, bucket:path
|
// NewFs constructs an Fs from the path, bucket:path
|
||||||
func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, error) {
|
func NewFs(name, root string, m configmap.Mapper) (fs.Fs, error) {
|
||||||
// Parse config into Options struct
|
// Parse config into Options struct
|
||||||
opt := new(Options)
|
opt := new(Options)
|
||||||
err := configstruct.Set(m, opt)
|
err := configstruct.Set(m, opt)
|
||||||
@@ -1644,45 +1561,27 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
if opt.BucketACL == "" {
|
if opt.BucketACL == "" {
|
||||||
opt.BucketACL = opt.ACL
|
opt.BucketACL = opt.ACL
|
||||||
}
|
}
|
||||||
if opt.SSECustomerKey != "" && opt.SSECustomerKeyMD5 == "" {
|
c, ses, err := s3Connection(opt)
|
||||||
// calculate CustomerKeyMD5 if not supplied
|
|
||||||
md5sumBinary := md5.Sum([]byte(opt.SSECustomerKey))
|
|
||||||
opt.SSECustomerKeyMD5 = base64.StdEncoding.EncodeToString(md5sumBinary[:])
|
|
||||||
}
|
|
||||||
srv := getClient(ctx, opt)
|
|
||||||
c, ses, err := s3Connection(ctx, opt, srv)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
ci := fs.GetConfig(ctx)
|
|
||||||
f := &Fs{
|
f := &Fs{
|
||||||
name: name,
|
name: name,
|
||||||
opt: *opt,
|
opt: *opt,
|
||||||
ci: ci,
|
|
||||||
ctx: ctx,
|
|
||||||
c: c,
|
c: c,
|
||||||
ses: ses,
|
ses: ses,
|
||||||
pacer: fs.NewPacer(ctx, pacer.NewS3(pacer.MinSleep(minSleep))),
|
pacer: fs.NewPacer(pacer.NewS3(pacer.MinSleep(minSleep))),
|
||||||
cache: bucket.NewCache(),
|
cache: bucket.NewCache(),
|
||||||
srv: srv,
|
srv: fshttp.NewClient(fs.Config),
|
||||||
pool: pool.New(
|
pool: pool.New(
|
||||||
time.Duration(opt.MemoryPoolFlushTime),
|
time.Duration(opt.MemoryPoolFlushTime),
|
||||||
int(opt.ChunkSize),
|
int(opt.ChunkSize),
|
||||||
opt.UploadConcurrency*ci.Transfers,
|
opt.UploadConcurrency*fs.Config.Transfers,
|
||||||
opt.MemoryPoolUseMmap,
|
opt.MemoryPoolUseMmap,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
if opt.ServerSideEncryption == "aws:kms" || opt.SSECustomerAlgorithm != "" {
|
|
||||||
// From: https://docs.aws.amazon.com/AmazonS3/latest/API/RESTCommonResponseHeaders.html
|
|
||||||
//
|
|
||||||
// Objects encrypted by SSE-S3 or plaintext have ETags that are an MD5
|
|
||||||
// digest of their data.
|
|
||||||
//
|
|
||||||
// Objects encrypted by SSE-C or SSE-KMS have ETags that are not an
|
|
||||||
// MD5 digest of their object data.
|
|
||||||
f.etagIsNotMD5 = true
|
|
||||||
}
|
|
||||||
f.setRoot(root)
|
f.setRoot(root)
|
||||||
f.features = (&fs.Features{
|
f.features = (&fs.Features{
|
||||||
ReadMimeType: true,
|
ReadMimeType: true,
|
||||||
@@ -1692,21 +1591,28 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
SetTier: true,
|
SetTier: true,
|
||||||
GetTier: true,
|
GetTier: true,
|
||||||
SlowModTime: true,
|
SlowModTime: true,
|
||||||
}).Fill(ctx, f)
|
}).Fill(f)
|
||||||
if f.rootBucket != "" && f.rootDirectory != "" {
|
if f.rootBucket != "" && f.rootDirectory != "" {
|
||||||
// Check to see if the (bucket,directory) is actually an existing file
|
// Check to see if the object exists
|
||||||
oldRoot := f.root
|
encodedDirectory := f.opt.Enc.FromStandardPath(f.rootDirectory)
|
||||||
newRoot, leaf := path.Split(oldRoot)
|
req := s3.HeadObjectInput{
|
||||||
f.setRoot(newRoot)
|
Bucket: &f.rootBucket,
|
||||||
_, err := f.NewObject(ctx, leaf)
|
Key: &encodedDirectory,
|
||||||
if err != nil {
|
|
||||||
// File doesn't exist or is a directory so return old f
|
|
||||||
f.setRoot(oldRoot)
|
|
||||||
return f, nil
|
|
||||||
}
|
}
|
||||||
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
|
_, err = f.c.HeadObject(&req)
|
||||||
|
return f.shouldRetry(err)
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
newRoot := path.Dir(f.root)
|
||||||
|
if newRoot == "." {
|
||||||
|
newRoot = ""
|
||||||
|
}
|
||||||
|
f.setRoot(newRoot)
|
||||||
// return an error with an fs which points to the parent
|
// return an error with an fs which points to the parent
|
||||||
return f, fs.ErrorIsFile
|
return f, fs.ErrorIsFile
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// f.listMultipartUploads()
|
// f.listMultipartUploads()
|
||||||
return f, nil
|
return f, nil
|
||||||
}
|
}
|
||||||
@@ -1727,7 +1633,7 @@ func (f *Fs) newObjectWithInfo(ctx context.Context, remote string, info *s3.Obje
|
|||||||
} else {
|
} else {
|
||||||
o.lastModified = *info.LastModified
|
o.lastModified = *info.LastModified
|
||||||
}
|
}
|
||||||
o.setMD5FromEtag(aws.StringValue(info.ETag))
|
o.etag = aws.StringValue(info.ETag)
|
||||||
o.bytes = aws.Int64Value(info.Size)
|
o.bytes = aws.Int64Value(info.Size)
|
||||||
o.storageClass = aws.StringValue(info.StorageClass)
|
o.storageClass = aws.StringValue(info.StorageClass)
|
||||||
} else {
|
} else {
|
||||||
@@ -1746,7 +1652,7 @@ func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Gets the bucket location
|
// Gets the bucket location
|
||||||
func (f *Fs) getBucketLocation(ctx context.Context, bucket string) (string, error) {
|
func (f *Fs) getBucketLocation(bucket string) (string, error) {
|
||||||
req := s3.GetBucketLocationInput{
|
req := s3.GetBucketLocationInput{
|
||||||
Bucket: &bucket,
|
Bucket: &bucket,
|
||||||
}
|
}
|
||||||
@@ -1754,7 +1660,7 @@ func (f *Fs) getBucketLocation(ctx context.Context, bucket string) (string, erro
|
|||||||
var err error
|
var err error
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.c.GetBucketLocation(&req)
|
resp, err = f.c.GetBucketLocation(&req)
|
||||||
return f.shouldRetry(ctx, err)
|
return f.shouldRetry(err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -1764,8 +1670,8 @@ func (f *Fs) getBucketLocation(ctx context.Context, bucket string) (string, erro
|
|||||||
|
|
||||||
// Updates the region for the bucket by reading the region from the
|
// Updates the region for the bucket by reading the region from the
|
||||||
// bucket then updating the session.
|
// bucket then updating the session.
|
||||||
func (f *Fs) updateRegionForBucket(ctx context.Context, bucket string) error {
|
func (f *Fs) updateRegionForBucket(bucket string) error {
|
||||||
region, err := f.getBucketLocation(ctx, bucket)
|
region, err := f.getBucketLocation(bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "reading bucket location failed")
|
return errors.Wrap(err, "reading bucket location failed")
|
||||||
}
|
}
|
||||||
@@ -1779,7 +1685,7 @@ func (f *Fs) updateRegionForBucket(ctx context.Context, bucket string) error {
|
|||||||
// Make a new session with the new region
|
// Make a new session with the new region
|
||||||
oldRegion := f.opt.Region
|
oldRegion := f.opt.Region
|
||||||
f.opt.Region = region
|
f.opt.Region = region
|
||||||
c, ses, err := s3Connection(f.ctx, &f.opt, f.srv)
|
c, ses, err := s3Connection(&f.opt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "creating new session failed")
|
return errors.Wrap(err, "creating new session failed")
|
||||||
}
|
}
|
||||||
@@ -1839,9 +1745,6 @@ func (f *Fs) list(ctx context.Context, bucket, directory, prefix string, addBuck
|
|||||||
if urlEncodeListings {
|
if urlEncodeListings {
|
||||||
req.EncodingType = aws.String(s3.EncodingTypeUrl)
|
req.EncodingType = aws.String(s3.EncodingTypeUrl)
|
||||||
}
|
}
|
||||||
if f.opt.RequesterPays {
|
|
||||||
req.RequestPayer = aws.String(s3.RequestPayerRequester)
|
|
||||||
}
|
|
||||||
var resp *s3.ListObjectsOutput
|
var resp *s3.ListObjectsOutput
|
||||||
var err error
|
var err error
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
@@ -1859,7 +1762,7 @@ func (f *Fs) list(ctx context.Context, bucket, directory, prefix string, addBuck
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return f.shouldRetry(ctx, err)
|
return f.shouldRetry(err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if awsErr, ok := err.(awserr.RequestFailure); ok {
|
if awsErr, ok := err.(awserr.RequestFailure); ok {
|
||||||
@@ -2006,7 +1909,7 @@ func (f *Fs) listBuckets(ctx context.Context) (entries fs.DirEntries, err error)
|
|||||||
var resp *s3.ListBucketsOutput
|
var resp *s3.ListBucketsOutput
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.c.ListBucketsWithContext(ctx, &req)
|
resp, err = f.c.ListBucketsWithContext(ctx, &req)
|
||||||
return f.shouldRetry(ctx, err)
|
return f.shouldRetry(err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -2121,7 +2024,7 @@ func (f *Fs) bucketExists(ctx context.Context, bucket string) (bool, error) {
|
|||||||
}
|
}
|
||||||
err := f.pacer.Call(func() (bool, error) {
|
err := f.pacer.Call(func() (bool, error) {
|
||||||
_, err := f.c.HeadBucketWithContext(ctx, &req)
|
_, err := f.c.HeadBucketWithContext(ctx, &req)
|
||||||
return f.shouldRetry(ctx, err)
|
return f.shouldRetry(err)
|
||||||
})
|
})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return true, nil
|
return true, nil
|
||||||
@@ -2157,7 +2060,7 @@ func (f *Fs) makeBucket(ctx context.Context, bucket string) error {
|
|||||||
}
|
}
|
||||||
err := f.pacer.Call(func() (bool, error) {
|
err := f.pacer.Call(func() (bool, error) {
|
||||||
_, err := f.c.CreateBucketWithContext(ctx, &req)
|
_, err := f.c.CreateBucketWithContext(ctx, &req)
|
||||||
return f.shouldRetry(ctx, err)
|
return f.shouldRetry(err)
|
||||||
})
|
})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
fs.Infof(f, "Bucket %q created with ACL %q", bucket, f.opt.BucketACL)
|
fs.Infof(f, "Bucket %q created with ACL %q", bucket, f.opt.BucketACL)
|
||||||
@@ -2187,7 +2090,7 @@ func (f *Fs) Rmdir(ctx context.Context, dir string) error {
|
|||||||
}
|
}
|
||||||
err := f.pacer.Call(func() (bool, error) {
|
err := f.pacer.Call(func() (bool, error) {
|
||||||
_, err := f.c.DeleteBucketWithContext(ctx, &req)
|
_, err := f.c.DeleteBucketWithContext(ctx, &req)
|
||||||
return f.shouldRetry(ctx, err)
|
return f.shouldRetry(err)
|
||||||
})
|
})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
fs.Infof(f, "Bucket %q deleted", bucket)
|
fs.Infof(f, "Bucket %q deleted", bucket)
|
||||||
@@ -2207,7 +2110,7 @@ func pathEscape(s string) string {
|
|||||||
return strings.Replace(rest.URLPathEscape(s), "+", "%2B", -1)
|
return strings.Replace(rest.URLPathEscape(s), "+", "%2B", -1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// copy does a server-side copy
|
// copy does a server side copy
|
||||||
//
|
//
|
||||||
// It adds the boiler plate to the req passed in and calls the s3
|
// It adds the boiler plate to the req passed in and calls the s3
|
||||||
// method
|
// method
|
||||||
@@ -2217,24 +2120,9 @@ func (f *Fs) copy(ctx context.Context, req *s3.CopyObjectInput, dstBucket, dstPa
|
|||||||
req.Key = &dstPath
|
req.Key = &dstPath
|
||||||
source := pathEscape(path.Join(srcBucket, srcPath))
|
source := pathEscape(path.Join(srcBucket, srcPath))
|
||||||
req.CopySource = &source
|
req.CopySource = &source
|
||||||
if f.opt.RequesterPays {
|
|
||||||
req.RequestPayer = aws.String(s3.RequestPayerRequester)
|
|
||||||
}
|
|
||||||
if f.opt.ServerSideEncryption != "" {
|
if f.opt.ServerSideEncryption != "" {
|
||||||
req.ServerSideEncryption = &f.opt.ServerSideEncryption
|
req.ServerSideEncryption = &f.opt.ServerSideEncryption
|
||||||
}
|
}
|
||||||
if f.opt.SSECustomerAlgorithm != "" {
|
|
||||||
req.SSECustomerAlgorithm = &f.opt.SSECustomerAlgorithm
|
|
||||||
req.CopySourceSSECustomerAlgorithm = &f.opt.SSECustomerAlgorithm
|
|
||||||
}
|
|
||||||
if f.opt.SSECustomerKey != "" {
|
|
||||||
req.SSECustomerKey = &f.opt.SSECustomerKey
|
|
||||||
req.CopySourceSSECustomerKey = &f.opt.SSECustomerKey
|
|
||||||
}
|
|
||||||
if f.opt.SSECustomerKeyMD5 != "" {
|
|
||||||
req.SSECustomerKeyMD5 = &f.opt.SSECustomerKeyMD5
|
|
||||||
req.CopySourceSSECustomerKeyMD5 = &f.opt.SSECustomerKeyMD5
|
|
||||||
}
|
|
||||||
if f.opt.SSEKMSKeyID != "" {
|
if f.opt.SSEKMSKeyID != "" {
|
||||||
req.SSEKMSKeyId = &f.opt.SSEKMSKeyID
|
req.SSEKMSKeyId = &f.opt.SSEKMSKeyID
|
||||||
}
|
}
|
||||||
@@ -2247,7 +2135,7 @@ func (f *Fs) copy(ctx context.Context, req *s3.CopyObjectInput, dstBucket, dstPa
|
|||||||
}
|
}
|
||||||
return f.pacer.Call(func() (bool, error) {
|
return f.pacer.Call(func() (bool, error) {
|
||||||
_, err := f.c.CopyObjectWithContext(ctx, req)
|
_, err := f.c.CopyObjectWithContext(ctx, req)
|
||||||
return f.shouldRetry(ctx, err)
|
return f.shouldRetry(err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2291,7 +2179,7 @@ func (f *Fs) copyMultipart(ctx context.Context, copyReq *s3.CopyObjectInput, dst
|
|||||||
if err := f.pacer.Call(func() (bool, error) {
|
if err := f.pacer.Call(func() (bool, error) {
|
||||||
var err error
|
var err error
|
||||||
cout, err = f.c.CreateMultipartUploadWithContext(ctx, req)
|
cout, err = f.c.CreateMultipartUploadWithContext(ctx, req)
|
||||||
return f.shouldRetry(ctx, err)
|
return f.shouldRetry(err)
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -2307,7 +2195,7 @@ func (f *Fs) copyMultipart(ctx context.Context, copyReq *s3.CopyObjectInput, dst
|
|||||||
UploadId: uid,
|
UploadId: uid,
|
||||||
RequestPayer: req.RequestPayer,
|
RequestPayer: req.RequestPayer,
|
||||||
})
|
})
|
||||||
return f.shouldRetry(ctx, err)
|
return f.shouldRetry(err)
|
||||||
})
|
})
|
||||||
})()
|
})()
|
||||||
|
|
||||||
@@ -2330,7 +2218,7 @@ func (f *Fs) copyMultipart(ctx context.Context, copyReq *s3.CopyObjectInput, dst
|
|||||||
uploadPartReq.CopySourceRange = aws.String(calculateRange(partSize, partNum-1, numParts, srcSize))
|
uploadPartReq.CopySourceRange = aws.String(calculateRange(partSize, partNum-1, numParts, srcSize))
|
||||||
uout, err := f.c.UploadPartCopyWithContext(ctx, uploadPartReq)
|
uout, err := f.c.UploadPartCopyWithContext(ctx, uploadPartReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return f.shouldRetry(ctx, err)
|
return f.shouldRetry(err)
|
||||||
}
|
}
|
||||||
parts = append(parts, &s3.CompletedPart{
|
parts = append(parts, &s3.CompletedPart{
|
||||||
PartNumber: &partNum,
|
PartNumber: &partNum,
|
||||||
@@ -2352,11 +2240,11 @@ func (f *Fs) copyMultipart(ctx context.Context, copyReq *s3.CopyObjectInput, dst
|
|||||||
RequestPayer: req.RequestPayer,
|
RequestPayer: req.RequestPayer,
|
||||||
UploadId: uid,
|
UploadId: uid,
|
||||||
})
|
})
|
||||||
return f.shouldRetry(ctx, err)
|
return f.shouldRetry(err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy src to this remote using server-side copy operations.
|
// Copy src to this remote using server side copy operations.
|
||||||
//
|
//
|
||||||
// This is stored with the remote path given
|
// This is stored with the remote path given
|
||||||
//
|
//
|
||||||
@@ -2400,7 +2288,7 @@ func (f *Fs) getMemoryPool(size int64) *pool.Pool {
|
|||||||
return pool.New(
|
return pool.New(
|
||||||
time.Duration(f.opt.MemoryPoolFlushTime),
|
time.Duration(f.opt.MemoryPoolFlushTime),
|
||||||
int(size),
|
int(size),
|
||||||
f.opt.UploadConcurrency*f.ci.Transfers,
|
f.opt.UploadConcurrency*fs.Config.Transfers,
|
||||||
f.opt.MemoryPoolUseMmap,
|
f.opt.MemoryPoolUseMmap,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -2447,7 +2335,7 @@ All the objects shown will be marked for restore, then
|
|||||||
rclone backend restore --include "*.txt" s3:bucket/path -o priority=Standard
|
rclone backend restore --include "*.txt" s3:bucket/path -o priority=Standard
|
||||||
|
|
||||||
It returns a list of status dictionaries with Remote and Status
|
It returns a list of status dictionaries with Remote and Status
|
||||||
keys. The Status will be OK if it was successful or an error message
|
keys. The Status will be OK if it was successfull or an error message
|
||||||
if not.
|
if not.
|
||||||
|
|
||||||
[
|
[
|
||||||
@@ -2583,7 +2471,7 @@ func (f *Fs) Command(ctx context.Context, name string, arg []string, opt map[str
|
|||||||
reqCopy.Key = &bucketPath
|
reqCopy.Key = &bucketPath
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
_, err = f.c.RestoreObject(&reqCopy)
|
_, err = f.c.RestoreObject(&reqCopy)
|
||||||
return f.shouldRetry(ctx, err)
|
return f.shouldRetry(err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
st.Status = err.Error()
|
st.Status = err.Error()
|
||||||
@@ -2612,7 +2500,7 @@ func (f *Fs) Command(ctx context.Context, name string, arg []string, opt map[str
|
|||||||
// listMultipartUploads lists all outstanding multipart uploads for (bucket, key)
|
// listMultipartUploads lists all outstanding multipart uploads for (bucket, key)
|
||||||
//
|
//
|
||||||
// Note that rather lazily we treat key as a prefix so it matches
|
// Note that rather lazily we treat key as a prefix so it matches
|
||||||
// directories and objects. This could surprise the user if they ask
|
// directories and objects. This could suprise the user if they ask
|
||||||
// for "dir" and it returns "dirKey"
|
// for "dir" and it returns "dirKey"
|
||||||
func (f *Fs) listMultipartUploads(ctx context.Context, bucket, key string) (uploads []*s3.MultipartUpload, err error) {
|
func (f *Fs) listMultipartUploads(ctx context.Context, bucket, key string) (uploads []*s3.MultipartUpload, err error) {
|
||||||
var (
|
var (
|
||||||
@@ -2631,7 +2519,7 @@ func (f *Fs) listMultipartUploads(ctx context.Context, bucket, key string) (uplo
|
|||||||
var resp *s3.ListMultipartUploadsOutput
|
var resp *s3.ListMultipartUploadsOutput
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.c.ListMultipartUploads(&req)
|
resp, err = f.c.ListMultipartUploads(&req)
|
||||||
return f.shouldRetry(ctx, err)
|
return f.shouldRetry(err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(err, "list multipart uploads bucket %q key %q", bucket, key)
|
return nil, errors.Wrapf(err, "list multipart uploads bucket %q key %q", bucket, key)
|
||||||
@@ -2746,38 +2634,30 @@ func (o *Object) Remote() string {
|
|||||||
|
|
||||||
var matchMd5 = regexp.MustCompile(`^[0-9a-f]{32}$`)
|
var matchMd5 = regexp.MustCompile(`^[0-9a-f]{32}$`)
|
||||||
|
|
||||||
// Set the MD5 from the etag
|
|
||||||
func (o *Object) setMD5FromEtag(etag string) {
|
|
||||||
if o.fs.etagIsNotMD5 {
|
|
||||||
o.md5 = ""
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if etag == "" {
|
|
||||||
o.md5 = ""
|
|
||||||
return
|
|
||||||
}
|
|
||||||
hash := strings.Trim(strings.ToLower(etag), `"`)
|
|
||||||
// Check the etag is a valid md5sum
|
|
||||||
if !matchMd5.MatchString(hash) {
|
|
||||||
o.md5 = ""
|
|
||||||
return
|
|
||||||
}
|
|
||||||
o.md5 = hash
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hash returns the Md5sum of an object returning a lowercase hex string
|
// Hash returns the Md5sum of an object returning a lowercase hex string
|
||||||
func (o *Object) Hash(ctx context.Context, t hash.Type) (string, error) {
|
func (o *Object) Hash(ctx context.Context, t hash.Type) (string, error) {
|
||||||
if t != hash.MD5 {
|
if t != hash.MD5 {
|
||||||
return "", hash.ErrUnsupported
|
return "", hash.ErrUnsupported
|
||||||
}
|
}
|
||||||
// If we haven't got an MD5, then check the metadata
|
hash := strings.Trim(strings.ToLower(o.etag), `"`)
|
||||||
if o.md5 == "" {
|
// Check the etag is a valid md5sum
|
||||||
|
if !matchMd5.MatchString(hash) {
|
||||||
err := o.readMetaData(ctx)
|
err := o.readMetaData(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if md5sum, ok := o.meta[metaMD5Hash]; ok {
|
||||||
|
md5sumBytes, err := base64.StdEncoding.DecodeString(*md5sum)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
}
|
}
|
||||||
return o.md5, nil
|
hash = hex.EncodeToString(md5sumBytes)
|
||||||
|
} else {
|
||||||
|
hash = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hash, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Size returns the size of an object in bytes
|
// Size returns the size of an object in bytes
|
||||||
@@ -2791,22 +2671,10 @@ func (o *Object) headObject(ctx context.Context) (resp *s3.HeadObjectOutput, err
|
|||||||
Bucket: &bucket,
|
Bucket: &bucket,
|
||||||
Key: &bucketPath,
|
Key: &bucketPath,
|
||||||
}
|
}
|
||||||
if o.fs.opt.RequesterPays {
|
|
||||||
req.RequestPayer = aws.String(s3.RequestPayerRequester)
|
|
||||||
}
|
|
||||||
if o.fs.opt.SSECustomerAlgorithm != "" {
|
|
||||||
req.SSECustomerAlgorithm = &o.fs.opt.SSECustomerAlgorithm
|
|
||||||
}
|
|
||||||
if o.fs.opt.SSECustomerKey != "" {
|
|
||||||
req.SSECustomerKey = &o.fs.opt.SSECustomerKey
|
|
||||||
}
|
|
||||||
if o.fs.opt.SSECustomerKeyMD5 != "" {
|
|
||||||
req.SSECustomerKeyMD5 = &o.fs.opt.SSECustomerKeyMD5
|
|
||||||
}
|
|
||||||
err = o.fs.pacer.Call(func() (bool, error) {
|
err = o.fs.pacer.Call(func() (bool, error) {
|
||||||
var err error
|
var err error
|
||||||
resp, err = o.fs.c.HeadObjectWithContext(ctx, &req)
|
resp, err = o.fs.c.HeadObjectWithContext(ctx, &req)
|
||||||
return o.fs.shouldRetry(ctx, err)
|
return o.fs.shouldRetry(err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if awsErr, ok := err.(awserr.RequestFailure); ok {
|
if awsErr, ok := err.(awserr.RequestFailure); ok {
|
||||||
@@ -2837,23 +2705,12 @@ func (o *Object) readMetaData(ctx context.Context) (err error) {
|
|||||||
if resp.ContentLength != nil {
|
if resp.ContentLength != nil {
|
||||||
size = *resp.ContentLength
|
size = *resp.ContentLength
|
||||||
}
|
}
|
||||||
o.setMD5FromEtag(aws.StringValue(resp.ETag))
|
o.etag = aws.StringValue(resp.ETag)
|
||||||
o.bytes = size
|
o.bytes = size
|
||||||
o.meta = resp.Metadata
|
o.meta = resp.Metadata
|
||||||
if o.meta == nil {
|
if o.meta == nil {
|
||||||
o.meta = map[string]*string{}
|
o.meta = map[string]*string{}
|
||||||
}
|
}
|
||||||
// Read MD5 from metadata if present
|
|
||||||
if md5sumBase64, ok := o.meta[metaMD5Hash]; ok {
|
|
||||||
md5sumBytes, err := base64.StdEncoding.DecodeString(*md5sumBase64)
|
|
||||||
if err != nil {
|
|
||||||
fs.Debugf(o, "Failed to read md5sum from metadata %q: %v", *md5sumBase64, err)
|
|
||||||
} else if len(md5sumBytes) != 16 {
|
|
||||||
fs.Debugf(o, "Failed to read md5sum from metadata %q: wrong length", *md5sumBase64)
|
|
||||||
} else {
|
|
||||||
o.md5 = hex.EncodeToString(md5sumBytes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
o.storageClass = aws.StringValue(resp.StorageClass)
|
o.storageClass = aws.StringValue(resp.StorageClass)
|
||||||
if resp.LastModified == nil {
|
if resp.LastModified == nil {
|
||||||
fs.Logf(o, "Failed to read last modified from HEAD: %v", err)
|
fs.Logf(o, "Failed to read last modified from HEAD: %v", err)
|
||||||
@@ -2870,7 +2727,7 @@ func (o *Object) readMetaData(ctx context.Context) (err error) {
|
|||||||
// It attempts to read the objects mtime and if that isn't present the
|
// It attempts to read the objects mtime and if that isn't present the
|
||||||
// LastModified returned in the http headers
|
// LastModified returned in the http headers
|
||||||
func (o *Object) ModTime(ctx context.Context) time.Time {
|
func (o *Object) ModTime(ctx context.Context) time.Time {
|
||||||
if o.fs.ci.UseServerModTime {
|
if fs.Config.UseServerModTime {
|
||||||
return o.lastModified
|
return o.lastModified
|
||||||
}
|
}
|
||||||
err := o.readMetaData(ctx)
|
err := o.readMetaData(ctx)
|
||||||
@@ -2912,9 +2769,6 @@ func (o *Object) SetModTime(ctx context.Context, modTime time.Time) error {
|
|||||||
Metadata: o.meta,
|
Metadata: o.meta,
|
||||||
MetadataDirective: aws.String(s3.MetadataDirectiveReplace), // replace metadata with that passed in
|
MetadataDirective: aws.String(s3.MetadataDirectiveReplace), // replace metadata with that passed in
|
||||||
}
|
}
|
||||||
if o.fs.opt.RequesterPays {
|
|
||||||
req.RequestPayer = aws.String(s3.RequestPayerRequester)
|
|
||||||
}
|
|
||||||
return o.fs.copy(ctx, &req, bucket, bucketPath, bucket, bucketPath, o)
|
return o.fs.copy(ctx, &req, bucket, bucketPath, bucket, bucketPath, o)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2930,9 +2784,6 @@ func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.Read
|
|||||||
Bucket: &bucket,
|
Bucket: &bucket,
|
||||||
Key: &bucketPath,
|
Key: &bucketPath,
|
||||||
}
|
}
|
||||||
if o.fs.opt.RequesterPays {
|
|
||||||
req.RequestPayer = aws.String(s3.RequestPayerRequester)
|
|
||||||
}
|
|
||||||
if o.fs.opt.SSECustomerAlgorithm != "" {
|
if o.fs.opt.SSECustomerAlgorithm != "" {
|
||||||
req.SSECustomerAlgorithm = &o.fs.opt.SSECustomerAlgorithm
|
req.SSECustomerAlgorithm = &o.fs.opt.SSECustomerAlgorithm
|
||||||
}
|
}
|
||||||
@@ -2962,7 +2813,7 @@ func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.Read
|
|||||||
var err error
|
var err error
|
||||||
httpReq.HTTPRequest = httpReq.HTTPRequest.WithContext(ctx)
|
httpReq.HTTPRequest = httpReq.HTTPRequest.WithContext(ctx)
|
||||||
err = httpReq.Send()
|
err = httpReq.Send()
|
||||||
return o.fs.shouldRetry(ctx, err)
|
return o.fs.shouldRetry(err)
|
||||||
})
|
})
|
||||||
if err, ok := err.(awserr.RequestFailure); ok {
|
if err, ok := err.(awserr.RequestFailure); ok {
|
||||||
if err.Code() == "InvalidObjectState" {
|
if err.Code() == "InvalidObjectState" {
|
||||||
@@ -3021,7 +2872,7 @@ func (o *Object) uploadMultipart(ctx context.Context, req *s3.PutObjectInput, si
|
|||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
var err error
|
var err error
|
||||||
cout, err = f.c.CreateMultipartUploadWithContext(ctx, &mReq)
|
cout, err = f.c.CreateMultipartUploadWithContext(ctx, &mReq)
|
||||||
return f.shouldRetry(ctx, err)
|
return f.shouldRetry(err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "multipart upload failed to initialise")
|
return errors.Wrap(err, "multipart upload failed to initialise")
|
||||||
@@ -3040,7 +2891,7 @@ func (o *Object) uploadMultipart(ctx context.Context, req *s3.PutObjectInput, si
|
|||||||
UploadId: uid,
|
UploadId: uid,
|
||||||
RequestPayer: req.RequestPayer,
|
RequestPayer: req.RequestPayer,
|
||||||
})
|
})
|
||||||
return f.shouldRetry(ctx, err)
|
return f.shouldRetry(err)
|
||||||
})
|
})
|
||||||
if errCancel != nil {
|
if errCancel != nil {
|
||||||
fs.Debugf(o, "Failed to cancel multipart upload: %v", errCancel)
|
fs.Debugf(o, "Failed to cancel multipart upload: %v", errCancel)
|
||||||
@@ -3116,7 +2967,7 @@ func (o *Object) uploadMultipart(ctx context.Context, req *s3.PutObjectInput, si
|
|||||||
uout, err := f.c.UploadPartWithContext(gCtx, uploadPartReq)
|
uout, err := f.c.UploadPartWithContext(gCtx, uploadPartReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if partNum <= int64(concurrency) {
|
if partNum <= int64(concurrency) {
|
||||||
return f.shouldRetry(ctx, err)
|
return f.shouldRetry(err)
|
||||||
}
|
}
|
||||||
// retry all chunks once have done the first batch
|
// retry all chunks once have done the first batch
|
||||||
return true, err
|
return true, err
|
||||||
@@ -3156,7 +3007,7 @@ func (o *Object) uploadMultipart(ctx context.Context, req *s3.PutObjectInput, si
|
|||||||
RequestPayer: req.RequestPayer,
|
RequestPayer: req.RequestPayer,
|
||||||
UploadId: uid,
|
UploadId: uid,
|
||||||
})
|
})
|
||||||
return f.shouldRetry(ctx, err)
|
return f.shouldRetry(err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "multipart upload failed to finalise")
|
return errors.Wrap(err, "multipart upload failed to finalise")
|
||||||
@@ -3182,9 +3033,8 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
|
|||||||
}
|
}
|
||||||
|
|
||||||
// read the md5sum if available
|
// read the md5sum if available
|
||||||
// - for non multipart
|
// - for non multpart
|
||||||
// - so we can add a ContentMD5
|
// - so we can add a ContentMD5
|
||||||
// - so we can add the md5sum in the metadata as metaMD5Hash if using SSE/SSE-C
|
|
||||||
// - for multipart provided checksums aren't disabled
|
// - for multipart provided checksums aren't disabled
|
||||||
// - so we can add the md5sum in the metadata as metaMD5Hash
|
// - so we can add the md5sum in the metadata as metaMD5Hash
|
||||||
var md5sum string
|
var md5sum string
|
||||||
@@ -3194,11 +3044,7 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
|
|||||||
hashBytes, err := hex.DecodeString(hash)
|
hashBytes, err := hex.DecodeString(hash)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
md5sum = base64.StdEncoding.EncodeToString(hashBytes)
|
md5sum = base64.StdEncoding.EncodeToString(hashBytes)
|
||||||
if (multipart || o.fs.etagIsNotMD5) && !o.fs.opt.DisableChecksum {
|
if multipart {
|
||||||
// Set the md5sum as metadata on the object if
|
|
||||||
// - a multipart upload
|
|
||||||
// - the Etag is not an MD5, eg when using SSE/SSE-C
|
|
||||||
// provided checksums aren't disabled
|
|
||||||
metadata[metaMD5Hash] = &md5sum
|
metadata[metaMD5Hash] = &md5sum
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3217,9 +3063,6 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
|
|||||||
if md5sum != "" {
|
if md5sum != "" {
|
||||||
req.ContentMD5 = &md5sum
|
req.ContentMD5 = &md5sum
|
||||||
}
|
}
|
||||||
if o.fs.opt.RequesterPays {
|
|
||||||
req.RequestPayer = aws.String(s3.RequestPayerRequester)
|
|
||||||
}
|
|
||||||
if o.fs.opt.ServerSideEncryption != "" {
|
if o.fs.opt.ServerSideEncryption != "" {
|
||||||
req.ServerSideEncryption = &o.fs.opt.ServerSideEncryption
|
req.ServerSideEncryption = &o.fs.opt.ServerSideEncryption
|
||||||
}
|
}
|
||||||
@@ -3268,7 +3111,6 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var resp *http.Response // response from PUT
|
|
||||||
if multipart {
|
if multipart {
|
||||||
err = o.uploadMultipart(ctx, &req, size, in)
|
err = o.uploadMultipart(ctx, &req, size, in)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -3298,24 +3140,24 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
|
|||||||
}
|
}
|
||||||
|
|
||||||
// create the vanilla http request
|
// create the vanilla http request
|
||||||
httpReq, err := http.NewRequestWithContext(ctx, "PUT", url, in)
|
httpReq, err := http.NewRequest("PUT", url, in)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "s3 upload: new request")
|
return errors.Wrap(err, "s3 upload: new request")
|
||||||
}
|
}
|
||||||
|
httpReq = httpReq.WithContext(ctx) // go1.13 can use NewRequestWithContext
|
||||||
|
|
||||||
// set the headers we signed and the length
|
// set the headers we signed and the length
|
||||||
httpReq.Header = headers
|
httpReq.Header = headers
|
||||||
httpReq.ContentLength = size
|
httpReq.ContentLength = size
|
||||||
|
|
||||||
err = o.fs.pacer.CallNoRetry(func() (bool, error) {
|
err = o.fs.pacer.CallNoRetry(func() (bool, error) {
|
||||||
var err error
|
resp, err := o.fs.srv.Do(httpReq)
|
||||||
resp, err = o.fs.srv.Do(httpReq)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return o.fs.shouldRetry(ctx, err)
|
return o.fs.shouldRetry(err)
|
||||||
}
|
}
|
||||||
body, err := rest.ReadBody(resp)
|
body, err := rest.ReadBody(resp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return o.fs.shouldRetry(ctx, err)
|
return o.fs.shouldRetry(err)
|
||||||
}
|
}
|
||||||
if resp.StatusCode >= 200 && resp.StatusCode < 299 {
|
if resp.StatusCode >= 200 && resp.StatusCode < 299 {
|
||||||
return false, nil
|
return false, nil
|
||||||
@@ -3328,26 +3170,6 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// User requested we don't HEAD the object after uploading it
|
|
||||||
// so make up the object as best we can assuming it got
|
|
||||||
// uploaded properly. If size < 0 then we need to do the HEAD.
|
|
||||||
if o.fs.opt.NoHead && size >= 0 {
|
|
||||||
o.md5 = md5sum
|
|
||||||
o.bytes = size
|
|
||||||
o.lastModified = time.Now()
|
|
||||||
o.meta = req.Metadata
|
|
||||||
o.mimeType = aws.StringValue(req.ContentType)
|
|
||||||
o.storageClass = aws.StringValue(req.StorageClass)
|
|
||||||
// If we have done a single part PUT request then we can read these
|
|
||||||
if resp != nil {
|
|
||||||
if date, err := http.ParseTime(resp.Header.Get("Date")); err == nil {
|
|
||||||
o.lastModified = date
|
|
||||||
}
|
|
||||||
o.setMD5FromEtag(resp.Header.Get("Etag"))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read the metadata from the newly created object
|
// Read the metadata from the newly created object
|
||||||
o.meta = nil // wipe old metadata
|
o.meta = nil // wipe old metadata
|
||||||
err = o.readMetaData(ctx)
|
err = o.readMetaData(ctx)
|
||||||
@@ -3361,12 +3183,9 @@ func (o *Object) Remove(ctx context.Context) error {
|
|||||||
Bucket: &bucket,
|
Bucket: &bucket,
|
||||||
Key: &bucketPath,
|
Key: &bucketPath,
|
||||||
}
|
}
|
||||||
if o.fs.opt.RequesterPays {
|
|
||||||
req.RequestPayer = aws.String(s3.RequestPayerRequester)
|
|
||||||
}
|
|
||||||
err := o.fs.pacer.Call(func() (bool, error) {
|
err := o.fs.pacer.Call(func() (bool, error) {
|
||||||
_, err := o.fs.c.DeleteObjectWithContext(ctx, &req)
|
_, err := o.fs.c.DeleteObjectWithContext(ctx, &req)
|
||||||
return o.fs.shouldRetry(ctx, err)
|
return o.fs.shouldRetry(err)
|
||||||
})
|
})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ func sign(AccessKey, SecretKey string, req *http.Request) {
|
|||||||
var md5 string
|
var md5 string
|
||||||
var contentType string
|
var contentType string
|
||||||
var headersToSign []string
|
var headersToSign []string
|
||||||
tmpHeadersToSign := make(map[string][]string)
|
|
||||||
for k, v := range req.Header {
|
for k, v := range req.Header {
|
||||||
k = strings.ToLower(k)
|
k = strings.ToLower(k)
|
||||||
switch k {
|
switch k {
|
||||||
@@ -63,24 +62,15 @@ func sign(AccessKey, SecretKey string, req *http.Request) {
|
|||||||
contentType = v[0]
|
contentType = v[0]
|
||||||
default:
|
default:
|
||||||
if strings.HasPrefix(k, "x-amz-") {
|
if strings.HasPrefix(k, "x-amz-") {
|
||||||
tmpHeadersToSign[k] = v
|
vall := strings.Join(v, ",")
|
||||||
|
headersToSign = append(headersToSign, k+":"+vall)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var keys []string
|
|
||||||
for k := range tmpHeadersToSign {
|
|
||||||
keys = append(keys, k)
|
|
||||||
}
|
|
||||||
// https://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html
|
|
||||||
sort.Strings(keys)
|
|
||||||
|
|
||||||
for _, key := range keys {
|
|
||||||
vall := strings.Join(tmpHeadersToSign[key], ",")
|
|
||||||
headersToSign = append(headersToSign, key+":"+vall)
|
|
||||||
}
|
|
||||||
// Make headers of interest into canonical string
|
// Make headers of interest into canonical string
|
||||||
var joinedHeadersToSign string
|
var joinedHeadersToSign string
|
||||||
if len(headersToSign) > 0 {
|
if len(headersToSign) > 0 {
|
||||||
|
sort.StringSlice(headersToSign).Sort()
|
||||||
joinedHeadersToSign = strings.Join(headersToSign, "\n") + "\n"
|
joinedHeadersToSign = strings.Join(headersToSign, "\n") + "\n"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package seafile
|
package seafile
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -28,7 +27,7 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// getPacer returns the unique pacer for that remote URL
|
// getPacer returns the unique pacer for that remote URL
|
||||||
func getPacer(ctx context.Context, remote string) *fs.Pacer {
|
func getPacer(remote string) *fs.Pacer {
|
||||||
pacerMutex.Lock()
|
pacerMutex.Lock()
|
||||||
defer pacerMutex.Unlock()
|
defer pacerMutex.Unlock()
|
||||||
|
|
||||||
@@ -38,7 +37,6 @@ func getPacer(ctx context.Context, remote string) *fs.Pacer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pacers[remote] = fs.NewPacer(
|
pacers[remote] = fs.NewPacer(
|
||||||
ctx,
|
|
||||||
pacer.NewDefault(
|
pacer.NewDefault(
|
||||||
pacer.MinSleep(minSleep),
|
pacer.MinSleep(minSleep),
|
||||||
pacer.MaxSleep(maxSleep),
|
pacer.MaxSleep(maxSleep),
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ type Fs struct {
|
|||||||
// ------------------------------------------------------------
|
// ------------------------------------------------------------
|
||||||
|
|
||||||
// NewFs constructs an Fs from the path, container:path
|
// NewFs constructs an Fs from the path, container:path
|
||||||
func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, error) {
|
func NewFs(name, root string, m configmap.Mapper) (fs.Fs, error) {
|
||||||
// Parse config into Options struct
|
// Parse config into Options struct
|
||||||
opt := new(Options)
|
opt := new(Options)
|
||||||
err := configstruct.Set(m, opt)
|
err := configstruct.Set(m, opt)
|
||||||
@@ -197,14 +197,15 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
opt: *opt,
|
opt: *opt,
|
||||||
endpoint: u,
|
endpoint: u,
|
||||||
endpointURL: u.String(),
|
endpointURL: u.String(),
|
||||||
srv: rest.NewClient(fshttp.NewClient(ctx)).SetRoot(u.String()),
|
srv: rest.NewClient(fshttp.NewClient(fs.Config)).SetRoot(u.String()),
|
||||||
pacer: getPacer(ctx, opt.URL),
|
pacer: getPacer(opt.URL),
|
||||||
}
|
}
|
||||||
f.features = (&fs.Features{
|
f.features = (&fs.Features{
|
||||||
CanHaveEmptyDirectories: true,
|
CanHaveEmptyDirectories: true,
|
||||||
BucketBased: opt.LibraryName == "",
|
BucketBased: opt.LibraryName == "",
|
||||||
}).Fill(ctx, f)
|
}).Fill(f)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
serverInfo, err := f.getServerInfo(ctx)
|
serverInfo, err := f.getServerInfo(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -296,8 +297,7 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Config callback for 2FA
|
// Config callback for 2FA
|
||||||
func Config(ctx context.Context, name string, m configmap.Mapper) {
|
func Config(name string, m configmap.Mapper) {
|
||||||
ci := fs.GetConfig(ctx)
|
|
||||||
serverURL, ok := m.Get(configURL)
|
serverURL, ok := m.Get(configURL)
|
||||||
if !ok || serverURL == "" {
|
if !ok || serverURL == "" {
|
||||||
// If there's no server URL, it means we're trying an operation at the backend level, like a "rclone authorize seafile"
|
// If there's no server URL, it means we're trying an operation at the backend level, like a "rclone authorize seafile"
|
||||||
@@ -306,7 +306,7 @@ func Config(ctx context.Context, name string, m configmap.Mapper) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Stop if we are running non-interactive config
|
// Stop if we are running non-interactive config
|
||||||
if ci.AutoConfirm {
|
if fs.Config.AutoConfirm {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -343,7 +343,7 @@ func Config(ctx context.Context, name string, m configmap.Mapper) {
|
|||||||
if !strings.HasPrefix(url, "/") {
|
if !strings.HasPrefix(url, "/") {
|
||||||
url += "/"
|
url += "/"
|
||||||
}
|
}
|
||||||
srv := rest.NewClient(fshttp.NewClient(ctx)).SetRoot(url)
|
srv := rest.NewClient(fshttp.NewClient(fs.Config)).SetRoot(url)
|
||||||
|
|
||||||
// We loop asking for a 2FA code
|
// We loop asking for a 2FA code
|
||||||
for {
|
for {
|
||||||
@@ -372,6 +372,7 @@ func Config(ctx context.Context, name string, m configmap.Mapper) {
|
|||||||
m.Set(configAuthToken, token)
|
m.Set(configAuthToken, token)
|
||||||
// And delete any previous entry for password
|
// And delete any previous entry for password
|
||||||
m.Set(configPassword, "")
|
m.Set(configPassword, "")
|
||||||
|
config.SaveConfig()
|
||||||
// And we're done here
|
// And we're done here
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -407,10 +408,7 @@ var retryErrorCodes = []int{
|
|||||||
|
|
||||||
// shouldRetry returns a boolean as to whether this resp and err
|
// shouldRetry returns a boolean as to whether this resp and err
|
||||||
// deserve to be retried. It returns the err as a convenience
|
// deserve to be retried. It returns the err as a convenience
|
||||||
func (f *Fs) shouldRetry(ctx context.Context, resp *http.Response, err error) (bool, error) {
|
func (f *Fs) shouldRetry(resp *http.Response, err error) (bool, error) {
|
||||||
if fserrors.ContextError(ctx, &err) {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
// For 429 errors look at the Retry-After: header and
|
// For 429 errors look at the Retry-After: header and
|
||||||
// set the retry appropriately, starting with a minimum of 1
|
// set the retry appropriately, starting with a minimum of 1
|
||||||
// second if it isn't set.
|
// second if it isn't set.
|
||||||
@@ -665,7 +663,7 @@ func (f *Fs) ListR(ctx context.Context, dir string, callback fs.ListRCallback) e
|
|||||||
|
|
||||||
// ==================== Optional Interface fs.Copier ====================
|
// ==================== Optional Interface fs.Copier ====================
|
||||||
|
|
||||||
// Copy src to this remote using server-side copy operations.
|
// Copy src to this remote using server side copy operations.
|
||||||
//
|
//
|
||||||
// This is stored with the remote path given
|
// This is stored with the remote path given
|
||||||
//
|
//
|
||||||
@@ -716,7 +714,7 @@ func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object,
|
|||||||
|
|
||||||
// ==================== Optional Interface fs.Mover ====================
|
// ==================== Optional Interface fs.Mover ====================
|
||||||
|
|
||||||
// Move src to this remote using server-side move operations.
|
// Move src to this remote using server side move operations.
|
||||||
//
|
//
|
||||||
// This is stored with the remote path given
|
// This is stored with the remote path given
|
||||||
//
|
//
|
||||||
@@ -806,7 +804,7 @@ func (f *Fs) adjustDestination(ctx context.Context, libraryID, srcFilename, dstP
|
|||||||
// ==================== Optional Interface fs.DirMover ====================
|
// ==================== Optional Interface fs.DirMover ====================
|
||||||
|
|
||||||
// DirMove moves src, srcRemote to this remote at dstRemote
|
// DirMove moves src, srcRemote to this remote at dstRemote
|
||||||
// using server-side move operations.
|
// using server side move operations.
|
||||||
//
|
//
|
||||||
// Will only be called if src.Fs().Name() == f.Name()
|
// Will only be called if src.Fs().Name() == f.Name()
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ func (f *Fs) getServerInfo(ctx context.Context) (account *api.ServerInfo, err er
|
|||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
||||||
return f.shouldRetry(ctx, resp, err)
|
return f.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
@@ -112,7 +112,7 @@ func (f *Fs) getUserAccountInfo(ctx context.Context) (account *api.AccountInfo,
|
|||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
||||||
return f.shouldRetry(ctx, resp, err)
|
return f.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
@@ -139,7 +139,7 @@ func (f *Fs) getLibraries(ctx context.Context) ([]api.Library, error) {
|
|||||||
var err error
|
var err error
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
||||||
return f.shouldRetry(ctx, resp, err)
|
return f.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
@@ -170,7 +170,7 @@ func (f *Fs) createLibrary(ctx context.Context, libraryName, password string) (l
|
|||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, &request, &result)
|
resp, err = f.srv.CallJSON(ctx, &opts, &request, &result)
|
||||||
return f.shouldRetry(ctx, resp, err)
|
return f.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
@@ -197,7 +197,7 @@ func (f *Fs) deleteLibrary(ctx context.Context, libraryID string) error {
|
|||||||
var err error
|
var err error
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
||||||
return f.shouldRetry(ctx, resp, err)
|
return f.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
@@ -228,7 +228,7 @@ func (f *Fs) decryptLibrary(ctx context.Context, libraryID, password string) err
|
|||||||
var err error
|
var err error
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.Call(ctx, &opts)
|
resp, err = f.srv.Call(ctx, &opts)
|
||||||
return f.shouldRetry(ctx, resp, err)
|
return f.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
@@ -271,7 +271,7 @@ func (f *Fs) getDirectoryEntriesAPIv21(ctx context.Context, libraryID, dirPath s
|
|||||||
var err error
|
var err error
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
||||||
return f.shouldRetry(ctx, resp, err)
|
return f.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
@@ -316,7 +316,7 @@ func (f *Fs) getDirectoryDetails(ctx context.Context, libraryID, dirPath string)
|
|||||||
var err error
|
var err error
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
||||||
return f.shouldRetry(ctx, resp, err)
|
return f.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
@@ -358,7 +358,7 @@ func (f *Fs) createDir(ctx context.Context, libraryID, dirPath string) error {
|
|||||||
var err error
|
var err error
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.Call(ctx, &opts)
|
resp, err = f.srv.Call(ctx, &opts)
|
||||||
return f.shouldRetry(ctx, resp, err)
|
return f.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
@@ -398,7 +398,7 @@ func (f *Fs) renameDir(ctx context.Context, libraryID, dirPath, newName string)
|
|||||||
var err error
|
var err error
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.Call(ctx, &opts)
|
resp, err = f.srv.Call(ctx, &opts)
|
||||||
return f.shouldRetry(ctx, resp, err)
|
return f.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
@@ -438,7 +438,7 @@ func (f *Fs) moveDir(ctx context.Context, srcLibraryID, srcDir, srcName, dstLibr
|
|||||||
var err error
|
var err error
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, &request, nil)
|
resp, err = f.srv.CallJSON(ctx, &opts, &request, nil)
|
||||||
return f.shouldRetry(ctx, resp, err)
|
return f.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
@@ -474,7 +474,7 @@ func (f *Fs) deleteDir(ctx context.Context, libraryID, filePath string) error {
|
|||||||
var err error
|
var err error
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, nil)
|
resp, err = f.srv.CallJSON(ctx, &opts, nil, nil)
|
||||||
return f.shouldRetry(ctx, resp, err)
|
return f.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
@@ -505,7 +505,7 @@ func (f *Fs) getFileDetails(ctx context.Context, libraryID, filePath string) (*a
|
|||||||
var err error
|
var err error
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
||||||
return f.shouldRetry(ctx, resp, err)
|
return f.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
@@ -539,7 +539,7 @@ func (f *Fs) deleteFile(ctx context.Context, libraryID, filePath string) error {
|
|||||||
}
|
}
|
||||||
err := f.pacer.Call(func() (bool, error) {
|
err := f.pacer.Call(func() (bool, error) {
|
||||||
resp, err := f.srv.CallJSON(ctx, &opts, nil, nil)
|
resp, err := f.srv.CallJSON(ctx, &opts, nil, nil)
|
||||||
return f.shouldRetry(ctx, resp, err)
|
return f.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to delete file")
|
return errors.Wrap(err, "failed to delete file")
|
||||||
@@ -565,7 +565,7 @@ func (f *Fs) getDownloadLink(ctx context.Context, libraryID, filePath string) (s
|
|||||||
var err error
|
var err error
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
||||||
return f.shouldRetry(ctx, resp, err)
|
return f.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
@@ -614,7 +614,7 @@ func (f *Fs) download(ctx context.Context, url string, size int64, options ...fs
|
|||||||
var err error
|
var err error
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.Call(ctx, &opts)
|
resp, err = f.srv.Call(ctx, &opts)
|
||||||
return f.shouldRetry(ctx, resp, err)
|
return f.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
@@ -659,7 +659,7 @@ func (f *Fs) getUploadLink(ctx context.Context, libraryID string) (string, error
|
|||||||
var err error
|
var err error
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
||||||
return f.shouldRetry(ctx, resp, err)
|
return f.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
@@ -682,7 +682,7 @@ func (f *Fs) upload(ctx context.Context, in io.Reader, uploadLink, filePath stri
|
|||||||
"need_idx_progress": {"true"},
|
"need_idx_progress": {"true"},
|
||||||
"replace": {"1"},
|
"replace": {"1"},
|
||||||
}
|
}
|
||||||
formReader, contentType, _, err := rest.MultipartUpload(ctx, in, parameters, "file", f.opt.Enc.FromStandardName(filename))
|
formReader, contentType, _, err := rest.MultipartUpload(in, parameters, "file", f.opt.Enc.FromStandardName(filename))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to make multipart upload")
|
return nil, errors.Wrap(err, "failed to make multipart upload")
|
||||||
}
|
}
|
||||||
@@ -739,7 +739,7 @@ func (f *Fs) listShareLinks(ctx context.Context, libraryID, remote string) ([]ap
|
|||||||
var err error
|
var err error
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
||||||
return f.shouldRetry(ctx, resp, err)
|
return f.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
@@ -777,7 +777,7 @@ func (f *Fs) createShareLink(ctx context.Context, libraryID, remote string) (*ap
|
|||||||
var err error
|
var err error
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, &request, &result)
|
resp, err = f.srv.CallJSON(ctx, &opts, &request, &result)
|
||||||
return f.shouldRetry(ctx, resp, err)
|
return f.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
@@ -818,7 +818,7 @@ func (f *Fs) copyFile(ctx context.Context, srcLibraryID, srcPath, dstLibraryID,
|
|||||||
var err error
|
var err error
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, &request, &result)
|
resp, err = f.srv.CallJSON(ctx, &opts, &request, &result)
|
||||||
return f.shouldRetry(ctx, resp, err)
|
return f.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
@@ -860,7 +860,7 @@ func (f *Fs) moveFile(ctx context.Context, srcLibraryID, srcPath, dstLibraryID,
|
|||||||
var err error
|
var err error
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, &request, &result)
|
resp, err = f.srv.CallJSON(ctx, &opts, &request, &result)
|
||||||
return f.shouldRetry(ctx, resp, err)
|
return f.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
@@ -900,7 +900,7 @@ func (f *Fs) renameFile(ctx context.Context, libraryID, filePath, newname string
|
|||||||
var err error
|
var err error
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, &request, &result)
|
resp, err = f.srv.CallJSON(ctx, &opts, &request, &result)
|
||||||
return f.shouldRetry(ctx, resp, err)
|
return f.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
@@ -938,7 +938,7 @@ func (f *Fs) emptyLibraryTrash(ctx context.Context, libraryID string) error {
|
|||||||
var err error
|
var err error
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, nil)
|
resp, err = f.srv.CallJSON(ctx, &opts, nil, nil)
|
||||||
return f.shouldRetry(ctx, resp, err)
|
return f.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
@@ -976,7 +976,7 @@ func (f *Fs) getDirectoryEntriesAPIv2(ctx context.Context, libraryID, dirPath st
|
|||||||
var err error
|
var err error
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
||||||
return f.shouldRetry(ctx, resp, err)
|
return f.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
@@ -1030,7 +1030,7 @@ func (f *Fs) copyFileAPIv2(ctx context.Context, srcLibraryID, srcPath, dstLibrar
|
|||||||
var err error
|
var err error
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.Call(ctx, &opts)
|
resp, err = f.srv.Call(ctx, &opts)
|
||||||
return f.shouldRetry(ctx, resp, err)
|
return f.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
@@ -1075,7 +1075,7 @@ func (f *Fs) renameFileAPIv2(ctx context.Context, libraryID, filePath, newname s
|
|||||||
var err error
|
var err error
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.Call(ctx, &opts)
|
resp, err = f.srv.Call(ctx, &opts)
|
||||||
return f.shouldRetry(ctx, resp, err)
|
return f.shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
|
|||||||
@@ -11,18 +11,17 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
"os/user"
|
||||||
"path"
|
"path"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/pkg/sftp"
|
"github.com/pkg/sftp"
|
||||||
"github.com/rclone/rclone/fs"
|
"github.com/rclone/rclone/fs"
|
||||||
"github.com/rclone/rclone/fs/accounting"
|
|
||||||
"github.com/rclone/rclone/fs/config"
|
"github.com/rclone/rclone/fs/config"
|
||||||
"github.com/rclone/rclone/fs/config/configmap"
|
"github.com/rclone/rclone/fs/config/configmap"
|
||||||
"github.com/rclone/rclone/fs/config/configstruct"
|
"github.com/rclone/rclone/fs/config/configstruct"
|
||||||
@@ -34,7 +33,6 @@ import (
|
|||||||
"github.com/rclone/rclone/lib/readers"
|
"github.com/rclone/rclone/lib/readers"
|
||||||
sshagent "github.com/xanzy/ssh-agent"
|
sshagent "github.com/xanzy/ssh-agent"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
"golang.org/x/crypto/ssh/knownhosts"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -45,7 +43,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
currentUser = env.CurrentUser()
|
currentUser = readCurrentUser()
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -84,21 +82,6 @@ func init() {
|
|||||||
Only PEM encrypted key files (old OpenSSH format) are supported. Encrypted keys
|
Only PEM encrypted key files (old OpenSSH format) are supported. Encrypted keys
|
||||||
in the new OpenSSH format can't be used.`,
|
in the new OpenSSH format can't be used.`,
|
||||||
IsPassword: true,
|
IsPassword: true,
|
||||||
}, {
|
|
||||||
Name: "pubkey_file",
|
|
||||||
Help: `Optional path to public key file.
|
|
||||||
|
|
||||||
Set this if you have a signed certificate you want to use for authentication.` + env.ShellExpandHelp,
|
|
||||||
}, {
|
|
||||||
Name: "known_hosts_file",
|
|
||||||
Help: `Optional path to known_hosts file.
|
|
||||||
|
|
||||||
Set this value to enable server host key validation.` + env.ShellExpandHelp,
|
|
||||||
Advanced: true,
|
|
||||||
Examples: []fs.OptionExample{{
|
|
||||||
Value: "~/.ssh/known_hosts",
|
|
||||||
Help: "Use OpenSSH's known_hosts file",
|
|
||||||
}},
|
|
||||||
}, {
|
}, {
|
||||||
Name: "key_use_agent",
|
Name: "key_use_agent",
|
||||||
Help: `When set forces the usage of the ssh-agent.
|
Help: `When set forces the usage of the ssh-agent.
|
||||||
@@ -193,50 +176,6 @@ Home directory can be found in a shared folder called "home"
|
|||||||
|
|
||||||
The subsystem option is ignored when server_command is defined.`,
|
The subsystem option is ignored when server_command is defined.`,
|
||||||
Advanced: true,
|
Advanced: true,
|
||||||
}, {
|
|
||||||
Name: "use_fstat",
|
|
||||||
Default: false,
|
|
||||||
Help: `If set use fstat instead of stat
|
|
||||||
|
|
||||||
Some servers limit the amount of open files and calling Stat after opening
|
|
||||||
the file will throw an error from the server. Setting this flag will call
|
|
||||||
Fstat instead of Stat which is called on an already open file handle.
|
|
||||||
|
|
||||||
It has been found that this helps with IBM Sterling SFTP servers which have
|
|
||||||
"extractability" level set to 1 which means only 1 file can be opened at
|
|
||||||
any given time.
|
|
||||||
`,
|
|
||||||
Advanced: true,
|
|
||||||
}, {
|
|
||||||
Name: "disable_concurrent_reads",
|
|
||||||
Default: false,
|
|
||||||
Help: `If set don't use concurrent reads
|
|
||||||
|
|
||||||
Normally concurrent reads are safe to use and not using them will
|
|
||||||
degrade performance, so this option is disabled by default.
|
|
||||||
|
|
||||||
Some servers limit the amount number of times a file can be
|
|
||||||
downloaded. Using concurrent reads can trigger this limit, so if you
|
|
||||||
have a server which returns
|
|
||||||
|
|
||||||
Failed to copy: file does not exist
|
|
||||||
|
|
||||||
Then you may need to enable this flag.
|
|
||||||
|
|
||||||
If concurrent reads are disabled, the use_fstat option is ignored.
|
|
||||||
`,
|
|
||||||
Advanced: true,
|
|
||||||
}, {
|
|
||||||
Name: "idle_timeout",
|
|
||||||
Default: fs.Duration(60 * time.Second),
|
|
||||||
Help: `Max time before closing idle connections
|
|
||||||
|
|
||||||
If no connections have been returned to the connection pool in the time
|
|
||||||
given, rclone will empty the connection pool.
|
|
||||||
|
|
||||||
Set to 0 to keep connections indefinitely.
|
|
||||||
`,
|
|
||||||
Advanced: true,
|
|
||||||
}},
|
}},
|
||||||
}
|
}
|
||||||
fs.Register(fsi)
|
fs.Register(fsi)
|
||||||
@@ -251,8 +190,6 @@ type Options struct {
|
|||||||
KeyPem string `config:"key_pem"`
|
KeyPem string `config:"key_pem"`
|
||||||
KeyFile string `config:"key_file"`
|
KeyFile string `config:"key_file"`
|
||||||
KeyFilePass string `config:"key_file_pass"`
|
KeyFilePass string `config:"key_file_pass"`
|
||||||
PubKeyFile string `config:"pubkey_file"`
|
|
||||||
KnownHostsFile string `config:"known_hosts_file"`
|
|
||||||
KeyUseAgent bool `config:"key_use_agent"`
|
KeyUseAgent bool `config:"key_use_agent"`
|
||||||
UseInsecureCipher bool `config:"use_insecure_cipher"`
|
UseInsecureCipher bool `config:"use_insecure_cipher"`
|
||||||
DisableHashCheck bool `config:"disable_hashcheck"`
|
DisableHashCheck bool `config:"disable_hashcheck"`
|
||||||
@@ -264,9 +201,6 @@ type Options struct {
|
|||||||
SkipLinks bool `config:"skip_links"`
|
SkipLinks bool `config:"skip_links"`
|
||||||
Subsystem string `config:"subsystem"`
|
Subsystem string `config:"subsystem"`
|
||||||
ServerCommand string `config:"server_command"`
|
ServerCommand string `config:"server_command"`
|
||||||
UseFstat bool `config:"use_fstat"`
|
|
||||||
DisableConcurrentReads bool `config:"disable_concurrent_reads"`
|
|
||||||
IdleTimeout fs.Duration `config:"idle_timeout"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fs stores the interface to the remote SFTP files
|
// Fs stores the interface to the remote SFTP files
|
||||||
@@ -275,7 +209,6 @@ type Fs struct {
|
|||||||
root string
|
root string
|
||||||
absRoot string
|
absRoot string
|
||||||
opt Options // parsed options
|
opt Options // parsed options
|
||||||
ci *fs.ConfigInfo // global config
|
|
||||||
m configmap.Mapper // config
|
m configmap.Mapper // config
|
||||||
features *fs.Features // optional features
|
features *fs.Features // optional features
|
||||||
config *ssh.ClientConfig
|
config *ssh.ClientConfig
|
||||||
@@ -284,10 +217,7 @@ type Fs struct {
|
|||||||
cachedHashes *hash.Set
|
cachedHashes *hash.Set
|
||||||
poolMu sync.Mutex
|
poolMu sync.Mutex
|
||||||
pool []*conn
|
pool []*conn
|
||||||
drain *time.Timer // used to drain the pool when we stop using the connections
|
|
||||||
pacer *fs.Pacer // pacer for operations
|
pacer *fs.Pacer // pacer for operations
|
||||||
savedpswd string
|
|
||||||
transfers int32 // count in use references
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Object is a remote SFTP file that has been stat'd (so it exists, but is not necessarily open for reading)
|
// Object is a remote SFTP file that has been stat'd (so it exists, but is not necessarily open for reading)
|
||||||
@@ -301,11 +231,25 @@ type Object struct {
|
|||||||
sha1sum *string // Cached SHA1 checksum
|
sha1sum *string // Cached SHA1 checksum
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// readCurrentUser finds the current user name or "" if not found
|
||||||
|
func readCurrentUser() (userName string) {
|
||||||
|
usr, err := user.Current()
|
||||||
|
if err == nil {
|
||||||
|
return usr.Username
|
||||||
|
}
|
||||||
|
// Fall back to reading $USER then $LOGNAME
|
||||||
|
userName = os.Getenv("USER")
|
||||||
|
if userName != "" {
|
||||||
|
return userName
|
||||||
|
}
|
||||||
|
return os.Getenv("LOGNAME")
|
||||||
|
}
|
||||||
|
|
||||||
// dial starts a client connection to the given SSH server. It is a
|
// dial starts a client connection to the given SSH server. It is a
|
||||||
// convenience function that connects to the given network address,
|
// convenience function that connects to the given network address,
|
||||||
// initiates the SSH handshake, and then sets up a Client.
|
// initiates the SSH handshake, and then sets up a Client.
|
||||||
func (f *Fs) dial(ctx context.Context, network, addr string, sshConfig *ssh.ClientConfig) (*ssh.Client, error) {
|
func (f *Fs) dial(network, addr string, sshConfig *ssh.ClientConfig) (*ssh.Client, error) {
|
||||||
dialer := fshttp.NewDialer(ctx)
|
dialer := fshttp.NewDialer(fs.Config)
|
||||||
conn, err := dialer.Dial(network, addr)
|
conn, err := dialer.Dial(network, addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -350,30 +294,13 @@ func (c *conn) closed() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show that we are doing an upload or download
|
|
||||||
//
|
|
||||||
// Call removeTransfer() when done
|
|
||||||
func (f *Fs) addTransfer() {
|
|
||||||
atomic.AddInt32(&f.transfers, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show the upload or download done
|
|
||||||
func (f *Fs) removeTransfer() {
|
|
||||||
atomic.AddInt32(&f.transfers, -1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// getTransfers shows whether there are any transfers in progress
|
|
||||||
func (f *Fs) getTransfers() int32 {
|
|
||||||
return atomic.LoadInt32(&f.transfers)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open a new connection to the SFTP server.
|
// Open a new connection to the SFTP server.
|
||||||
func (f *Fs) sftpConnection(ctx context.Context) (c *conn, err error) {
|
func (f *Fs) sftpConnection() (c *conn, err error) {
|
||||||
// Rate limit rate of new connections
|
// Rate limit rate of new connections
|
||||||
c = &conn{
|
c = &conn{
|
||||||
err: make(chan error, 1),
|
err: make(chan error, 1),
|
||||||
}
|
}
|
||||||
c.sshClient, err = f.dial(ctx, "tcp", f.opt.Host+":"+f.opt.Port, f.config)
|
c.sshClient, err = f.dial("tcp", f.opt.Host+":"+f.opt.Port, f.config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "couldn't connect SSH")
|
return nil, errors.Wrap(err, "couldn't connect SSH")
|
||||||
}
|
}
|
||||||
@@ -411,22 +338,12 @@ func (f *Fs) newSftpClient(conn *ssh.Client, opts ...sftp.ClientOption) (*sftp.C
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
opts = opts[:len(opts):len(opts)] // make sure we don't overwrite the callers opts
|
|
||||||
opts = append(opts,
|
|
||||||
sftp.UseFstat(f.opt.UseFstat),
|
|
||||||
// FIXME disabled after library reversion
|
|
||||||
// sftp.UseConcurrentReads(!f.opt.DisableConcurrentReads),
|
|
||||||
)
|
|
||||||
if f.opt.DisableConcurrentReads { // FIXME
|
|
||||||
fs.Errorf(f, "Ignoring disable_concurrent_reads after library reversion - see #5197")
|
|
||||||
}
|
|
||||||
|
|
||||||
return sftp.NewClientPipe(pr, pw, opts...)
|
return sftp.NewClientPipe(pr, pw, opts...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get an SFTP connection from the pool, or open a new one
|
// Get an SFTP connection from the pool, or open a new one
|
||||||
func (f *Fs) getSftpConnection(ctx context.Context) (c *conn, err error) {
|
func (f *Fs) getSftpConnection() (c *conn, err error) {
|
||||||
accounting.LimitTPS(ctx)
|
|
||||||
f.poolMu.Lock()
|
f.poolMu.Lock()
|
||||||
for len(f.pool) > 0 {
|
for len(f.pool) > 0 {
|
||||||
c = f.pool[0]
|
c = f.pool[0]
|
||||||
@@ -443,7 +360,7 @@ func (f *Fs) getSftpConnection(ctx context.Context) (c *conn, err error) {
|
|||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
c, err = f.sftpConnection(ctx)
|
c, err = f.sftpConnection()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return true, err
|
return true, err
|
||||||
}
|
}
|
||||||
@@ -487,51 +404,13 @@ func (f *Fs) putSftpConnection(pc **conn, err error) {
|
|||||||
}
|
}
|
||||||
f.poolMu.Lock()
|
f.poolMu.Lock()
|
||||||
f.pool = append(f.pool, c)
|
f.pool = append(f.pool, c)
|
||||||
if f.opt.IdleTimeout > 0 {
|
|
||||||
f.drain.Reset(time.Duration(f.opt.IdleTimeout)) // nudge on the pool emptying timer
|
|
||||||
}
|
|
||||||
f.poolMu.Unlock()
|
f.poolMu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drain the pool of any connections
|
|
||||||
func (f *Fs) drainPool(ctx context.Context) (err error) {
|
|
||||||
f.poolMu.Lock()
|
|
||||||
defer f.poolMu.Unlock()
|
|
||||||
if transfers := f.getTransfers(); transfers != 0 {
|
|
||||||
fs.Debugf(f, "Not closing %d unused connections as %d transfers in progress", len(f.pool), transfers)
|
|
||||||
if f.opt.IdleTimeout > 0 {
|
|
||||||
f.drain.Reset(time.Duration(f.opt.IdleTimeout)) // nudge on the pool emptying timer
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if f.opt.IdleTimeout > 0 {
|
|
||||||
f.drain.Stop()
|
|
||||||
}
|
|
||||||
if len(f.pool) != 0 {
|
|
||||||
fs.Debugf(f, "closing %d unused connections", len(f.pool))
|
|
||||||
}
|
|
||||||
for i, c := range f.pool {
|
|
||||||
if cErr := c.closed(); cErr == nil {
|
|
||||||
cErr = c.close()
|
|
||||||
if cErr != nil {
|
|
||||||
err = cErr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
f.pool[i] = nil
|
|
||||||
}
|
|
||||||
f.pool = nil
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewFs creates a new Fs object from the name and root. It connects to
|
// NewFs creates a new Fs object from the name and root. It connects to
|
||||||
// the host specified in the config file.
|
// the host specified in the config file.
|
||||||
func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, error) {
|
func NewFs(name, root string, m configmap.Mapper) (fs.Fs, error) {
|
||||||
// This will hold the Fs object. We need to create it here
|
ctx := context.Background()
|
||||||
// so we can refer to it in the SSH callback, but it's populated
|
|
||||||
// in NewFsWithConnection
|
|
||||||
f := &Fs{
|
|
||||||
ci: fs.GetConfig(ctx),
|
|
||||||
}
|
|
||||||
// Parse config into Options struct
|
// Parse config into Options struct
|
||||||
opt := new(Options)
|
opt := new(Options)
|
||||||
err := configstruct.Set(m, opt)
|
err := configstruct.Set(m, opt)
|
||||||
@@ -544,21 +423,12 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
if opt.Port == "" {
|
if opt.Port == "" {
|
||||||
opt.Port = "22"
|
opt.Port = "22"
|
||||||
}
|
}
|
||||||
|
|
||||||
sshConfig := &ssh.ClientConfig{
|
sshConfig := &ssh.ClientConfig{
|
||||||
User: opt.User,
|
User: opt.User,
|
||||||
Auth: []ssh.AuthMethod{},
|
Auth: []ssh.AuthMethod{},
|
||||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||||
Timeout: f.ci.ConnectTimeout,
|
Timeout: fs.Config.ConnectTimeout,
|
||||||
ClientVersion: "SSH-2.0-" + f.ci.UserAgent,
|
ClientVersion: "SSH-2.0-" + fs.Config.UserAgent,
|
||||||
}
|
|
||||||
|
|
||||||
if opt.KnownHostsFile != "" {
|
|
||||||
hostcallback, err := knownhosts.New(opt.KnownHostsFile)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "couldn't parse known_hosts_file")
|
|
||||||
}
|
|
||||||
sshConfig.HostKeyCallback = hostcallback
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if opt.UseInsecureCipher {
|
if opt.UseInsecureCipher {
|
||||||
@@ -568,7 +438,6 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
keyFile := env.ShellExpand(opt.KeyFile)
|
keyFile := env.ShellExpand(opt.KeyFile)
|
||||||
pubkeyFile := env.ShellExpand(opt.PubKeyFile)
|
|
||||||
//keyPem := env.ShellExpand(opt.KeyPem)
|
//keyPem := env.ShellExpand(opt.KeyPem)
|
||||||
// Add ssh agent-auth if no password or file or key PEM specified
|
// Add ssh agent-auth if no password or file or key PEM specified
|
||||||
if (opt.Pass == "" && keyFile == "" && !opt.AskPassword && opt.KeyPem == "") || opt.KeyUseAgent {
|
if (opt.Pass == "" && keyFile == "" && !opt.AskPassword && opt.KeyPem == "") || opt.KeyUseAgent {
|
||||||
@@ -638,39 +507,8 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to parse private key file")
|
return nil, errors.Wrap(err, "failed to parse private key file")
|
||||||
}
|
}
|
||||||
|
|
||||||
// If a public key has been specified then use that
|
|
||||||
if pubkeyFile != "" {
|
|
||||||
certfile, err := ioutil.ReadFile(pubkeyFile)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "unable to read cert file")
|
|
||||||
}
|
|
||||||
|
|
||||||
pk, _, _, _, err := ssh.ParseAuthorizedKey(certfile)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "unable to parse cert file")
|
|
||||||
}
|
|
||||||
|
|
||||||
// And the signer for this, which includes the private key signer
|
|
||||||
// This is what we'll pass to the ssh client.
|
|
||||||
// Normally the ssh client will use the public key built
|
|
||||||
// into the private key, but we need to tell it to use the user
|
|
||||||
// specified public key cert. This signer is specific to the
|
|
||||||
// cert and will include the private key signer. Now ssh
|
|
||||||
// knows everything it needs.
|
|
||||||
cert, ok := pk.(*ssh.Certificate)
|
|
||||||
if !ok {
|
|
||||||
return nil, errors.New("public key file is not a certificate file: " + pubkeyFile)
|
|
||||||
}
|
|
||||||
pubsigner, err := ssh.NewCertSigner(cert, signer)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "error generating cert signer")
|
|
||||||
}
|
|
||||||
sshConfig.Auth = append(sshConfig.Auth, ssh.PublicKeys(pubsigner))
|
|
||||||
} else {
|
|
||||||
sshConfig.Auth = append(sshConfig.Auth, ssh.PublicKeys(signer))
|
sshConfig.Auth = append(sshConfig.Auth, ssh.PublicKeys(signer))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Auth from password if specified
|
// Auth from password if specified
|
||||||
if opt.Pass != "" {
|
if opt.Pass != "" {
|
||||||
@@ -678,81 +516,39 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
sshConfig.Auth = append(sshConfig.Auth,
|
sshConfig.Auth = append(sshConfig.Auth, ssh.Password(clearpass))
|
||||||
ssh.Password(clearpass),
|
|
||||||
ssh.KeyboardInteractive(func(user, instruction string, questions []string, echos []bool) ([]string, error) {
|
|
||||||
return f.keyboardInteractiveReponse(user, instruction, questions, echos, clearpass)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config for password if none was defined and we're allowed to
|
// Ask for password if none was defined and we're allowed to
|
||||||
// We don't ask now; we ask if the ssh connection succeeds
|
|
||||||
if opt.Pass == "" && opt.AskPassword {
|
if opt.Pass == "" && opt.AskPassword {
|
||||||
sshConfig.Auth = append(sshConfig.Auth,
|
|
||||||
ssh.PasswordCallback(f.getPass),
|
|
||||||
ssh.KeyboardInteractive(func(user, instruction string, questions []string, echos []bool) ([]string, error) {
|
|
||||||
pass, _ := f.getPass()
|
|
||||||
return f.keyboardInteractiveReponse(user, instruction, questions, echos, pass)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return NewFsWithConnection(ctx, f, name, root, m, opt, sshConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do the keyboard interactive challenge
|
|
||||||
//
|
|
||||||
// Just send the password back for all questions
|
|
||||||
func (f *Fs) keyboardInteractiveReponse(user, instruction string, questions []string, echos []bool, pass string) ([]string, error) {
|
|
||||||
fs.Debugf(f, "keyboard interactive auth requested")
|
|
||||||
answers := make([]string, len(questions))
|
|
||||||
for i := range answers {
|
|
||||||
answers[i] = pass
|
|
||||||
}
|
|
||||||
return answers, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we're in password mode and ssh connection succeeds then this
|
|
||||||
// callback is called. First time around we ask the user, and then
|
|
||||||
// save it so on reconnection we give back the previous string.
|
|
||||||
// This removes the ability to let the user correct a mistaken entry,
|
|
||||||
// but means that reconnects are transparent.
|
|
||||||
// We'll re-use config.Pass for this, 'cos we know it's not been
|
|
||||||
// specified.
|
|
||||||
func (f *Fs) getPass() (string, error) {
|
|
||||||
for f.savedpswd == "" {
|
|
||||||
_, _ = fmt.Fprint(os.Stderr, "Enter SFTP password: ")
|
_, _ = fmt.Fprint(os.Stderr, "Enter SFTP password: ")
|
||||||
f.savedpswd = config.ReadPassword()
|
clearpass := config.ReadPassword()
|
||||||
|
sshConfig.Auth = append(sshConfig.Auth, ssh.Password(clearpass))
|
||||||
}
|
}
|
||||||
return f.savedpswd, nil
|
|
||||||
|
return NewFsWithConnection(ctx, name, root, m, opt, sshConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewFsWithConnection creates a new Fs object from the name and root and an ssh.ClientConfig. It connects to
|
// NewFsWithConnection creates a new Fs object from the name and root and an ssh.ClientConfig. It connects to
|
||||||
// the host specified in the ssh.ClientConfig
|
// the host specified in the ssh.ClientConfig
|
||||||
func NewFsWithConnection(ctx context.Context, f *Fs, name string, root string, m configmap.Mapper, opt *Options, sshConfig *ssh.ClientConfig) (fs.Fs, error) {
|
func NewFsWithConnection(ctx context.Context, name string, root string, m configmap.Mapper, opt *Options, sshConfig *ssh.ClientConfig) (fs.Fs, error) {
|
||||||
// Populate the Filesystem Object
|
f := &Fs{
|
||||||
f.name = name
|
name: name,
|
||||||
f.root = root
|
root: root,
|
||||||
f.absRoot = root
|
absRoot: root,
|
||||||
f.opt = *opt
|
opt: *opt,
|
||||||
f.m = m
|
m: m,
|
||||||
f.config = sshConfig
|
config: sshConfig,
|
||||||
f.url = "sftp://" + opt.User + "@" + opt.Host + ":" + opt.Port + "/" + root
|
url: "sftp://" + opt.User + "@" + opt.Host + ":" + opt.Port + "/" + root,
|
||||||
f.mkdirLock = newStringLock()
|
mkdirLock: newStringLock(),
|
||||||
f.pacer = fs.NewPacer(ctx, pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant)))
|
pacer: fs.NewPacer(pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))),
|
||||||
f.savedpswd = ""
|
|
||||||
// set the pool drainer timer going
|
|
||||||
if f.opt.IdleTimeout > 0 {
|
|
||||||
f.drain = time.AfterFunc(time.Duration(opt.IdleTimeout), func() { _ = f.drainPool(ctx) })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
f.features = (&fs.Features{
|
f.features = (&fs.Features{
|
||||||
CanHaveEmptyDirectories: true,
|
CanHaveEmptyDirectories: true,
|
||||||
SlowHash: true,
|
SlowHash: true,
|
||||||
}).Fill(ctx, f)
|
}).Fill(f)
|
||||||
// Make a connection and pool it to return errors early
|
// Make a connection and pool it to return errors early
|
||||||
c, err := f.getSftpConnection(ctx)
|
c, err := f.getSftpConnection()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "NewFs")
|
return nil, errors.Wrap(err, "NewFs")
|
||||||
}
|
}
|
||||||
@@ -820,7 +616,7 @@ func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) {
|
|||||||
fs: f,
|
fs: f,
|
||||||
remote: remote,
|
remote: remote,
|
||||||
}
|
}
|
||||||
err := o.stat(ctx)
|
err := o.stat()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -829,11 +625,11 @@ func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) {
|
|||||||
|
|
||||||
// dirExists returns true,nil if the directory exists, false, nil if
|
// dirExists returns true,nil if the directory exists, false, nil if
|
||||||
// it doesn't or false, err
|
// it doesn't or false, err
|
||||||
func (f *Fs) dirExists(ctx context.Context, dir string) (bool, error) {
|
func (f *Fs) dirExists(dir string) (bool, error) {
|
||||||
if dir == "" {
|
if dir == "" {
|
||||||
dir = "."
|
dir = "."
|
||||||
}
|
}
|
||||||
c, err := f.getSftpConnection(ctx)
|
c, err := f.getSftpConnection()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, errors.Wrap(err, "dirExists")
|
return false, errors.Wrap(err, "dirExists")
|
||||||
}
|
}
|
||||||
@@ -862,7 +658,7 @@ func (f *Fs) dirExists(ctx context.Context, dir string) (bool, error) {
|
|||||||
// found.
|
// found.
|
||||||
func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) {
|
func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) {
|
||||||
root := path.Join(f.absRoot, dir)
|
root := path.Join(f.absRoot, dir)
|
||||||
ok, err := f.dirExists(ctx, root)
|
ok, err := f.dirExists(root)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "List failed")
|
return nil, errors.Wrap(err, "List failed")
|
||||||
}
|
}
|
||||||
@@ -873,7 +669,7 @@ func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err e
|
|||||||
if sftpDir == "" {
|
if sftpDir == "" {
|
||||||
sftpDir = "."
|
sftpDir = "."
|
||||||
}
|
}
|
||||||
c, err := f.getSftpConnection(ctx)
|
c, err := f.getSftpConnection()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "List")
|
return nil, errors.Wrap(err, "List")
|
||||||
}
|
}
|
||||||
@@ -892,7 +688,7 @@ func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err e
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
oldInfo := info
|
oldInfo := info
|
||||||
info, err = f.stat(ctx, remote)
|
info, err = f.stat(remote)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !os.IsNotExist(err) {
|
if !os.IsNotExist(err) {
|
||||||
fs.Errorf(remote, "stat of non-regular file failed: %v", err)
|
fs.Errorf(remote, "stat of non-regular file failed: %v", err)
|
||||||
@@ -917,7 +713,7 @@ func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err e
|
|||||||
|
|
||||||
// Put data from <in> into a new remote sftp file object described by <src.Remote()> and <src.ModTime(ctx)>
|
// Put data from <in> into a new remote sftp file object described by <src.Remote()> and <src.ModTime(ctx)>
|
||||||
func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
|
func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
|
||||||
err := f.mkParentDir(ctx, src.Remote())
|
err := f.mkParentDir(src.Remote())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "Put mkParentDir failed")
|
return nil, errors.Wrap(err, "Put mkParentDir failed")
|
||||||
}
|
}
|
||||||
@@ -940,19 +736,19 @@ func (f *Fs) PutStream(ctx context.Context, in io.Reader, src fs.ObjectInfo, opt
|
|||||||
|
|
||||||
// mkParentDir makes the parent of remote if necessary and any
|
// mkParentDir makes the parent of remote if necessary and any
|
||||||
// directories above that
|
// directories above that
|
||||||
func (f *Fs) mkParentDir(ctx context.Context, remote string) error {
|
func (f *Fs) mkParentDir(remote string) error {
|
||||||
parent := path.Dir(remote)
|
parent := path.Dir(remote)
|
||||||
return f.mkdir(ctx, path.Join(f.absRoot, parent))
|
return f.mkdir(path.Join(f.absRoot, parent))
|
||||||
}
|
}
|
||||||
|
|
||||||
// mkdir makes the directory and parents using native paths
|
// mkdir makes the directory and parents using native paths
|
||||||
func (f *Fs) mkdir(ctx context.Context, dirPath string) error {
|
func (f *Fs) mkdir(dirPath string) error {
|
||||||
f.mkdirLock.Lock(dirPath)
|
f.mkdirLock.Lock(dirPath)
|
||||||
defer f.mkdirLock.Unlock(dirPath)
|
defer f.mkdirLock.Unlock(dirPath)
|
||||||
if dirPath == "." || dirPath == "/" {
|
if dirPath == "." || dirPath == "/" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
ok, err := f.dirExists(ctx, dirPath)
|
ok, err := f.dirExists(dirPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "mkdir dirExists failed")
|
return errors.Wrap(err, "mkdir dirExists failed")
|
||||||
}
|
}
|
||||||
@@ -960,11 +756,11 @@ func (f *Fs) mkdir(ctx context.Context, dirPath string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
parent := path.Dir(dirPath)
|
parent := path.Dir(dirPath)
|
||||||
err = f.mkdir(ctx, parent)
|
err = f.mkdir(parent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
c, err := f.getSftpConnection(ctx)
|
c, err := f.getSftpConnection()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "mkdir")
|
return errors.Wrap(err, "mkdir")
|
||||||
}
|
}
|
||||||
@@ -979,7 +775,7 @@ func (f *Fs) mkdir(ctx context.Context, dirPath string) error {
|
|||||||
// Mkdir makes the root directory of the Fs object
|
// Mkdir makes the root directory of the Fs object
|
||||||
func (f *Fs) Mkdir(ctx context.Context, dir string) error {
|
func (f *Fs) Mkdir(ctx context.Context, dir string) error {
|
||||||
root := path.Join(f.absRoot, dir)
|
root := path.Join(f.absRoot, dir)
|
||||||
return f.mkdir(ctx, root)
|
return f.mkdir(root)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rmdir removes the root directory of the Fs object
|
// Rmdir removes the root directory of the Fs object
|
||||||
@@ -995,7 +791,7 @@ func (f *Fs) Rmdir(ctx context.Context, dir string) error {
|
|||||||
}
|
}
|
||||||
// Remove the directory
|
// Remove the directory
|
||||||
root := path.Join(f.absRoot, dir)
|
root := path.Join(f.absRoot, dir)
|
||||||
c, err := f.getSftpConnection(ctx)
|
c, err := f.getSftpConnection()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "Rmdir")
|
return errors.Wrap(err, "Rmdir")
|
||||||
}
|
}
|
||||||
@@ -1011,11 +807,11 @@ func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object,
|
|||||||
fs.Debugf(src, "Can't move - not same remote type")
|
fs.Debugf(src, "Can't move - not same remote type")
|
||||||
return nil, fs.ErrorCantMove
|
return nil, fs.ErrorCantMove
|
||||||
}
|
}
|
||||||
err := f.mkParentDir(ctx, remote)
|
err := f.mkParentDir(remote)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "Move mkParentDir failed")
|
return nil, errors.Wrap(err, "Move mkParentDir failed")
|
||||||
}
|
}
|
||||||
c, err := f.getSftpConnection(ctx)
|
c, err := f.getSftpConnection()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "Move")
|
return nil, errors.Wrap(err, "Move")
|
||||||
}
|
}
|
||||||
@@ -1035,7 +831,7 @@ func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DirMove moves src, srcRemote to this remote at dstRemote
|
// DirMove moves src, srcRemote to this remote at dstRemote
|
||||||
// using server-side move operations.
|
// using server side move operations.
|
||||||
//
|
//
|
||||||
// Will only be called if src.Fs().Name() == f.Name()
|
// Will only be called if src.Fs().Name() == f.Name()
|
||||||
//
|
//
|
||||||
@@ -1052,7 +848,7 @@ func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string
|
|||||||
dstPath := path.Join(f.absRoot, dstRemote)
|
dstPath := path.Join(f.absRoot, dstRemote)
|
||||||
|
|
||||||
// Check if destination exists
|
// Check if destination exists
|
||||||
ok, err := f.dirExists(ctx, dstPath)
|
ok, err := f.dirExists(dstPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "DirMove dirExists dst failed")
|
return errors.Wrap(err, "DirMove dirExists dst failed")
|
||||||
}
|
}
|
||||||
@@ -1061,13 +857,13 @@ func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Make sure the parent directory exists
|
// Make sure the parent directory exists
|
||||||
err = f.mkdir(ctx, path.Dir(dstPath))
|
err = f.mkdir(path.Dir(dstPath))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "DirMove mkParentDir dst failed")
|
return errors.Wrap(err, "DirMove mkParentDir dst failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do the move
|
// Do the move
|
||||||
c, err := f.getSftpConnection(ctx)
|
c, err := f.getSftpConnection()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "DirMove")
|
return errors.Wrap(err, "DirMove")
|
||||||
}
|
}
|
||||||
@@ -1083,8 +879,8 @@ func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string
|
|||||||
}
|
}
|
||||||
|
|
||||||
// run runds cmd on the remote end returning standard output
|
// run runds cmd on the remote end returning standard output
|
||||||
func (f *Fs) run(ctx context.Context, cmd string) ([]byte, error) {
|
func (f *Fs) run(cmd string) ([]byte, error) {
|
||||||
c, err := f.getSftpConnection(ctx)
|
c, err := f.getSftpConnection()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "run: get SFTP connection")
|
return nil, errors.Wrap(err, "run: get SFTP connection")
|
||||||
}
|
}
|
||||||
@@ -1092,7 +888,7 @@ func (f *Fs) run(ctx context.Context, cmd string) ([]byte, error) {
|
|||||||
|
|
||||||
session, err := c.sshClient.NewSession()
|
session, err := c.sshClient.NewSession()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "run: get SFTP session")
|
return nil, errors.Wrap(err, "run: get SFTP sessiion")
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = session.Close()
|
_ = session.Close()
|
||||||
@@ -1112,7 +908,6 @@ func (f *Fs) run(ctx context.Context, cmd string) ([]byte, error) {
|
|||||||
|
|
||||||
// Hashes returns the supported hash types of the filesystem
|
// Hashes returns the supported hash types of the filesystem
|
||||||
func (f *Fs) Hashes() hash.Set {
|
func (f *Fs) Hashes() hash.Set {
|
||||||
ctx := context.TODO()
|
|
||||||
if f.opt.DisableHashCheck {
|
if f.opt.DisableHashCheck {
|
||||||
return hash.Set(hash.None)
|
return hash.Set(hash.None)
|
||||||
}
|
}
|
||||||
@@ -1131,7 +926,7 @@ func (f *Fs) Hashes() hash.Set {
|
|||||||
}
|
}
|
||||||
*changed = true
|
*changed = true
|
||||||
for _, command := range commands {
|
for _, command := range commands {
|
||||||
output, err := f.run(ctx, command)
|
output, err := f.run(command)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -1176,7 +971,7 @@ func (f *Fs) About(ctx context.Context) (*fs.Usage, error) {
|
|||||||
if len(escapedPath) == 0 {
|
if len(escapedPath) == 0 {
|
||||||
escapedPath = "/"
|
escapedPath = "/"
|
||||||
}
|
}
|
||||||
stdout, err := f.run(ctx, "df -k "+escapedPath)
|
stdout, err := f.run("df -k " + escapedPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "your remote may not support About")
|
return nil, errors.Wrap(err, "your remote may not support About")
|
||||||
}
|
}
|
||||||
@@ -1195,12 +990,6 @@ func (f *Fs) About(ctx context.Context) (*fs.Usage, error) {
|
|||||||
return usage, nil
|
return usage, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shutdown the backend, closing any background tasks and any
|
|
||||||
// cached connections.
|
|
||||||
func (f *Fs) Shutdown(ctx context.Context) error {
|
|
||||||
return f.drainPool(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fs is the filesystem this remote sftp file object is located within
|
// Fs is the filesystem this remote sftp file object is located within
|
||||||
func (o *Object) Fs() fs.Info {
|
func (o *Object) Fs() fs.Info {
|
||||||
return o.fs
|
return o.fs
|
||||||
@@ -1245,7 +1034,7 @@ func (o *Object) Hash(ctx context.Context, r hash.Type) (string, error) {
|
|||||||
return "", hash.ErrUnsupported
|
return "", hash.ErrUnsupported
|
||||||
}
|
}
|
||||||
|
|
||||||
c, err := o.fs.getSftpConnection(ctx)
|
c, err := o.fs.getSftpConnection()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.Wrap(err, "Hash get SFTP connection")
|
return "", errors.Wrap(err, "Hash get SFTP connection")
|
||||||
}
|
}
|
||||||
@@ -1353,8 +1142,8 @@ func (o *Object) setMetadata(info os.FileInfo) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// statRemote stats the file or directory at the remote given
|
// statRemote stats the file or directory at the remote given
|
||||||
func (f *Fs) stat(ctx context.Context, remote string) (info os.FileInfo, err error) {
|
func (f *Fs) stat(remote string) (info os.FileInfo, err error) {
|
||||||
c, err := f.getSftpConnection(ctx)
|
c, err := f.getSftpConnection()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "stat")
|
return nil, errors.Wrap(err, "stat")
|
||||||
}
|
}
|
||||||
@@ -1365,8 +1154,8 @@ func (f *Fs) stat(ctx context.Context, remote string) (info os.FileInfo, err err
|
|||||||
}
|
}
|
||||||
|
|
||||||
// stat updates the info in the Object
|
// stat updates the info in the Object
|
||||||
func (o *Object) stat(ctx context.Context) error {
|
func (o *Object) stat() error {
|
||||||
info, err := o.fs.stat(ctx, o.remote)
|
info, err := o.fs.stat(o.remote)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return fs.ErrorObjectNotFound
|
return fs.ErrorObjectNotFound
|
||||||
@@ -1384,10 +1173,8 @@ func (o *Object) stat(ctx context.Context) error {
|
|||||||
//
|
//
|
||||||
// it also updates the info field
|
// it also updates the info field
|
||||||
func (o *Object) SetModTime(ctx context.Context, modTime time.Time) error {
|
func (o *Object) SetModTime(ctx context.Context, modTime time.Time) error {
|
||||||
if !o.fs.opt.SetModTime {
|
if o.fs.opt.SetModTime {
|
||||||
return nil
|
c, err := o.fs.getSftpConnection()
|
||||||
}
|
|
||||||
c, err := o.fs.getSftpConnection(ctx)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "SetModTime")
|
return errors.Wrap(err, "SetModTime")
|
||||||
}
|
}
|
||||||
@@ -1396,36 +1183,33 @@ func (o *Object) SetModTime(ctx context.Context, modTime time.Time) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "SetModTime failed")
|
return errors.Wrap(err, "SetModTime failed")
|
||||||
}
|
}
|
||||||
err = o.stat(ctx)
|
}
|
||||||
|
err := o.stat()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "SetModTime stat failed")
|
return errors.Wrap(err, "SetModTime stat failed")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Storable returns whether the remote sftp file is a regular file (not a directory, symbolic link, block device, character device, named pipe, etc.)
|
// Storable returns whether the remote sftp file is a regular file (not a directory, symbolic link, block device, character device, named pipe, etc)
|
||||||
func (o *Object) Storable() bool {
|
func (o *Object) Storable() bool {
|
||||||
return o.mode.IsRegular()
|
return o.mode.IsRegular()
|
||||||
}
|
}
|
||||||
|
|
||||||
// objectReader represents a file open for reading on the SFTP server
|
// objectReader represents a file open for reading on the SFTP server
|
||||||
type objectReader struct {
|
type objectReader struct {
|
||||||
f *Fs
|
|
||||||
sftpFile *sftp.File
|
sftpFile *sftp.File
|
||||||
pipeReader *io.PipeReader
|
pipeReader *io.PipeReader
|
||||||
done chan struct{}
|
done chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fs) newObjectReader(sftpFile *sftp.File) *objectReader {
|
func newObjectReader(sftpFile *sftp.File) *objectReader {
|
||||||
pipeReader, pipeWriter := io.Pipe()
|
pipeReader, pipeWriter := io.Pipe()
|
||||||
file := &objectReader{
|
file := &objectReader{
|
||||||
f: f,
|
|
||||||
sftpFile: sftpFile,
|
sftpFile: sftpFile,
|
||||||
pipeReader: pipeReader,
|
pipeReader: pipeReader,
|
||||||
done: make(chan struct{}),
|
done: make(chan struct{}),
|
||||||
}
|
}
|
||||||
// Show connection in use
|
|
||||||
f.addTransfer()
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
// Use sftpFile.WriteTo to pump data so that it gets a
|
// Use sftpFile.WriteTo to pump data so that it gets a
|
||||||
@@ -1455,8 +1239,6 @@ func (file *objectReader) Close() (err error) {
|
|||||||
_ = file.pipeReader.Close()
|
_ = file.pipeReader.Close()
|
||||||
// Wait for the background process to finish
|
// Wait for the background process to finish
|
||||||
<-file.done
|
<-file.done
|
||||||
// Show connection no longer in use
|
|
||||||
file.f.removeTransfer()
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1475,7 +1257,7 @@ func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.Read
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c, err := o.fs.getSftpConnection(ctx)
|
c, err := o.fs.getSftpConnection()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "Open")
|
return nil, errors.Wrap(err, "Open")
|
||||||
}
|
}
|
||||||
@@ -1490,18 +1272,16 @@ func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.Read
|
|||||||
return nil, errors.Wrap(err, "Open Seek failed")
|
return nil, errors.Wrap(err, "Open Seek failed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
in = readers.NewLimitedReadCloser(o.fs.newObjectReader(sftpFile), limit)
|
in = readers.NewLimitedReadCloser(newObjectReader(sftpFile), limit)
|
||||||
return in, nil
|
return in, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update a remote sftp file using the data <in> and ModTime from <src>
|
// Update a remote sftp file using the data <in> and ModTime from <src>
|
||||||
func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) error {
|
func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) error {
|
||||||
o.fs.addTransfer() // Show transfer in progress
|
|
||||||
defer o.fs.removeTransfer()
|
|
||||||
// Clear the hash cache since we are about to update the object
|
// Clear the hash cache since we are about to update the object
|
||||||
o.md5sum = nil
|
o.md5sum = nil
|
||||||
o.sha1sum = nil
|
o.sha1sum = nil
|
||||||
c, err := o.fs.getSftpConnection(ctx)
|
c, err := o.fs.getSftpConnection()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "Update")
|
return errors.Wrap(err, "Update")
|
||||||
}
|
}
|
||||||
@@ -1512,7 +1292,7 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
|
|||||||
}
|
}
|
||||||
// remove the file if upload failed
|
// remove the file if upload failed
|
||||||
remove := func() {
|
remove := func() {
|
||||||
c, removeErr := o.fs.getSftpConnection(ctx)
|
c, removeErr := o.fs.getSftpConnection()
|
||||||
if removeErr != nil {
|
if removeErr != nil {
|
||||||
fs.Debugf(src, "Failed to open new SSH connection for delete: %v", removeErr)
|
fs.Debugf(src, "Failed to open new SSH connection for delete: %v", removeErr)
|
||||||
return
|
return
|
||||||
@@ -1535,34 +1315,16 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
|
|||||||
remove()
|
remove()
|
||||||
return errors.Wrap(err, "Update Close failed")
|
return errors.Wrap(err, "Update Close failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the mod time - this stats the object if o.fs.opt.SetModTime == true
|
|
||||||
err = o.SetModTime(ctx, src.ModTime(ctx))
|
err = o.SetModTime(ctx, src.ModTime(ctx))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "Update SetModTime failed")
|
return errors.Wrap(err, "Update SetModTime failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stat the file after the upload to read its stats back if o.fs.opt.SetModTime == false
|
|
||||||
if !o.fs.opt.SetModTime {
|
|
||||||
err = o.stat(ctx)
|
|
||||||
if err == fs.ErrorObjectNotFound {
|
|
||||||
// In the specific case of o.fs.opt.SetModTime == false
|
|
||||||
// if the object wasn't found then don't return an error
|
|
||||||
fs.Debugf(o, "Not found after upload with set_modtime=false so returning best guess")
|
|
||||||
o.modTime = src.ModTime(ctx)
|
|
||||||
o.size = src.Size()
|
|
||||||
o.mode = os.FileMode(0666) // regular file
|
|
||||||
} else if err != nil {
|
|
||||||
return errors.Wrap(err, "Update stat failed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove a remote sftp file object
|
// Remove a remote sftp file object
|
||||||
func (o *Object) Remove(ctx context.Context) error {
|
func (o *Object) Remove(ctx context.Context) error {
|
||||||
c, err := o.fs.getSftpConnection(ctx)
|
c, err := o.fs.getSftpConnection()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "Remove")
|
return errors.Wrap(err, "Remove")
|
||||||
}
|
}
|
||||||
@@ -1578,6 +1340,5 @@ var (
|
|||||||
_ fs.Mover = &Fs{}
|
_ fs.Mover = &Fs{}
|
||||||
_ fs.DirMover = &Fs{}
|
_ fs.DirMover = &Fs{}
|
||||||
_ fs.Abouter = &Fs{}
|
_ fs.Abouter = &Fs{}
|
||||||
_ fs.Shutdowner = &Fs{}
|
|
||||||
_ fs.Object = &Object{}
|
_ fs.Object = &Object{}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ type UploadSpecification struct {
|
|||||||
ChunkURI string `json:"ChunkUri"` // Specifies the URI the client must send the file data to
|
ChunkURI string `json:"ChunkUri"` // Specifies the URI the client must send the file data to
|
||||||
FinishURI string `json:"FinishUri"` // If provided, specifies the final call the client must perform to finish the upload process
|
FinishURI string `json:"FinishUri"` // If provided, specifies the final call the client must perform to finish the upload process
|
||||||
ProgressData string `json:"ProgressData"` // Allows the client to check progress of standard uploads
|
ProgressData string `json:"ProgressData"` // Allows the client to check progress of standard uploads
|
||||||
IsResume bool `json:"IsResume"` // Specifies a Resumable upload is supported.
|
IsResume bool `json:"IsResume"` // Specifies a Resumable upload is supproted.
|
||||||
ResumeIndex int64 `json:"ResumeIndex"` // Specifies the initial index for resuming, if IsResume is true.
|
ResumeIndex int64 `json:"ResumeIndex"` // Specifies the initial index for resuming, if IsResume is true.
|
||||||
ResumeOffset int64 `json:"ResumeOffset"` // Specifies the initial file offset by bytes, if IsResume is true
|
ResumeOffset int64 `json:"ResumeOffset"` // Specifies the initial file offset by bytes, if IsResume is true
|
||||||
ResumeFileHash string `json:"ResumeFileHash"` // Specifies the MD5 hash of the first ResumeOffset bytes of the partial file found at the server
|
ResumeFileHash string `json:"ResumeFileHash"` // Specifies the MD5 hash of the first ResumeOffset bytes of the partial file found at the server
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ func init() {
|
|||||||
Name: "sharefile",
|
Name: "sharefile",
|
||||||
Description: "Citrix Sharefile",
|
Description: "Citrix Sharefile",
|
||||||
NewFs: NewFs,
|
NewFs: NewFs,
|
||||||
Config: func(ctx context.Context, name string, m configmap.Mapper) {
|
Config: func(name string, m configmap.Mapper) {
|
||||||
oauthConfig := newOauthConfig("")
|
oauthConfig := newOauthConfig("")
|
||||||
checkAuth := func(oauthConfig *oauth2.Config, auth *oauthutil.AuthResult) error {
|
checkAuth := func(oauthConfig *oauth2.Config, auth *oauthutil.AuthResult) error {
|
||||||
if auth == nil || auth.Form == nil {
|
if auth == nil || auth.Form == nil {
|
||||||
@@ -155,7 +155,7 @@ func init() {
|
|||||||
opt := oauthutil.Options{
|
opt := oauthutil.Options{
|
||||||
CheckAuth: checkAuth,
|
CheckAuth: checkAuth,
|
||||||
}
|
}
|
||||||
err := oauthutil.Config(ctx, "sharefile", name, m, oauthConfig, &opt)
|
err := oauthutil.Config("sharefile", name, m, oauthConfig, &opt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to configure token: %v", err)
|
log.Fatalf("Failed to configure token: %v", err)
|
||||||
}
|
}
|
||||||
@@ -237,7 +237,6 @@ type Fs struct {
|
|||||||
name string // name of this remote
|
name string // name of this remote
|
||||||
root string // the path we are working on
|
root string // the path we are working on
|
||||||
opt Options // parsed options
|
opt Options // parsed options
|
||||||
ci *fs.ConfigInfo // global config
|
|
||||||
features *fs.Features // optional features
|
features *fs.Features // optional features
|
||||||
srv *rest.Client // the connection to the server
|
srv *rest.Client // the connection to the server
|
||||||
dirCache *dircache.DirCache // Map of directory path to directory id
|
dirCache *dircache.DirCache // Map of directory path to directory id
|
||||||
@@ -299,10 +298,7 @@ var retryErrorCodes = []int{
|
|||||||
|
|
||||||
// shouldRetry returns a boolean as to whether this resp and err
|
// shouldRetry returns a boolean as to whether this resp and err
|
||||||
// deserve to be retried. It returns the err as a convenience
|
// deserve to be retried. It returns the err as a convenience
|
||||||
func shouldRetry(ctx context.Context, resp *http.Response, err error) (bool, error) {
|
func shouldRetry(resp *http.Response, err error) (bool, error) {
|
||||||
if fserrors.ContextError(ctx, &err) {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
return fserrors.ShouldRetry(err) || fserrors.ShouldRetryHTTP(resp, retryErrorCodes), err
|
return fserrors.ShouldRetry(err) || fserrors.ShouldRetryHTTP(resp, retryErrorCodes), err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,7 +323,7 @@ func (f *Fs) readMetaDataForIDPath(ctx context.Context, id, path string, directo
|
|||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, &item)
|
resp, err = f.srv.CallJSON(ctx, &opts, nil, &item)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if resp != nil && resp.StatusCode == http.StatusNotFound {
|
if resp != nil && resp.StatusCode == http.StatusNotFound {
|
||||||
@@ -414,7 +410,8 @@ func (f *Fs) setUploadCutoff(cs fs.SizeSuffix) (old fs.SizeSuffix, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewFs constructs an Fs from the path, container:path
|
// NewFs constructs an Fs from the path, container:path
|
||||||
func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, error) {
|
func NewFs(name, root string, m configmap.Mapper) (fs.Fs, error) {
|
||||||
|
ctx := context.Background()
|
||||||
// Parse config into Options struct
|
// Parse config into Options struct
|
||||||
opt := new(Options)
|
opt := new(Options)
|
||||||
err := configstruct.Set(m, opt)
|
err := configstruct.Set(m, opt)
|
||||||
@@ -440,25 +437,23 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
oauthConfig := newOauthConfig(opt.Endpoint + tokenPath)
|
oauthConfig := newOauthConfig(opt.Endpoint + tokenPath)
|
||||||
var client *http.Client
|
var client *http.Client
|
||||||
var ts *oauthutil.TokenSource
|
var ts *oauthutil.TokenSource
|
||||||
client, ts, err = oauthutil.NewClient(ctx, name, m, oauthConfig)
|
client, ts, err = oauthutil.NewClient(name, m, oauthConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to configure sharefile")
|
return nil, errors.Wrap(err, "failed to configure sharefile")
|
||||||
}
|
}
|
||||||
|
|
||||||
ci := fs.GetConfig(ctx)
|
|
||||||
f := &Fs{
|
f := &Fs{
|
||||||
name: name,
|
name: name,
|
||||||
root: root,
|
root: root,
|
||||||
opt: *opt,
|
opt: *opt,
|
||||||
ci: ci,
|
|
||||||
srv: rest.NewClient(client).SetRoot(opt.Endpoint + apiPath),
|
srv: rest.NewClient(client).SetRoot(opt.Endpoint + apiPath),
|
||||||
pacer: fs.NewPacer(ctx, pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))),
|
pacer: fs.NewPacer(pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))),
|
||||||
}
|
}
|
||||||
f.features = (&fs.Features{
|
f.features = (&fs.Features{
|
||||||
CaseInsensitive: true,
|
CaseInsensitive: true,
|
||||||
CanHaveEmptyDirectories: true,
|
CanHaveEmptyDirectories: true,
|
||||||
ReadMimeType: false,
|
ReadMimeType: false,
|
||||||
}).Fill(ctx, f)
|
}).Fill(f)
|
||||||
f.srv.SetErrorHandler(errorHandler)
|
f.srv.SetErrorHandler(errorHandler)
|
||||||
f.fillBufferTokens()
|
f.fillBufferTokens()
|
||||||
|
|
||||||
@@ -523,7 +518,7 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
f.features.Fill(ctx, &tempF)
|
f.features.Fill(&tempF)
|
||||||
// XXX: update the old f here instead of returning tempF, since
|
// XXX: update the old f here instead of returning tempF, since
|
||||||
// `features` were already filled with functions having *f as a receiver.
|
// `features` were already filled with functions having *f as a receiver.
|
||||||
// See https://github.com/rclone/rclone/issues/2182
|
// See https://github.com/rclone/rclone/issues/2182
|
||||||
@@ -537,8 +532,8 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
|
|
||||||
// Fill up (or reset) the buffer tokens
|
// Fill up (or reset) the buffer tokens
|
||||||
func (f *Fs) fillBufferTokens() {
|
func (f *Fs) fillBufferTokens() {
|
||||||
f.bufferTokens = make(chan []byte, f.ci.Transfers)
|
f.bufferTokens = make(chan []byte, fs.Config.Transfers)
|
||||||
for i := 0; i < f.ci.Transfers; i++ {
|
for i := 0; i < fs.Config.Transfers; i++ {
|
||||||
f.bufferTokens <- nil
|
f.bufferTokens <- nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -634,7 +629,7 @@ func (f *Fs) CreateDir(ctx context.Context, pathID, leaf string) (newID string,
|
|||||||
}
|
}
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, &req, &info)
|
resp, err = f.srv.CallJSON(ctx, &opts, &req, &info)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.Wrap(err, "CreateDir")
|
return "", errors.Wrap(err, "CreateDir")
|
||||||
@@ -666,7 +661,7 @@ func (f *Fs) listAll(ctx context.Context, dirID string, directoriesOnly bool, fi
|
|||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return found, errors.Wrap(err, "couldn't list files")
|
return found, errors.Wrap(err, "couldn't list files")
|
||||||
@@ -915,7 +910,7 @@ func (f *Fs) updateItem(ctx context.Context, id, leaf, directoryID string, modTi
|
|||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, &update, &info)
|
resp, err = f.srv.CallJSON(ctx, &opts, &update, &info)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -969,7 +964,7 @@ func (f *Fs) move(ctx context.Context, isFile bool, id, oldLeaf, newLeaf, oldDir
|
|||||||
return item, nil
|
return item, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move src to this remote using server-side move operations.
|
// Move src to this remote using server side move operations.
|
||||||
//
|
//
|
||||||
// This is stored with the remote path given
|
// This is stored with the remote path given
|
||||||
//
|
//
|
||||||
@@ -1011,7 +1006,7 @@ func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DirMove moves src, srcRemote to this remote at dstRemote
|
// DirMove moves src, srcRemote to this remote at dstRemote
|
||||||
// using server-side move operations.
|
// using server side move operations.
|
||||||
//
|
//
|
||||||
// Will only be called if src.Fs().Name() == f.Name()
|
// Will only be called if src.Fs().Name() == f.Name()
|
||||||
//
|
//
|
||||||
@@ -1039,7 +1034,7 @@ func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy src to this remote using server-side copy operations.
|
// Copy src to this remote using server side copy operations.
|
||||||
//
|
//
|
||||||
// This is stored with the remote path given
|
// This is stored with the remote path given
|
||||||
//
|
//
|
||||||
@@ -1095,7 +1090,7 @@ func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (dst fs.Obj
|
|||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return nil, errors.Wrap(err, "copy: failed to examine destination dir")
|
return nil, errors.Wrap(err, "copy: failed to examine destination dir")
|
||||||
} else {
|
} else {
|
||||||
// otherwise need to copy via a temporary directory
|
// otherwise need to copy via a temporary directlry
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1136,7 +1131,7 @@ func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (dst fs.Obj
|
|||||||
var info *api.Item
|
var info *api.Item
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, &info)
|
resp, err = f.srv.CallJSON(ctx, &opts, nil, &info)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -1297,7 +1292,7 @@ func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.Read
|
|||||||
var dl api.DownloadSpecification
|
var dl api.DownloadSpecification
|
||||||
err = o.fs.pacer.Call(func() (bool, error) {
|
err = o.fs.pacer.Call(func() (bool, error) {
|
||||||
resp, err = o.fs.srv.CallJSON(ctx, &opts, nil, &dl)
|
resp, err = o.fs.srv.CallJSON(ctx, &opts, nil, &dl)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "open: fetch download specification")
|
return nil, errors.Wrap(err, "open: fetch download specification")
|
||||||
@@ -1312,7 +1307,7 @@ func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.Read
|
|||||||
}
|
}
|
||||||
err = o.fs.pacer.Call(func() (bool, error) {
|
err = o.fs.pacer.Call(func() (bool, error) {
|
||||||
resp, err = o.fs.srv.Call(ctx, &opts)
|
resp, err = o.fs.srv.Call(ctx, &opts)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "open")
|
return nil, errors.Wrap(err, "open")
|
||||||
@@ -1344,7 +1339,7 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
|
|||||||
Overwrite: true,
|
Overwrite: true,
|
||||||
CreatedDate: modTime,
|
CreatedDate: modTime,
|
||||||
ModifiedDate: modTime,
|
ModifiedDate: modTime,
|
||||||
Tool: o.fs.ci.UserAgent,
|
Tool: fs.Config.UserAgent,
|
||||||
}
|
}
|
||||||
|
|
||||||
if isLargeFile {
|
if isLargeFile {
|
||||||
@@ -1354,7 +1349,7 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
|
|||||||
} else {
|
} else {
|
||||||
// otherwise use threaded which is more efficient
|
// otherwise use threaded which is more efficient
|
||||||
req.Method = "threaded"
|
req.Method = "threaded"
|
||||||
req.ThreadCount = &o.fs.ci.Transfers
|
req.ThreadCount = &fs.Config.Transfers
|
||||||
req.Filesize = &size
|
req.Filesize = &size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1368,7 +1363,7 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
|
|||||||
}
|
}
|
||||||
err = o.fs.pacer.Call(func() (bool, error) {
|
err = o.fs.pacer.Call(func() (bool, error) {
|
||||||
resp, err = o.fs.srv.CallJSON(ctx, &opts, &req, &info)
|
resp, err = o.fs.srv.CallJSON(ctx, &opts, &req, &info)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "upload get specification")
|
return errors.Wrap(err, "upload get specification")
|
||||||
@@ -1393,7 +1388,7 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
|
|||||||
var finish api.UploadFinishResponse
|
var finish api.UploadFinishResponse
|
||||||
err = o.fs.pacer.CallNoRetry(func() (bool, error) {
|
err = o.fs.pacer.CallNoRetry(func() (bool, error) {
|
||||||
resp, err = o.fs.srv.CallJSON(ctx, &opts, nil, &finish)
|
resp, err = o.fs.srv.CallJSON(ctx, &opts, nil, &finish)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "upload file")
|
return errors.Wrap(err, "upload file")
|
||||||
@@ -1429,7 +1424,7 @@ func (f *Fs) remove(ctx context.Context, id string) (err error) {
|
|||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.Call(ctx, &opts)
|
resp, err = f.srv.Call(ctx, &opts)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "remove")
|
return errors.Wrap(err, "remove")
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ type largeUpload struct {
|
|||||||
wrap accounting.WrapFn // account parts being transferred
|
wrap accounting.WrapFn // account parts being transferred
|
||||||
size int64 // total size
|
size int64 // total size
|
||||||
parts int64 // calculated number of parts, if known
|
parts int64 // calculated number of parts, if known
|
||||||
info *api.UploadSpecification // where to post chunks, etc.
|
info *api.UploadSpecification // where to post chunks etc
|
||||||
threads int // number of threads to use in upload
|
threads int // number of threads to use in upload
|
||||||
streamed bool // set if using streamed upload
|
streamed bool // set if using streamed upload
|
||||||
}
|
}
|
||||||
@@ -58,7 +58,7 @@ func (f *Fs) newLargeUpload(ctx context.Context, o *Object, in io.Reader, src fs
|
|||||||
return nil, errors.Errorf("can't use method %q with newLargeUpload", info.Method)
|
return nil, errors.Errorf("can't use method %q with newLargeUpload", info.Method)
|
||||||
}
|
}
|
||||||
|
|
||||||
threads := f.ci.Transfers
|
threads := fs.Config.Transfers
|
||||||
if threads > info.MaxNumberOfThreads {
|
if threads > info.MaxNumberOfThreads {
|
||||||
threads = info.MaxNumberOfThreads
|
threads = info.MaxNumberOfThreads
|
||||||
}
|
}
|
||||||
@@ -155,7 +155,7 @@ func (up *largeUpload) finish(ctx context.Context) error {
|
|||||||
err := up.f.pacer.Call(func() (bool, error) {
|
err := up.f.pacer.Call(func() (bool, error) {
|
||||||
resp, err := up.f.srv.Call(ctx, &opts)
|
resp, err := up.f.srv.Call(ctx, &opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
}
|
}
|
||||||
respBody, err = rest.ReadBody(resp)
|
respBody, err = rest.ReadBody(resp)
|
||||||
// retry all errors now that the multipart upload has started
|
// retry all errors now that the multipart upload has started
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ func init() {
|
|||||||
Name: "sugarsync",
|
Name: "sugarsync",
|
||||||
Description: "Sugarsync",
|
Description: "Sugarsync",
|
||||||
NewFs: NewFs,
|
NewFs: NewFs,
|
||||||
Config: func(ctx context.Context, name string, m configmap.Mapper) {
|
Config: func(name string, m configmap.Mapper) {
|
||||||
opt := new(Options)
|
opt := new(Options)
|
||||||
err := configstruct.Set(m, opt)
|
err := configstruct.Set(m, opt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -85,7 +85,7 @@ func init() {
|
|||||||
|
|
||||||
if opt.RefreshToken != "" {
|
if opt.RefreshToken != "" {
|
||||||
fmt.Printf("Already have a token - refresh?\n")
|
fmt.Printf("Already have a token - refresh?\n")
|
||||||
if !config.ConfirmWithConfig(ctx, m, "config_refresh_token", true) {
|
if !config.ConfirmWithConfig(m, "config_refresh_token", true) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -106,12 +106,12 @@ func init() {
|
|||||||
Method: "POST",
|
Method: "POST",
|
||||||
Path: "/app-authorization",
|
Path: "/app-authorization",
|
||||||
}
|
}
|
||||||
srv := rest.NewClient(fshttp.NewClient(ctx)).SetRoot(rootURL) // FIXME
|
srv := rest.NewClient(fshttp.NewClient(fs.Config)).SetRoot(rootURL) // FIXME
|
||||||
|
|
||||||
// FIXME
|
// FIXME
|
||||||
//err = f.pacer.Call(func() (bool, error) {
|
//err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = srv.CallXML(context.Background(), &opts, &authRequest, nil)
|
resp, err = srv.CallXML(context.Background(), &opts, &authRequest, nil)
|
||||||
// return shouldRetry(ctx, resp, err)
|
// return shouldRetry(resp, err)
|
||||||
//})
|
//})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to get token: %v", err)
|
log.Fatalf("Failed to get token: %v", err)
|
||||||
@@ -248,10 +248,7 @@ var retryErrorCodes = []int{
|
|||||||
|
|
||||||
// shouldRetry returns a boolean as to whether this resp and err
|
// shouldRetry returns a boolean as to whether this resp and err
|
||||||
// deserve to be retried. It returns the err as a convenience
|
// deserve to be retried. It returns the err as a convenience
|
||||||
func shouldRetry(ctx context.Context, resp *http.Response, err error) (bool, error) {
|
func shouldRetry(resp *http.Response, err error) (bool, error) {
|
||||||
if fserrors.ContextError(ctx, &err) {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
return fserrors.ShouldRetry(err) || fserrors.ShouldRetryHTTP(resp, retryErrorCodes), err
|
return fserrors.ShouldRetry(err) || fserrors.ShouldRetryHTTP(resp, retryErrorCodes), err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,7 +264,7 @@ func (f *Fs) readMetaDataForPath(ctx context.Context, path string) (info *api.Fi
|
|||||||
}
|
}
|
||||||
|
|
||||||
found, err := f.listAll(ctx, directoryID, func(item *api.File) bool {
|
found, err := f.listAll(ctx, directoryID, func(item *api.File) bool {
|
||||||
if strings.EqualFold(item.Name, leaf) {
|
if item.Name == leaf {
|
||||||
info = item
|
info = item
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -291,7 +288,7 @@ func (f *Fs) readMetaDataForID(ctx context.Context, ID string) (info *api.File,
|
|||||||
}
|
}
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallXML(ctx, &opts, nil, &info)
|
resp, err = f.srv.CallXML(ctx, &opts, nil, &info)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if resp != nil && resp.StatusCode == http.StatusNotFound {
|
if resp != nil && resp.StatusCode == http.StatusNotFound {
|
||||||
@@ -328,7 +325,7 @@ func (f *Fs) getAuthToken(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallXML(ctx, &opts, &authRequest, &authResponse)
|
resp, err = f.srv.CallXML(ctx, &opts, &authRequest, &authResponse)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to get authorization")
|
return errors.Wrap(err, "failed to get authorization")
|
||||||
@@ -353,7 +350,7 @@ func (f *Fs) getAuth(req *http.Request) (err error) {
|
|||||||
// if have auth, check it is in date
|
// if have auth, check it is in date
|
||||||
if f.opt.Authorization == "" || f.opt.User == "" || f.authExpiry.IsZero() || time.Until(f.authExpiry) < expiryLeeway {
|
if f.opt.Authorization == "" || f.opt.User == "" || f.authExpiry.IsZero() || time.Until(f.authExpiry) < expiryLeeway {
|
||||||
// Get the auth token
|
// Get the auth token
|
||||||
f.srv.SetSigner(nil) // temporarily remove the signer so we don't infinitely recurse
|
f.srv.SetSigner(nil) // temporariliy remove the signer so we don't infinitely recurse
|
||||||
err = f.getAuthToken(ctx)
|
err = f.getAuthToken(ctx)
|
||||||
f.srv.SetSigner(f.getAuth) // replace signer
|
f.srv.SetSigner(f.getAuth) // replace signer
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -376,7 +373,7 @@ func (f *Fs) getUser(ctx context.Context) (user *api.User, err error) {
|
|||||||
}
|
}
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallXML(ctx, &opts, nil, &user)
|
resp, err = f.srv.CallXML(ctx, &opts, nil, &user)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to get user")
|
return nil, errors.Wrap(err, "failed to get user")
|
||||||
@@ -398,7 +395,9 @@ func parseExpiry(expiryString string) time.Time {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewFs constructs an Fs from the path, container:path
|
// NewFs constructs an Fs from the path, container:path
|
||||||
func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, error) {
|
func NewFs(name, root string, m configmap.Mapper) (fs.Fs, error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
opt := new(Options)
|
opt := new(Options)
|
||||||
err := configstruct.Set(m, opt)
|
err := configstruct.Set(m, opt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -406,20 +405,20 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
root = parsePath(root)
|
root = parsePath(root)
|
||||||
client := fshttp.NewClient(ctx)
|
client := fshttp.NewClient(fs.Config)
|
||||||
f := &Fs{
|
f := &Fs{
|
||||||
name: name,
|
name: name,
|
||||||
root: root,
|
root: root,
|
||||||
opt: *opt,
|
opt: *opt,
|
||||||
srv: rest.NewClient(client).SetRoot(rootURL),
|
srv: rest.NewClient(client).SetRoot(rootURL),
|
||||||
pacer: fs.NewPacer(ctx, pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))),
|
pacer: fs.NewPacer(pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))),
|
||||||
m: m,
|
m: m,
|
||||||
authExpiry: parseExpiry(opt.AuthorizationExpiry),
|
authExpiry: parseExpiry(opt.AuthorizationExpiry),
|
||||||
}
|
}
|
||||||
f.features = (&fs.Features{
|
f.features = (&fs.Features{
|
||||||
CaseInsensitive: true,
|
CaseInsensitive: true,
|
||||||
CanHaveEmptyDirectories: true,
|
CanHaveEmptyDirectories: true,
|
||||||
}).Fill(ctx, f)
|
}).Fill(f)
|
||||||
f.srv.SetSigner(f.getAuth) // use signing hook to get the auth
|
f.srv.SetSigner(f.getAuth) // use signing hook to get the auth
|
||||||
f.srv.SetErrorHandler(errorHandler)
|
f.srv.SetErrorHandler(errorHandler)
|
||||||
|
|
||||||
@@ -534,7 +533,7 @@ func (f *Fs) FindLeaf(ctx context.Context, pathID, leaf string) (pathIDOut strin
|
|||||||
//fs.Debugf(f, "FindLeaf(%q, %q)", pathID, leaf)
|
//fs.Debugf(f, "FindLeaf(%q, %q)", pathID, leaf)
|
||||||
// Find the leaf in pathID
|
// Find the leaf in pathID
|
||||||
found, err = f.listAll(ctx, pathID, nil, func(item *api.Collection) bool {
|
found, err = f.listAll(ctx, pathID, nil, func(item *api.Collection) bool {
|
||||||
if strings.EqualFold(item.Name, leaf) {
|
if item.Name == leaf {
|
||||||
pathIDOut = item.Ref
|
pathIDOut = item.Ref
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -570,14 +569,14 @@ func (f *Fs) CreateDir(ctx context.Context, pathID, leaf string) (newID string,
|
|||||||
}
|
}
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallXML(ctx, &opts, mkdir, nil)
|
resp, err = f.srv.CallXML(ctx, &opts, mkdir, nil)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
newID = resp.Header.Get("Location")
|
newID = resp.Header.Get("Location")
|
||||||
if newID == "" {
|
if newID == "" {
|
||||||
// look up ID if not returned (e.g. for syncFolder)
|
// look up ID if not returned (eg for syncFolder)
|
||||||
var found bool
|
var found bool
|
||||||
newID, found, err = f.FindLeaf(ctx, pathID, leaf)
|
newID, found, err = f.FindLeaf(ctx, pathID, leaf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -621,7 +620,7 @@ OUTER:
|
|||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallXML(ctx, &opts, nil, &result)
|
resp, err = f.srv.CallXML(ctx, &opts, nil, &result)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return found, errors.Wrap(err, "couldn't list files")
|
return found, errors.Wrap(err, "couldn't list files")
|
||||||
@@ -777,7 +776,7 @@ func (f *Fs) delete(ctx context.Context, isFile bool, id string, remote string,
|
|||||||
}
|
}
|
||||||
return f.pacer.Call(func() (bool, error) {
|
return f.pacer.Call(func() (bool, error) {
|
||||||
resp, err := f.srv.Call(ctx, &opts)
|
resp, err := f.srv.Call(ctx, &opts)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// Move file/dir to deleted files if not hard delete
|
// Move file/dir to deleted files if not hard delete
|
||||||
@@ -838,7 +837,7 @@ func (f *Fs) Precision() time.Duration {
|
|||||||
return fs.ModTimeNotSupported
|
return fs.ModTimeNotSupported
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy src to this remote using server-side copy operations.
|
// Copy src to this remote using server side copy operations.
|
||||||
//
|
//
|
||||||
// This is stored with the remote path given
|
// This is stored with the remote path given
|
||||||
//
|
//
|
||||||
@@ -883,7 +882,7 @@ func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object,
|
|||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallXML(ctx, &opts, ©File, nil)
|
resp, err = f.srv.CallXML(ctx, &opts, ©File, nil)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -924,7 +923,7 @@ func (f *Fs) Purge(ctx context.Context, dir string) error {
|
|||||||
return f.purgeCheck(ctx, dir, false)
|
return f.purgeCheck(ctx, dir, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// moveFile moves a file server-side
|
// moveFile moves a file server side
|
||||||
func (f *Fs) moveFile(ctx context.Context, id, leaf, directoryID string) (info *api.File, err error) {
|
func (f *Fs) moveFile(ctx context.Context, id, leaf, directoryID string) (info *api.File, err error) {
|
||||||
opts := rest.Opts{
|
opts := rest.Opts{
|
||||||
Method: "PUT",
|
Method: "PUT",
|
||||||
@@ -937,7 +936,7 @@ func (f *Fs) moveFile(ctx context.Context, id, leaf, directoryID string) (info *
|
|||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallXML(ctx, &opts, &move, &info)
|
resp, err = f.srv.CallXML(ctx, &opts, &move, &info)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -952,7 +951,7 @@ func (f *Fs) moveFile(ctx context.Context, id, leaf, directoryID string) (info *
|
|||||||
return info, nil
|
return info, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// moveDir moves a folder server-side
|
// moveDir moves a folder server side
|
||||||
func (f *Fs) moveDir(ctx context.Context, id, leaf, directoryID string) (err error) {
|
func (f *Fs) moveDir(ctx context.Context, id, leaf, directoryID string) (err error) {
|
||||||
// Move the object
|
// Move the object
|
||||||
opts := rest.Opts{
|
opts := rest.Opts{
|
||||||
@@ -967,11 +966,11 @@ func (f *Fs) moveDir(ctx context.Context, id, leaf, directoryID string) (err err
|
|||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
return f.pacer.Call(func() (bool, error) {
|
return f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallXML(ctx, &opts, &move, nil)
|
resp, err = f.srv.CallXML(ctx, &opts, &move, nil)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move src to this remote using server-side move operations.
|
// Move src to this remote using server side move operations.
|
||||||
//
|
//
|
||||||
// This is stored with the remote path given
|
// This is stored with the remote path given
|
||||||
//
|
//
|
||||||
@@ -1007,7 +1006,7 @@ func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DirMove moves src, srcRemote to this remote at dstRemote
|
// DirMove moves src, srcRemote to this remote at dstRemote
|
||||||
// using server-side move operations.
|
// using server side move operations.
|
||||||
//
|
//
|
||||||
// Will only be called if src.Fs().Name() == f.Name()
|
// Will only be called if src.Fs().Name() == f.Name()
|
||||||
//
|
//
|
||||||
@@ -1056,7 +1055,7 @@ func (f *Fs) PublicLink(ctx context.Context, remote string, expire fs.Duration,
|
|||||||
var info *api.File
|
var info *api.File
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallXML(ctx, &opts, &linkFile, &info)
|
resp, err = f.srv.CallXML(ctx, &opts, &linkFile, &info)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -1185,7 +1184,7 @@ func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.Read
|
|||||||
}
|
}
|
||||||
err = o.fs.pacer.Call(func() (bool, error) {
|
err = o.fs.pacer.Call(func() (bool, error) {
|
||||||
resp, err = o.fs.srv.Call(ctx, &opts)
|
resp, err = o.fs.srv.Call(ctx, &opts)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -1207,7 +1206,7 @@ func (f *Fs) createFile(ctx context.Context, pathID, leaf, mimeType string) (new
|
|||||||
}
|
}
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
resp, err = f.srv.CallXML(ctx, &opts, &mkdir, nil)
|
resp, err = f.srv.CallXML(ctx, &opts, &mkdir, nil)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -1265,7 +1264,7 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
|
|||||||
}
|
}
|
||||||
err = o.fs.pacer.CallNoRetry(func() (bool, error) {
|
err = o.fs.pacer.CallNoRetry(func() (bool, error) {
|
||||||
resp, err = o.fs.srv.Call(ctx, &opts)
|
resp, err = o.fs.srv.Call(ctx, &opts)
|
||||||
return shouldRetry(ctx, resp, err)
|
return shouldRetry(resp, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to upload file")
|
return errors.Wrap(err, "failed to upload file")
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
package swift
|
package swift
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ncw/swift/v2"
|
"github.com/ncw/swift"
|
||||||
)
|
)
|
||||||
|
|
||||||
// auth is an authenticator for swift. It overrides the StorageUrl
|
// auth is an authenticator for swift. It overrides the StorageUrl
|
||||||
@@ -29,19 +28,19 @@ func newAuth(parentAuth swift.Authenticator, storageURL string, authToken string
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Request creates an http.Request for the auth - return nil if not needed
|
// Request creates an http.Request for the auth - return nil if not needed
|
||||||
func (a *auth) Request(ctx context.Context, c *swift.Connection) (*http.Request, error) {
|
func (a *auth) Request(c *swift.Connection) (*http.Request, error) {
|
||||||
if a.parentAuth == nil {
|
if a.parentAuth == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
return a.parentAuth.Request(ctx, c)
|
return a.parentAuth.Request(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Response parses the http.Response
|
// Response parses the http.Response
|
||||||
func (a *auth) Response(ctx context.Context, resp *http.Response) error {
|
func (a *auth) Response(resp *http.Response) error {
|
||||||
if a.parentAuth == nil {
|
if a.parentAuth == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return a.parentAuth.Response(ctx, resp)
|
return a.parentAuth.Response(resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The public storage URL - set Internal to true to read
|
// The public storage URL - set Internal to true to read
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user