Timing bounds in parallel tests leave room for a slow race runner, the
block 502 test checks its fault fired, refusedCommit's 408 exclusion is
pinned, the user docs explain the "server doesn't show it yet" error and
the conformance checklist matches the current draft and trust rules.
Every other minor from the review is listed in docs/TODO.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Log lines no longer cut a name inside a UTF-8 sequence, the "superseded
upload" error says the file's state rather than claiming a revision is
active, a test-only block decrypter moves to the tests, a one-line alias
of encryptPassphrase goes, the upload godoc is rewrapped and the fake's
server struct is gofmt clean.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the access token expired and the refresh request got a 5xx, 429 or
408, the operation failed instead of being retried, and a 408 even ended
the session as if the refresh token had been rejected. Such failures are
now retried like any other call.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only fstest.Initialise, run by the integration test, applied
RCLONE_CONFIG, so a filtered test run read the default rclone config,
which may hold a real Proton remote. TestMain now applies it first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The race suite ran under Go's default 10 minute limit, which a slow
runner would hit; it now has 40 minutes in a 60 minute job. Go is the
1.27 line the code was verified with, not go.mod's minimum, so
govulncheck checks a patched standard library, and govulncheck is
pinned to v1.9.0 so a new release can't break the build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Proton announces requirements it will enforce, such as a minimum client
version, in x-pm-drive-requirements response headers. They are now
logged at debug level whenever they change, so they show in -vv bug
reports.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Proton's generic "requirements not met" code 2000, which it returns for
a node changed since it was read (as when a lost rename response is
resent), aborted the whole run with "upgrade rclone". It is now an
ordinary error that isn't retried. Code 2028 (a feature disabled,
rate-limited or blocked) is retried, and the codes Proton actually uses
for a rejected or missing app version, 5003 and 5001, now give the
upgrade message.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A file whose extended attributes don't record its block sizes, as some
other clients write, was read from the first block for every ranged
read, so a multi-thread copy, which opens each chunk as a range,
downloaded data quadratically: about 8.5 GiB for a 1 GiB file. The sizes
of blocks already read are now remembered per Fs (bounded to 16
revisions and 65536 blocks), so later chunks start at the right block.
A block list that disagrees with what was learnt is an integrity error.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A remote set up with environment variables or a connection string can't
save its draft client ID, so it got a new random one on every run, and a
draft left by an earlier run then blocked uploading that name as if
another client were uploading it. When the ID can't be saved it is now
derived from the remote name and the account's volume, so it is the same
on every run. A remote in the config file still gets a random ID, saved.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Extended attributes whose signature failed or was missing were used, so
a server could set a file's modification time, size and SHA-1 at will:
for example make `rclone sync --update` skip every changed file. Without
ignore_signature_errors they are now not used and an ERROR is logged:
the modification time is the server's, there is no SHA-1, and the size
comes from the recorded block sizes, which reads check. The file stays
readable.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Renaming or moving a node whose name signature failed reused the old
name's session key, which a server that made that name knows, so it could
read the new name. The new name now gets a new session key unless the
old name's signature verified.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A folder's hash key whose signature failed or was missing was only
logged, and the names of everything written into the folder were then
hashed with it, so a server which made the key could test guesses at
those names. Creating, uploading, renaming, moving or copying into such
a folder is now refused unless ignore_signature_errors is set, when an
ERROR is logged once per folder. Reading is unaffected.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A node or share key from the server was unlocked with whatever Argon2
parameters it carried, so a crafted key could make every listing
including it allocate up to 2 TiB and crash rclone. Node and share keys
locked with Argon2 beyond 4 passes, parallelism 8 or 128 MiB are now
refused before the key derivation runs, as an integrity error.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A server could replace a file's content key with one it made, without a
signature: updates were then encrypted with a key the server knows, and
ranged reads returned whatever it encrypted under that key. A missing or
invalid content key signature now makes the file unverified, like an
unverified node key: reads and updates are refused unless
ignore_signature_errors is set, when an ERROR is logged once per file and
operation. The fake gains corruptions for this and for the hash key,
name and extended attribute checks that follow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Captured from a throwaway test account on 2026-11-12 and re-keyed onto
test-only keys; no key, passphrase, ID or email of the account is in them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After a failed commit the client read the file back and deleted the
draft if the read showed it uncommitted. Proton's server can take a
moment to show a commit, so a stale read could send a delete against a
file or revision which had in fact been committed.
The draft is now deleted only when the server refused the commit with a
4xx API error on a single attempt which was not resent; any other
failure leaves it and reports that the upload may have succeeded. A
cancelled upload whose commit succeeded stops waiting for the server to
show it at once. The fake server's read lag can also serve a file
without an active revision.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The capture names its folder with -capture-remote and refuses -remote,
so a mistyped -run can't start the integration tests against the
account. Every key written has a placeholder user ID; a node key holding
another user's email, or a signature by or intended for a key not being
replaced, fails the capture without naming it. Salt notations are
written only where the original had one.
TestGoldenFixtures now also walks every key, key packet and signature of
a fixture, inside encrypted messages too, and checks folder modification
times; tampered fixtures are shown to fail.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Proton's server can take a moment to show a change: a file read back
just after its commit may be missing or still a draft with no active
revision. Uploads then failed with "upload committed, but the file now
has another revision (revision is active ...)" or "object not found".
The read back after a commit, the read of a server-side copy and, with
enable_caching off, the read of a folder to write into now retry with
backoff for about 7 s while the change doesn't show. A committed upload
the server still doesn't show after that fails with a new "committed,
but the server doesn't show it yet" error, deletes nothing and is not
retried at once. The fake server gains SetReadLag to model the lag.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TestGoldenCapture (-capture DIR -capture-label NAME -remote NAME:PATH)
captures a folder another client wrote, re-keyed onto keys made for the
tests: blocks and signature-free SEIPD packets kept byte for byte, key
packets re-wrapped, signatures re-made with the same parameters (names,
hash keys and extended attributes encrypted again with their own session
keys, as their signatures are embedded), IDs and emails replaced.
TestGoldenFixtures reads every fixture in testdata/golden through the
backend from the fake server with every signature checked.
The fake server can now seed stored folders and keeps the times of
seeded nodes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A file written by an old client without recorded block sizes was read
in ranges by assuming 4 MiB blocks, so a range past the first block could
return the wrong bytes, or too few, without an error. Such a range is now
read from the first block, discarding the bytes before it, with every
block still checked against its hash.
Also identify the SDK the conformance checklist was checked against by
commit, and align the enable_caching help with the docs page.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rewrite the hand-written parts of the Proton Drive docs page for the
native client: browser and password sign-in with a transcript taken from
the current code, the third-party notice, what is verified on upload and
download, the ranged-read caveat, the trust rules and the
ignore_signature_errors option, the two protocol limits, the --dump
warning, filename rules, drafts, trash and limits. The generated options
section is left for make backenddocs.
Describe everything ignore_signature_errors allows in its help text,
update the feature list and score in protondrive.yaml, and add the SDK
conformance checklist and fork notes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The owner added the rule; docs/PERMISSIONS.md already listed it in the
Allow tier. Also record that the force-with-lease deny rule misses the
`--force-with-lease=` form.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The old libraries were only used by the differential crypto tests. Those
go, and TestFrozenFixtures moves into crypto_internal_test.go without its
-update branch, so files the old library wrote stay checked through the
frozen fixtures in testdata/oldlib. go mod tidy removes the two libraries
and the modules only they pulled in; relvacode/iso8601 leaves go.mod.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add the TestProtonDriveFake init.d script, which builds and starts
protonfake on port 28642 and passes on the session it prints, and a
test_all entry for it, so fs/operations, fs/sync, vfs and cmd/bisync
run against the fake server. Five tests that need a second Fs of the
same account to see what the first wrote are ignored, as each Fs
caches separately, and cmd/gitannex is skipped as for every init.d
remote; both are in docs/TODO.md.
Add a Gitea Actions workflow that builds, vets, lints, race-tests,
fuzzes and vulnerability-checks the backend, and re-verify the agent
permission table on the host toolchain.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A reader with only the repository walked the handoff and the remaining
plan tasks against the code. Mark the plan and spec lines that later
rulings superseded, add corrections for Tasks 18 to 21, fix stale TODO
and PERMISSIONS entries, ignore the skill workspace, point AGENTS.md at
the handoff and take the test account usernames out of it.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Development of plan 001 is paused after Task 17 of 21. Record the state,
the rulings made during execution, the test setup and how to resume in
docs/plans/001-protondrive-tier1-HANDOFF.md, commit the execution ledger,
task briefs, implementer reports and reviews, and mark the plan and spec
with their current status.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The comment and spec put the block metadata a full read holds at a few
hundred bytes a block; with realistic tokens and URLs the cap allows
about 250-350 MiB, and those fields and API response bodies aren't
bounded at all. TODO also asks the live check to confirm the
root-of-trust rule on a drive made by Proton's own apps.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A range covering the whole file was read as a plain full read, so a
file whose recorded size understated its content returned more bytes
than the range asked for. It is now checked as a full read but stops
at the end of the range.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fetching a file or folder fetches its folders up to one already known,
and a server reporting folders as each other's parents made that
recurse until rclone crashed. A cycle, or folders nested more than 2048
deep, is now an error.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With ignore_signature_errors set, reading a file, writing into a folder,
or moving, copying or renaming either when its key isn't verified went
ahead without a word. Each now logs an error, once per file or folder
and operation, saying what was done and why the key isn't trusted - and
that what is moved or copied out is trusted from then on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The check that the drive is shared and signed by your own address
ignored letter case, but keys were looked up by exact address, so a
server naming your address in capitals could serve keys of its own for
it. Addresses are now matched ignoring ASCII case only, everywhere, and
your own address in any such case is always verified with your own
keys, never ones the server supplies.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The warm listing budget, with caching on and off, reuses the folders
TestBudgetList seeds instead of seeding its own, which saves about 15s
under -race. The assertions are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With an encoding which lets invalid UTF-8 through, rclone could create
a file or folder whose name Proton's clients, and rclone itself, then
refuse to read, so it vanished from listings. Such names are now
refused when writing.
Found by FuzzValidateName.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A file whose extended attributes had a SHA-1 but a missing, negative or
malformed Size was listed with size 0, so copying it failed with a size
mismatch. Its size is now the sum of its recorded block sizes, if they
are usable, and otherwise unknown, as for a file without extended
attributes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A listing whose pages were read before a file or folder was made in,
copied into or trashed from the folder cached what it read when it
finished, overwriting the change. As new files are added to a cached
listing rather than dropping it, the stale listing then lasted: a file
just uploaded stayed missing, and a trashed one stayed listed, until
something else dropped the listing.
The node cache now counts changes to the children of each folder being
listed, and a listing is cached only if none happened since its first
page was read. The count is kept only while a listing is in progress.
The fake server gains a Hold fault, which answers a request with the
response made on arrival only once released, to test this without
sleeps.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The password test's cleanup restored the config path before the
storage, and setting the storage does nothing while the path is empty,
so with an in-memory config the storage was left behind.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TestPasswordStoredObscured installed the config file storage for its
temporary config and left it in place for the tests after it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A full read holds the list of a file's blocks until it has checked the
signed manifest, and how long that list could grow was bounded only by
a size the server sends unsigned. A file listing more than 2^19 blocks
(2 TiB in 4 MiB blocks) is now an error.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A request for the whole file as a range, as "bytes=0-" from serve http
or webdav clients, was read as a ranged read, which can't check the
file's signed block list. It is now read as a full read.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The drive's share and root folder were accepted when signed by any
address whose keys the server served, so a hostile server could vouch
for a whole drive of its own making. If the share wasn't created, or
the root folder signed, by one of the account's own addresses, reading
and writing now fail with an error saying so, unless
ignore_signature_errors is set.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Moving or copying a file or folder out of a folder with a forged key
had rclone sign the server's key as the user's, after which it read
without error. Moving, copying or renaming a node whose key isn't
verified now fails unless ignore_signature_errors is set, and anything
beneath such a folder, including what rclone writes there with the
option set, stays unverified.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Request budgets, measured against the fake server and asserted at the
measured counts, for listing (cold and warm, with and without
enable_caching), a no-op sync by modification time and by checksum,
NewObject three folders deep, uploads and making folders. A TestMain
hook fails the package if any test, through any fake server, requested
a removed share-scoped route; the fake keeps a process-wide tally of
them for it.
Fuzz targets, seeded from testdata/oldlib and from what the fake
writes, for name validation, link decoding, extended attributes, API
error bodies with Retry-After and session fork payloads. Each checks
invariants of what is accepted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A node which decrypted was handed on whatever its link ID and encrypted
size, so a faulty or hostile server could make rclone cache a node
under an empty ID or list a file with a negative size. Such nodes are
now left out of listings and logged, as nodes with invalid names are.
Found by FuzzDecodeLink.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Names, node passphrases, hash keys and extended attributes are
encrypted to public keys, so the server, or anyone, can write them as a
small compressed message which expands to gigabytes when decrypted.
They were decompressed into memory without a limit.
Names, passphrases and hash keys are now limited to 64 KiB and extended
attributes to 16 MiB, enough for the block sizes of an 8 TiB file.
Extended attributes with a negative Size are now read as having no
size, as the reference SDK reads other invalid sizes, so a file is never
listed with a negative size.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Making a file or folder, or a server-side copy, forgot the cached
listing of its folder, so the next upload into that folder - which
checks the name is free - listed the whole folder again. Copying M new
files into a folder of N cost on the order of M*N/100 extra requests.
The new node is now added to the cached listing. A failed call still
forgets it, as the call may have taken effect.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The password test now runs rclone config with the password in plain
text and checks it is stored obscured; a test checks a file whose key
isn't its owner's can't be updated; and the log test checks every
session token the fake issued, which it now labels by kind, not only
the saved ones.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A token refresh still in flight when the remote was disconnected could
write its new tokens back to the config after Disconnect removed them.
Disconnect now waits for it and removes them afterwards.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A full read verified the manifest signature only at the end, so blocks
a server swapped, dropped or took from an older revision reached the
reader - and rclone cat or a mount - before the error. A full read now
lists every block and verifies the manifest before releasing the first
byte, and a block listed again with a different hash is an integrity
error. Ranged reads are unchanged.
The fake's "rollback" corruption is renamed "block-rollback": serving a
whole earlier revision under the current revision ID can't be detected,
which the spec and TODO now say.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A server could put a folder with a key of its own making into a real
folder and serve anonymous files beneath it whose every signature
checked out against that key, so they were read without an error.
A key checked against an unverified folder key is now unverified too,
all the way down: reading such a file, or creating, uploading, moving
or copying anything into such a folder, fails unless
ignore_signature_errors is set. Listing it still works.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A hostile fake server tampers with block hashes, block order and count,
serves an older revision's or another key's blocks, signs a node
passphrase with its own key and gives nodes names such as "../x", "a/b",
NUL and 10 MiB: each read fails or the node is left out, with and
without ignore_signature_errors, and a copy or sync writes nothing
wrong or outside its destination. The log at debug level, and with
--dump headers, holds no password, key password, session or storage
token, node passphrase or session key; browser sign-in leaves no
password in the config and password sign-in keeps it obscured.
The fake gains rollback and foreign-block corruptions and exposes the
secrets it issued and a file's node secrets.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A call in flight when the remote was disconnected, and then rejected
as unauthorised, was sent again with empty credentials. It now fails
with the "not signed in" error instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An expired session, or one signed out with "rclone config disconnect"
while a long running rclone was using it, told the user to reconnect a
remote literally called "remote:". The error now names the remote.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A file whose node passphrase signature didn't verify was read with
only an error logged when it was listed, although a server could have
substituted a key of its own making and with it any content. Reading
such a file now fails unless ignore_signature_errors is set, as for a
content key or manifest signature which fails.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The error for a content key or manifest signature which fails now
ends with a hint to set ignore_signature_errors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A CAPTCHA on password sign-in told the user to reconnect a remote
literally called "remote:". The error from NewFs now names the remote
and is fatal, and rclone config goes back to the choice of sign-in
method with a hint to use the web browser.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- rmdir asks the server whether a folder has any child, so it never
trashes a folder holding Proton Docs, unreadable files or children
added by another client since it was listed
- rclone config asks before replacing a saved session (default no), and
signs the old session out after signing in again
- Shutdown only drops caches; after Disconnect the remote reports that
it isn't signed in
- update, move and copy refuse a file another client has trashed
- ranged reads are computed on the decrypted size whatever
original_file_size says
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The backend no longer uses Proton-API-Bridge or go-proton-api at run
time. Saved sessions from earlier versions keep working without signing
in again, and every option keeps its name and meaning. rclone config
now offers sign-in with a web browser. The full backend test suite runs
against an in-process fake Proton server.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add protonfake, a command which serves the fake Proton server on a
given address and prints the endpoint and a saved session of a user
with a Drive volume as key=value lines, for an fstest test server
script. protontest gains NewStandalone and Close for it.
Add setupFakeRemote, which configures TestProtonDriveFake: with a
fresh session on the fake through environment variables, and
TestIntegrationFake, which runs the backend contract suite against it.
It is skipped until the backend has an endpoint option.
client_salted_key_pass is written base64 encoded, the meaning the key
has in existing configs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TestClientBootstrapCreateVolumeRace failed now and then in whole
package race runs with "expected 2 POST /drive/volumes, got 1".
loadAccount holds the process-wide unlockMu while unlocking keys, so
the second client's first GET drive/v2/shares/my-files is sent one
key unlock, or under load many queued unlocks, after the first's. The
500ms Delay fault on both GETs shifted them equally and so did not
keep them together: when the first client's create had finished
before the second client's GET was handled, the second client found
the volume and never POSTed. The client handles that ordering
correctly; only the test's assertion depended on timing.
Add a Gather option to protontest faults, which holds each faulted
request until all Times have arrived, and use it for both GETs and
both POSTs. The test now starts the second client as late as the
server lets it, which failed 10 of 10 runs with the Delay fault.
Gathering the POSTs runs both create volume checks in the fake at
once, which exposed a data race there: go-crypto records signature
checks in a key on its first use, and the new user's address key had
never been used. Check each request with its own copy of the key.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Files uploaded by the new client are read whole by the old library's
readers, and files written by the old library (go-proton-api v1.0.4,
Proton-API-Bridge v1.0.5), in the legacy and the v6 format, are
downloaded by the new client through the fake server. Frozen fixtures
of both formats keep the second direction tested once the old library
is gone.
protontest gains read-only access to a file as stored, seeding of a
stored file, and users built from fixed address and root keys. It now
signs no earlier than the signing key's creation time, which a key
supplied by a test may be.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After a failed commit the draft was deleted whenever the file read
back did not show the upload's revision active. A file changed by
another client since (a newer revision, or trashed) proves nothing,
and deleting then removed committed content. Now the draft is deleted
only when the read shows a new file still a draft, or the revision the
upload started from still active; otherwise nothing is deleted and
the outcome is reported as unknown. A successful commit whose file
already has another revision when read back is an error rather than
returning another client's revision as the upload's.
The fake gains SeedRevision, giving a file a new revision as another
client would.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After a failed commit the upload read the file back with the caller's
context. When that context was cancelled, or the read failed, the
commit was taken as not done and its draft deleted, which deletes the
file or revision if the commit had in fact taken effect. The read now
runs past cancellation, the draft is deleted only when the read shows
the revision is not active, and an outcome that can't be read is
reported as possibly successful without deleting anything.
Also fix a data race in the fake's storage upload handler and add
tests for a failing source and for a failing block with the producer
blocked.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Upload follows the SDK's upload module (js/v0.23.0) step for step:
draft file or draft revision, name-conflict handling (own draft
deleted and retried once, another client's draft refused unless
replace_existing_draft, an existing file given a new revision),
verification code, blocks encrypted and checked against the server's
content key, upload URLs requested in batches, uploads bounded per file
(upload_concurrency, default 4, max 5) and per client (10 blocks),
size and SHA1 checks, signed manifest and extended attributes, commit
with a check that a failed commit is not in fact active, and draft
cleanup on any failure or cancellation. Blocks are streamed through
lib/pool buffers; a block is tried 3 times and a storage 404 requests a
new URL. Cancellation is bounded everywhere, including waits for the
client's upload tokens.
The fake server gains the upload routes with strict checks: names,
passphrases and extended attributes must be non-AEAD, the file key a
v4 key with an ordinary S2K, the content key packet a v6 PKESK and
blocks SEIPDv2; verifier tokens, block indexes, declared hashes and the
manifest signature are checked at commit. It now signs a day in the
past and checks signatures in Dump at its own clock, so clock-skew
tests can run in both directions.
Where the plan and the SDK differ on the wire, the SDK wins:
- IntendedUploadSize follows the SDK: for a new file it is sent only
when the rounded size is over 100,000 bytes; for a new revision it is
reduceSizePrecision (4095 under 4 KiB, 100,000 under 100 kB).
- BlockList entries carry no Hash or Size (deprecated, unused); the
fake checks a Hash only if one is sent.
- An empty stream of unknown size is uploaded as zero blocks, which
the SDK refuses.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A recorded block size above 8 MiB is now an integrity error, so the
sizes can't overflow, and a full read always lists the blocks so it
reaches the manifest check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Download a range of a file's content block by block: block URLs come
from the revision's paged block list (20 per page), blocks are fetched
through the storage pacer holding a download token without the
session's credentials, checked against their SHA-256 before decrypting
and decrypted into pool buffers. A full read checks the manifest
signature, by the content author's address or the node key for an
anonymous upload, and the content key packet signature is checked on
open with the SDK's fallback to every address key for files created
before 2023.
The retry and capped Retry-After logic of apiCall is shared with block
downloads through the new pacedCall.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add createFolder, rename, move, copyNode, trash and emptyTrash to the
new client, following the SDK's nodesManagement.ts: names and
passphrases are re-encrypted to the new parent with their session keys
and signed by the share's address; the node passphrase signature is
sent on move and copy only for an anonymous node; API code 2500 maps to
errExists and 2501 to errNotFound. Every mutation updates the node
cache, and nodeCache.put forgets both parents' listings when a node's
parent changes.
The fake gains the matching routes plus checkAvailableHashes. It
rejects SEIPDv2 or v6 PKESK names, passphrases, hash keys and extended
attributes, and folder keys which are not v4, advertise AEAD or are not
locked with an iterated and salted S2K. It enforces name hash
uniqueness, OriginalHash, no move into a descendant and the anonymous
signature rule. Dump now also checks every name hash, and
SeedAnonymousFile seeds a public-link upload.
Wire format: the create folder request sends SignatureEmail as the SDK
does, although CreateFolderDto lists SignatureAddress.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A 429's Retry-After is now honoured for at most 60 seconds and is
waited out by the call alone, so a hostile value can neither stall the
client nor hold every other call behind the shared pacer. Public key
lookups during listing are paced and retried like every other call
instead of failing the listing on one 429 or 5xx.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add the client the new backend will use above the wire: it loads the
account, finds or creates the main volume, share and root, and lists
folders by paging child IDs and fetching their metadata 100 at a time
with at most 15 requests in flight, decrypting with the verification
keys the SDK's nodes module chooses.
A brand-new account has no volume: my-files answers code 2501 and the
client creates the volume as the SDK's shares module does, accepting
code 2500 from a concurrent creator.
Nodes whose name or key can't be decrypted, or whose name is not valid,
are left out with one error each; Proton Docs and Sheets are left out
with one notice; failed signature checks are logged once and the node
is still listed.
API calls are paced and retried: 429 waits for Retry-After before
retrying, 408 and 5xx are retried, other client errors are not.
errExists moves to Task 9, which first uses it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every fake user now has a real, correctly encrypted drive: a volume, a
main share and a root folder, built as the SDK's
generateVolumeBootstrap does. Seeding helpers create folders, files in
the current format (v6 PKESK content key, SEIPDv2 GCM blocks, encrypted
block signatures, signed manifest, extended attributes), legacy files
(v3 PKESK, SEIPDv1) and Proton Docs nodes; Corrupt damages them as a
hostile server would and Dump decrypts and verifies the whole tree.
The read routes (my-files, volume, share, link metadata, children,
revision) answer with the field names of the SDK's driveTypes.ts, and
block bytes are served from the same server behind pm-storage-token.
AddUserWithoutDrive models a brand-new account, whose my-files answers
HTTP 422 code 2501, and POST /drive/volumes creates its volume after
checking the key material decrypts with the address key.
protontest/crypto.go is written from the SDK independently of the
backend's crypto.go. Interface additions are recorded in the plan.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cross-test crypto.go against go-proton-api v1.0.4 and Proton-API-Bridge
v1.0.5 in both directions, compare account key unlocking with
proton.Unlock, and freeze old-library output under testdata/oldlib so
it stays tested once the old libraries are gone. github.com/relvacode/iso8601
becomes a direct requirement because the test parses modification
times as the bridge does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Folder hash keys written by go-proton-api (and any message gopenpgp
encrypts and signs in one go) carry intended recipient fingerprints in
their signature. Decrypting with the session key alone made gopenpgp
reject those as "intended recipients in non-encrypted message", so
every folder created by released rclone reported a hash key signature
error. The intended recipients are now checked against the decryption
keys by hand.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gopenpgp looks the signer of an inline signature up in the decryption
keys as well as the verification keys, so a name or extended attributes
signed by the parent or node key reported as verified whatever keys the
caller asked for. Decrypt the session key first and the data with the
session key alone, so only the verification keys count, as in the SDK.
Also clear the decrypted passphrase when the node key fails to unlock.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add the OpenPGP operations of the Drive data model as pure functions:
node keys and passphrases, names, hash keys and lookup hashes, content
keys, blocks, verifier tokens, manifests and extended attributes.
The formats follow the reference SDK (driveCrypto.ts, openPGPCrypto.ts),
which differs from the plan's Task 5 interfaces in these ways, now
updated in the plan:
- node keys are v4 EdDSA/ECDH with user ID "Drive key" and ordinary S2K
locking; file keys (aead=true) advertise SEIPDv2 with AES-256-GCM and
folder keys don't, as the SDK's enableAead
- the content key is v6 only when the node key advertises SEIPDv2, as
the SDK's preference-following session key generation
- the content key signature is checked on read against the node key and
extra (address) keys
- block signatures are encrypted with the content session key, as the
SDK's encryptSignature
- hash keys are signed by the node key; reading also accepts address keys
- reencryptPassphrase signs anew, and encryptName takes the current name
session key for rename and move
- name length is counted in UTF-16 code units, as the SDK
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add a fake server switch that signs an address key's passphrase with an
unrelated key or removes the signature, and test that loading fails on
the primary address and leaves out a secondary one. Also clear an
unlocked key that can't be added to its key ring.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Load the user and addresses and unlock their keys, one unlock at a time
across all sessions so concurrent sign-ins don't multiply peak memory.
Address key passphrases are taken from their Token only when the user
key's signature on it verifies, as the SDK requires. Other users' public
keys are fetched once per account and cached, an email without a Proton
address having no keys.
Password sign-in now derives the key password from the salt of the
primary user key rather than the first salt, which is wrong for accounts
whose user keys have different salts. The SDK uses the first salt; it
remains the fallback when no salt matches.
The fake server gives each user real Curve25519 user and address keys,
and serves GET /core/v4/users, /core/v4/addresses and /core/v4/keys/all.
gopenpgp v3 moves to a direct dependency.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add SRP password sign-in (with TOTP, two-password accounts and a clear
error when Proton asks for a CAPTCHA), browser sign-in through a session
fork, and sign-out to the new session, with the matching routes in the
fake server. The fake does real server-side SRP with go-srp and issues
a per-user key salt.
Where the plan and the SDK differ, the SDK wins:
- the fork sign-in URL escapes its payload like encodeURIComponent
(authWeb.ts generateSignInUrl), so the plan's literal URL is the
unescaped form;
- POST /core/v4/auth sends PersistentCookies 1 and an empty Payload
(auth.ts authViaPassword);
- the key password uses the first KeySalts entry (auth.ts
getUserKeyPassword).
protontest.ApproveFork takes the sign-in URL instead of the user code,
since the payload key exists only in the URL; the plan is updated.
go-srp moves to a direct dependency.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When two sessions that share stored credentials refresh at the same
time, both post the same single-use refresh token and the server
rejects one. The session that loses now waits briefly for the winner's
tokens to appear in the store and uses them, instead of failing with
"session expired".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add the session used for authenticated API calls: it sends x-pm-uid,
the bearer token, x-pm-appversion and the Proton Accept header, decodes
failures into api.Error, and on HTTP 401 refreshes the tokens and
retries once. Refresh is single-flight, first adopts tokens another
session saved to the shared store, and on a rejected refresh token
returns a fatal "session expired" error without clearing the stored
credentials or retrying the refresh. The session also tracks the
server's clock from the Date header.
The app version maps development builds to the "alpha" channel, as
Proton's x-pm-appversion grammar has no "dev" channel.
The fake server gains users, sessions, single-use refresh tokens and
401 for invalid access tokens on authenticated routes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add the api.Error type with Proton response codes and the protontest
package: an httptest-based fake that rejects legacy share-scoped routes
with 410, enforces the published x-pm-appversion grammar, refuses
x-pm-drive-sdk-version, counts requests per route, skews the Date header
and injects scripted faults.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fork-only. Appends a Fork Workflow section to AGENTS.md and adds
GEMINI.md, per-tool permission files and docs/ pages for agent driven
development. Not for upstream.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Files uploaded through the multi-thread path were finalized with
CompleteFileUpload only, which did not add the new file to the parent
folder's shares and public links, so large files never showed up in
public links. filen-sdk-go v0.0.40 moves that step into
CompleteFileUpload, so no backend changes are needed.
Apply metadata overrides and the metadata mapper before queuing a
server-side copy. Preserve the new update tracker and read-only file
attributes, and return metadata errors before sending a PUT.
Fixes#7901
Assisted-by: OpenAI Codex
Co-authored-by: david.castro <david.castro@refinery89.com>
librclone/python/test_rclone.py builds ./librclone.so with
-buildmode=c-shared, which also writes librclone.h. The test removes
the .so but not the header, so the CI Linux job built the release
binaries from a tree with an untracked file and they reported
vcs.modified=true in their build info.
Ignore librclone.h so it no longer counts as a modification.
The beta site now has index.html and index.json directory listings
made by rclone index. fetch_binaries copied them into build/ where
make upload and make upload_github would have published them with the
release.
Commits which were only local, such as security fixes, aren't tested
by CI until master is pushed. Pushing the tag at the same time meant a
CI failure in them could stop the release binaries being built, which
happened for the Windows build of v1.76.0.
The tests timed one rejection at a time, but checking a --user/--pass
password takes less than the resolution of the clock on Windows, so
the known and unknown users both measured 0s and the comparison
failed.
Time many rejections with testing.Benchmark instead, which runs
enough of them to measure on a coarse clock.
On Windows os.Root.Symlink always makes a file symlink when the
target is an absolute path outside the root, and Windows can't
traverse a file symlink as a directory. The planted link can't reach
the outside files, so the test failed setting up the attack with "The
filename, directory name, or volume label syntax is incorrect".
Skip the test on Windows as the other planted symlink tests do.
The rc server decoded the whole POST body before looking up the method
and checking whether it needed authorisation, then echoed the decoded
input back in the 403 or 404 error. A client that was not allowed to
call a method could still make rclone read, decode and re-encode its
request.
The method lookup and authorisation check now happen before the body
is read, and these errors no longer echo the request input.
When the rc server was running without HTTP authentication and without
--rc-no-auth, anyone able to connect to it could POST an arbitrarily
large body to any rc path. A JSON body was decoded in full and then
echoed back in the response, so a single large request could make
rclone use several times the body size in memory and be killed by the
OOM killer.
In this mode only the small set of NoAuth methods can be called, so
POST bodies are now limited to 64 KiB and oversized requests are
rejected with 413 Request Entity Too Large without echoing the input.
Servers with authentication or --rc-no-auth are unaffected.
When a username wasn't in the htpasswd file the request was rejected
without checking the password at all, whereas a known username with a
wrong password went through the (deliberately slow) bcrypt check, so
the time taken let an unauthenticated client find out which usernames
exist. The same was true of --user/--pass with MD5-crypt.
Unknown usernames are now checked against a decoy hash of a random
password made in the same format and with the same cost as the most
expensive entry in the htpasswd file, remade whenever the file
changes, and --user/--pass always hash the password.
This affects serve http, serve webdav, serve restic, serve s3, rcd
--rc-htpasswd and --metrics-htpasswd.
With -l/--links the local backend recreates a source ".rclonelink" as a real
symlink at the destination. DirMove created the parent of the destination
directory by path, which follows a symlink in any parent component, so when
the destination contained a planted symlink "pwn" pointing outside the root
a directory move to "pwn/moved" moved the directory outside the destination.
rclone's sync and the VFS never name a destination through a symlink, so this
was only reachable with a caller supplied name.
Create the parent through os.Root when translating symlinks, as the other
writes do, so a planted symlink can no longer redirect the move out of the
destination.
With -l/--links the local backend recreates a source ".rclonelink" as a real
symlink at the destination. Remove and Rmdir deleted by path, which follows a
symlink in any parent component, so when the destination contained a planted
symlink "pwn" pointing outside the root, an object or directory named through
it such as "pwn/secret" resolved to the outside file or directory and was
deleted there. rclone's own sync never names destination paths through a
symlink as the listing shows it as a ".rclonelink" file, but the clean up of a
failed copy looks up and removes a ".partial" name derived from the source,
and callers of the rc API or the Go API may pass such a name directly.
Delete through os.Root when translating symlinks so a planted symlink can no
longer redirect the removal out of the destination. This also covers the
removal of a partially written file after a failed write. In-tree removals
and the Windows sharing violation retry are unchanged.
With -l/--links the local backend recreates a source ".rclonelink" as a real
symlink at the destination. MkdirMetadata checked whether the directory
already existed with a plain lstat, which follows a symlink in any parent
component. When the destination contained a planted symlink "pwn" pointing
outside the root and the target held an existing child "sub", syncing a source
directory "pwn/sub" found the outside directory, skipped the confined Mkdir
(which would have refused the path) and went on to apply the source's metadata
to it. The mode, time and user metadata writes are individually confined, but
the birth time write on Windows was not, so an untrusted source could set the
creation time of a directory outside the destination.
Do the existence check through os.Root when translating symlinks so a path
through a planted symlink is refused up front, whatever metadata follows. The
check is unchanged when not translating symlinks.
With -l/--links the local backend faithfully recreates a source ".rclonelink"
as a real symlink at the destination. Directory and file metadata writes
(chmod/chown/chtimes/btime) are confined to the destination with os.Root so a
symlink planted by an untrusted source can't redirect them outside it. The
user metadata write (user.* xattrs) was not confined though: setXattr called
xattr.LSet on the raw path, which doesn't follow the leaf but does follow any
symlink in the parent components. When the destination already contained a
planted symlink "pwn" pointing outside the root and the target held an
existing child "sub", syncing a source directory "pwn/sub" with --metadata
set attacker chosen user.* xattrs on the outside directory, and reported
success as the confined mode and time writes had nothing to do.
Write the xattrs through a descriptor opened under os.Root when translating
symlinks, so a planted symlink can no longer redirect the write out of the
destination, while legitimate in-tree writes are unaffected. A write only
file which can't be opened for reading is opened for writing instead so its
owner can still set xattrs on it. Translated links are themselves the leaf
and keep the non-following path based write.
With --auth-proxy, serve sftp, ftp, http and webdav cached the
program's answer for a user and password/public key and never asked
the program again while the entry stayed in use. As the cache only
expires after 5 minutes of disuse, a user revoked in the program kept
access for as long as they kept making requests or logging in from the
same address with the same credential.
A cached answer is now checked with the program again after 5
minutes, as serve s3 already does for access key IDs, so revoking a
user takes effect within 5 minutes. For serve sftp this applies to new
logins; an existing connection is not closed.
Every download sends the app token (X-Auth-Token) and the session
token (X-User-Token) to the download URL handed out by the API so that
premium access is recognised. The http client followed any redirect
from that URL with Go's default policy, which only strips Authorization
and Cookie style headers on a change of host and never looks at the
scheme, so both tokens were forwarded to any host the download server
redirected to, including over plaintext HTTP.
The client now uses rest.StripHeadersOnCrossHostRedirectFn, so it
refuses to follow a redirect which downgrades HTTPS to HTTP and strips
both tokens from any redirect which leaves the host and port of the
original request, matching the b2, s3, webdav and http backends.
The two-factor authentication step of the config flow checked whether
the server URL started with a slash rather than ended with one, so a
URL which already ended in a slash had another appended and the token
request went to "//api2/auth-token/".
The seafile backend used Go's default redirect policy, which copies the
Authorization header onto a redirect to the same host even when the
redirect downgrades from HTTPS to plaintext HTTP. A server or proxy
which issued such a redirect would cause the long lived Seafile API
token, or the user's password during the 2FA configuration flow, to be
sent in cleartext where anyone on the network path could read it.
Refuse to follow redirects which downgrade HTTPS to HTTP, as the webdav
backend already does.
Every NetStorage request carries the account name and a request
signature in the X-Akamai-ACS-Auth-Data and X-Akamai-ACS-Auth-Sign
headers, with the signed operation in X-Akamai-ACS-Action. The http
client followed any redirect with Go's default policy, which only
strips Authorization and Cookie style headers on a change of host and
never looks at the scheme, so all three headers were forwarded to any
host the NetStorage endpoint redirected to, including over plaintext
HTTP. The signature is bound to the path and action but can be
replayed for that same request within the server's time window.
The client now uses rest.StripHeadersOnCrossHostRedirectFn, so it
refuses to follow a redirect which downgrades HTTPS to HTTP and strips
the three headers from any redirect which leaves the host and port of
the original request, matching the b2, s3, webdav and http backends.
A v2 or v3 auth request posts the user's password, or the application
credential secret, to the auth server in a JSON body which net/http
can rewind. On a 307 or 308 redirect to another host it resent that
body, password included, to the new host.
The transport now refuses to follow a redirect which leaves the host
and port of the original request if it would resend the request body.
Every Swift storage request carries the account scoped session token
in the X-Auth-Token header, and a v1 auth request carries the user
name and API key in X-Auth-User and X-Auth-Key. The swift library
makes its own http.Client with Go's default redirect policy, which
only strips Authorization and Cookie style headers on a change of host
and never looks at the scheme, and it does not let rclone replace that
client. So the token, or the API key, was forwarded to any host the
storage or auth server redirected to, including over plaintext HTTP.
rclone does supply the library's Transport, so the redirect policy is
now applied there instead: the transport refuses to follow a redirect
which downgrades HTTPS to HTTP and removes the three headers from any
hop once the redirect chain has left the host and port of the original
request, matching the b2, s3, webdav and http backends.
A server which redirects an https:// request to an http:// URL makes
rclone replay the request in cleartext. Any credentials the backend
sets as headers are then readable by anyone on the network path, and
the response can be altered in transit.
The transport now refuses to follow such a redirect, so this holds for
every client built on it, including those of libraries which install
their own redirect policy. Use --allow-https-downgrade to follow the
redirect anyway, for example when downloading from a mirror which
redirects to plain HTTP.
Backends which check redirects themselves still refuse a downgrade
which would send their credentials in cleartext, regardless of the
flag.
With --auth-proxy, the proxy program was run from PublicKeyCallback,
which the SSH library also calls when a client merely offers a public
key without a signature to ask whether it would be accepted. Anyone
who knew a user's public key could make the proxy program run and a
backend be created and cached for that user without holding the
private key. Combined with GHSA-66qv-66mg-9h74 this let an attacker
log in as any user whose public key they knew, from their own IP
address and without the user having logged in.
The proxy program is now run from VerifiedPublicKeyCallback, which is
only called once the client has signed with the private key.
With --auth-proxy, the cache of backends made by the auth proxy
program was keyed on the user, the credential and the client IP but
not on whether the credential was a password or a public key. A public
key is not a secret, so once its owner had logged in with it anyone
who knew it could, from the same IP address and within the cache
lifetime, log in as that user by sending the base64 public key as
their password. The cached backend was returned without the proxy
program being asked, so a program which refuses all passwords was
bypassed. A password login arriving at the same time as the public key
login could also share its proxy call for the same reason.
The kind of credential is now part of the cache key and is checked on
a cache hit, so a public key sent as a password is passed to the proxy
program to decide, as it was on a cold cache.
Newer Homebrew makes 'brew untap --force' fail with 'No available tap'
if the tap isn't installed. Whether homebrew/core is tapped varies
between macOS runner images, so the mac_arm64 and mac_amd64 jobs were
failing intermittently. Only untap the taps which are present.
Microsoft is retiring pre-authenticated (tempauth) URLs in OneDrive
for Business and SharePoint from April 1, 2027. Rclone versions before
v1.76 rely on them, so after that date their downloads, chunked
uploads and server-side copies will fail. Rclone v1.76 and later keep
working with the existing configuration.
Call this out at the top of the OneDrive docs and in the
Pre-authenticated URLs section.
See: #10070
If the server sent the whole file, or a different range, in response
to a ranged download rclone returned it as if it were the requested
range, which gave wrong data when seeking.
Check the response with rest.CheckContentRange and return an error if
it doesn't satisfy the requested range. fs.ErrorRangeIgnored lets
multi-thread copies fall back to a single stream.
This guards against Graph serving downloads itself after the
retirement of pre-authenticated (tempauth) URLs.
See: #10070
Microsoft is retiring pre-authenticated (tempauth) URLs in OneDrive for
Business and SharePoint from April 1, 2027. After that the download
redirect, upload session and copy monitor URLs need a SharePoint token,
which the Graph URL tenant setting doesn't change for upload sessions
or the copy monitor.
When SharePoint returns one of these URLs without tempauth rclone now
gets a token for the SharePoint host by redeeming the existing refresh
token, which works with the existing Graph consent so no reconnect or
config change is needed. Pre-authenticated URLs are still called
without authentication, so nothing changes until Microsoft retires
them.
If no SharePoint token can be got, for example with client credentials
using a secret, files under 250 MiB are uploaded in a single request
instead and server-side copies fall back to download and upload. A
copy monitor which redirects to the new item, as authenticated ones
do, is treated as finished, a refused monitor fails straight away
instead of polling until the timeout, and 401 errors explain the
retirement.
See: #10070
ScopedTokenSource returns a token source which fetches tokens for
different scopes using the same credentials. Without the client
credentials flow it redeems the current refresh token for the new
scopes, which Microsoft Entra ID allows for other resources such as
SharePoint. The tokens are cached but never stored in the config file.
The help for these options named flags that do not exist, so copying
them gives "unknown flag": --sse-customer-key(-base64) is
--b2-/--s3-sse-customer-key(-base64), --azurefile-upload-concurrency is
--azurefiles-upload-concurrency and --ignore size is --ignore-size.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Authorization header parser only accepted the scheme spelled
exactly "Basic", so a client sending "basic" or "BASIC" was refused
with 401 even with the right credentials. Auth schemes are
case-insensitive (RFC 9110 section 11.1), and net/http's
Request.BasicAuth already compares the scheme with EqualFold.
HTTPConnectDial built the Basic credential from url.Userinfo.String(),
which percent-encodes the user name and password. A proxy URL such as
http://me%40example.com:p%40ss@proxy:3128 sent "me%40example.com:p%40ss"
instead of "me@example.com:p@ss" and the proxy refused it.
Use Username() and Password() like net/http does, which also always
puts the ":" separator in.
Every close of a dirty cache item re-adds it to the writeback queue,
and WriteBack.Add reset the upload timer even when the file had not
been modified since it was last queued. A file which is opened and
closed more often than --vfs-write-back - for example a git object
that another process keeps reading - therefore never got uploaded
while the reads continued.
Before this change, with --vfs-write-back 2s, a file reread every
0.5s through a mount was still not uploaded after 12s of reads. After
it the file is uploaded after 2.6s, while the reads carry on.
This only moves the timer on when the item has been modified, which
was already the condition for cancelling an upload in progress. An
unmodified re-add now also leaves the retry backoff of a failed
upload alone.
Presigning failed with "not found: FixupRequest" for providers with
use_x_id or sign_accept_encoding set to false, breaking rclone link,
the link backend command and use_presigned_request uploads.
Presigning replaces the Signing middleware with PresignHTTPRequest
before APIOptions run, so anchor the request fixups on whichever is
present.
An SSH_FXP_RMDIR request on a file removed the file, as rmdir and
remove were both handled by VFS.Remove. Make rmdir return ENOTDIR for
anything which isn't a directory, as serve ftp already does.
A Browse request with a negative StartingIndex or RequestedCount made
the handler panic slicing the directory listing, so the client got a
plain text 500 with the Go runtime error instead of a SOAP fault.
These arguments are ui4 in the ContentDirectory SCPD, so reject
negative values with the UPnP "Argument Value Invalid" (600) error.
The brotli decoder before v1.2.7 can panic on malformed input
(GHSA-c9gr-gmg4-2vqv). It is reached via github.com/mholt/archives when
"rclone archive list" or "rclone archive extract" sniffs the format of
an input file, so a crafted file could crash those commands.
Thanks to @rvkumar396 for reporting.
The 5 chunker remotes with 50 byte chunks split each test file into
many objects which made them expensive to run against AWS S3. Run
them against the TestS3Minio docker server instead and keep
TestChunkerS3 with the default chunk size on AWS.
If a remote has no start script but wraps another remote via its
"remote" config value, start the server for the wrapped remote
instead. This lets wrapping backends be tested against the docker
test servers, e.g. chunker over TestS3Minio.
test_all now honours RCLONE_CONFIG in the same way as the tests it
runs, so it finds the same wrapping remotes.
After an upload PikPak completes it in an asynchronous task. rclone
polled the task with low level retries which gave up after about 12
seconds, and upload tasks now regularly take longer than that (up to
34s seen), so the upload was cancelled and failed with "can't verify
the task is completed".
Poll the task with its own backoff for up to 5 minutes instead. This
also stops a pending task from slowing every other API call by
counting as rate limiting in the pacer.
The go-http-auth library is unmaintained and rclone only used a small
part of it, so this replaces it with a small implementation of basic
auth and htpasswd file checking supporting the same bcrypt,
MD5-crypt ($1$ and $apr1$) and {SHA} formats.
It is tested against hashes made by htpasswd, openssl, mkpasswd and
Python bcrypt, expecting the same results as go-http-auth gave.
A missing or unreadable htpasswd file now logs an error and rejects
requests rather than panicking, malformed lines are logged and skipped,
and entries in unsupported formats are logged when the file is loaded.
Their fixed ports, 39980 and 38081, are in Linux's ephemeral range
(32768-60999), so a socket from another concurrent test could hold
them and stop the server starting. This made all 5 tries of the Sia
fs/operations tests fail with "address already in use".
The server-side overwrite tests ran norename transactions on the
nometa test remotes, which can't support them, so they failed with
"readXactID requires valid metaobject".
When a negative --offset reached back past the start of a file and
--count was set, rclone cat printed bytes from the end of the file
instead of from its start. For example on a 10 byte file
rclone cat --offset -12 --count 5 file
printed the last 2 bytes rather than the first 5. With several files
of different sizes, each file shorter than -offset was affected.
The start of the range stayed negative after the file size was added,
and a negative Start with an End set is a suffix range (the last End
bytes). Clamp the start to 0 instead, which matches what
`tail -c 12 file | head -c 5` prints. --tail and --offset without
--count were not affected.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The titlecase transform is documented as converting the file name to
Title Case, but it used strings.ToTitle, which maps every letter to its
title case form - upper case for nearly all letters - so it did the
same as uppercase: "hello world.txt" became "HELLO WORLD.TXT".
Use golang.org/x/text/cases instead, so it becomes "Hello World.txt".
A ranged read through the VFS opens the object at offset 0 and then
seeks, so the first RETR is closed before anything was read. Egnyte
answers that close with 425 instead of 426.
425 was not masked, so ftpReadCloser.Close quit the connection and
returned an error. The VFS then retried and closed the same reader
again; the second Close got nil from the already closed response, put
the connection that had been quit back in the pool and returned its
token a second time. Later operations failed with "use of closed
network connection" and serve s3 returned InternalError.
Now a second Close does nothing, a connection is never pooled after
Quit, and 425 is masked only when nothing was read, so a transfer cut
after data was read is still an error.
strconv.ParseFloat accepts "NaN", and NaN compares false with every
bound, so it got past the overflow check added for 300y and was then
converted to a time.Duration, which gives the minimum int64 on amd64.
--max-age NaN or --min-age NaNM silently became about -292 years,
which moves the filter's ModTimeFrom into the future and excludes every
file, instead of being rejected.
waitForJob polled the copy monitor URL with http.Get, which uses
http.DefaultClient and no context. The poll ignored the configured
transport (--ca-cert, --no-check-certificate, --timeout, --user-agent,
--dump and so on) and could not be cancelled, so a monitor that stopped
answering blocked a server-side copy indefinitely.
Build the request with the context and send it through the unAuth
client, which uses the configured transport without adding the
Authorization header the monitor URL does not need.
When a server-side copy or move finished without reporting progress
along the way, its bytes were added to the stats but not to the
transfer, so until the transfer ended core/stats reported totalBytes as
the bytes done plus the whole size of the transfer still in progress.
A 1 GiB server-side copy on the local backend showed 1 GiB of 2 GiB
while rclone checked the hashes.
The bytes now go on the transfer as well, as they already did for
backends which report progress through NewServerSideCopyAccounter.
Configuring traditional authentication set client_id but kept any
existing client_secret, for example one left in the config from legacy
authentication with a machine specific API key. Whitelabel services
that have moved to an Ory Hydra based identity provider, such as elkjop
and elko, reject a client secret for their public clients, so the token
exchange failed with:
invalid_client: The OAuth 2.0 Client supports client authentication
method 'none', but method 'client_secret_post' was requested.
Clear client_secret, as standard authentication already does.
Elkjøp Cloud and ELKO Cloud have moved from Keycloak to an Ory Hydra
based identity provider. rclone picks up the new endpoints from the
OpenID discovery document, but authentication failed with:
invalid_scope: The OAuth 2.0 Client is not allowed to request scope
'jotta-default'.
The discovery documents now only list openid, offline and
offline_access as supported scopes, so drop jotta-default and request
openid and offline_access, as already done for phonero and letsgo,
which are also on Hydra.
Existing remotes must be reconnected with "rclone config reconnect",
as tokens issued by the old provider are rejected with invalid_grant
when refreshed.
Seafile stores file and directory names in NFC whatever form they are
uploaded in, but looks them up byte for byte. A file uploaded with a
decomposed (NFD) name, as macOS produces, could therefore not be found,
renamed, copied or moved afterwards, and the operations integration
test TestMoveUnicodeEquivalentWithoutServerSideMove failed.
This normalizes paths inside a library to NFC before they are sent to
the server. Library names are left as they are since Seafile stores
them verbatim.
findLeafItem matches names with EqualFold, but the backend did not set
CaseInsensitive, so a case-only rename was not seen as the same object
and the file being renamed was trashed before the move.
validateHour only checked the length of the hh:mm field and then
parsed the two halves with strconv.Atoi. It never checked the third
character, so "10h20" or "Mon-10.20" were accepted as 10:20, and
since Atoi accepts a sign, "+1:00" and "Mon-10:+5" were accepted
as 01:00 and 10:05.
Require the colon and four digits so these are reported as invalid
time specifications like other malformed times.
SizeSuffix.Set and CountSuffix.Set accepted "nan" before a suffix, so
--max-transfer nank became -1 ("off") without an error. CountSuffix.Set
also had no overflow check, unlike SizeSuffix, so 10E or infk wrapped
to the same value.
A glob like file[^a].txt was translated to (^|/)file[^a]\.txt$ in path
mode, so it matched file/.txt across a directory boundary, unlike ?
and the [!...] form.
Treat a leading ^ in a character class the same as a leading !: in
path mode it now also excludes /, and [^] is rejected as an empty
character class. Document both negation forms in the filtering
grammar.
The filtering docs give the character class syntax as [ [ ! ]
{ character-range } ], but globToRegexp copied the ! into the regexp
unchanged, so file[!a].txt matched filea.txt and file!.txt and
excluded fileb.txt - the opposite of what was asked for.
Translate a ! at the start of a character class to ^. In path mode
the class also excludes /, in the same way ? does. [!] is rejected
as an empty character class.
The docs say Time options take the same relative forms as Duration
options, with "1d12h" as an example, but ParseTime only tried
time.ParseDuration and single-suffix values, so --max-age style
Time options such as --s3-version-at 1d12h were rejected.
Go enables MPTCP on listening sockets on Linux by default, which
exposes rclone servers (rcd, serve http/webdav/s3/sftp/ftp etc) to the
Linux kernel use-after-free CVE-2026-98432, demonstrated as remote code
execution via an MPTCP listener.
This sets `godebug multipathtcp=0` in go.mod so rclone listens with
plain TCP. Outgoing connections are unaffected as Go does not use MPTCP
for them by default.
This is a mitigation only - the fix is to upgrade to a patched kernel.
Users who need MPTCP can re-enable it with GODEBUG=multipathtcp=2.
See: https://go.dev/issue/82717
OverrideRemote.UnWrap returns nil when the wrapped ObjectInfo is not an
fs.Object, as when serve s3 uploads through a combine remote over crypt.
Hash then called Fs() on that nil object and panicked. Return an empty
hash instead, as for other sources which cannot be unwrapped.
The Box OAuth page, the SSH CBC paper on isg.rhul.ac.uk and the
nextcloud/nextcloud-snap issue all 404 now. Point them at the current
Box refresh token guide, the IEEE S&P 2009 copy of the paper and the
moved nextcloud-snap/nextcloud-snap repo.
Downgrade the google.golang.org/grpc dependency from a dev-version
(v1.85.0-dev.0.20260825072537-93e31b48545e) to the latest stable version
(v1.84.0). The downgrade does not introduce any vulnerabilities according to
govulncheck. The -dev version got carried forward from a110fa5d4 when it was
needed.
Seek documents that a position past the cached data returns the maximum
usable offset with an error, but it returned offset - (abs - cacheLen),
which is only the cache length for io.SeekStart. For io.SeekCurrent and
io.SeekEnd it returned an arbitrary, possibly negative, number.
TestVarious computed the expected date when it built the test table and
the transformed name later. When a minute ended between the two, the
macfriendlytime case failed, for example 0243PM against 0244PM.
The date cases are now in TestDate, which reads the clock before and
after the call to Path and accepts either date.
With --metadata rclone applied the uid and gid of the source with
chown, which fails with "operation not permitted" for a user other than
root whenever the source belongs to another user or group (a file of
root or of a colleague), so the copy of such a file failed.
As a user other than root, leave the uid as it is (only root can give
a file away) and set the gid only to a group the process is a member
of, as rsync -a does; skip the chown if neither applies. Root is
unchanged.
Before this change, a Range request starting at or after the end of the
object, or ending before it started, was answered with a 206 and a
malformed Content-Range such as "bytes 10-9/10". Resuming a complete
download with curl -C - or wget -c, which send such a range and expect a
416, saw a broken partial response instead.
After this change these requests get a 416 with "Content-Range: bytes */size",
and a Range on an empty object is ignored, the same as http.ServeContent
does for rclone serve http.
Remove the unused progress notification goroutine, which kept each
Uploader and its buffered Upload alive after uploading finished or failed.
Test that uploads can be collected after success, failure and empty uploads,
while preserving retries and repeated Upload calls.
newPutTestBackend returned a second backend rather than the server's
own, so the server's Shutdown never aborted the multipart uploads made
through it. Their temporary files were left open, which stopped the
test's temporary directory being removed on Windows:
TempDir RemoveAll cleanup: unlinkat ...\test-37: The directory is not empty.
canSetModTime looked up the object it had just written without any
retries, so on backends with read-after-write lag such as Internxt the
lookup failed. As the result is only worked out once, the remaining
TestFileSetModTime subtests then ran assuming the modtime could be set.
Seahub throttles anonymous API calls, which includes logging in, to 60
per minute. When cmd/bisync and backend/seafile ran concurrently they
exceeded this and every new remote failed with "failed to
authenticate" until the end of the retries.
Huawei Drive doesn't decode JSON request bodies as UTF-8 unless the
charset is given. This meant creating directories or empty files with
non-ASCII names failed with "The fileName can not be blank and can not
contain..." and server-side copies mangled the names, eg `équivalent.txt`
became `�quivalent.txt`.
Send "application/json; charset=UTF-8" on all requests with a JSON
body as is already done for the multipart upload metadata.
The fauria/vsftpd image limits each IP to 50 connections. When
cmd/bisync and backend/ftp ran concurrently against the shared server
the backend tests were refused with 421 "There are too many
connections from your internet address" on every retry.
Explain the choices behind many of the limitations of serve s3, such as
keeping metadata in memory rather than in a database, and link the
limitations to them. Also fix the statement that empty directories
can't be made below the buckets, as they can by putting a directory
marker.
--no-cleanup was documented as keeping the directories emptied by
DeleteObject, but has never been implemented. --directory-markers
keeps them, listing them as directory markers so the listings stay
consistent, so make --no-cleanup do that, logging that it is
deprecated.
With --directory-markers, deleting the directory marker of a directory
holding other keys had no effect, so the directory was still listed
and wasn't removed when the last key below it was deleted. This left
directories behind when the keys of a directory were deleted in an
order other than deepest first, as rclone's s3 backend does when
purging.
Remember the deletion in memory, so the marker isn't listed and the
directory is removed with the last key below it.
Rmdir only removed the directory marker of a directory below the root
of the remote, so `rclone rmdir remote:bucket/dir` left the marker of
dir behind, and with it the directory.
gofakes3 v0.0.12 checks the bodies of requests which aren't object
data, such as DeleteObjects, against their Content-MD5 and
x-amz-checksum-* headers.
See #10082
Clients which rely on directory markers, such as rclone's s3 backend
with --s3-directory-markers, need the markers of directories holding
other keys listed, which serve s3 only does while it remembers the
marker was put.
With --directory-markers every directory is listed as a directory
marker, and isn't removed when the last key below it is deleted.
See #10082
A CompleteMultipartUpload which failed because the parts it listed
didn't match those uploaded, or had a gap in the part numbers, aborted
the whole upload. Leave the upload open instead, as S3 does, so the
client can upload any missing parts and complete it again.
See #10082
Check the Last-Modified of HEAD and GET is the modification time set
from the X-Amz-Meta-Mtime or mtime metadata, as in listings, rather
than the time of the PUT.
See #10082
A directory marker such as dir/ was only listed while its directory
was empty, as nothing recorded whether it had been put or the
directory made by putting a key below it. On S3 a marker which was put
is listed until it is deleted, which some clients rely on.
The markers put are now remembered with the object metadata, in memory,
and listed while the directory holds other keys, and their directories
are no longer removed when the last key below them is deleted.
See #10082
Replace the list of supported operations with a table of what is and
isn't compatible with AWS S3, and remove the bug about multipart server
side copies, which work now that UploadPartCopy is implemented.
See #7454
The MD5 of the whole object, needed only for MD5 ETags, was computed
for every multipart upload, costing about a CPU-second per GiB even
with --etag-hash "" or a different hash.
With a delimiter other than "/" found in directory names, each
directory rolling up into a common prefix was read to see if it had
any keys, even if that prefix had already been listed, on this page or
an earlier one.
With --etag-hash MD5, the default, on a remote which can't give the
MD5 of objects, such as crypt or sftp, PutObject returned the MD5 of
the data as the ETag but HeadObject and listings returned "".
Listings also ignored the MD5 kept for multipart uploads, and copying
an object onto itself to replace its metadata lost it, along with the
sizes of its parts.
The MD5 of a PutObject is now kept with the object's metadata, as for
multipart uploads, listings return it, and replacing the metadata of
an object keeps it.
See #10082
A PutObject or CreateMultipartUpload which failed left behind the
directories made for its key, which are listed as directory markers,
so the client saw keys such as "newdir/sub/" it never made.
They now remove the directories they made if they are still empty, as
aborting a multipart upload already did.
CopyObject with a directory marker such as "dir/" as its source failed
with InvalidArgument, although HeadObject and GetObject found it.
It now copies the marker, making the directory of another marker or an
empty object.
See #10082
Putting a directory marker such as "dir/" made the directory, but a
listing without a delimiter didn't return the "dir/" key, so clients
which make empty directories with markers, as the AWS console and many
sync tools do, lost them.
An empty directory is now listed as its directory marker. Other
directories aren't, as they may not have been made by putting one.
See #10082
With an --etag-hash other than MD5, including "" to turn hashing off,
PutObject returned the MD5 of the data and CompleteMultipartUpload an
S3 style multipart ETag, neither of which matched the ETag HeadObject
and listings returned for the object afterwards.
These now return the ETag HeadObject does. Listings now quote an empty
ETag as HeadObject does.
See #10082
This updates expected-pass.txt for the fixes to gofakes3 and serve
s3, which make 125 more of the s3-tests pass, 297 in all.
36 tests no longer pass, as they only passed because requests were
silently ignored or mishandled:
- 19 used SSE-C or SSE-KMS, now refused as they aren't supported
- 12 browser POST uploads expected 400, but unauthenticated requests
now get 403 as on AWS
- 5 set an ACL other than private, or object lock, now refused
The s3-tests config now gives the owner gofakes3 uses for the main
user, so the ACLs returned match it, and no api_name, as bucket
locations aren't stored.
gofakes3 now checks the If-Match and If-None-Match conditions of
PutObject and CompleteMultipartUpload before the object is written,
but another write can replace the object before this one finishes.
For example, of many concurrent uploads of a new object with
If-None-Match: *, most succeeded.
Now when the object is written to a temporary object and moved into
place, the conditions are checked again as it is moved, with the moves
of objects to the same path through the server serialised, so only one
of the uploads in the example succeeds.
CompleteMultipartUpload returned the AWS S3 style multipart ETag (the
MD5 of the parts' MD5s with a "-N" suffix), but HeadObject and GET
returned the hash of the object, so clients saw the ETag of an object
change as soon as it was uploaded.
Now with --etag-hash MD5, the default, the ETag returned by
CompleteMultipartUpload is the MD5 of the whole object, worked out as
it is streamed, so is the same as HeadObject returns. With other
hashes it is stored with the object's metadata and returned by
HeadObject while the metadata is kept.
The sizes of the parts are kept with the metadata too, so GET and
HEAD with partNumber and GetObjectAttributes can return the parts of
the object.
Keys ending in "/", such as "dir/", are used by S3 clients as
directory markers. gofakes3 used to strip the "/", so putting "dir/"
made a file "dir", which then stopped "dir/file" being written. Now
that gofakes3 keeps the "/" these keys were refused.
Now a key ending in "/" stands for the directory: putting an empty one
makes the directory, putting one with data is refused, HEAD and GET
return an empty object if the directory exists, and deleting one
removes the directory if it is empty.
Writing an object which conflicts with a directory, or whose directory
conflicts with an object, now fails with InvalidRequest rather than an
internal error, which clients retried.
Listings treated any delimiter as "/", so a listing with a delimiter
such as "a" or "." returned the keys and directories rather than the
keys rolled up into common prefixes at the delimiter. A delimiter of
whitespace was ignored altogether.
Now any delimiter is supported. Delimiters other than "/" are found by
walking the directory tree below the prefix, skipping directories
whose names contain the delimiter as all their keys roll up into the
same common prefix.
A prefix starting with "/" now matches nothing, as no key can start
with "/", rather than listing the whole bucket.
The ETags of objects in listings were returned without the double
quotes S3 uses, as in HEAD and GET responses, so clients which compare
them saw a difference.
Object tags given when an object is created are kept with its metadata
by gofakes3. This implements gofakes3's ObjectTaggingBackend so the
tags of an existing object can be changed with PutObjectTagging and
DeleteObjectTagging.
Like the rest of the metadata, tags are only kept in memory.
CopyObject merged the source object's metadata into the metadata it
was given, so a copy with x-amz-metadata-directive: REPLACE kept the
source's metadata. It also kept the source's modification time by
adding it to the metadata, so every copy gained "mtime" user metadata
the client hadn't set.
gofakes3 now works out the complete metadata of the copy from the
metadata directive, so it is used as given. The source's modification
time is still kept, unless the metadata sets one, but without adding
it to the metadata.
Starting a multipart upload makes the directory its object will be in,
but aborting the upload didn't remove it. The empty directory isn't
visible to S3 clients, so the bucket looked empty, but deleting it
failed with BucketNotEmpty.
Now aborting an upload removes the directories made for it if they are
still empty, and deleting a bucket holding nothing but empty
directories removes them and succeeds. Deleting a bucket which fails
for some other reason than holding objects now returns that error
rather than BucketNotEmpty.
Removing the empty directories left after deleting an object now
checks they are empty first, rather than logging an error for each
directory which isn't.
This adds cmd/serve/s3/s3tests/run.sh which runs the ceph/s3-tests S3
compatibility suite against rclone serve s3, built from the tree.
It fetches s3-tests at a pinned commit, makes a venv with pinned
dependencies, starts rclone serve s3 with the users s3-tests needs and
summarises the results by feature area.
As serve s3 doesn't implement many S3 features (ACLs, versioning,
lifecycle, policies, etc) the results are checked against
expected-pass.txt, a list of the tests which should pass, rather than
a list of failures. The run fails if any of these don't pass, and
run.sh --update records newly passing tests.
A small pytest plugin adapts the s3-tests cleanup to serve s3 so a
bucket which can't be deleted fails only the test which left it
rather than every test after it.
Linkbox renames an upload to name(1) when its name is the start of an
existing file's name, which breaks TestCaseInsensitiveMoveWithoutServerSideMove.
Internxt listings lag behind directory renames and deletes for longer than
the list retries allow, which breaks TestDirRename and
TestDirRenameParentEntries.
Remotes made by RandomRemote aren't in the fs cache so they were never
shut down, leaving any pooled connections open until they timed out.
On TestFTPProftpd this used up the server's 30 connection limit part way
through the fs/operations tests, making all the following tests fail
with EOF when connecting.
- CVE-2026-111943: crypto/x509: mitigation for security concerns with a specific Certificate Authority
This also updates the go1.26 test job to go1.26.10 which contains the
same fix.
Upgrade to v0.62.0 of golang.org/x/net in order to address:
- CVE-2026-78661: html: avoid quadratic complexity when coalescing text nodes
- CVE-2026-97045: html: check all ancestor namespace transitions when rendering raw text
Linkbox converts file extensions to lower case on upload, and renames an
uploaded file to name(1) if its name is the start of the name of a file
already in the directory, e.g. "apple" next to "applebanana". Document
both and ignore the TestTransform cases which they break.
Since control characters in names are no longer encoded by default, a
file uploaded with a newline in its name keeps it on the server. Some
restricted shells, such as the one on rsync.net, end the command at a
newline even when it is quoted, so the hash command ran on a different
path and failed, or could have hashed a different file.
rclone now doesn't use a remote command to hash a file with a newline in
its name on a unix shell, and reports the hash as unavailable instead.
GCS can't store CR or LF in object names and the S3 provider doesn't
encode them. These cases ran once the bisync pre-flight check was fixed
to stop skipping them.
Some S3 providers, such as Wasabi and Alibaba, report success when
creating a bucket which already exists, so rclone logs that the bucket
was created. The during-sync hooks in the equal_skipped test create a
new Fs, which logged this in the middle of the bisync run and made the
log differ from the golden one. Ignore these messages.
The test looked up the object it had just uploaded without retrying, so
it failed with "object not found" on backends such as Cloudinary and
Internxt where new objects take a moment to appear.
Jottacloud is case insensitive, so a server-side copy to "File1" while
"file1" was in the trash restored the trashed file under its old name
"file1". The restored file is now renamed to the requested name.
When a server-side copy landed on a file that was in the trash, rclone
restored it with a zero creation and modification time unless
--metadata was in use. The server then returned a time which couldn't be
parsed, the copy was reported as having size 0 and failed with
"corrupted on transfer".
The restored file now gets the source's times, as it did before
metadata support was added.
Add the hash type crc32c. It is the checksum that AWS S3 stores for
objects uploaded with x-amz-checksum-crc32c (FULL_OBJECT checksum),
and the checksum that iSCSI, ext4 and Btrfs use. Go calculates it with
the SSE 4.2 and ARMv8 CRC32 instructions where they are available.
With it, rclone can calculate the value that S3 returns in
ChecksumCRC32C for a local file:
rclone hashsum crc32c file
prints it as hex; `--base64` prints it in the URL-safe base64 alphabet
that rclone uses for every hash (`-` and `_` where S3 prints `+` and
`/`).
The new type is registered after the existing core types, so their
Type values do not change. The types that backends register (dropbox,
hidrive, mailru, quickxor) move up one bit, as they did when blake3,
xxh3 and xxh128 were added; no Type value is persisted (the hasher
cache and bisync listings store hash names).
If a file is changed on both sides and the conflict names it gets renamed
to (e.g. X.conflict1 and X.conflict2) were deleted on both sides since the
last run, the listing update removed the just-copied conflict files again
when processing the deleted-on-both queue. The rename bookkeeping then
could not find them and bisync aborted with "bisync.renames: internal
error: missing info".
Skip names that this run copied when removing files deleted on both sides,
and add a test case for the scenario.
Before this change, `TestMoveUnicodeEquivalentWithoutServerSideMove`
failed on backends that can't store a file under the NFD name it was given:
`mailru` rejects the name ("invalid characters in object name"), and
`cloudinary`, `filelu` and `seafile` don't find the file again under it.
Tests in `fs/operations` run against every backend, so this one needs to
skip where the precondition doesn't hold.
After this change, the test skips when the NFD name can't be uploaded or
the file isn't found again under it.
Before this change, `operations.Move` and `operations.MoveTransfer`
deleted a file when asked to move it onto its own path without a `dst`
object. Without server-side `Move`, `operations.move` copied the file onto
itself and then deleted the source, which was the same file, and the
`memory` backend's server-side `Move` did the same. `storj` stores every
name in NFC, so there a move from a file's NFD name to its NFC name is a
move onto its own path, and `TestMoveUnicodeEquivalentWithoutServerSideMove`
lost the file on `TestStorj`.
After this change, a move whose destination is the file's own path on the
same remote does nothing, including when the destination `Fs` has a
different root.
Server-side copies with the local backend on macOS failed with `errno -1` when the destination file already existed, for example with `--inplace` or when used under chunker. This was because copyfile(3) with COPYFILE_CLONE implies COPYFILE_EXCL.
This clones to a temporary name in the same directory and renames it over the destination, which keeps APFS cloning for overwrites and leaves the destination untouched if the clone fails.
PikPak's download node sometimes answers a ranged GET with 502 Bad
Gateway and a short HTML body. client.Do treats any completed response
as success, and shouldRetry returns immediately when err is nil, so
that HTML was saved as the file and rclone exited 0. With the VFS
cache those bytes were then served again.
Non-2xx download responses are now converted with the backend error
handler, which closes the body. Statuses in retryErrorCodes (429 and
5xx, including 502) are retried by the pacer. Any other status is
returned as an error. Successful 200 and 206 responses are unchanged.
gofakes3 adds the time of the request as "Last-Modified" to the
metadata it passes to PutObject, CopyObject and multipart uploads.
storeMeta cached it with the rest, so HeadObject and GetObject
replaced the object's modification time with the upload time until the
cache entry was evicted, while ListObjects reported the right time.
storeMeta now drops "Last-Modified" before caching the metadata.
ParseDuration checked for overflow in multi-unit durations such as
300y1d but not in single-suffix ones, so --max-age 300y silently
became a large negative duration, which moved the filter's
ModTimeFrom into the future and excluded every file.
Document forward slashes and escaped backslashes in JSON, including
object parameters sent through a query string or form data. Distinguish
these from ordinary string parameters that do not use JSON encoding.
Fixes#6673
Co-authored-by: Codex <noreply@openai.com>
Before this change, `cmd/bisync/testdata/bisync_vscode_debuggers_launch.json`
was in the repository, and every `-golden` run regenerated it from
`fstest/test_all/config.yaml`, adding unnecessary noise to commit diffs.
After this change, the file is in `.gitignore` and no longer tracked.
`-golden` still writes it locally, for copying debug configurations into
VS Code's `launch.json`.
Before this change, `docs/content/bisync.md` kept its own changelog, and
each bisync pull request added a line under the same heading. The rclone
changelog is already made from commit messages (`bin/make_changelog.py`),
so the entries duplicated it, and caused unnecessary merge conflicts: open
pull requests conflicted with each other whenever one of them merged.
After this change, the section links to the rclone changelog and to the
last version of the bisync changelog on GitHub. The `## Changelog` heading
stays, so links to `/bisync/#changelog` still work.
Before this change, GitHub showed the bisync golden test files
(`cmd/bisync/testdata/*/golden/`) in full in pull request diffs, where a
new test scenario can add hundreds of lines of expected output.
After this change, they are marked `linguist-generated`, like `MANUAL.*`
and `rclone.1`, so GitHub collapses them in diffs and leaves them out of
the language statistics. They are still one click away in a diff.
Before this change, `cmd/bisync` was excluded on `TestS3Rclone`
(`rclone serve s3`), one of the backends `108504963` excluded because the
tests always failed there at the time. After this change, they run there
again: with `test_all` on Linux, every case passes or skips, with or
without the preceding changes to the test harness.
Before this change, the `{eol}`, `{tab}` and `{chr:XX}` placeholders in a
scenario became raw bytes in the name the harness handed to `Put`. A raw
control character isn't a valid rclone name: rclone names are in the
`Standard` encoding, where a newline is `␊`, and `FsEncoding` in
`fstests` encodes every raw name before uploading it for the same
reason.
The `extended_filenames` scenario dates from 2021, when bisync was tested
on local, Drive, Dropbox, OneDrive, S3 and SFTP, but not on crypt or
memory. Its header said that all rclone backends, including local,
return `\n` as a glyph. That was an assumption, and it doesn't hold. A
backend with an encoder lists a raw control character as its glyph
because `ToStandardName` runs `Standard.Encode` on every name it
returns. A backend without one (crypt, memory, linkbox, netstorage,
pixeldrain, storj) lists exactly the name it was given. The same thing
happens with no bisync involved: `rclone touch` with a raw newline in
the name stores a raw newline on crypt, and `sync` from there to local
then copies and deletes the file on alternate runs.
The scenario has never passed on crypt or memory for the right reason.
Its golden listings were recorded on local with `␊`, so until January
2024 they would not have matched the raw names crypt lists, but the
scenario wasn't run there. `57624629d` ran it on crypt, treated the raw
names as an encoding difference, and normalized them away in both the
log and the listings, so from then on the goldens matched. `81d7b6f29`
added the check that each listing matches its path, which that
normalization doesn't cover, and the step has failed on those backends
since: local lists the copy as `␊` while the listing, built from the
source names, holds the raw newline. The pre-flight probe fixed in the
previous commit skipped the scenario on many nightly backends in the
meantime, which kept the failure out of the nightly results.
After this change, a placeholder for a control character (0x01-0x1f and
0x7f) becomes its `Standard` glyph, so every backend receives the same
name. On local the file on disk is unchanged, since the local backend
decodes the glyph to the raw character, and the golden logs are
unchanged. Placeholders for other bytes, such as the invalid UTF-8 in
`{chr:81}` and `{chr:fe}`, still become raw bytes, which `Standard`
leaves as they are.
Before this change, the pre-flight check for the `normalization` and
`extended_filenames` cases uploaded its probe file to the path root, which
already held the scenario's files, and compared the first entry each path
listed. That entry was usually an unrelated file, so the result depended on
listing order rather than on how each backend stored the probe name. On 42
of 71 backends in the 2026-10-15 nightly, the paths appeared to differ and
the cases were skipped; elsewhere the check passed without examining the
probe at all. The check also named `extended_char_paths`, a case renamed to
`ext_paths` in 2024. The probe's uploads counted in the step's transfer
stats, which removed `There was nothing to transfer` lines from the logs of
the checked cases.
After this change, the probe goes in a fresh subdirectory with its own
stats group, and the check compares the name listed back from there. It
names only `normalization` and `extended_filenames`: `ext_paths` uses none
of the probe's invalid UTF-8 or emoji, and doesn't use `--fix-case`. The
`normalization` and `extended_filenames` golden logs gain the lines the
probe's stats had hidden.
Before this change, the default SFTP encoding (`Slash,Del,Ctl,Dot`, the same
as rclone's standard form) passed names through unchanged, so a name on the
server containing a control character, such as a raw newline, reached rclone
in a non-standard form. Other backends, such as local on Linux, list the same
name in the standard form (`␊`), so the two never matched: syncing such a file
from SFTP to local deleted it on every second run, and bisync recorded
listings that didn't match the files.
After this change, the default encoding is `Slash,Dot`, the same as the local
backend's default on Linux, so control characters in names on the server are
converted to and from the standard form. Servers running on Windows can add the
Windows restricted characters with `--sftp-encoding`, as now documented.
The issue was discovered on the bisync integration tests.
This also fixes `mkParentDir`, which encoded the parent `.` of a top-level
file as a name. With the new default encoding, that created a stray `.`
directory on the server.
A connection returned after an error without an FTP reply code can no
longer be trusted, so close it instead of returning it to the pool.
Errors carrying an FTP reply code still get a NOOP liveness probe.
Add a unit test driving putFtpConnection against a fake FTP server
covering network-error discard, live-connection pooling after an FTP
reply-code error, and discard when the NOOP probe fails.
deleteObject() replied 404 for any error from newObject(), while
serveObject() in the same file replies 404 only for
fs.ErrorObjectNotFound and 500 otherwise.
restic treats a 404 on DELETE as final and never retries, so a
transient lookup failure while removing a lock left the lock behind
and broke the next exclusive operation (eg forget --prune failing with
"repository is already locked").
This change mirrors serveObject() handling of 404/500 errors in
deleteObject().
Fixes#10100
Co-authored-by: bunnysayzz <bunnysayzz@users.noreply.github.com>
Before this change, renaming a file or directory through a name that
matched it only by ignoring case (`--vfs-case-insensitive`, the default on
macOS and Windows) or Unicode normalization left its old entry in the
parent directory. The item was listed twice, once under its new name and
once under the old key, and the old name still resolved to it until the
directory cache expired. `Dir.Rename` removed the parent's entry under the
name it was given rather than the item's real name. For a directory
renamed within its own parent, this was masked until `36ce1a28f`, which
removed a second deletion in `Dir.rename`.
After this change, `Dir.Rename` removes the entry under the item's real
name. The issue was discovered on the bisync integration tests.
Before this change, on backends without server-side `Move`, renaming a
file to an equivalent name deleted it: a name differing only in case on
a case-insensitive backend, or only in Unicode normalization on a
backend such as Cloudflare R2, which treats NFC and NFD names as
equivalent. `operations.move` fell back to copying the file to the new
name and then deleting the source, but both resolved to the same object,
so the delete removed the file it just copied. This affected
`--fix-case` renames in `sync` and `bisync`, `--track-renames` with
`--no-unicode-normalization`, and renames through the VFS
(`rclone mount`, `rclone serve`). `MoveFile` and backends with
server-side `Move` already handled these renames by moving through a
temporary name.
After this change, the copy-and-delete fallback also moves through a
temporary name when the two names are equivalent. The issue was discovered on
the bisync integration tests.
Cellar is the S3-compatible object storage service of Clever Cloud,
hosted in France.
rclone's defaults work for it: virtual-host style addressing, v2
listings, URL-encoded listings and BucketAlreadyExists behave like AWS.
Cellar does not apply Object Lock headers sent with
CreateMultipartUpload, so object_lock_supported is set to false and the
Object Lock integration tests are skipped. The docs tell users to set
--s3-object-lock-set-after-upload, which applies the lock on multipart
uploads with separate API calls.
The SSE-C options are exposed since Cellar supports customer-provided
keys. The docs note that Cellar rejects CopyObject with an SSE-C source,
so --no-update-modtime is needed and encrypted objects smaller than
--s3-copy-cutoff can't be moved server-side.
The integration tests pass against cellar-c2.services.clever-cloud.com.
jwt v5 removes StandardClaims so use RegisteredClaims instead. v5
sends a single audience as an array of strings, so the audience is
overridden in boxCustomClaims to keep sending it as a string as
before.
This adds a test which checks the JWT assertion sent to the token
endpoint is identical to the one sent with jwt v4.
The tcell v2 pin at v2.9.0 is removed as tcell v3 contains the fix for
duplicated key events on Windows (tcell issue 1124).
Note that tcell v3 uses the Windows VT console modes so ncdu requires
Windows 10 build 1703 or later.
See: #9539
Sorter.Send selects on both the output and error channels of the ext
sort. If the ext sort failed it closes both, so the select could pick
the closed output channel and return without the error. This was seen
as a flaky TestSorterExtTempFileError under the race detector.
extsort now only creates temp files when the data doesn't fit in
one chunk, so the test needs to add more than a chunk of entries to
provoke the temp file error.
This fixes the code to compile with the updated dependencies:
- dlna: pass a *slog.Logger to the ssdp server as required by
anacrolix/dms v1.8.0
github.com/Files-com/files-sdk-go/v3 was not updated as the latest
version requires go 1.26.9.
The tcell and go-systemd pins are kept and github.com/hanwen/go-fuse/v2
is pinned to v2.11.0 as v2.12.0 fails to compile on 32 bit linux - see
https://github.com/hanwen/go-fuse/issues/628
With a relative path override such as `--sftp-path-override @.` (useful
on chrooted hosting where the SSH shell starts in the directory that
SFTP shows as "/"), a file in the root gets the shell path "-name".
`quoteOrEscapeShellPath` does not escape "-", so the hash command
became `md5sum -name`, which md5sum parses as an option. The hash then
failed and the transfer was reported as "corrupted on transfer".
Absolute shell paths always start with "/", so only relative overrides
were affected. Prefix such paths with "./".
When the sftp backend uses an external ssh program (--sftp-ssh), one
goroutine waits for the ssh program to finish by calling cmd.Wait().
cmd.Wait() sets cmd.ProcessState when the program ends.
At the same time, CanReuse() calls exited(), which reads
cmd.ProcessState without any lock. This is a data race and the Go
race detector reports it. It happens in normal use, for example when
a connection is put back in the pool after a stat.
The race only decides whether a pooled connection can be reused, so
it is unlikely to cause problems in practice, but it makes programs
that use the sftp backend fail when they are tested with -race.
This fix sets an atomic.Bool after cmd.Wait() returns and makes
exited() read that instead of cmd.ProcessState.
It also adds a test which calls exited() while Wait() is running. It
fails with -race before this fix and passes after it.
Auto Drive is permanent storage on the Autonomys Network, with an S3
compatible API at s3.auto-drive.autonomys.xyz.
Storage is permanent: a delete hides an object, and every version is
kept. The provider sets:
- force_path_style, as the API uses path-style requests.
- copy_cutoff to the maximum. UploadPartCopy is not supported, so
server-side copies always use a single CopyObject.
- object_lock_supported false. Object Lock is always on in COMPLIANCE
mode and can't be set by clients, so rclone's Object Lock tests
can't pass.
- No Purge. Deleting a version is always refused, so the S3 Purge,
which deletes every version, fails. rclone's generic purge hides
the current version of each object instead.
- no_check_bucket. There are no bucket endpoints: buckets are created
by the first upload into them, and a CreateBucket request would be
stored as an object.
Before this change, the integration tests dropped Dropbox's "Too many
requests or write operations" notice from the logs only in the format it
had when the tests were written, which began with the error code
(`too_many_requests/...`). Since 110bf468a (v1.65), the notice begins with
`Error `, so it was left in the log and caused a miscompare whenever
Dropbox rate limited a run.
After this change, the notice is dropped in either format.
Before this change, the integration tests checked whether each remote could
set modtimes on every `bisync` step (`checkPreReqs`, called from
`runBisync`), not once per remote. On a remote that isn't `IsLocal`, each
check sleeps for 1s twice (the Google Cloud Storage per-object rate limit
workaround from be73a10a9), for each side. That added 2-4s to each of the
~110 `bisync` steps in each test, which on a fast remote was most of the
run: on the integration test server, the `:memory:` tests took about 14
minutes.
After this change, the result of the check is remembered for each remote,
so it runs, and sleeps, once per remote per test run. The `:memory:` tests
now take about 7s locally.
When a remote is configured with RCLONE_CONFIG_<NAME>_<OPTION> the remote
name is only converted to uppercase, so characters such as "-" and "." are
not changed to "_" and may not be settable in some shells.
Explain this in the environment variables section and link to it from
"Valid remote names".
Deleting the contents of a directory and then the directory itself,
before a listing has confirmed the deletes, left the directory's
virtual entries behind. They are counted in every parent directory, and
a directory which counts virtual entries refuses to forget its cache, so
vfs/forget could not make changes made outside rclone visible.
This lasted until the removed directory's own cleanup timer purged its
entries, which happens 2 x --dir-cache-time later.
This fix drops the removed directory's virtual entries once it has been
removed.
Fixes#10083
When a Drive listing error closed the ListR input while directories
were still in the overflow queue, the overflow goroutine sent on that
nil channel while holding the mutex. The main loop then blocked on the
same mutex, so the sync never returned the error.
refillListRInput now stops when the input is already closed, and it
does not add those jobs to the wait group.
Fixes#10069
If a file that hadn't been read was shrunk with Truncate, the cache
file was cut to the new size but the kept part of the file still had
to be fetched on close. The downloader wrote the whole remote object
into the cache file, which grew it back to its old size, so the upload
replaced the object with the original contents.
Stop the downloader writing past the end of the item, and add tests at
the vfscache and vfs levels.
Fixes#10047
gofakes3 prior to v0.0.11 defined the IsTruncated XML field in
ListMultipartUploadsResult and ListMultipartUploadPartsResult with
`xml:"IsTruncated,omitempty"`. When an upload result was not truncated
(IsTruncated == false), the <IsTruncated> tag was omitted from the XML
response.
According to the AWS S3 REST API specifications for ListParts and
ListMultipartUploads, IsTruncated is a required boolean element. Strict
S3 clients such as the official AWS Go SDK (used by Docker Distribution
and OCI registries) unmarshal this field as a *bool pointer. Because the
tag was omitted, the pointer remained nil, causing Docker Distribution
to panic with a nil-pointer dereference at s3.go:593 (for *resp.IsTruncated)
during multi-chunk blob uploads.
Updating github.com/rclone/gofakes3 to v0.0.11 brings in rclone/gofakes3#22,
which removes omitempty and ensures <IsTruncated>false</IsTruncated> is
always present in the response XML.
Fixes#10078
Before this change, nothing checked that the rc `sync/bisync` call accepted
every bisync flag, so the rc could fall behind the command line as flags
were added, as in #7799.
After this change, `TestRcFlagParity` passes each non-hidden bisync flag to
the rc, under the parameter name `rc.md` uses, with a value of the wrong
type. Every getter `rcBisync` uses rejects it, so the call fails only if
the parameter is read, and the test names any flag the rc doesn't handle.
Before this change, since v1.74.0 (bb78eb8ab), the rc `sync/bisync` call
read the `backupdir1` and `backupdir2` parameters (the names used before
they were renamed to `backupDir1` and `backupDir2`) only when the new name
was also given with an invalid value, because the fallback sat inside an
`rc.NotErrParamNotFound(err)` check, which is true only for an invalid
value. Otherwise, `backupdir1` and `backupdir2` had no effect.
The same change made an invalid value for any optional parameter (for
example `"resync": "yes"`, or a `filtersFile` that isn't a string) be
treated as if the parameter were missing, instead of returning an error.
After this change, the old backup dir names are read when the new ones are
missing, and an invalid value returns an error again, as it did before
v1.74.0.
Before this change, bisync treated a `--max-delete` above 100 as 100%, so
the `--max-delete` check never triggered. A run that left no file on a side
unchanged was still stopped, by the "all files were changed" or empty
listing checks, but one that deleted most files while leaving even one
unchanged was not.
For bisync, `--max-delete` has always been a
percentage, "in range 0..100" as Ivan put it when he designed it
(https://github.com/rclone/rclone/pull/5164#discussion_r633470815), but the
range was never enforced. So a value meant as a file count, such as
`--max-delete 500`, turned the check off. Now that the rc takes
`--max-delete` from the global config too, that includes a `--max-delete`
the rcd itself was started with.
After this change, a `--max-delete` above 100 is an error, as the rc's
`maxDelete` parameter already was.
Before this change, `--max-delete` and `--dry-run` were read from the
global config by `applyContext`, which only the command line called. The
rc `sync/bisync` call started from empty `Options` and never ran it, so it
ignored both settings:
- A dry run requested through the global option (`_config`
`{"DryRun": true}` or the flat `dry_run` parameter) was ignored.
`setDryRun` resets `ci.DryRun` from `opt.DryRun` before copying, and
`opt.DryRun` was only set by bisync's own `dryRun` parameter.
- When the `maxDelete` parameter was omitted, the `--max-delete` limit was
0%. A `max_delete` set through `_config` or the flat parameter was not
used for that limit, and applied to the inner syncs as a file count
instead.
After this change, `Bisync` runs `applyContext` itself, on its own copy of
the config, so the command line, the rc, and any other caller follow the
same rules, and `Options.MaxDelete` and `Options.DryRun` are set by
`Bisync`. With no `--max-delete` anywhere, the limit is 50%, as #5680 says
the rc should.
The rc's `maxDelete` and `dryRun` parameters now set the call's config.
`maxDelete` overrides the global `--max-delete`. `dryRun: true` asks for a
dry run, but `dryRun: false` doesn't cancel one from the global config, so
the run is a dry run if either asks for one. The `maxDelete` parameter is
also documented again; it was left out when the rc help became generated
from the command's flags.
Before this change, when bisync updated a listing entry, it kept the old
modtime if the new one was less than a second earlier. This dated back to
when bisync truncated modtimes to the destination's precision before
recording them (`ConvertPrecision`), so that a truncated time would not
overwrite the more precise original.
`ConvertPrecision` was removed in 956c2963f, and since then, bisync has kept
the original precision and relied on the modify window when comparing. But
the rule in `ls.put` was left behind, and it then fired only when a file
really had an earlier modtime, recording a time the file did not have. This
could cause:
- a `--resync` to abort with "path1 and path2 are out of sync" when Path1's
version was slightly older than Path2's
- later runs to report changes that didn't happen, for a file changed to a
version less than a second older, or copied from a remote with lower
modtime precision within the same second as the old version (for example,
from SFTP to local)
After this change, bisync records the modtime it is given, like any other
update. Comparisons already account for differences in precision through
the modify window.
deleteEmptyDirectories gated on the cumulative accounting stats
(accounting.Stats(ctx).Errored()) instead of the current operation's error.
On a long-running rcd that reuses one stats group (rc "_group") across many
sync/move calls, a single earlier error left Errored() true forever, which
permanently disabled empty-source-directory deletion for every later,
error-free operation sharing the group - each run just logged "not deleting
directories as there were IO errors" and never cleaned up.
Gate on s.currentError() instead, so empty-directory cleanup depends only on
the current operation's outcome. This matches the per-operation guards already
used elsewhere in run(), and is a no-op for normal single runs where
Errored() (s.errors != 0) tracks the same errors that reach s.currentError().
Opening an archive in the root of a remote, such as remote:file.zip,
failed with "file does not exist" because the archive's leaf name was
found with path.Base, which doesn't understand remote names. It is now
found with fspath.Split.
Drime won't list more than 20,000 entries of a directory. Asking for a
later page gets the last page it allows again, so rclone asked for the
same page forever and used more and more memory until it was killed.
Return an error instead. An error stops sync deleting files based on
the listing and uploading duplicates of files it couldn't see.
Also return listing errors from getItem rather than "object not found".
- TestCheckConflictsSkipsCheck: the check for identical files is skipped
with --size-only, and with --ignore-checksum only when the paths have no
hash in common.
- test_equal_skipped: an identical file that is new on both paths with
--ignore-checksum is recorded, so deleting it on Path1 deletes it on
Path2. Adds the "ignore-checksum" scenario argument.
- test_conflict_window: Path1 changes between the listing snapshot and
the check for identical files.
Co-Authored-By: ASRodrigo1 <45823308+asrodrigo1@users.noreply.github.com>
Before renaming a sync conflict, bisync checks whether the two files are
identical. It compares hashes using check or cryptcheck, and as a last
resort, when neither is available, it compares the files by downloading
them (--download), so that the result is certain regardless of checksum
support.
Before this change, the fallback to --download happened only when bisync
knew up front that the remotes had no usable hash. It did not happen
when a hash that was expected to be present turned out to be missing
(for example, a backend that normally supports hashes returned a blank
one, or cryptcheck could not get the underlying hash). Those files were
counted as identical based on size alone.
After this change, a missing hash falls back to --download, the same as
when the remotes have no hash in common. --size-only and
--ignore-checksum still disable the fallback and keep the size-only
comparison.
Co-Authored-By: ASRodrigo1 <45823308+asrodrigo1@users.noreply.github.com>
Before this change, when a file was new or changed on both paths and the pre-sync
conflict check deemed the two copies identical, bisync skipped the copy and then
rechecked the file after the sync to get its listing info. If the recheck could
not confirm equality (for example, hashes unavailable due to --size-only or
(--ignore-checksum + no common hash)), the file was rolled back to be retried on
the next run, and every later run repeated the same check with the same result.
A later change on one side was then handled against a missing or stale listing
entry.
After this change, files we've declared identical pre-sync are no longer
rechecked post-sync. The pre-sync conflict check now also returns the values it
compared, and a skipped file is recorded from the listing snapshot. If either
path changed between the snapshot and the check, the prior listing is kept and
the next run rechecks.
When --size-only or (--ignore-checksum + no common hash) are in use, the "new or
changed on both paths and equal" case skips the conflict check entirely, because
it is unnecessary. The check could only compare sizes, which we already know are
equal from the snapshot.
The new `test_equal_skipped`, `test_conflict_window`, and
`test_conflict_window_size` scenarios cover identical files that change on
one path during the sync, files that change between the listing snapshot and
the check for identical files, and `--size-only` files with the same size but
different content.
Co-Authored-By: ASRodrigo1 <45823308+asrodrigo1@users.noreply.github.com>
Adds a test-only TestFnConflictCheck option, called before checking
potential conflicts, and a "hook <when> <step>" scenario command that runs
an ordinary scenario step once when the next bisync reaches that point:
before-check (TestFnConflictCheck) or during-sync (TestFn). A hook that
never runs is noted in the log so a scenario can't silently skip its setup.
Before this change setting LogLevel in the _config parameter of an rc
call had no effect as both the log functions and the log handler only
looked at the global log level.
The context aware log functions now use the log level from the config
in the context, and the log handler lets a log level in the context
which differs from the global one override its own level. This means
an rc call can be made more or less verbose than the rest of rclone
with, for example
"_config": {"LogLevel": "DEBUG"}
This applies to the logs made with the context aware log functions, so
not to those made by the backends which still use the global log level.
Note that a config copied into a context no longer follows later
changes to the global log level.
Use the context aware log functions throughout the code which runs
sync, copy, move, check and the other operations, and the directory
listings they do, so that the logs they make, such as the per file
"Copied (new)" logs, are attributed to the rc job and stats group of
the operation.
This adds ErrorfCtx, LogfCtx, InfofCtx, DebugfCtx and LogPrintfCtx
which take a context and pass it on to the log handler. The handler
can then attribute the log to what made it using the new
StatsGroupFromContext and JobIDFromContext function pointers, which
fs/accounting and fs/rc/jobs fill in.
The format string is no longer assigned to in logPrintf so that go
vet recognises these as printf wrappers and checks the arguments of
their callers.
Several links on rclone.org pointed at anchors or pages that don't exist:
- bisync: --color and --dry-run anchors, relative commands/rclone_check/ links
- ftp: on-the-fly link pointed at a local anchor instead of /docs/
- faq: --sftp-http-proxy linked to the ftp page instead of sftp
- index: Arvan Cloud and Liara provider config anchors
- rc, sftp: Hugo drops leading dashes from heading ids (#--rc-no-auth -> #rc-no-auth)
- onedrive: webdav #sharepoint -> #sharepoint-online
- webdav: relative commands/rclone_serve_webdav/ link
- s3: https:/// typo in the China Mobile Ecloud link
Directory listings showed every directory with a size of -1, so
`rclone lsd` and `operations/list` couldn't say how big a directory
was, and finding out meant walking it with `rclone size`.
Microsoft Graph already returns each folder's `size` as the total size
of its contents, and the backend read it but dropped it. Directories
now carry that size, as they already carry the item count. Listing a
directory's parent is enough to get the size of all its subdirectories,
without listing the subdirectories themselves.
OneDrive updates a folder's size in the background, so for a few
minutes after a change inside it the size may be out of date.
Fixes#7502
On shutdown the batcher logged "Committing uploads - please wait..."
but nothing once the commit had completed, so the log looked as if
rclone had stopped part way through. Log "Committing uploads - done"
when the pending batches have been committed.
This affects the backends which use the batcher, dropbox and
googlephotos.
When container soft delete is enabled, listing the containers at the
root of the account returned the soft deleted ones too, because the
listing asked for them with Include Deleted. rclone treated them as
normal containers and then failed to list each of them with
"directory not found". With at least one error, sync refused to delete
anything on the destination ("not deleting files as there were IO
errors") and ended with exit code 3.
The listing now asks for live containers only, as it did before the
port to the new SDK in a131da2c3. Nothing else in the backend used the
deleted flag of the containers.
Nextcloud public shares served from public.php/dav/files reject requests
that lack an X-Requested-With header. Send "XMLHttpRequest" for these
URLs when vendor is nextcloud, unless the user already set the header
with --webdav-headers.
This change adds a step to the bisync tests that checks that after a successful
run, the files named in each listing are exactly the files on that path. This
ensures that nothing is silently missing from the listings.
Before this change, when an existing file had changed on both paths, the
conflict winner was chosen using only the attributes that had changed on each
side. An attribute that hadn't changed was treated as zero, so --conflict-resolve
larger could pick the smaller file (for example, when one side only had its
modtime updated), and --conflict-resolve newer could fail to pick a winner when one
side's modtime hadn't changed.
After this change, the current size, modtime, and checksum (to the extent
enabled with compare settings) are now recorded for every changed file.
Before this change, when --conflict-resolve picked Path2 as the winner, the winning
file was copied to Path1 correctly but then removed from both listings while
updating them for the renamed (or deleted) Path1 copy. The next run saw the file
as new on both paths and had to check it again, and a change to it on one path
in that run was handled as if the file had never been synced. When Path1 won,
the listings happened to be updated in an order that avoided this.
This change fixes the issue by adding a guard to remove the file from the
listing only when dstNewName != dstOldName.
See #9939
- add the release freeze rule, GUI update and security advisory steps
- list the extra software and remotes needed
- explain what make tag, make doc and make retag do
- check integration.rclone.org instead of running make test
- describe how to run the integration tests on the stable branch
- spell out the differences when making a point release
- note that stable branches are abandoned once a new release is out
make startdev now starts the next minor version on master and the next
patch version on a v*-stable branch, so the same target works for
releases and point releases. It refuses to run on other branches or if
the current version hasn't been tagged yet, which catches running it
twice. make startstable is kept as an alias.
golang.org/x/net v0.60.0 deprecates its HTTP/2 server and transport in
favour of net/http.
Make the h2c test use http.Transport with unencrypted HTTP/2. serve
restic --stdio still needs http2.Server to serve HTTP/2 on a single
connection as http.Server has no way of doing that.
Upgrade to v0.60.0 of golang.org/x/net in order to address:
- CVE-2026-78659: net/http: HTTP/2 server memory exhaustion due to Trailer headers
- CVE-2026-78669: net/http: excessive CPU consumption from repeated initial window changes
- CVE-2026-78663: net/http: double flow control refund on HTTP/2 server streams
- CVE-2026-97032: net/http: HTTP/2 server crash due to HPACK encoder race
This also updates golang.org/x/crypto, sync, sys, term and text to the
versions golang.org/x/net v0.60.0 requires.
Before go1.27 net/http compares hosts case sensitively when deciding
whether to keep Authorization on a redirect, so it dropped the header
itself on the localhost -> LOCALHOST redirect in the
SameHostDifferentCase test, before StripHeadersOnCrossHostRedirectFn
was consulted. The other headers still show the function treats the
hosts as the same.
The metadata of the objects of all users was kept in one store, so with
--auth-proxy or several --auth-key credentials one user storing the
metadata of many objects could make the server forget that of every
other user.
Each user (each access key) now has a store of their own.
--multipart-max-uploads and --multipart-streaming-buffer-total limited
the uploads of all users together, so with --auth-proxy or several
--auth-key credentials one user could use up the limits and stop every
other user uploading.
They now limit the uploads of each user (each access key) separately.
A multipart part which arrives before the parts ahead of it is buffered
and acknowledged to the client, then written to the backend when its
turn comes. If that write failed the part was dropped, possibly with
some of it already written, but the upload carried on as if nothing
had happened, so it could be completed with the part missing, or with
a resend of the part appended to the partial data.
Such an upload is now failed, so the client starts it again.
The SigV4 verification in gofakes3 only checked the headers the
client listed as signed and never looked at the other headers which
arrived. A presigned PUT URL signs only the host header, so a holder
of one could add an unsigned x-amz-copy-source header and have the
server copy any object it can reach - in any bucket, and in any
remote when serving a cloud backend - into the object the URL names,
then read it back with the same URL. This affected both --auth-key
and --auth-proxy. AWS refuses such a request with AccessDenied, as
it requires every x-amz-* header to be signed.
This updates gofakes3 to a version which refuses requests carrying
an x-amz-* header not in their signed headers list with AccessDenied,
and adds tests that a presigned PUT URL can't be turned into a copy
in either auth mode. Real S3 clients always sign x-amz-* headers so
they are unaffected.
In v1.75 the multipart reorder buffer admitted the next part the stream
needed however big, adding its client-declared length to the running
total unchecked. Two concurrent requests for the next part declaring
huge lengths overflowed the total negative, after which out-of-order
parts were admitted past --multipart-streaming-buffer-limit.
The next part is now streamed straight to the backend without being
charged to the buffer, and a buffered part is only admitted if it fits
in what is left of the limits, so this can no longer happen. Add a
test of the attack against both the per-upload and server-wide limits.
serve s3 keeps the metadata of objects uploaded through it in memory.
If the object was then changed some other way - by another --auth-proxy
user sharing the backend, another server or directly on the backend -
serve s3 went on returning the old metadata, including the old
modification time, for the new object.
The size and modification time of the object are now stored with its
metadata, which is ignored if they no longer match.
With --auth-proxy the backend of each user is shut down once it has
been unused for 5 minutes. A request didn't count as using it, so a
long upload or download could have its backend shut down under it and
fail. A multipart upload used the backend it started on throughout, so
failed if the client paused for more than 5 minutes between parts, or
the auth proxy changed the user's secret.
Each request and multipart upload now holds the backend it uses until
it is finished with it. In-progress multipart uploads are aborted when
the server is shut down so their backends are released.
With --auth-proxy each access key ID may be mapped to a different
backend, but gofakes3 kept a single record of multipart uploads for
everyone. Any user could list the uploads of every other user with
ListMultipartUploads (including their upload IDs) and the parts of
them with ListParts.
With --disable-multipart-streaming the uploads are held by gofakes3,
whose upload IDs are sequential so easy to guess. Any user could
upload parts to, abort, or complete another user's upload - which
wrote it to the backend of the user completing it.
This updates gofakes3 to a version which can keep each user's uploads
private and uses the access key ID as the owner.
With --auth-proxy each access key ID may be mapped to a different
backend, but serve s3 found a streaming multipart upload by its upload
ID alone and stored object metadata by path alone.
A user who learnt another user's upload ID (for example from
ListMultipartUploads on a bucket of the same name) could upload parts
to, complete or abort that upload on the other user's backend. A user
with an object at the same bucket and key as another user was returned
the other user's metadata by HeadObject and GetObject and could
overwrite it.
Keep the uploads and metadata of each access key ID apart.
Refactor the state serve s3 keeps between requests - in-flight
multipart uploads and object metadata - so that it is reached through
a tenant struct for the user making the request. This makes it
possible to keep users' state apart when they are serving different
backends.
There is only one tenant for now, so there is no change in behaviour.
serve s3 keeps the metadata of the objects uploaded to it in memory,
with no limit on the number of objects, so memory use grew with every
object uploaded, and a client could use a lot by uploading many empty
objects with large metadata.
The new --metadata-max-objects (default 100000, 0 for no limit) limits
the number of objects whose metadata is kept, forgetting the metadata of
the least recently used first. As the metadata is only kept in memory
it is already lost when the server restarts; the modification time is
stored on the object itself so is never lost.
When an abandoned multipart upload expired after --multipart-expiry,
gofakes3's record of it was left behind, so it stayed in memory and in
ListMultipartUploads until the server was restarted.
That record is now removed too.
This picks up these fixes to serve s3 from gofakes3:
- XML request bodies (CompleteMultipartUpload, DeleteObjects) bigger
than 10 MiB are rejected rather than read into memory.
- DeleteObjects requests with more than 1000 keys are rejected.
- CompleteMultipartUpload checks the upload exists before reading the
request body.
- With --disable-multipart-streaming, parts are no longer allocated at
their declared size before the data arrives.
- Browser POST uploads hold at most 1 MiB in memory and are limited to
5 GB.
It also brings a SlowDown error, so the SlowDown replies serve s3 sends
when a part waits too long for buffer space, or too many multipart
uploads are in progress, now have status 503 as S3 does rather than
500.
There was no limit on the number of multipart uploads in progress, and
each one holds server resources until it completes or expires - once it
has data, a VFS upload and the buffers of the remote's own upload - so a
client could use unbounded memory by starting many uploads.
The new --multipart-max-uploads (default 1000, 0 for no limit) limits
the streamed multipart uploads in progress. Starting another fails with
SlowDown, which S3 clients retry, before anything is created on the
remote.
With --multipart-expiry 0 nothing ever cleans up abandoned uploads, so
they go on counting towards the limit until no new upload can be
started. A warning is logged at startup when both are set.
Starting a multipart upload opened its VFS upload straight away, which
allocates a 100 KiB buffer (--streaming-upload-cutoff) and starts a
goroutine and a transfer, all held until the upload finished or was
expired after --multipart-expiry (default 24h). A client could make the
server hold about 100 KiB per tiny CreateMultipartUpload request.
The VFS upload is now only opened when the first part is written to it,
so an upload with no parts holds none of these.
--multipart-streaming-buffer-limit only limits the memory buffered for
parts arriving out of order in each upload, so a client could use as
much memory as it liked by starting more uploads.
The new --multipart-streaming-buffer-total (default 1G, 0 for no limit)
limits the memory buffered by all uploads together. A part which would
take either over its limit waits as before. Since the part the stream
needs next is never buffered, every upload can still make progress.
Every multipart part was buffered in memory in full before being written
to the backend, even the part the stream needed next. There is no limit
on part size, and several concurrent requests for the current part were
each admitted, so a client could make the server hold as much memory as
it sent, whatever --multipart-streaming-buffer-limit was set to.
The part the stream needs next is now written straight through to the
backend as it arrives, so only parts which arrive ahead of their turn
are buffered, and those always within the limit - a part bigger than
the limit waits until it is the next part rather than being admitted
when the buffer is empty. Further requests for the part being streamed
wait, holding no memory, and are then checked against it.
Since what reaches the backend can't be taken back, a part whose body
fails part way records how far it got, and the client's retry of the
part, which must have the same content, carries on from there. Such an
upload can't be completed without the rest of the part.
The body is read to its end, so that an error it only reports there -
as the Content-MD5 check does - fails the part rather than being
missed, and a body longer than declared is rejected. A part rejected
once it has been streamed leaves data which can be neither completed
nor replaced, so the whole upload is failed and cleaned up and the
client starts again.
A part waiting for room in the out-of-order buffer waited forever,
ignoring the request being cancelled, and a waiting part stopped its
upload being expired, so a client could hold requests, goroutines and
uploads open indefinitely.
The wait now ends when the request is cancelled, and after a minute the
part fails with SlowDown so the client retries it later.
A part uploaded again after it had been streamed to the backend (a
client retrying after a timeout) was buffered in memory in full just to
compare it with the original, and the buffer limit didn't apply to it.
Such a re-upload is now hashed as it is received and compared with the
original without being buffered.
The out-of-order multipart buffer was charged the declared length of
each part, but a buffered part occupies whole 1 MiB pool pages however
small it is. A client could therefore send up to 10000 one-byte parts
ahead of the part the stream needed and pin about 10 GiB of memory in a
single upload while --multipart-streaming-buffer-limit saw only 10 KB.
Each buffered part is now charged the whole pages it occupies.
The headers set with the `headers` option were sent to any host the
server redirected to, as net/http only strips Authorization and Cookie
on a cross domain redirect and copies every other header onto each
hop. A server which could be made to redirect to another host would
therefore receive any credential carried in a custom header such as
X-Auth-Token.
Strip the configured headers from a redirect hop once the chain has
left the configured host (a different hostname, subdomain or port)
using rest.StripHeadersOnCrossHostRedirectFn. This logs at debug level
when it happens so a 401 from the redirect target can be diagnosed.
The `auth_redirect` option is unchanged as it is an explicit opt-in to
keep credentials across redirects.
The Authorization header set from `user`, `pass` and `bearer_token` is
deliberately left to net/http's policy, which strips it on a change of
domain but keeps it for a subdomain or a different port, as servers
which redirect to a subdomain rely on that today.
The manual release verification steps checked the signature with gpg
--verify and then checked the binaries against the whole SHA256SUMS
file. gpg only verifies the clearsigned part, so an unsigned hash line
added outside it would be used by sha256sum -c or by rclone hashsum -C.
The instructions now extract the signed hashes with gpg --decrypt and
check the binaries against those only.
See: GHSA-w33h-3qgq-8mv8
rclone selfupdate checked the PGP signature on the clearsigned block
of SHA256SUMS but then looked up the archive hash in the whole
downloaded file. Unsigned data can appear in that file without
invalidating the signature. A hash line placed before or after the
signed text was accepted, allowing whoever controlled the download to
install an arbitrary unsigned archive.
The hash is now only looked up in the signed text.
Affects stable (non --beta) selfupdate since v1.55.0. --beta updates
are not signature checked.
The transport strips the headers set with --header once a redirect
chain leaves the originally requested host, but the host comparison
ignores the URL scheme. A server which redirects https://host:8443/
to http://host:8443/ keeps the same host and port, so the headers
were added to the plaintext request and could be read by anyone on
the network path.
The redirect chain walk now also treats a hop which downgrades the
original HTTPS request to HTTP as leaving the host, so the headers are
removed from that hop and every one after it.
The transport also logs at debug level when it removes the headers,
for either reason, so that a resulting 401 from the redirect target
can be diagnosed.
Dir.Create, Dir.Mkdir and Dir.Rename joined whatever name they were
given onto the directory's path. A name of "..", "." or one containing
a "/" isn't a child of the directory, and at the VFS root
path.Join("", "..") preserves the ".." so the new node's path pointed
outside the root. Backends which join the Fs root with the remote then
resolved it to a location above the directory the VFS was created on.
serve nfs was exploitable through this. The go-nfs library refuses a
name of exactly "." or ".." in MKDIR only, doesn't check the name in
CREATE, RENAME or SYMLINK, and never refuses a name containing a "/".
serve webdav was exploitable in the same way and has its own fix. The
other serve front-ends rely on their own or their protocol library's
path cleaning to keep such names out of the VFS.
Reject such names with EINVAL in the VFS itself, so that every
front-end and mount is protected regardless of what the layer above it
checks. This is the fix for serve nfs and defence in depth for the
others. The serve webdav check is kept as the front-end is the trust
boundary and gives the client a clear error. The source name of a
rename is only used to look the node up so it isn't checked.
VFS.Mkdir of the root, which previously went through Dir.Mkdir with an
empty name and added a nameless child node to the root, now makes the
root directory on the backend directly, still honouring --read-only.
serve webdav and serve ftp made directories through Dir.Mkdir rather
than VFS.Mkdir, so they are switched to VFS.Mkdir to keep "MKCOL /" and
"MKD /" working.
A request path with a ".." element, such as "MKCOL /..", was passed
straight through to the VFS. At the VFS root path.Join("", "..")
preserves the "..", so a directory node with the path ".." was added
under the root and every path built beneath it started "../". Backends
which join the Fs root with the remote before encoding it - such as
webdav, ftp, sftp, http and memory - resolved that "../" away, letting
an unauthenticated client create, overwrite, copy, move and delete
objects and directories outside the directory rclone was told to serve.
Moving a directory or file from outside the served tree into it also
made it readable.
Reject any request with a "." or ".." element in the request path, or
in the Destination header of COPY and MOVE, with 400 Bad Request. The
check is made once in ServeHTTP rather than in each webdav.FileSystem
method so that the client gets a consistent error. Rejected requests
are logged at INFO level like other requests.
The webdav handler strips --baseurl from the Destination as a string
prefix, so "/base../x" reaches the FileSystem as "../x". The Destination
is therefore checked after stripping the prefix in the same way.
The resume start value returned by the NetStorage list API is joined
onto the endpoint URL as a URL reference to build the next listing
request. A value beginning with `//` is a network-path reference, so a
server (or an on-path attacker when `protocol = http`) which returns
`//other.example/path` could make rclone send the next request, signed
with the account secret, to a host other than the configured one.
Use rest.URLJoinRoot so the resume token can only change the path.
The netstorage backend joins the path after `remote:` onto the
configured host URL according to the URL standard. A path beginning
with `//` is a network-path reference under that standard, so a path
such as `//other.example/dir/` replaced the configured host (and port
and userinfo) rather than being treated as a path. Every request,
including the initial stat, was then signed with the account secret and
sent to the substituted host. The signature covers the request path and
action but not the host, so the substituted host received signatures
valid for replay against the real NetStorage endpoint for the duration
of their validity.
Use rest.URLJoinRoot, which refuses any root that changes the scheme,
host or userinfo of the configured URL.
The http backend resolves the path after `remote:` against the
configured url according to the URL standard. A path beginning with
`//` is a network-path reference under that standard, so a path such as
`//other.example/dir/` replaced the configured host (and port and
userinfo) rather than being treated as a path. The configured headers
were then sent to the substituted host on the initial HEAD request and
every request after it.
Use rest.URLJoinRoot, which refuses any root carrying a scheme, host
or userinfo of its own, to join the path onto the configured url.
Add an integration test which starts a listener on localhost, makes
the remote with roots naming that listener and checks that nothing
connects to it. The listener replies with junk so a backend which does
connect fails at once rather than retrying a closed connection.
rest.URLJoin resolves its path as a URL reference according to the URL
standard, so a path beginning with `//` is a network-path reference
which replaces the host (and port and userinfo) of the base URL. That
is often the wrong behaviour.
Add rest.URLJoinRoot which joins an unescaped root onto a base URL and
refuses any root which parses with a scheme, host or userinfo of its own.
Before this change the basic, htpasswd and auth proxy authentication
middlewares let every OPTIONS request through without checking
credentials so that browser CORS preflights, which can't carry
credentials, would succeed.
However in serve webdav the OPTIONS request is answered by the WebDAV
handler whose Allow header depends on whether the path is an existing
file, an existing directory or doesn't exist, so an unauthenticated
client could discover the existence and type of any path on the served
remote even with --user/--pass or --htpasswd set.
After this change he authentication middlewares no longer special-case
OPTIONS. Instead the CORS middleware, which runs before authentication
and owns the --allow-origin setting, answers genuine CORS preflights
itself with an empty 200 carrying the CORS headers only when
--allow-origin is set.
All other OPTIONS requests need credentials like any other method, so
clients which probe with OPTIONS before authenticating will get a 401
challenge and must retry with credentials.
When pCloud redirects the browser back to rclone's local OAuth
callback server it adds a `hostname` parameter saying which regional
API host (api.pcloud.com or eapi.pcloud.com) the account lives on.
rclone used this for the token exchange and saved it in the config as
the host for all of the remote's later traffic, without checking it.
This only accepts a well formed hostname within pCloud's own domain,
lower cased, failing the config with an error otherwise, and adds a
unit test for the check. This is defence in depth for GHSA-hvpr-p4pv-4f46
The local web server which receives the OAuth callback during
`rclone config` and `rclone authorize` checks that the `state`
parameter matches the random value rclone generated when starting the
flow. The pcloud backend opted out of this check with the
`StateBlankOK` option in lib/oauthutil, which made the local server
accept a callback with no state at all.
The callback URL (http://127.0.0.1:53682/) is fixed and well known,
so while a pcloud OAuth flow was in progress any web page open in the
user's browser could complete it with a single cross-site request
carrying an attacker's authorization code. This is a CSRF
(CWE-352) on the auth callback.
This commit removes the `StateBlankOK` option so that every callback
must carry the correct state and adds a test that the local auth
server rejects callbacks with a blank, missing or wrong state.
The option was added in May 2020 (#4210) because pCloud briefly
omitted the state parameter from its redirect. The callbacks recorded
five weeks later when EU region support was added show both
api.pcloud.com and eapi.pcloud.com returning `state=` again, so the
exemption has been unnecessary since then.
See: GHSA-hvpr-p4pv-4f46
When reading a directory with --links the VFS skipped entries called "."
and ".." before removing the ".rclonelink" suffix, so an object called
"...rclonelink" (or "..rclonelink" or ".rclonelink") produced a directory
entry called ".." (or "." or "") whose path resolved to the parent
directory. The same happened for names added back from the VFS cache.
The check is now done after the suffix is removed.
See GHSA-55qp-jrwr-x827
In --links mode the local backend checked that a source name stayed inside
the destination root and only then removed the ".rclonelink" suffix from the
resulting OS path. A source entry called "...rclonelink" passed the check and
was then trimmed to "..", so the object addressed the parent of the
destination the user chose.
Writing the content of such an entry was already refused, as it is done
through an os.Root, but metadata was not: with --metadata rclone rewrote the
timestamps, ownership and extended attributes of the directory above the
destination from values supplied by the source.
The suffix is now removed from the remote name before it is joined to the
root, so the containment check applies to the name that is actually used. A
translated link must also leave a real native file name once the suffix is
removed.
A regular file whose name is exactly ".rclonelink" (or "..rclonelink" or
"...rclonelink") is not a valid translated link and is now skipped in
directory listings.
In --links mode the local backend removed the ".rclonelink" suffix from
directory names as well as file names when building the OS path. Only files
are ever translated into symlinks, so a directory called "sub.rclonelink"
addressed the sibling "sub" instead: listing it logged "Failed to read link
size", its metadata was read from the wrong place, and setting metadata on
it with --metadata failed or landed on the sibling.
Directories now use their name as is, whatever suffix it has, in the same
way DirSetModTime already does.
Every file download sends the live account authorization token and,
when configured, the SSE-C customer key headers to the download host.
With --b2-download-url that host is a CDN or proxy the user does not
operate, and the http client followed its redirects with Go's default
policy:
- Go only strips Authorization when the redirect target is not the same
host or a subdomain, and never looks at the scheme, so a same-host
HTTPS to HTTP redirect replayed the full-capability B2 token in
cleartext.
- Go has no idea that the X-Bz-Server-Side-Encryption-Customer-* headers
carry the customer encryption key, so it forwarded them to any
redirect target including a different host.
The B2 client now uses rest.StripHeadersOnCrossHostRedirectFn, so it
refuses to follow a redirect which downgrades HTTPS to HTTP and strips
the Authorization and SSE-C headers from any redirect which leaves the
host and port of the original request, matching the protection already
given to the S3, WebDAV and HTTP backends.
Fixes GHSA-gpw7-77cq-9f72
The s3 and http backends each carried their own copy of the same
CheckRedirect policy. This moves it into lib/rest as
StripHeadersOnCrossHostRedirectFn, so that any backend which sends
credentials in headers to a host it does not control can use the same
policy. It logs at debug level when it strips headers so that a
resulting 401 from the redirect target can be diagnosed. A header
starting with "*" is matched without canonicalising, as that is how
SetHeader and Opts.ExtraHeaders send it.
This changes the s3 policy in two edge cases:
- It compared the raw URL host string, so a redirect to the same host
in different case or with an explicit default port stripped the
credentials. It now uses SameHost like the http backend, which treats
those as the same host.
- The downgrade check compared each hop with the previous one. It now
compares with the original request like the other backends, so a
chain which starts on plaintext HTTP may return to it.
The behaviour tests move to lib/rest. The backend tests which check the
policy is wired into each client are kept.
- CVE-2026-97032: net/http: HTTP/2 server crash due to HPACK encoder race
- CVE-2026-78659: net/http: HTTP/2 server memory exhaustion due to Trailer headers
- CVE-2026-97031: crypto/tls: reject malformed ECH outer extension references
- CVE-2026-94444: cmd/go: checksum bypass for golang.org/fips140
- CVE-2026-94447: cmd/go: checksum database bypass for golang.org/toolchain
- CVE-2026-94448: html/template: reset context tracking on consecutive template expressions
- CVE-2026-97030: html/template: recognize |yield| as regexp preceder keyword
- CVE-2026-94440: net/textproto, mime/multipart: memory limit bypass when parsing MIME headers
- CVE-2026-56866: net/http: HTTP/1 client connection desynchronization after CONNECT rejection
- CVE-2026-94439: net/http: HTTP/1 server connection desynchronization after 2xx CONNECT response
- CVE-2026-78669: net/http: excessive CPU consumption from repeated initial window changes
- CVE-2026-78660: net/http: HTTP/2 transport accepts malformed framing-related headers
- CVE-2026-56857: os: Root.Mkdir(All) can follow junctions out of the root on Windows
- CVE-2026-78667: net/http: lack of limit on size of parsed Range headers
- CVE-2026-78663: net/http: double flow control refund on HTTP/2 server streams
This also updates the go1.26 test job to go1.26.9 which contains the
same fixes.
Nextcloud chunk uploads currently return a retryable error when a
chunk PUT fails. The outer low-level retry then restarts the whole
upload, removing the temporary upload directory and sending previously
accepted chunks again.
Retry each chunk in place using the configured low-level retry budget,
rewinding its repeatable body before each attempt. Once that budget is
exhausted, prevent the outer retry from restarting the entire object.
WriteChunk checksums the chunk with io.Copy before sending it. For a
reader that implements neither WriterTo nor a memory backed Read - a
local file opened by a multi-thread copy - io.Copy falls back to its own
32 KiB buffer, so the source is read in 32 KiB pieces and the disk sees
thousands of requests per second where it should see hundreds.
Hand io.CopyBuffer a 1 MiB buffer instead. It is allocated for the
checksum pass rather than taken from the pool, where it could wait for
ever on a --max-buffer-memory below the page size. Readers that do
implement WriterTo, such as pool.RW, keep their fast path and don't get
a buffer.
FsOpenChunkWriter wrote three chunks of its own fixed sizes whatever
ChunkWriterInfo.ChunkSize the backend returned. That happened to be
the size the s3 backend uses by default, so the test passed there, but
a chunk writer which works out where a chunk goes from its number needs
every chunk but the last to be exactly the size it asked for - as a
multi-thread copy writes them - and the test could neither exercise nor
catch that.
Cut the file into chunks of info.ChunkSize, the last one short, still
written out of order. Where the backend lets the test set its chunk
size, ask for 5 MiB first so the file takes several chunks. Where it
doesn't and the chunks leave the file in fewer than three, start again
with a file of two full chunks and a short one, for chunks up to
128 MiB.
A failed delta poll used to wipe the stored resume token: changeNotifyRunner
returned a named nextDeltaToken which stayed empty on error, and the caller
stored it unconditionally. Later polls then asked for /root/delta?token=
instead of resuming, so notifications stopped until the mount was restarted.
changeNotifyNextChange also called the API without the pacer, so a single
transient 503 was enough to trigger this.
While fixing that, walk @odata.nextLink as well. A change set spanning more
than one page returns nextLink without a deltaLink, and the token parsed from
the missing deltaLink was empty - changes were dropped even when no error
occurred.
Fixes#10009
Before this change the parent process kept the remote control and
metrics servers it started in initConfig listening, so the daemon child
could not bind the same addresses and exited.
After this change the parent shuts those servers down just before
daemonizing, and the child starts them as usual.
A symlink in an archive was listed as an ordinary file holding the link
target by the zip backend, and dropped entirely by the squashfs one.
Symlinks are now skipped unless -l/--links is in use, as they are on the
other backends. With that flag they are listed with the .rclonelink
suffix and read back as the path they point at, so they can be copied to
a destination which supports symlinks.
This changes what the zip backend lists by default: a symlink used to
appear as a small file whose contents were the link target, and is now
skipped with a NOTICE naming the flag.
Fixes#9569
If a backend's ListR returned a directory after objects inside it,
the directory was listed twice when using --max-depth or filters. This
happens with azureblob on storage accounts with a hierarchical
namespace.
When a backend saved config, for example a refreshed OAuth token, for
a remote which wasn't in the config file, a section with just that
value was created in the config file for environment variable remotes
and the config file was rewritten for on the fly remotes.
Config is now only saved if the remote is in the config file.
Listing "dir/" on a storage account with a hierarchical namespace
returns the file "dir" if it exists, so a file was listed as a
directory containing itself.
On storage accounts with a hierarchical namespace, directories can be
read as blobs so NewObject on a directory returned fs.ErrorNotAFile
rather than fs.ErrorObjectNotFound.
Storage accounts with a hierarchical namespace (ADLS Gen2) have real
directories. Rclone could not remove these - with directory_markers it
failed with "InvalidUri" and without it rmdir silently did nothing.
Directories are also left behind when the last file in them is
deleted.
This adds the hns option which is read from the storage account if
not set and saved in the config file. If set, rclone:
- supports empty directories whatever the directory_markers setting
- creates directories with Mkdir
- removes directories with Rmdir
- doesn't create directory markers for parent directories on upload
With --auth-proxy, when several logins with the same credentials
arrived together and none was in the cache yet, as when an FTP client
opens several connections at once, each one ran the auth proxy program
and took its own reference to the backend. Only one of them was
remembered, so the others were never given back and the backend was
never shut down.
Logins which arrive together now share one run of the auth proxy
program and one reference to the backend.
With --auth-proxy the backend of each user is shut down once it has
been unused for 5 minutes. With --vfs-cache-mode writes or full, files
are uploaded to the backend after the transfer which wrote them has
finished, which didn't count as a use. So an upload which hadn't
finished 5 minutes after the user's last command (or disconnection for
serve sftp) was stopped, leaving the file in the VFS cache but not on
the backend.
The backend is now kept until it has no files open for write and
nothing in the VFS cache waiting to be uploaded.
This was introduced in v1.75.1 by
f425f8d46 serve: refactor VFS and proxy handling into Provider
Busy returns true if the VFS has files open for write or files in the
VFS cache which are open or waiting to be uploaded. It is for code
which wants to shut a VFS down only once that wouldn't lose any work.
An entry is expired when it hasn't been used for a while, but the user
of the cache may know it is still doing something in the background.
SetCanExpire sets a function which is asked before expiring an entry
and can keep it in the cache until it is ready to go.
The function is called without the cache locked as it may take a
while, so an entry which is used in the meantime isn't expired.
With --auth-proxy the backend of each user is shut down once it has
been unused for 5 minutes. Only the start of each request counted as a
use, so an upload or download lasting longer than that had its backend
shut down under it and failed.
Each request now holds the backend it uses until it has been served.
This was introduced in v1.75.1 by
f425f8d46 serve: refactor VFS and proxy handling into Provider
With --auth-proxy the backend of each user is shut down once it has
been unused for 5 minutes. Only the start of each request counted as a
use, so a download lasting longer than that had its backend shut down
under it and failed.
Each request now holds the backend it uses until it has been served.
This was introduced in v1.75.1 by
f425f8d46 serve: refactor VFS and proxy handling into Provider
With --auth-proxy the backend of each user is shut down once it has
been unused for 5 minutes. Only the start of each FTP command counted
as a use, so an upload or download lasting longer than that had its
backend shut down under it and failed with "context canceled".
Each FTP command now holds the backend it uses until it has finished,
which for a download is when the file is closed.
This was introduced in v1.75.1 by
f425f8d46 serve: refactor VFS and proxy handling into Provider
Add a new backend for dosya.dev, a cloud file storage, sharing, and
synchronization platform with workspace-based multi-tenancy.
Features:
- Server-side copy, move, rename (files and directories)
- Purge (recursive directory delete)
- Recursive listing (ListR) for --fast-list
- Multipart upload for large files (>10MB)
- Public link sharing via rclone link
- About (workspace storage quota)
- MIME type detection
- Empty directory support
- Workspace-based multi-tenancy (one remote per workspace)
- Download via presigned R2 URLs with Range header support
Co-authored-by: firatkaya <firat@netiket.com.tr>
When an upload failed during the transfer, the partial file was
removed but its handle was never closed. The server kept the deleted
file open, so its space stayed allocated until the pooled connection
was closed, which in rcd, mount or serve could be until rclone exited.
This closes the handle before removing the partial file.
Tests in fs/operations, fs/sync, cmd/bisync, cmd/gitannex and vfs are
run by the integration tests against every backend, so they need to
cope with backends with limited functionality, ideally by skipping on
the Fs.Features() flags.
Files.com now treats the replacement characters rclone uses for / and
\ in file names (/ and \) as if they were the originals, and the
names . and .. as if they were . and .., and rejects them all with
"Invalid path". There is no other way of encoding these names, so
document the restriction and ignore the integration tests for them.
When an upload was stopped part way through by --max-transfer, HiDrive
kept the part of the file it had received, leaving a truncated file at
the destination, or replacing an existing file with a truncated one.
This was introduced in e8f421d28 which accounted the buffered start of
each upload as it was sent rather than as it was read, so the transfer
limit could abort the request which creates the file half way through.
Account the requests which create or replace a file in one go as they
are buffered once more, so the limit is hit before anything is sent.
The chunks of larger uploads are still accounted as they are sent.
ownCloud 10.16 answers a PROPFIND for a path which doesn't exist with
a 207 Multi-Status response, rather than a 404, with a body which
starts a multistatus document then appends a Sabre NotFound error to
it. This isn't valid XML so rclone failed with
read metadata failed: XML syntax error on line 2: expected attribute name in element
whenever it was pointed at a directory which didn't exist yet.
Detect the NotFound error in the response and treat it as a 404.
Koofr refuses to download the HTML listings written by the index tests
with "FileBlocked: File download restricted due to possible dangerous
content" and OpenDrive refuses with 403 Forbidden, so the tests can't
read back what they wrote. The index tests which write other formats
still run.
With meta_format = none, listing a directory which contained the
temporary chunks of an interrupted upload, but no completed chunks for
that file, panicked with "invalid chunked object".
The listing made a placeholder object for the file on seeing a
temporary chunk, which then had no chunks in it when it was validated.
Only make the placeholder on seeing a data chunk, so files which have
nothing but temporary chunks are ignored as they are when metadata is
in use.
- Only check the MIME type of the listings on backends which declare
both ReadMimeType and WriteMimeType, and ignore its parameters, as
many backends report a MIME type they chose themselves or drop the
charset.
- Write the test file before turning --dry-run on. Chunker finishes
an upload with operations.Move, which honours --dry-run, so it left
temporary chunks behind which stopped the test directory being
removed and made the tests following fail.
- Don't check the transfer and delete counts on chunker, as moving
its chunks into place is counted in the same stats.
- Don't expect a new file to change the time of the directories above
it on backends which can't set modification times, as it may get the
same time as the files already there.
The Blob Listing with Apache Arrow feature is now public and shipped
in azblob v1.8.1, so the temporary backend/azureblob/arrowlist package
which implemented it on top of the previous SDK has been removed and
the backend now uses the SDK's own listing pager with ResponseFormat
set to Arrow.
As the SDK client handles every credential type this also makes Arrow
and parallel listing work with connection_string auth, and the
use_arrow_list and list_parallelism options are no longer hidden.
Pointing the archive backend at a file inside a squashfs image, for
example `rclone cat :archive:image.sqfs/dir/file.txt`, listed every
file in the directory containing it instead of just that file. The zip
archiver already behaved correctly.
The squashfs archiver now remembers the file the root points at and
only exposes that one, as zip does. Its Root() includes the file so
that the fs cache does not hand back the Fs for the whole directory in
its place.
With -l/--links a .rclonelink object is buffered in memory to become
the target of a symlink. The read was unbounded, so a hostile or
corrupt source serving a large .rclonelink object made rclone use that
much memory and then log the whole body in the resulting error.
A symlink target can never be longer than a path, so the read now stops
at 128 KiB and anything longer is refused without retrying.
Deleting a selection finished with "Successfully deleted all items!",
which claimed more than had happened - only the selected entries were
removed. It now names the number of items deleted.
The screen was not redrawn until the whole selection had been deleted,
which made the UI look hung, so a progress box is drawn before each
deletion.
Fixes#9516
The docs describe a duration as a sequence of numbers each with a unit
suffix, including d, w, M and y, but a sequence that used one of those
units, such as --max-age 1d12h, failed with a confusing error about
parsing it as a date. Only a single number with one of them, like 1.5d,
or a sequence of the time.ParseDuration units was accepted.
Parse such sequences before falling back to dates, rejecting ones too
long to represent.
Add an opt-in preflight that excludes only guaranteed tracked renames
from the max-delete count. Reuse normal bisync listing metadata,
validate strategy and flag conflicts early, and skip preflight work
when --force bypasses the safety check.
The max_delete_track_renames scenario uses the default hash
track-renames strategy, which requires a hash common to both paths.
Skip it on remote combinations without one, such as crypt, instead of
failing the integration tests.
Fixes#8685
Move strategy parsing, capability checks, and matcher behavior into a
reusable package without changing sync behavior. Add a lightweight
candidate API so callers can count guaranteed matches without
retaining filesystem objects.
Setting the modtime of a closed file refreshed the cache item's
fingerprint before the new modtime had been applied to the remote
object. The next open then saw a fingerprint mismatch, logged "removed
cache file as stale (remote is different)" and downloaded the whole
file again, even though only the modtime had changed.
Refresh the cache fingerprint again once the remote modtime has been
set.
When a file was reopened within the --vfs-handle-caching grace period
(default 5s) after its remote fingerprint changed - for example after a
touch or any other modtime change - _checkObject removed the stale
cache file and open recreated an empty one, but nothing sized it. The
next GetSize stat'd the empty file and set the item size to 0, so
ReadAt's _ensure clamped the request to nothing and skipped the
download, then the size check zero-extended the file and returned the
zeros. Reads through a mount returned the correct length but zeros for
the first read (128 KiB through FUSE).
This is easy to hit with git on a --vfs-cache-mode full mount: git
freshens pack files with utime, and a concurrent reader then sees
"not a GIT packfile".
Size the recreated cache file from the object before opening it, as a
normal open does.
Shutdown was calling expiryTimer.Stop without holding ts.mu, so it could
panic if OnExpiry had not created the timer yet, and raced the write in
OnExpiry. Hold the lock and skip Stop when the timer is still nil.
Fixes#9980
Reading an object in an archive storage class failed with "Object in
GLACIER, restore first" even when the object was in DEEP_ARCHIVE or in
an Intelligent-Tiering archive access tier.
Use the storage class and access tier from the InvalidObjectState
error. The storage class is optional in that error, so fall back to
the storage class from the listing or HEAD, and then to GLACIER as
before.
Until the first time slot of the week, the timetable used its last entry
as the limit carried over from the week before. That is only the latest
slot of the week when the entries are in order, so a timetable written
weekend first, like "Sat-00:00,off Mon-00:00,1M", limited Sunday to 1M
instead of leaving it unlimited.
Carry over the latest time slot of the week instead.
When listing with --s3-versions or --s3-version-at the storage class
of each object was dropped, so it read as STANDARD unless the object's
metadata was fetched separately. This meant backend restore skipped
objects in GLACIER or DEEP_ARCHIVE with "Not GLACIER or DEEP_ARCHIVE
or INTELLIGENT_TIERING storage class", and lsf --format T showed the
wrong tier.
This happened because ObjectVersion.StorageClass has a different type
from Object.StorageClass so the generated setFrom helper does not copy
it. Convert it explicitly after the setFrom call.
The Docker image had no /etc/mime.types, so MIME types came only from
Go's built-in table plus rclone's small extra list, and many
extensions uploaded as application/octet-stream.
This installs Alpine's mailcap package to fix the problem.
To find the latest patch release of a minor version, for example
`rclone selfupdate --version 1.75`, selfupdate searched the listing of
downloads.rclone.org for href="./vX.Y.Z/". The leading ./ is a detail
of how Caddy's file server writes its links. A listing which linked to
the same directories as "vX.Y.Z/", which is an equally valid relative
URL, made selfupdate fail with "could not find the minor release".
This makes the ./ optional in the pattern, so selfupdate no longer
depends on which program wrote the listing, and takes the version from
a capture group rather than from fixed offsets into the match.
The listings written by rclone index now include the ./ for the
benefit of rclone versions without this fix, so this is to remove the
dependency for the future.
downloads.rclone.org is about to be served from static listings
written by rclone index instead of by Caddy's file server. Released
versions of rclone selfupdate find the latest patch release of a minor
version by searching that listing for href="./vX.Y.Z/", as written by
Caddy. rclone's listings linked to "vX.Y.Z/" without the ./ so `rclone
selfupdate --version X.Y` would have failed with "could not find the
minor release" for every rclone already installed.
Caddy prefixes every link with ./ so that a name with a colon in its
first path segment is not read as an absolute URL with a scheme (RFC
3986 section 4.2). rclone was already safe from that as url.URL.String
adds the ./ but only to names which need it, so links to plain names
had no prefix.
This prefixes all the links with ./ as Caddy does. It changes the
output of serve http, serve webdav and the rc server as well as rclone
index, since they share the code. The links resolve identically, and
it keeps the listings consistent with each other.
Now every URL has the prefix, the caddy.json template no longer needs
to add it.
After a sync which changed a known set of files there is no need to
walk the whole remote. --changed PATH, --changed-from FILE and
--changed-combined FILE tell rclone index what changed, and it
re-indexes only the directories containing those paths and their
ancestors, each with one non-recursive listing.
The listing now shows the folder path with clickable breadcrumbs
above a card containing the entries:
- A summary of the directories, files and total size
- The directory and file counts are toggles
- The search box sits beside the Name heading (focussable with /)
- Sizes right aligned, empty columns are gone and the icons are simpler.
- The colours are CSS variables
The template data is unchanged so custom templates still work, and
the zip download links and ?sort= parameters work as before.
This makes directory listings for buckets and other remotes served as
static websites, for example S3 website endpoints or R2 behind
Cloudflare, so they can be browsed without directory listing support
on the host.
Index writes a directory listing (e.g. index.html) into every
directory of a remote so it can be browsed when served as a static
website. It is also available over the rc as operations/index.
It works like sync. It walks the remote once, renders the listings in
memory and compares them with the existing ones by size and hash from
the listing, or by reading them where the backend has no hash.
Listings in directories which contain nothing else are deleted on
backends which can't have empty directories.
Listings can be written in the serve http HTML format, the lsjson
format, Caddy's browse JSON format, or from a user template. A second
rule set (--index-include and friends) controls which directories get
listings, --dir-time controls the time shown for directories and
--no-modtime avoids reading modification times altogether.
- .Static hides the "up" link at the root
- .SetLinkIndex makes directory links point at an index doc
- .Render writes the listing to an io.Writer
- .Path, .IsRoot, .UpLink, .NumDirs, .NumFiles, .TotalSize and .MimeType.
- Sorting is now stable so the rendered output is deterministic
Clicking the Name, Size or Modified heading now sorts the listing in
the browser and clicking again reverses it, rather than reloading the
page with ?sort= and ?order= parameters. Names sort naturally so
v1.9.0 comes before v1.10.0, and directories stay first when sorting
by name.
The ?sort= and ?order= parameters still work for the initial order and
are shown in the heading, so existing links and custom templates are
unaffected.
MinIO no longer publishes images on quay.io (nor Docker Hub or ghcr.io)
so pulling quay.io/minio/minio returns "unauthorized". pgsty/minio is a
maintained community fork with the same entrypoint layout.
ENETUNREACH and ENETDOWN were not in the list of retriable errors on
non-Windows platforms, so a single failed connection aborted the
transfer instead of being retried as a low level retry. The Windows
list already includes the equivalent WSAENETUNREACH and WSAENETDOWN.
This is easy to hit on a host with IPv6 enabled but no IPv6 route: if
the A lookup fails transiently while the AAAA lookup succeeds, the only
address to dial is IPv6 and the connect fails with ENETUNREACH. A retry
resolves again and normally succeeds.
BwPair.String printed a sub-KiB bandwidth as a bare number, and Set
reads a bare number as KiB, so a rate under 1 KiB grew by a factor of
1024 whenever it went through a string. rc core/bwlimit reports the
current rate in that format and accepts it back, so reading the limit
and setting it again raised it.
63c4fef27 fixed the same corruption for config values by suffixing bare
numbers with B inside Option.String. Move that into a SizeSuffix method
and use it from BwPair.String as well.
Use mime.FormatMediaType instead of unescaped string concatenation so a
directory name containing a double quote cannot inject extra disposition
parameters.
Fixes#9962
Use mime.FormatMediaType instead of unescaped string concatenation so a
directory name containing a double quote cannot inject extra disposition
parameters.
Fixes#9962
Copying an object onto itself, as clients do to replace its metadata,
reported success when the object didn't exist, and copying a missing
object to a different key failed with an internal error. Both now fail
with NoSuchKey as S3 does.
With --etag-hash auto the hash for the ETags was chosen once from the
remote serve s3 was started with. With --auth-proxy there is no such
remote so serve s3 crashed on startup, and when started by the rc it
was the wrong remote, so users whose backends lacked that hash got no
ETags.
The hash is now chosen from the backend of the user making each
request.
The metadata of every object uploaded was kept in memory for as long as
the server ran, even after the object was deleted, so it grew without
limit and the metadata of a deleted object reappeared on any object
later created at the same key other than through serve s3.
Deleting an object now forgets its metadata.
Listing a prefix with no delimiter walked the entire subtree below it into
memory and only then sliced out the requested page, so every page of a
listing cost a full traversal of the tree.
Walk the tree lazily instead, stopping as soon as the page is full and
skipping the subtrees an earlier page already returned. A page now costs a
number of directory reads proportional to the keys it returns rather than to
the size of the subtree, and ETags are computed only for the objects that
are actually returned.
Entries are emitted in the order their keys have in a flat keyspace, with a
directory sorting as if it carried its trailing slash, so that "a.txt" comes
before "a/b" as it does in a real S3 bucket. Without this a resumed listing
would silently skip keys across a page boundary.
When the last user of a VFS shut it down at the same time as a new
user of the same remote asked for one, the new user could be given the
VFS being shut down, with its cache and background tasks stopped.
Only a VFS which is still in use is now reused, otherwise a new one is
made.
A VFS is shut down when the last of the references to it from New is
given back with Shutdown. Hold takes another reference, for code using
a VFS it didn't create itself, which may otherwise be shut down under
it.
Rclone always asked for the delta listing of the whole drive and threw
away the items which weren't under the directory being listed.
The delta API now works on any folder, so ask for the delta listing of
the directory being listed instead, falling back to listing from the
root of the drive for drives which only support delta there.
When a backend rejected the multipart upload as too small, the subtest
skipped without removing its local source file, so the following
upload subtests failed their local listing checks.
Moving a directory can take longer than the API gateway allows, so the
request fails with a 520 or 502 error even though the move completes.
The retry then failed with "Folder ... was already moved to that
location (status 409)".
Treat that error as success.
The Internxt API serves listings from read replicas which lag behind
writes, so for a short while after files or directories are moved or
deleted they can still be listed in their old location.
This caused removing a directory which had just been emptied to fail
with "directory not empty" (eg when moving a directory without server
side directory moves or purging a directory) and a directory which had
just been moved to be found in its old location.
Remember the directories this process has moved or deleted and ignore
directory and file entries which contradict that when listing, finding
directories and checking a directory is empty before removing it.
Moving a file into a directory which had just been created failed with
"Not Found (status 404)", and moving a file over one which had just been
deleted (as sync does with --backup-dir and --suffix) failed with "A file
with the same name already exists in destination folder (status 409)".
The API checks moves against read replicas which lag behind writes, so
retry these errors until the replicas catch up.
The Internxt API serves lookups and listings from read replicas which
lag behind writes, so for a short while after a file is moved it can
still be returned from its old location.
When sync moved a file into the backup location and then uploaded its
replacement, the upload could find the moved file under its old name
and overwrite it. Overwriting renames the existing file by UUID and
deletes it once the upload succeeds, so the file that had just been
moved into the backup location was deleted.
Remember the files this process has moved or deleted for a minute and
ignore lookups and listing entries which contradict that.
With hard_delete set, deleted files and directories carried on being
listed for about a second afterwards because the Drime server doesn't
invalidate its cache of the parent folder listing when entries are
deleted forever. This made removing a directory straight after
emptying it fail with "directory not empty".
Moving an entry to the trash does invalidate the cache, so with
hard_delete set rclone now moves the entry to the trash first and then
deletes it forever.
When server-side copying to a destination which already existed, the
Drime server gave the copy the name "name (1)" and rclone only renamed
it if the source and destination leaf names differed. The existing file
was then deleted leaving the copy under the wrong name.
The server refuses to rename an entry to a name which is already in
use, so this removes the existing file straight after the copy, then
renames the copy whenever its name differs from the destination name.
The premiumize.me upload server has started truncating the multipart
file name at the first ";", so uploading "a;b.txt" created a file
called "a" and the upload then failed with "object not found".
Directory creation and renames still accept ";", so files whose names
contain ";" are now uploaded under a temporary name and renamed into
place. The encoding is left unchanged so existing files and
directories containing ";" remain accessible.
Shortcuts to shared items (and Personal Vault) are stored as remote
items pointing at another drive and listing them can fail with
"The provided drive id appears to be malformed". This aborts a
--fast-list listing of the whole drive.
rest.ReadBody read the whole response body into memory with no limit.
It is used by the default error handler and by many backends' error
handlers and small API calls, so a server which answered with an error
status and then streamed an endless body could make rclone allocate
memory until it was killed.
ReadBody now reads at most 10 MiB (the same limit drainAndClose
already uses to discard unread bodies) and returns an error if the
body is bigger than that. Every caller reads small API responses -
error bodies, status documents and upload tokens - so no legitimate
response is affected.
Reported by @manus-pi
When S3 returns URL-encoded keys from ListObjectVersions, URL encoding
can change their lexical order. This could make mergeDeleteMarkers
place a delete marker after older versions of the same key, so
--s3-version-at reported deleted objects as live.
Compare decoded keys while merging, while preserving the encoded keys
for the existing listing decode path.
Fixes#9948
Testing against OneDrive personal (free) and OneDrive for Business shows
- personal accounts now create versions on setting the modification
time and can delete them, so --onedrive-no-versions and rclone
cleanup work there
- --onedrive-link-password works on OneDrive for Business
- personal free accounts can't set a link password or --expire
- --onedrive-link-type embed only works on OneDrive personal
- personal accounts store times with 1s precision, not mS
See #9917
With --vfs-metadata-extension set, looking up the metadata file of a
file which was open for write and not yet uploaded caused a nil pointer
panic. Such a file has no object to read the modification time from.
Use the modification time of the VFS node instead, which is valid
whether or not the file has been uploaded.
TestDeleteFatalError set --max-delete before writing its files, so on
chunker, which deletes while uploading, the setup failed.
TestDirMoveMoveError and TestDirMoveContext test the core DirMove logic
with a wrapping Fs. This fails on remotes without Move (eg s3, memory)
and on those whose objects don't belong to the wrapped Fs (eg archive),
so they now only run on local.
findItem read the response status code when the lookup failed, so a
failure with no HTTP response panicked.
Thanks to @manus-pi for finding this problem.
DirMove discarded the error from the destination check and read the
response status code, so a failure with no HTTP response panicked.
Other errors were reported as the destination existing; they are now
returned.
Thanks to @manus-pi for finding this problem.
Rmdir and Purge read the response status code before checking for an
error, so a failed DeleteFolder call with no HTTP response (a network
error, or purging the root which fails validation) panicked.
Thanks to @manus-pi for finding this problem.
The request log printed the escaped URL, so non-ASCII file names showed
up as long runs of %XX escapes. Log the unescaped URL path instead, as
the other serve commands do.
VFS.AddVirtual always added a file entry to the directory cache, even
when called with isDir set, so a virtual directory showed up as a file
and nothing could be added inside it.
See #9310
With -L/--copy-links a symlink pointing to one of its parent
directories was followed until the OS gave up with "too many levels
of symbolic links" 40 levels deep. As every looping symlink multiplies
the listing, a small tree with three such symlinks listed millions of
entries.
When a followed symlink points to a directory, compare it with the
directory being listed and each of its parents using os.SameFile and
if it matches report an error and skip it, the same way as a circular
symlink. A symlink to a sibling directory is still followed. Loops
excluded by the directory filters are skipped quietly as before.
The default for --smb-user was the name of the user running rclone
config, and rclone does not write defaults to the config file, so
entering your own user name left it out. A remote made that way then
logged in as whoever ran it later, e.g. root under systemd.
Make the default blank and look up the current user when the remote is
used, as the ftp backend does. Existing configs work as before.
When the source matched a file in --copy-dest, copyDest server-side
copied it over an existing destination which differed, so sync, copy
and copyto with --copy-dest could modify a file --immutable should have
protected.
Leave such a destination for the caller, which rejects it with
ErrorImmutableModified as for any other modified file.
--immutable was only enforced by sync, so copyto, moveto, copy and move
of a single file, and the operations/copyfile and operations/movefile rc
calls overwrote a destination which differed from the source.
Fail these with the same ErrorImmutableModified as sync, and reject
--no-check-dest with --immutable as sync does, since without checking
the destination it cannot be protected.
The box backend logged an int64 with %q which printed
%!q(int64=123) instead of the sequence ID.
serve s3 passed the message from gofakes3 as the format string, so
any % in it was interpreted as a formatting directive and the message
was mangled.
job/status returns the complete output of the job so it is as
sensitive as the call which started the job, e.g. config/dump started
with _async returns the config file including any secrets. It was
possible to read this without authentication where an authenticated
and an unauthenticated rc server share the same process, e.g. rclone
gui --rc.
An unauthenticated client can't start jobs on a server which needs
authentication, other than with the calls which don't need it, so
this shouldn't affect existing users.
Handle read the list of extra outputs without holding the mutex, so
calling AddOutput while logging caused a data race.
This reads the extra outputs once under the mutex.
The wait to obey a Retry-After error from the server used
time.Sleep() so it carried on sleeping when the context was
cancelled, e.g. by job/stop or --max-duration, and it also slept
after the last try when there was nothing left to retry.
Copying a file also logged the low level retry number counting from
0 rather than 1 as everything else does.
When the backend has no DirMove method, operations.DirMove moves the
objects one by one. If one of the moves failed the movers stopped
without receiving the rest of the objects, so DirMove blocked forever
once the channel filled up. This could be seen renaming a directory on
a mount of a backend without DirMove, such as s3.
The moves were also done with context.Background() instead of the
caller's context, so they didn't have the caller's config, couldn't
be cancelled and weren't counted in the caller's stats.
When a fatal error such as --max-delete being reached stopped the
deletions, the deleters returned without receiving the rest of the
objects, so whatever was sending them blocked forever once the
channel filled up.
For example "rclone delete --max-delete 1" on a directory with more
files than --checkers hung instead of returning the error.
The deleters now keep receiving objects without deleting them after a
fatal error.
Params.GetInt64 parsed string parameters with strconv.ParseInt(x, 10, 0).
A bitSize of 0 means int, which is 32 bits on 386, arm, mips and mipsle,
so any value outside the int32 range was rejected as out of range even
though it fits in the int64 the method returns.
Parameters reach the rc API as strings on the paths that matter here: the
rc server puts every URL query and form value into Params as a string, and
rclone rc turns each key=value argument into a string. So on a 32 bit build
operations/getfile could not be given an offset, count, head or tail beyond
2 GiB, and debug/set-soft-memory-limit could not be given a limit beyond
2 GiB, while the same commands worked on a 64 bit build.
Parse with a bitSize of 64 to match the declared return type. This is the
string half of the range checking that was added to the float64 branch of
the same function in 976d05e1.
Return the first non-EOF input error encountered while Rcat probes whether an
upload is small. Previously that error was ignored and the full probe buffer,
including bytes that were never read, could be passed to Put or PutStream.
- Reject uploads when object lookup fails for reasons other than "not found".
- Serialize uploads to the same object to prevent races between the
existence check and the write.
Gzip metadata is read from the wrapped remote and could contain an
invalid block size or incomplete block index. A range read could then
panic in the seekable gzip reader.
Validate the gzip sidecar invariants before constructing a reader so
malformed remote metadata returns an error instead of crashing rclone.
The start function was run inside an if condition, where bash ignores
errexit, so a failed docker run was not noticed. The script then
printed its connection details anyway and the test spent 100 seconds
trying to connect before failing with a message that hid the real
error.
Run start in a subshell with errexit on and check its status
explicitly so the failure is reported immediately with docker's
error message.
The minio/minio repository has been removed from Docker Hub so the
TestS3Minio test server could not be started and every Linux CI run
failed. quay.io/minio/minio still serves the final release so use
that instead.
OCI rejects a multipart completion request with no parts. Probe unknown-size
streams through a buffered reader and use a regular upload when the stream is
empty. Peek preserves a non-empty stream for the selected upload path.
Internxt stores a file as a (plainName, type) pair and never derives the
split itself, so it is a convention shared between clients. This backend
split at the final dot, storing and looking up ".bashrc" as an empty name
of type "bashrc", where the web, desktop, Linux and macOS clients all keep
the leading dot in plainName. List rebuilt the full name so such files
appeared, but NewObject looked them up by the split and missed them.
- Replaced the preUploadCheck function with findFile for better clarity and efficiency in checking file existence.
- Introduced splitNameExt to encapsulate name and extension parsing logic.
- Updated NewObject and Update methods to utilize findFile for improved file metadata handling.
- Enhanced error handling and reduced redundant code in file checks.
Use OCI BatchDeleteObjects to remove up to 1,000 objects per request when
purging. Include directory markers so purging a bucket leaves it empty for
deletion.
OCI Object Storage rejects NUL, carriage return and line feed in
object names. NUL is already encoded by rclone, while carriage returns
and line feeds need EncodeCrLf so rclone can represent those names
without issuing invalid OCI requests.
https://docs.oracle.com/en-us/iaas/Content/Object/Tasks/managingobjects.htm
When rclone archive create is run without a destination the archive
should be written to stdout, but ArchiveCreate called
CheckValidDestination on the nil dst and panicked with a nil pointer
dereference. Skip the destination check when there is no destination.
Also remove a leftover debug Printf that wrote to stdout before the
archive data.
Signed-off-by: Vladimir Babin <vovababin@gmail.com>
Use OCI RenameObject for moves within a bucket, avoiding a server-side copy
followed by source deletion. Cross-bucket moves continue to fall back to copy
and delete.
The link pointed at a bare relative path (--check-filename) instead of
the in-page anchor for the ### --check-filename heading further down the
page, so it 404s on the rendered docs site. Point it at #check-filename
(Hugo strips leading dashes when generating header anchors).
---------
Co-authored-by: no-hup <19599684+no-hup@users.noreply.github.com>
Before this --onedrive-upload-cutoff was capped at 4 MiB and made
larger single-part uploads impossible.
Microsoft documents the limit for a single-request upload as "250 MB".
Measured against SharePoint Online the figure is binary and exclusive:
a body of 262143999 bytes is accepted and one of 262144000 is not.
Raise the constant to 250 MiB and record where it comes from.
The default is unchanged (upload_cutoff = -1, always chunk), so this
only affects users who raise the cutoff deliberately.
Co-authored-by: Can Arslan <carslan@viyaenv.com>
Listing a bucket without -R shows each prefix as one directory entry, so the
objects under it are not in the output. This is easy to read as a truncated
listing rather than as one level of a hierarchy.
Fixes#9797
A vulnerability exists in gRPC-Go servers configured with
xds.NewGRPCServer() where a crafted request missing both :authority
and Host headers can cause a server panic, resulting in a Denial of
Service (DoS).
This update fixes the problem.
NewUsageValue exists to clip an oversized quota to the maximum value of an
int64, which is what dc95f36bc added it for when Box raised the Enterprise
space_amount to 1e+18 and started returning it as a float.
For the float64 instantiation the guard misses its own boundary.
float64(math.MaxInt64) is not 2**63-1, it rounds up to 2**63, so a quota of
exactly 2**63 fails the comparison and falls through to the int64 conversion,
which the spec leaves implementation dependent for an unrepresentable value.
On linux/amd64 it wraps:
Before: rclone about -> Total=-9223372036854775808
After: rclone about -> Total=9223372036854775807
A negative total is not just a wrong number. vfs.Statfs documents -1 as "not
known", vfs.fillInMissingSizes branches on total < 0, and serve sftp only
computes its usage percentage when total > 0, so the value is read back as a
missing quota.
The int64 and uint64 instantiations are unaffected, since for them
T(int64(math.MaxInt64)) is exact and clipping MaxInt64 to MaxInt64 is a no-op.
Set built the timetable with *x = append(*x, ts), so setting a bandwidth
timetable on a value that already held one kept both schedules. The single-value
branch of the same function has always done *x = BwTimetable{ts}, and the other
multi-token Set methods in this package build into a local and assign at the end.
The visible effect is through the rc API. The "main" options block registered in
fs.RegisterGlobalOptions is the live globalConfig, and options/set reshapes JSON
straight into it, so
rclone rc options/set --json '{"main": {"BwLimit": "Mon-10:00,1Mi"}}'
added to the running daemon's timetable rather than replacing it, and the older
slot kept winning: LimitAt for a Sunday returned the previous 10Mi. The same
applies to a _config override on a single call, since AddConfig shallow-copies
the global.
Building into a local also stops a failed parse from leaving the previous
timetable partly overwritten, which the existing error cases already expect.
Ceph RGW (and Linode Object Storage, which is Ceph-backed) can break
SigV4 when Accept-Encoding is included in the signature, especially
when a reverse proxy rewrites that header. GCS already sets this quirk;
apply the same default for Ceph and Linode as suggested in #8206.
Set the OCI source SSE-C request headers when using a customer key.
Server-side copies need these headers to decrypt the source object, in
addition to the existing headers that encrypt the destination.
The test asks InstallUpdate to install the latest beta into an
unwritable file and expects an error. When the binary under test
reports exactly the latest beta version (as make quicktest does right
after a beta is published from the same commit), InstallUpdate
correctly decides there is nothing to do and returns nil, and the test
then dereferences the nil error and panics.
Pin fs.Version to a fixed old value for the duration of the test so an
update is always attempted, and use require.Error so a missing error
fails the test instead of crashing it.
Decrypting the config with --daemon runs --password-command twice, which
means two authentications when the command needs one, such as a hardware
key touch for `pass show`.
SetConfigPassword saves the obscured key to the temp file named by
_RCLONE_CONFIG_KEY_FILE so the daemon process can pick it up, but the
process that wrote it then read and deleted that file itself before
daemonizing. The daemon started with the variable pointing at a file that
was already gone, found no key, and ran the password command again.
Skip acquiring a password when _RCLONE_CONFIG_KEY_FILE is set, as the
PassConfigKeyForDaemonization documentation already describes, and only
consume the key file in a process that has no key of its own. The parent
then leaves the key for the daemon, and the daemon uses it.
Fixes#7341
applyOptions consumes the "path" option into vol.Path rather than leaving
it in vol.Options, but restoreState rebuilt the options with only fs and
type. The explicit path was therefore dropped when the plugin restarted,
and since fsString is rebuilt from those options the volume was remounted
at the root of the remote instead of at its subpath.
Before this change a volume created with type + path lost its path
completely, and one created with remote + path silently fell back to the
path of the connection string. With a backend whose credentials are
scoped to the subpath the restored mount then failed every operation
rather than serving the wrong directory.
Feed the persisted path back like fs and type, so applyOptions applies
the same precedence on restore that it applies when the volume is
first created.
Fixes#9853
The Dropbox backend advertises CaseInsensitive: true, but the two
shared-mode lookup helpers compared names with an exact, case-sensitive
==, so a shared folder or received file named "Project" could not be
found when requested as "project". Use strings.EqualFold in both
findSharedFolder and findSharedFile to honour the advertised
case-insensitivity.
Fixes#9706
In shared_folders mode NewFs derived the shared folder name with
path.Dir(f.root), which returns the parent path rather than the first
path component. For a root like "SharedFolder/subdir/deeper" this yielded
"SharedFolder/subdir", which findSharedFolder cannot match, so NewFs
failed with ErrorDirNotFound. Use the first path component of the root,
as the shared_folders option documents, so deeply nested roots mount.
Fixes#9705
The flag applies to out of space errors while writing and while creating
files or directories, not only while writing. Describe those operations
without naming ENOSPC, which is a Unix error that Windows never reports, so
the help is accurate on every platform.
KickCleaner sets the out of space flag, kicks the cleaner and then waits for
that flag to clear. Only the cleaner clears it, and the cleaner returns
immediately when the cache poll interval is not positive, so when it is
disabled nothing ever reads the kick or clears the flag and the caller waits
forever.
It now returns straight away in that case, under the same condition the
cleaner itself uses to decide it is disabled. Callers already retry a bounded
number of times and then report the error, which is the right outcome when
nothing is going to free space.
IsErrNoSpace compared against syscall.ENOSPC. Go defines that constant on
Windows as a value in its application reserved range which no Windows API
returns, so the comparison could never be true there. A full disk on Windows
reports ERROR_DISK_FULL or ERROR_HANDLE_DISK_FULL instead.
Preallocation failures were still caught, because those return a separate
sentinel, but a disk that is already full fails at the directory creation or
at the open long before preallocation is reached. That is the case reported.
The errors are now held in a list which platform specific files add to in
their init, which is the shape retriable_errors already uses in this package,
and the comparison itself is unchanged. Windows appends the two codes that
lib/file already recognises when preallocation fails. Every other platform
keeps exactly the behaviour it had.
This also reaches the VFS cache, which uses the same helper and has no
preallocation path of its own, so its out of space handling has been inert
on Windows.
Google now requires an app homepage URL and privacy policy URL to be
set on the OAuth consent screen's "Branding" page before the "PUBLISH
APP" button becomes clickable, even for a personal single-user app.
The existing instructions jumped straight to publishing in step 9
without mentioning this, leaving the button greyed out with no
explanation of why.
Fixes#9854
Add operations/cat endpoint to the Remote Control (RC) API to allow reading
and streaming file contents in-process over librclone / FFI and HTTP RC.
Supports range options (offset, count, head, tail), separator, optional maxSize
buffer limit, and returns both string and base64 encoded results.
The code which opens the --combined, --differ etc report files (or
stdout for "-") and closes them afterwards was duplicated between the
check command and the sync logger flags. This moves it into
operations.OpenReportFiles which both now use. It also closes any
files already opened if a later one fails to open.
The sync report flags (--combined, --missing-on-src, --missing-on-dst,
--match, --differ, --error and --dest-after) were only wired up in the
CLI commands so there was no way to get these reports over the rc or
from librclone.
This adds boolean parameters of the same names as operations/check
(combined, missingOnSrc, missingOnDst, match, differ, error and
destAfter) to sync/sync, sync/copy and sync/move. Each requested
report is returned as an array of strings in the output, just as
operations/check does. All default to off so existing callers see no
change in the output.
To share the code between the CLI and the rc the report writer helper
from operations/check is exported as operations.RcReportWriter, the
lsf defaults for --dest-after are moved into
operations.NewSyncLoggerOpt and the listing setup and --no-traverse
warnings from operationsflags.ConfigureLoggers into LoggerOpt.Init.
Before this change `rclone dedupe --dedupe-mode rename` probed the
backend for each candidate `name-N.ext` in turn and gave up when it
had tried 100 names for a given object. With daily runs against the
same duplicated filename this ceiling was eventually reached and
rclone logged "Could not find an available new name". Each probe was
also a backend lookup, so a run against 99 existing names took
minutes on Google Drive.
The rename now uses the listing dedupe has already made to skip names
known to be taken without asking the backend, and only confirms the
final candidate with NewObject (the listing may be incomplete because
of filters). The suffix counter is shared between the objects being
renamed so no name is checked twice. The safety limit is raised to
10000 which, thanks to the listing, no longer costs a lookup per name.
The deferred cleanup in the constructor checked a local error variable
rather than the error being returned, so failures after the VFS was
created (such as an invalid --passive-port) never shut it down. Name
the error return so the cleanup sees the returned error.
When the HTTP server failed to initialise, for example because the
listen address was already in use, rclone panicked with a nil pointer
dereference instead of reporting the error.
The deferred cleanup in the constructor read the provider from the
named return value, but `return nil, err` sets that to nil before the
deferred function runs. Use a local variable for the server instead.
Writing to the mount with os.WriteFile made the Go runtime register
the file with its poller so the kernel then polled the file from
epoll_ctl and epoll_wait, sending POLL requests to the FUSE server
running in this same process. A thread waiting inside epoll cannot be
preempted by the runtime, so a garbage collection starting while such
a POLL was outstanding stopped the world for good - the test binary
could not be killed even with SIGKILL and the mount was left behind,
wedging anything that touched it.
Now we write through the mount with a descriptor straight from
open(2), which os.NewFile keeps out of the poller, check that it
really is out of the poller with SetDeadline, and check at the end of
the test that the mountpoint is unmounted.
In this commit we fixed the same problem for mount by running in a
subprocess however changing one write file routine here was much
easier than re-arranging the tests.
4a382c09ec mount: run tests in a subprocess to fix deadlock - #3259
Note that go-fuse (and hence mount2) works around this problem it by
forcing an early POLL it can answer with ENOSYS.
See: https://github.com/golang/go/issues/21014
The check that an entry returned by an archiver is a direct child of
the directory being listed normalised a parent of "/" to the root, so
an entry named "/x" passed as a child of the root while "x/" and
"dir//x" were rejected.
Decide by stripping the directory prefix and checking what is left
with sanitize.Leaf, which rejects an empty name, ".", ".." and any
name containing a "/". This also covers the leading slash case.
A file entry in a zip whose name refers to a directory, such as ".",
"/", "" or "sub/.", was only skipped when it named the root of an
archive which was itself the root of the remote. When the archive was
found by listing its parent directory the entry appeared as a file
with the same name as the archive alongside the directory for it, and
copying the archive tried to write both. When the entry named a
subdirectory it appeared as a file alongside that directory, and with
that subdirectory mounted as the archive root the entry was taken to
be the single file the root points at, hiding every real entry.
Skip any file entry whose last path component is "", "." or "..",
checked on the raw name before it is cleaned or joined on the prefix.
The zip archiver handed out its cached directory tree directly. Any
caller which filters a listing in place (as the core listing code
does) altered the cache, so later listings of the same directory could
be corrupted.
Return a copy of the cached listing instead.
When an upload failed with a 500 error the upload was retried with a
new upload link but the same input stream. The stream had already been
consumed by the first attempt so the retry uploaded an empty file.
This fixes it by returning a RetryError instead so the caller retries
the upload with a fresh stream, which will fetch a new upload link.
When an upload failed with a retryable error the pacer retried the
whole upload with the same input stream. The stream had already been
consumed by the first attempt so the retry uploaded an empty file.
This fixes it by using CallNoRetry for the upload, as the other
backends do, so retryable errors are returned wrapped in a RetryError
for the caller to retry the upload with a fresh stream.
It also makes 5xx errors from the upload storage servers retryable.
These come back from the SDK as a different error type to API errors
so were not being retried at all.
When an upload failed with a retryable error the pacer retried the
whole PUT with the same input stream. The stream had already been
consumed by the first attempt so the retry uploaded an empty file.
This fixes it by using CallNoRetry for the upload, as the other
backends do, so retryable errors are returned wrapped in a RetryError
for the caller to retry the upload with a fresh stream.
MkdirModTime checked --interactive/--dry-run itself and then called
MkdirMetadata or Mkdir which check again, so --interactive asked twice
about making the same directory and --dry-run skipped before the
operation could be shown in the progress display or counted as a
check.
Now MkdirModTime decides how to make the directory first and delegates
entirely to MkdirMetadata, or Mkdir followed by SetDirModTime, each of
which does its own --interactive/--dry-run check exactly once. This
also means the modtime setting fallback shows in the progress display,
respects --no-update-dir-modtime and has its errors counted.
When syncing to a backend which supports directory modtimes, a
directory which needed its modtime updated and which had files
transferred into it would get its modtime set twice - once when the
directory was checked and once in the pass at the end of the sync.
Now, when the end of sync pass is in use (which it is for all default
syncs), directories which need their modtime (or metadata) updating
are marked for that pass instead of being updated immediately. This
halves the number of directory modtime updates in a typical sync and
makes the "Updated dirs" stat count each directory once.
Syncs to backends which preserve directory modification times (eg
sftp, local) can update the modtime or metadata on many directories.
This count makes that work visible in the stats output, the core/stats
rc and the prometheus metrics (as dirs_updated_total).
Syncs which update lots of directories (eg to sftp) could spend a long
time setting directory modification times, making directories or
removing directories with no feedback in the --progress display or
stats, making rclone appear to have hung.
This shows directory operations (setting modtime, updating metadata,
making and removing directories) in the Checking section of the stats
and counts them as checks, in the same way file deletes are shown.
This creates a checking transfer which is shown in the progress
display while it is running but is not kept in the completed
transfers history, so it never appears in core/transferred and is not
retained in memory after it finishes.
This is for repeated bookkeeping operations (eg directory modtime
updates) which would otherwise crowd file transfers out of the
history.
This was fixed in this commit in an inelegant way
399bc6a6a6 fshttp: don't send --header values to other hosts on redirect
The current commit fixes it properly with AddConfig.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The headers set with --header and --header-download are added to
every request by the rclone transport, including redirect hops which
net/http makes to other hosts, so a credential passed with --header
for one host could be sent to any host that server chose to redirect
to.
The transport now walks the redirect chain net/http records on each
redirected request and, once the chain has visited a host other than
the one originally requested, removes the headers rather than adding
them.
Also restore the global --client-cert and --client-key config after
TestCertificates so its temporary files are not used by later tests.
The headers set with --http-headers are documented for passing
credentials such as Authorization or Cookie. The backend used the
default net/http redirect policy which copies all but a handful of
well known headers to any redirect target, so a redirect from the
configured server to another host would send those credentials to
that host, and a redirect from https to http would send them in
plaintext.
When headers are configured this installs a CheckRedirect function
which:
- removes the configured headers from every hop once the redirect
chain has left the originally requested host
- refuses a redirect from https to http with an error
SameHost compares two URLs by host name (case insensitively) and port
(treating the scheme's default port as no port) so redirect policies
can tell a real change of host from a server spelling its own host
differently, e.g. redirecting "https://example.com/" to
"https://EXAMPLE.com:443/".
The HTTPS downgrade check now compares the redirect target against
the original request rather than the previous hop, so a chain which
started on plaintext http, passed through an https server and came
back to http is no longer refused - nothing is being downgraded
relative to what the user asked for. A chain which started on https
and reaches http via any number of hops is still refused.
Whether an archive entry name can escape the archive's namespace was
left entirely to each archiver. Enforce it in the archive backend too.
List only passes on direct children of the directory listed and
NewObject only returns the object asked for, so a future archiver
which forgets to validate names cannot expose a traversal to fs/sync
and fs/operations.
The path inside the archive was compared against the cleaned entry
names without being cleaned itself, so `archive.zip/sub/./dir` or
`archive.zip/sub//dir` failed to list even though `archive.zip/sub/dir`
worked.
A zip containing a file entry whose name refers to the archive's own
root (".", "/" or "") was presented as a single file called "." and
all its other entries disappeared. A file at the root can only be the
archive member the backend was pointed at, so with no root such an
entry is skipped like any other unsafe name.
Entry names read from a squashfs directory are not sanitized by
go-diskfs. The squashfs backend joined each leaf name onto its
directory to form the object's remote, so a crafted image could escape
its directory.
Use sanitize.Leaf to skip unsafe entries in List. A "\" is an
ordinary character in a file name on the systems squashfs images are
made on and in an rclone remote path, so it is deliberately not
rejected; making it safe for the destination is the destination
backend's job.
Skipped entries are logged at DEBUG with a single NOTICE count per
listing so a crafted image under a mount cannot flood the log.
When a zip archive was mounted at a subdirectory root, readZip used a bare
strings.HasPrefix to decide which entries fell inside the root. This
matched on a raw string prefix rather than a path boundary, so mounting
root "foo" also exposed sibling entries such as "foobar/..." with their
names left uncorrected.
Require a path boundary when filtering by root.
The zip backend mounts a zip file as a browsable Fs. Go's archive/zip
does not sanitize entry names, and readZip applied path.Clean but did
not reject a cleaned name that still pointed outside the archive. A
crafted zip could make rclone copy/sync attempt writes outside the
intended destination.
Sanitize entry names with sanitize.Path - the same check used by
rclone archive extract - skipping any entry with a ".." path
component, whether separated by "/" or "\". A backslash is otherwise
kept as an ordinary character in the name, as archive extract does. It
is up to the destination backend to make names safe for its storage.
Skipped entries are logged as a single count per archive so a crafted
archive with many escaping entries cannot flood the log.
Move the archive entry name validation added for CVE-2026-59732 from
cmd/archive/extract into a new lib/sanitize package as sanitize.Path,
so the same check can be shared with the archive backend which mounts
archives as a filesystem.
sanitize.Path keeps the extract semantics - reject any name with a
".." path component, treating both "/" and "\" as separators - and
additionally cleans the name with path.Clean. This corrects two edge
cases in extract: a repeated "./" prefix ("././file.txt") is now fully
stripped rather than only the first, and a bare "." entry is now
treated as the archive root and skipped.
Add sanitize.Leaf, which rejects a name that is empty, ".", ".." or
contains a "/", for checking a single directory entry name read from
an archive.
The names handled are rclone remote paths, in which "/" is the only
separator and "\" an ordinary character, so Leaf does not reject a
backslash: making a name safe for its storage is the destination
backend's job (the local backend encodes "\" on Windows and refuses
paths which escape its root). Path's rejection of ".." between
backslashes is kept as defence in depth for extract.
With --links/-l, a symlink is served as a .rclonelink object whose
content is the target path. A Range request with a start offset beyond
the target length (e.g. "Range: bytes=99999999999-") reached
openTranslatedLink and sliced the target string at that offset, panicking
with "slice bounds out of range".
Clamp the offset to the target length so an out-of-range start reads
empty, matching how a real file read past EOF behaves.
The birth-time (btime) write in writeMetadataToFile followed symlinks for
any object that was not a translated link, so under -l/--links a symlink
planted by an untrusted source at the destination path could redirect the
btime write to a target outside the backup destination on OSes where
birth time is settable (Windows).
Use the NOFOLLOW birth-time write whenever translating symlinks, not only
for translated links. It is a no-op on a real file or directory and stops
a planted symlink from being followed out of the destination.
With -l/--links the local backend faithfully recreates a source ".rclonelink" as
a real symlink at the destination. Directory metadata (chmod/chown/chtimes),
however, was applied with the raw following syscalls
os.Chmod/os.Chown/os.Chtimes rather than through the os.Root sandbox used for
content writes. A Directory is never a translatedLink, so when the destination
path already existed as a symlink planted by an untrusted source, the metadata
was applied through it to a target outside the backup destination.
Route directory metadata through os.Root when translating symlinks, so a planted
symlink can no longer redirect chmod/chown/chtimes out of the destination, while
legitimate in-tree directories are unaffected.
An empty or "." volume name joined onto the base directory resolves to the
base directory itself. newVolume does not call validate, so such a name
would mount a remote over the base directory and shadow every other
volume's mountpoint.
Require the resolved mountpoint to be a strict descendant of the base
directory so these degenerate names are refused.
When the plugin restarts it reads its persisted state file and used the
stored mountpoint verbatim. A state file written by an older rclone that
allowed escaping volume names, or one that was tampered with, could point
the mountpoint outside the base directory, so upgrading did not remediate
an already-escaped volume.
Re-derive the mountpoint from the base directory and the volume name on
restore, confined to the base directory, rather than trusting the stored
path.
A Docker VolumeDriver.Create request carries a raw volume name that was
joined onto the base directory with filepath.Join and used verbatim as the
mountpoint. filepath.Join collapses ".." components, so a crafted name such
as "../../../etc/foo" resolved to a host path outside the base directory,
where the plugin then created a directory and mounted the remote.
Confine the mountpoint to the base directory and refuse any name that
resolves outside it.
When serving FTP with --auth-proxy, the obscured password was cached in a
driver-global map keyed only by the username. Two sessions that logged in
with the same username but different credentials shared one map entry, so a
later login overwrote it and every subsequent operation on the earlier,
still-authenticated session was re-authorized with the later session's
credential and executed against the later session's backend.
Bind the credential to the FTP session by storing the obscured password in
the per-session goftp Session.Data map instead, so each session always
resolves the backend it authenticated for.
With --auth-proxy set and --auth-key unset, serve s3 registered every client
supplied access key ID with an empty secret and verified the SigV4 signature
against that, so anyone could sign a request for an arbitrary access key ID with
an empty secret and be let in. The proxy program was only ever given the access
key ID (as both user and pass) so it had nothing with which to authenticate the
client either.
An S3 client never sends its secret, only a signature made with it, so the
server has to know the secret to check the request. The auth proxy protocol as
been changed to handle this. For serve s3 the proxy program is given just the
access key ID as the user (no pass or public_key) and must return the matching
secret as _secret_access_key in its output. rclone verifies the request's
signature against that secret, refusing the request if the proxy rejects the
access key ID, doesn't return a secret or returns an empty one, or the signature
doesn't match. The secret is only used for this server's own verification and is
never registered with gofakes3, so other serve s3 instances in the same process
don't honour it.
The proxy's answers are cached. If a signature fails against a cached secret the
proxy is consulted again so a rotated secret takes effect immediately - but only
for a signature mismatch, and at most once every 10 seconds per access key ID
and client IP, so a stream of bad signatures can't make the proxy program run
for every request. A rotation never shuts down the cached backend under requests
still using it. A cached answer is checked with the proxy again once it is 5
minutes old even if in constant use, so revoking an access key ID takes effect
within 5 minutes.
This means --auth-key is no longer needed with --auth-proxy: it is ignored and a
warning is given at startup if both are set. The proxy is the source of truth
for both the credentials and the backend they map to. Presigned URLs (credential
in the query string) are now recognised by the proxy middleware too. The auth
proxy docs are added to serve s3.
Note that the serve s3 auth proxy protocol has changed. The proxy program is now
given the access key ID as "user" (it was previously given an MD5 hash of it,
with the access key ID as "pass") and must return the matching secret as
"_secret_access_key".
This needs gofakes3 v0.0.9 for signature.V4SignVerifyWithSecret.
gofakes3 kept the keys given with --auth-key in a store global to the process,
so when more than one serve s3 was running in one rclone (eg started via the rc)
each accepted the others' credentials and a client with the key for one server
could read and write the backend of another.
This updates gofakes3 to v0.0.9 which keeps auth keys per instance and adds a
test that two servers only accept their own keys.
From v1.70.0, an SFTP server started through the rc serve/start API with
a per-server proxyOpt.AuthProxy decided whether to enable proxy
authentication by checking the process-global proxy.Opt.AuthProxy
instead of the supplied proxyOpt.AuthProxy. In the normal rc case the
global is empty, so the auth proxy was silently ignored: the server
either failed to start with "no authorization found" or authenticated
against the local authorized_keys file instead of routing each login
through the proxy the operator configured.
The serve Provider refactor (f425f8d46) fixed the constructor by building the
provider from the supplied proxyOpt, but the authorized-keys handling in
configure() still consulted the global option. Make it depend on whether
proxy mode is actually active, and add a regression test for the
per-server option.
From v1.70.0 until the serve Provider refactor (f425f8d46), an S3 server started
through the rc serve/start API with a per-server proxyOpt.AuthProxy
decided whether to enable proxy authentication by checking the
process-global proxy.Opt.AuthProxy instead of the supplied
proxyOpt.AuthProxy. In the normal rc case the global is empty, so the
auth proxy was silently ignored and the server served the fixed
filesystem supplied to serve/start rather than routing each access key
to the backend chosen by the proxy, bypassing the operator's intended
per-key authorization.
The Provider refactor fixed this incidentally by building the provider
from the proxyOpt passed to the constructor. This adds a regression test
so the per-server option cannot silently stop working again, and only
logs "allowing anonymous access" when neither an auth key nor an auth
proxy is configured so the log reflects the effective mode.
From v1.70.0 until the serve Provider refactor (f425f8d46), an FTP server started
through the rc serve/start API with a per-server proxyOpt.AuthProxy
decided whether to enable proxy authentication by checking the
process-global proxy.Opt.AuthProxy instead of the supplied
proxyOpt.AuthProxy. In the normal rc case the global is empty, so the
auth proxy was silently ignored and the server fell back to its
fixed-backend mode, whose default account accepts user "anonymous" with
any password - a complete authentication bypass.
The Provider refactor fixed this incidentally by building the provider
from the proxyOpt passed to the constructor. This adds a regression test
so the per-server option cannot silently stop working again.
The multipart reorder-buffer admission trusted the client-declared part length.
A negative length was accepted, and `buffered + size` could overflow int64 for
a huge declared length, wrapping the running total negative and admitting
further parts past --multipart-streaming-buffer-limit.
Reject a negative length and use the overflow-safe comparison `size <=
bufferLimit - buffered` so an untrusted Content-Length can neither poison nor
overflow the budget.
Streamed multipart UploadPart called Reserve(contentLength) before reading any
body bytes, so the pool immediately allocated one 1 MiB page per MiB of the
client-declared Content-Length (or X-Amz-Decoded-Content-Length). An client
could declare a huge part size, send no body, and force an arbitrarily large
allocation without paying the bandwidth cost of the declared body.
Drop the Reserve so the pool-backed buffer grows a page at a time as the body
is actually read: memory now tracks the bytes received, not the unverified
header.
The rclone core does not sanitise ".." in an object's Remote(). Such a name can
arrive from a malicious or buggy backend - an object store permits keys
containing ".." or a leading "/" - and, if acted on, lets a listing or transfer
escape the configured root. A source object named "../../other/x" is copied to
"other/x" outside the destination root, and a crafted listing name surfaces
outside the directory being listed.
Add list.RemoteEscapesRoot, which reports whether a Remote climbs above the
root when joined onto it, and list.RemoveEscaping, which drops and logs such
entries.
Apply RemoveEscaping unconditionally - independent of the include/exclude
filters - at the three per-entry filtering points every listing passes through:
filterDir, walk.listR and walk.walkRDirTree (recursive ListR).
operations.StatJSON calls List and NewObject directly, bypassing those, so it
rejects an escaping remote up front.
This confines every backend at once, so no per-backend change is needed.
CVE-2026-78662: a malicious peer could flood an undecided channel's
incoming requests, deadlocking the whole connection in
golang.org/x/crypto/ssh (GO-2026-6354)
CVE-2026-56855: a malicious peer could send crafted messages on an
established channel, deadlocking the whole connection in
golang.org/x/crypto/ssh (GO-2026-6355)
When bisync is interrupted with a graceful shutdown it keeps the files
which transferred successfully in its listings and rolls the rest back.
An operator precedence mistake in that check meant a transfer of an
empty file (or one of unknown size) was kept even when it had failed,
so bisync recorded it as synced when it had not been.
Single part uploads with Object Lock parameters need a Content-MD5
header, which the SDK can't compute from a stream, so the whole body
was read into memory with io.ReadAll to hash it - up to
--s3-upload-cutoff per file. prepareUpload already sets Content-MD5
from the source object's hash when it has one, so skip the buffering
entirely in that case and only buffer when the hash is unavailable.
When buffering is needed, read the body into a multipart.NewRW buffer
from the global pool, hashing in transit, so the memory is reused
across uploads and released after the request. The presigned request
path hands the body straight to http.NewRequest, so wrap it in
readers.NoCloser there to stop the transport closing the pooled buffer.
With speedup enabled, files up to --mailru-speedup-max-memory are read
into memory so their hash can be tried against the server before
uploading. This used io.ReadAll, which allocates a fresh heap slice per
file and grows it by doubling, so with the default 32 MiB limit and
several transfers this churned a lot of garbage outside rclone's memory
accounting.
Buffer the file with multipart.NewRW instead, hashing it in transit,
so the memory comes from the global pool and is reused.
When the hash isn't known to the server the buffered file is uploaded
from the same buffer. Previously a low level retry of that upload
resent an already drained reader, so the retry always failed. Rewind
seekable bodies at the start of each attempt so retries resend the
whole file. Add a test which drops the connection on the first attempt
and checks the retried body is complete.
The body is sent through lib/rest, which wraps it in readers.NoCloser,
so the transport can't close the pooled buffer early; Update closes it
when it returns.
The Linkbox API needs the MD5 of the first 10 MiB of each uploaded
file, so Update reads that prefix into memory before the upload. This
used io.ReadAll, which allocates a fresh heap slice per file and grows
it by doubling, churning well over 10 MiB of garbage per upload.
Read the prefix into a multipart.NewRW buffer instead so the memory
comes from rclone's global pool and is reused across uploads, and hash
it in transit rather than computing the same MD5 twice.
The PUT body goes through lib/rest, which stops the http transport
closing it, so Update owns the buffer and closes it on every exit path.
When the source has no MD5, Update reads the whole file into memory to
hash it before uploading if it is under --jottacloud-md5-memory-limit.
This used io.ReadAll, which grows a fresh heap slice per file (up to
10 MiB by default, roughly doubled by the growth strategy), so syncs
of many files churned allocations and GC.
Buffer the data with multipart.NewRW instead so the memory comes from
rclone's global pool, is reused across uploads and is released by the
existing cleanup function.
Unknown sized streams previously took the in-memory branch regardless
of the limit, so an rcat of an arbitrarily large stream could read it
all into memory. Spool those to the temporary file instead, as is
already done for files over the limit.
The buffered body is sent through lib/rest, which wraps request bodies
in readers.NoCloser, so the transport can't close the pooled buffer
early.
The multipart upload allocated a fresh chunk-sized buffer (64 MiB by
default) plus a 1 MiB scratch buffer per large file, copying every byte
twice, and never returned them to rclone's memory pool.
Buffer each part with multipart.NewRW instead so the memory is reused
across uploads and part of rclone's central memory management.
The pooled buffer is seekable, so a part can now be re-sent.
uploadPart previously had no retry at all and any transient error
failed the whole upload. It is now wrapped in the pacer with the
backend's usual shouldRetry rules, seeking to the start before each
attempt. The body is wrapped in readers.NoCloser so the http transport
can't close the pooled buffer between attempts, and Content-Length is
set explicitly since net/http can't infer it from a pool.RW.
The FsPutRetry integration test covers the retry of a failed upload
request and checks the buffers are returned to the pool.
Each part of a multipart upload allocated a fresh part-sized buffer
(the size is chosen by Box, typically 8-32 MiB) with up to --transfers
parts in flight, so large uploads churned allocations and GC.
Buffer parts with multipart.NewRW instead so the memory comes from
rclone's global pool, is reused across parts and files, and is part of
rclone's central memory management.
The pool.RW is seekable so the retry closure seeks back to the start
before each attempt instead of rebuilding a bytes.Reader, and the
per-part SHA1 digest is computed by reading the buffer and seeking
back. The body goes through lib/rest which already stops the transport
from closing it; the uploading goroutine owns and closes the buffer.
The whole-file SHA1 used for the commit is unchanged.
The FsPutRetry integration test covers the retry of a failed upload
request and checks the buffers are returned to the pool.
Each upload allocated a fresh chunk-sized buffer (10 MiB by default)
regardless of the file size, so bulk transfers of many files churned
allocations and GC.
Buffer chunks with multipart.NewRW instead so chunk memory is reused
across uploads and is part of rclone's central memory management. The
pool.RW is seekable, so the existing rewind on retry carries over.
The body goes through lib/rest which already wraps it so the transport
can't close the pool buffer. The upload loop closes it after every
chunk, on error paths included. A source which ends before the
declared size is now reported as a short read before the chunk is sent
rather than as an incomplete write afterwards.
The FsPutRetry integration test covers the retry of a failed upload
request and checks the buffers are returned to the pool.
Each chunked upload allocated a fresh chunk-sized buffer (48 MiB by
default), so bulk transfers of many files churned allocations and GC.
Buffer chunks with multipart.NewRW instead so chunk memory is reused
across uploads and is part of rclone's central memory management. The
pool.RW is seekable, so the existing IncorrectOffset recovery which
skips already-received bytes on retry carries over unchanged, and the
"chunk received OK" check now compares against the bytes actually
buffered so a short final chunk is recognised too.
The Dropbox SDK wraps the request body in io.NopCloser, so the transport
never closes the pool buffer. The upload loop closes it after every
chunk, on error paths included.
The FsPutRetry integration test covers the retry of a failed upload
request and checks the buffers are returned to the pool.
Each upload allocated a fresh 48 MiB chunk buffer, so bulk transfers of
many files churned allocations and GC, and small files paid for the full
buffer.
Buffer chunks with multipart.NewRW instead so chunk memory is reused
across uploads and is part of rclone's central memory management.
The pool.RW implements io.Closer, so the PATCH request body is wrapped in
readers.NoCloser to stop the http transport closing it after a failed
attempt and freeing its pages before the retry. The retry closure now
seeks the chunk back to the start explicitly before resending, a short
read of the source is reported as an error rather than sent as an
under-length chunk, and the request carries an explicit ContentLength.
The FsPutRetry integration test covers the retry of a failed upload
request and checks the buffers are returned to the pool.
The compressibility heuristic compressed a 1 MiB sample of each upload
into a bytes.Buffer only to read its length, growing up to ~1 MiB of
garbage per file. Write the sample through a counting io.Discard-style
writer instead so no output buffer is allocated at all.
Every compressed upload allocated a fresh buffer the size of
--compress-ram-cache-limit (20 MiB by default) regardless of how big
the file actually was, so uploading many small files churned large
allocations and the memory sat outside rclone's pool accounting.
Read the head of the stream into a multipart.NewRW instead, which takes
pages from the global pool only for the bytes actually read and returns
them when the upload finishes. The pool.RW is seekable, so a wrapped
backend which needs to retry a small upload can rewind the body, which
the previous bytes.Buffer did not allow. A read error while filling the
cache is returned rather than falling through to the streaming path.
Add a unit test covering the buffered, streamed and spooled paths which
checks the body handed to the wrapped remote, that it can be re-read
for a retry, and that the pool pages are returned.
Each resumable upload allocated a fresh chunk-sized buffer (8 MiB by
default, up to 64 MiB), so bulk transfers of large files churned
allocations and GC.
Buffer each chunk in a pool.RW from the global page pool instead so
the memory is reused across uploads and bounded by rclone's central
memory management.
The pool.RW is seekable, so the chunk is rewound at the start of each
retry rather than re-wrapped. lib/rest wraps request bodies in
readers.NoCloser so the transport cannot return the pages to the pool
between attempts - the Content-Length is set through rest.Opts because
the wrapped body is not a *bytes.Reader net/http can measure. A source
that runs dry before its declared size is reported as an unexpected EOF
rather than sending the short chunk.
Files below upload_cutoff (up to 20 MiB) were assembled into a bytes.Buffer
which grows by doubling, so each upload allocated roughly twice its size
and threw it away afterwards, churning the GC on bulk transfers. Write the
multipart/related body into a pool.RW from the global page pool instead so
the memory is reused across uploads and bounded by rclone's memory
management.
The pool.RW is seekable, so the same body is rewound at the start of each
retry rather than being re-wrapped. lib/rest already wraps request bodies
in readers.NoCloser so the transport cannot free the pages between
attempts; the Content-Length is passed explicitly because the wrapped body
is no longer a *bytes.Reader net/http can measure.
The FsPutRetry integration test covers the retry of a failed upload
request and checks the buffers are returned to the pool.
Updating a file below --hidrive-upload-cutoff copied the whole file into
an append-grown slice so the request could be retried, costing roughly
twice the file size in transient allocations for every such update.
Buffer the file in a multipart.NewRW from rclone's global page pool
instead and return it to the pool once the request has finished. The
pool.RW is seekable so retries re-send the same buffer. Accounting is
applied as the buffer is sent so bandwidth limits and progress still
track the upload. The request now carries an explicit Content-Length
rather than being sent chunked.
The upload is bounded by the size the source declares - a source that
delivers more bytes than its declared size has the excess ignored, where
previously the stream was sent to EOF.
The FsPutRetry integration test covers the retry of a failed upload
request and checks the buffers are returned to the pool.
Each chunk of a chunked upload allocated a fresh buffer of
--hidrive-chunk-size bytes (48 MiB by default), with up to
--hidrive-upload-concurrency of them in flight, so large uploads churned
allocations and GC and a short final chunk still cost a whole chunk.
Buffer chunks with multipart.NewRW from rclone's global page pool
instead, sized to the data actually read, and return each buffer to the
pool once its PATCH request has finished. The pool.RW is seekable, so a
chunk which fails with a retryable error is re-sent from the same
buffer. Accounting is applied as a chunk is sent so bandwidth limits
and progress still track the upload. Chunk requests now carry an
explicit Content-Length rather than being sent chunked.
The prefix sent with the creating request in PutUnchecked is buffered
through the same helper.
The FsPutRetry integration test covers the retry of a failed upload
request and checks the buffers are returned to the pool.
Every upload allocated a fresh buffer of --hidrive-upload-cutoff bytes
(96 MiB by default) to hold the part of the file sent with the creating
request, however small the file was, so copying many small files churned
large allocations and GC.
Buffer that prefix in a multipart.NewRW from rclone's global page pool
instead, sized to the smaller of the declared file size and the cutoff,
and return it to the pool once the file has been created. Accounting is
applied as the buffer is sent so bandwidth limits and progress still
track the upload. The request now carries an explicit Content-Length
rather than being sent chunked.
The upload is bounded by the size the source declares - a source that
delivers more bytes than its declared size has the excess ignored,
where previously they were read up to the cutoff.
The FsPutRetry integration test covers the retry of a failed upload
request and checks the buffers are returned to the pool.
Upload chunks are buffered into a bytes.Reader and then, when transfer
accounting is active (always for a real copy), re-wrapped in accounting
before being handed to cachedReader. cachedReader only recognised a bare
*bytes.Reader, so the accounted chunk fell through to
readers.NewRepeatableReader, which copied the whole chunk again into an
append-grown slice. Every chunk (and the upload-cutoff prefix of every
file) therefore cost roughly twice its size in memory.
Look through the accounting wrapper when deciding whether the reader is
already a seekable buffer and, if so, seek the buffer underneath while
still reading through the accounting, so retries rewind without a copy.
Each chunk of a resumable upload was buffered in a fresh RepeatableReader
which grew by appending, so bulk transfers churned up to a chunk size (10
MiB by default) of heap per chunk and GC pressure.
Buffer chunks instead with multipart.NewRW from the global page pool
instead so memory is reused across uploads and bounded by rclone's
central memory management.
A source which delivers fewer bytes than its declared size now fails
before the chunk is sent with an unexpected EOF error rather than being
rejected by the transport.
Note that accounting now happens as the chunk is read into the buffer
rather than as it is sent, as in the drive backend.
When the source can't be re-opened, the whole input is read once to
compute its gcid before upload and held back for the upload proper. For
inputs at or below --pikpak-hash-memory-limit this used a bytes.Buffer,
a fresh heap allocation of up to the limit (and more while growing) per
file.
Hold the data in a buffer from the global memory pool instead so the
pages are reused and released on cleanup.
Inputs of unknown size were also always held in memory regardless of
their length, as only sizes above the limit chose the temp file.
Spool unknown sizes to the temp file so a large stream can't exhaust
memory.
The multipart uploader kept its own private buffer pool, a copy of the
one in lib/pool with identical settings, so its chunk memory was never
shared with the rest of rclone. Pages cached here were invisible to
other backends and vice versa, costing up to 64 MiB of extra idle cache.
Allocate chunks with multipart.NewRW, the global pool used by the
other backends instead.
WriteChunk copied the whole chunk, which lib/multipart already hands over
in a buffer from the global memory pool, into a bytes.Buffer so that
retries could re-send it. That doubled the per-part memory and made a
fresh chunk-sized heap allocation (64 MiB by default) for every part,
times the upload concurrency.
The chunk reader is seekable, so find its size with Seek and rewind it
inside the pacer closure instead, sending the pooled buffer directly.
Also fix the error for a part that fails to upload, which formatted the
buffer instead of the part number.
Each upload chunk is buffered in a pool.RW from the global memory pool
but was never closed, so its pages were never returned to the pool.
Close the buffer after each chunk is uploaded and on the read error
path.
A chunk that failed with a retryable error was also retried without
rewinding the buffer, so the retry sent an empty body with the original
Content-Length and Content-Range and failed.
Seek the chunk back to the start inside the pacer closure so each
attempt re-sends it in full.
The FsPutRetry integration test covers the retry of a failed upload
request and checks the buffers are returned to the pool.
Before this change the transport could still be reading the request
body when Call returned, in two ways:
- the goroutine writing a multipart upload form was never stopped or
waited for, so after a failed request it kept reading the file body
while the caller retried
- the transport itself is documented to close the request body,
possibly in a different goroutine after the request has finished,
and hiding the body's Close method removed any way to wait for that
Both of these raced with callers which seek to the start of a pooled
buffer and retry the request, corrupting the retried body, and the
abandoned multipart goroutine could read pooled pages after they had
been returned to the pool.
Call now waits for the transport to finish with the request body
before returning, and the multipart form writer is stopped and waited
for once the request has finished.
Add an FsPutRetry integration test which uses the fshttp fault injector
to fail the first request carrying upload data, before it is sent, with
either a synthetic 500 or a connection reset, and then checks that Put
either succeeds with the correct contents or fails cleanly without
leaving a corrupt object behind.
It runs a single part upload and, for backends implementing
SetUploadChunkSizer, a chunked one. This catches backends which retry
an upload from a buffer which has already been consumed or returned to
the memory pool. Backends which don't use HTTP skip the test.
Also check in FsPutRetry and the large upload tests that the number of
buffers in use in the global memory pool is unchanged after an upload,
to catch backends which leak pooled buffers.
Add fshttp.SetFaultInjector, which installs a function consulted by
every Transport before a request is sent. The injector can synthesise
an error status code or a transport error for chosen requests. The
request body is drained and closed as a real round trip would, but
nothing reaches the server.
This lets the integration tests check that backends cope with a
transient failure part way through an upload - in particular that a
retry re-sends the same data rather than an already consumed or
freed buffer - without needing a fake server for each backend.
NoCloserNotify hides the Close method of the reader passed in like
NoCloser, but calls a notify function (once only) when the returned
body is closed. This lets callers of http.NewRequest find out when the
transport has finished with a request body, as it is documented to
possibly close it in a different goroutine after the request has
finished.
When both /me/drives and /me/drive fail during config (for example an
account-level 403 serviceReadOnly "Database Is Read Only"), send the
config state machine to the existing manual drive ID entry state
instead of dead-ending at choose_type with the raw error. The drive
itself remains usable when only the enumeration API is blocked.
Fixes#9794
Account.WriteTo wrote each buffer to the destination in full before
trimming the byte count for --max-transfer, so up to one buffer past
the limit could reach the wire and go unaccounted. This matters now
that NoCloser forwards WriteTo.
Truncate the write to the remaining allowance before writing.
NoCloser hides the Close method of an io.Reader but in doing so it
also hid io.WriterTo if the underlying reader implemented it, forcing
io.Copy to fall back to a buffered Read loop.
Following io.NopCloser, return a variant which forwards WriteTo when
the wrapped reader supports it.
Each resumable upload allocated a fresh chunk-sized buffer (8 MiB by
default), so bulk transfers of many files churned allocations and GC.
Buffer chunks with multipart.NewRW instead — the global page pool used
by the other backends — so chunk memory is reused across uploads and
bounded by rclone's central memory management. The pool.RW is seekable,
which also keeps chunk reads repeatable for retries.
The pool.RW implements io.Closer, so http.NewRequestWithContext upgraded
it to the request body and the transport closed it after each attempt,
returning its pages to the global pool — a chunk retried after a 5xx
then read a freed buffer and panicked in pool.(*RW).readPage. Wrap the
request body in readers.NoCloser so the transport can't take ownership
and the upload loop remains solely responsible for the buffer's
lifetime. Add a regression test that fails a chunk with a 500 and then
accepts the retry; it reproduces the panic without the fix.
Fixes#9684
The sftp backend could be used as the source of a multi-thread copy -
each chunk of a download is read on its own connection - but not as the
destination, because it did not implement OpenWriterAt. Uploading a
single large file was therefore limited to one connection while
downloading the same file was not, capping upload throughput well below
the link speed on high latency connections.
This implements OpenWriterAt for the sftp backend using lib/filepool. A
small pool of open write handles, each backed by its own connection,
lets the core write the chunks of a large file concurrently over several
connections. The file is created and truncated once up front so every
chunk offset is valid before the concurrent writes start.
Multi-thread uploads are off by default and turned on with the new
--sftp-multithread-upload flag, since many sftp servers only accept
sequential writes and would fail large uploads otherwise. Even when
enabled they fall back to a single connection when
--sftp-disable-concurrent-writes is set (a server that can't take
out-of-order packets on one handle won't take several handles either) or
when --sftp-connections caps the pool (the per-file fan-out would
otherwise deadlock waiting on it).
The OpenSSH test server (TestSFTPOpenssh) enables
--sftp-multithread-upload so the feature is exercised in CI against a
real server.
Tested against OpenSSH: a single large upload over a high latency link
went from ~12 MB/s to ~90 MB/s.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Splainte <r.wycke@hotmail.fr>
Factor the connection-backed write handle pool out of the smb backend
into a generic lib/filepool.Pool[T] with its own tests, so it can be
reused by other backends that implement fs.OpenWriterAter over a
connection pool.
The smb backend keeps its behaviour, opening and releasing handles
through small closures passed to the pool.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Splainte <r.wycke@hotmail.fr>
*pool.RW implements io.Closer, so when one is used directly as an
http.Request body the transport closes it after each attempt. A retry
which then seeks and re-reads the RW would index the freed pages and
panic with "index out of range" in readPage.
Read, Write, ReadFrom, WriteTo and Seek now return ErrClosed (which
wraps io/fs.ErrClosed) after Close, and Close is safe to call more
than once.
Nine tests fail on an ordinary Windows machine, eight in backend/local and
TestEnvironmentVariables in cmdtest, all with
symlink file.txt \?\C:\Users\...\symlink.txt: A required privilege is not
held by the client.
Windows grants SeCreateSymbolicLinkPrivilege only to an elevated process or one
running with Developer Mode enabled, and a default install gives an ordinary
user neither. CI does not see this because the windows-latest runner is
elevated, so the failures only show up on a contributor's own machine, where
AGENTS.md asks for make quicktest to pass before opening a pull request.
cmdtest already recognised the situation and attached a note to the failure
saying the test could safely be ignored. If it is safe to ignore then the test
knows it cannot run, so skip it and say why instead.
backend/local gains a helper that tries a symlink in t.TempDir() and skips if it
cannot make one, called from the six tests that need the privilege. Where a
platform can create symlinks the probe succeeds and nothing is skipped, so other
platforms are unchanged.
TestMetadata is skipped whole because it creates its symlink before anything
else and the object built from it is used throughout.
TestSymlinkEscapeConcurrent is left alone: it goes through putLink and ignores
the error, so it never needed the privilege.
Directory names which look like they have a --b2-versions version
string are now encrypted in full, so directories created by older
rclone (which left the version string in plain text) no longer
decrypt and vanished silently from listings.
DecryptDirName now falls back to the old form for such names so the
directory is listed, and logs the name it needs to be renamed to on
the underlying remote to make it accessible again. Document this in
the crypt docs.
The refresh endpoint returns a rotated token with a fresh expiry on
every successful call, but getUserInfo discarded it, so routine use
never extended the stored token's life. Once the stored token aged
out, accounts with 2FA enabled could not recover non-interactively
and required a manual reconnect.
Carry the rotated token out of getUserInfo and persist it in NewFs
via the same jwtToOAuth2Token + oauthutil.PutToken path that
refreshJWTToken uses, keeping f.cfg.Token in sync (same pattern as
refreshOrReLogin).
Fixes#9584
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The --b2-versions support added in 3fe2aaf96 strips a version string
from the last segment of a path before encrypting it, so that the
plain text version suffixes which the underlying backend appends to
encrypted file leaf names can be handled. EncryptDirName and
DecryptDirName share that code, so the last segment of a *directory*
name was version stripped too. Only file leaf names are ever given a
version string by the backend - a directory gets a
version-string-like name from the user, and such a name is encrypted
verbatim when it appears as the parent of a file name, so the same
directory ended up with two different encryptions.
Before this change, with a directory whose name matches rclone's
version format, eg dir-v2001-02-03-040506-123:
rclone copy file.txt crypt:dir-v2001-02-03-040506-123/
rclone ls crypt:dir-v2001-02-03-040506-123
# => "directory not found" - the file is invisible to listings
rclone mkdir crypt:dir-v2001-02-03-040506-123
# => creates a second directory with the same decrypted name
After this change EncryptDirName and DecryptDirName encrypt directory
names verbatim, so a directory encrypts the same way whether it is
named on its own or as the parent of a file. Version strings are only
added to file names by the underlying backend, so --b2-versions is
unaffected and the existing version tests are untouched.
A directory which was created by the old EncryptDirName will no longer
decrypt and will be reported as undecryptable in listings. Such
directories were already unusable - anything copied into one was
written to a different encrypted directory - so nothing which worked
before is broken by this.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
With no_head_object set, NewObject does not read any metadata, so the
destination object returned from a server side copy had a size of 0.
The size check in operations.Copy then failed with "corrupted on
transfer: sizes differ N vs 0" and deleted the newly copied object.
This also broke Move and hence renames through rclone mount.
Populate the destination object's size and MD5 from the source object
when no_head_object is set, as a server side copy produces an object
with identical content.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dropbox is case insensitive and the path_display it returns in
change notifications may not match the case of the configured root.
Before this change the root was trimmed with a case sensitive prefix
match, so when the cases differed the full path was passed to the
ChangeNotify callback and the notification was ignored.
This trims the root case insensitively while preserving the display
case of the remaining path.
Write() overwrote a successful write's nil error with the stale
lastErr returned by kickWaiters() once the downloader had recorded
too many errors. download() then wrapped that stale error again and
stored it back as the new lastErr, so every subsequent write added
another "vfs reader: failed to write to cache file:" prefix - fixes#4998
Several documentation links pointed at anchors or paths that no longer
resolve, and the S3 directory buckets section named the config option
and flag in the plural, which does not match the backend.
Co-authored-by: shaurya <19599684+no-hup@users.noreply.github.com>
Co-authored-by: no-hup <shauryaj.finance@gmail.com>
A successful UploadPart whose response carries no ETag header made
WriteChunk panic dereferencing uout.ETag in a debug log line. The part
ETag is required by CompleteMultipartUpload, so an ETag-less 200 is
unusable: return a retryable error from inside the pacer callback so
the chunk is retried instead of crashing the transfer or completing
the upload with a broken part list.
Fixes#9822
Co-authored-by: Shurong Cao <170531907+CAOShurong@users.noreply.github.com>
Pikpak never returns MD5 for uploads which causes this test to fail.
Perhaps Pikpak should not declare MD5 but that is a bigger decision
being discussed in #9826
Nextcloud only stores a checksum which is supplied in the OC-Checksum
header of an upload, and discards it again when the modification time
is set with PROPPATCH. Re-sending the checksum in the PROPPATCH (as is
done for ownCloud) is rejected by Nextcloud with 403 Forbidden which
made the whole PROPPATCH fail, so SetModTime returned an error on any
object which had a hash. Uploads from sources without hashes, eg
streamed uploads with `rclone rcat`, were stored with no hash at all.
Use the Nextcloud PATCH extension with the X-Recalculate-Hash header
to have the server calculate and store the SHA1 of an object after a
streamed upload and after setting the modification time. This gives
a server side hash of the stored data which also lets rclone verify
streamed uploads.
If the source supplied fewer bytes than its declared size, the single
part upload path accepted the short body and stored a truncated file
recorded with the declared size, reporting a successful upload. The
multipart path already checks for this.
Count the bytes actually read and fail the upload if they do not match
the declared size, which cancels the partially created file.
This was found by the FsPutShortEOF integration test.
listSharedFolders already decoded shared-folder names with
f.opt.Enc.ToStandardName, but listReceivedFiles stored the raw name
returned by the Dropbox API unchanged. Names that require encoding
(e.g. a trailing space, which Dropbox itself rejects, so rclone
stores it as "name␠" via EncodeRightSpace) were therefore shown under
their raw, encoded form for received files instead of being decoded
back to the standard name, and findSharedFile could not resolve such
a file by its standard name.
Apply the same ToStandardName conversion listSharedFolders uses.
On a ranged download the metadata decoder stored the response's
Content-Length (the length of the range, not the blob) in the object's
size and only corrected it from the Content-Range total afterwards.
Object.Size() is read concurrently by the VFS cache and chunked reader
while a download is in progress, so with --vfs-read-chunk-size a reader
could observe the chunk length (e.g. 67108864 for 64M chunks) as the
object size. The VFS cache then logged
vfs cache: cached file (N) is unexpectedly larger than the remote
object (67108864). The cached file is likely corrupted after an
unclean shutdown; recovering ...
and truncated the read request against the bogus size, breaking
sequential reads of large blobs with --vfs-cache-mode full.
This applies the Content-Range correction before the size is stored so
the range length is never published as the object size.
The multipart upload copied the source into the request buffer without
checking how many bytes it had read, so a source that supplied fewer
bytes than its declared size was accepted by the server and reported as
a success with a truncated file stored.
Count the bytes actually read and fail the upload if they do not match
the declared size.
Signed-off-by: Rohit Behera <126186063+r0h1tb@users.noreply.github.com>
rclone v1.75.0 started creating files in Proton Drive's new
crypto-refresh encryption format, following guidance from Proton that
new file node keys should use the v6/AEAD profile. It turns out the
official Proton web app cannot decrypt files whose node key is a v6
key (but the Android app can), so every file uploaded with v1.75.0 (or
a beta after 2026-07-13) shows 'Item cannot be decrypted' in the web
app, even though rclone itself reads the files fine. Inspecting a file
created by the web app shows Proton itself still creates v4 node keys,
using the new format only for the file content.
New files are now created with the same fully pre-crypto-refresh
format as v1.74.4 (v4 node key, v3 PKESK content key, v1 SEIPD
blocks), which every Proton client can read. Reading files in the new
format still works, new revisions of files which already use the new
content format keep it, and the auxiliary fields (name, node
passphrase, extended attributes, block signatures) are pinned to the
old format regardless of the recipient key's preferences, as Proton
requires.
Files already uploaded with v1.75.0 cannot be repaired in place -
uploading a new revision does not change the file's node key. To make
such a file readable by the Proton apps again, delete it from the
remote and upload it again with a fixed version of rclone.
This updates Proton-API-Bridge to v1.0.5 and go-proton-api to v1.0.4.
See: https://forum.rclone.org/t/proton-drive-unable-to-decrypt/54087
The single-shot upload path sent the source straight to Box as a multipart
body with no Content-Length, so a source that supplied fewer bytes than its
declared size produced a short request that Box accepted and stored, and the
upload was reported as a success.
Count the bytes actually read and fail the upload if they do not match the
declared size. The multipart path already reads each chunk with io.ReadFull
and so already fails in this case.
The concurrent walker created by walk() only stopped when the callback
returned an error or the whole tree had been listed. Cancelling the
context (for example via the rc job/stop endpoint for an async
operations/size or recursive operations/list call) was therefore
ignored: the checkers kept pulling list jobs from the channel and kept
listing the entire tree, burning CPU and making job cancellation
useless for every backend without a native ListR implementation.
Make every checker select on ctx.Done() so a cancelled walk shuts down
promptly through the existing quit/drain path and reports the context
error. Also check the context between directory read chunks in the
local backend so a single huge directory does not block cancellation.
Update already wrapped the source in a counting reader but never looked at the
count, so a source that supplied fewer bytes than its declared size was
uploaded as a chunked request, accepted by the server and reported as a
success with a truncated file stored.
Compare the bytes actually read against the declared size.
When copying to the local backend with checksums enabled (the default),
rclone hashed the incoming data by wrapping the source reader in an
io.TeeReader. TeeReader has no WriteTo method, so io.Copy could not use
the source's fast path and fell back to its generic 32 KiB buffer loop.
The same wrapping also stopped the destination *os.File using
copy_file_range, since the source was no longer a raw fd.
This meant checksummed copies were written in 32 KiB chunks whereas
--ignore-checksum copies were written in much larger blocks (typically
1 MiB). On filesystems where small writes are expensive, such as FUSE
mounts like LucidLink, this made a big difference: copying a 100 MiB
file took 3203 x 32 KiB writes in 3.6s, and now takes 108 x ~1 MiB
writes in 0.47s.
The dataflow analysis behind SA4023, new in the staticcheck 0.8.0
bundled with golangci-lint v2.13.0, makes linting large packages more
than 10x slower (89s vs 7s for backend/s3 alone) which took the CI
lint job past its 30 minute limit. golangci-lint no longer enforces
its run timeout during analysis so the job ran until cancelled, and
the cancellation meant the lint cache was never saved, making every
subsequent run cold and guaranteeing the timeout repeated.
The check also produces false positives (eg claiming operations.Delete
never returns nil).
This commit removes the workings of the old web ui which hasn't been
maintained for 6 years. If users supply --rc-web-gui then rclone will
exit with an error pointing users at the maintained `rclone gui`
command.
Five of the advisories released with v1.75.0 now have CVEs assigned:
- GHSA-45pq-889g-fcgh serve restic path traversal: CVE-2026-71309
- GHSA-xhf4-832v-7xcr lib/proxy CONNECT header OOM: CVE-2026-71310
- GHSA-8c48-q9wj-3w37 ftp command injection: CVE-2026-71311
- GHSA-2m8m-jhrm-w6j2 sftp PowerShell command injection: CVE-2026-71312
- GHSA-7p4m-qxvv-g567 local file name escape: CVE-2026-71313
GHSA-6jcg-q3wp-x2f4 (squashfs) loses its CVE-PENDING marker as GitHub
declined to issue a CVE from the rclone repository - the vulnerable code
is in go-diskfs so any CVE must come from an advisory there.
GHSA-mfvx-7rcj-9m5g (pprof) keeps its CVE-PENDING marker as the CVE
request is still awaiting allocation.
The test skipped every buffer count above 1 under -race, pointing at
golang/go#27070. That issue was closed in September 2018, so the
workaround outlived its cause: -race covered 137 of the 681 subtests.
Without the guard the race build runs all 681 and passes.
_dispatchWaiters decided whether a waiter was satisfied by clipping its
range against dls.src.Size(), the size of the fs.Object snapshot taken
when the Downloaders was created. _ensureDownloader decided whether to
start a downloader from Item.FindMissing, which clips against
item.info.Size instead.
When item.info.Size dropped below the offset a waiter was parked on while
the source object still reported the full size, the two disagreed.
_ensureDownloader found nothing missing so it started no downloader, and
_dispatchWaiters found the range absent so it never released the waiter.
Nothing was downloaded and no error was produced, so the error count never
reached maxErrorCount and the waiter was never woken. The reader blocked
forever with nothing logged at any level.
Wake a waiter when FindMissing reports nothing left to download for it as
well as when its data has arrived. Since _ensureDownloader starts a
downloader only when FindMissing is non empty, a waiter with nothing
missing has nothing that could ever wake it.
Fixes#9769
Allow --order-by to rank files using comma-separated rclone path globs. Patterns are evaluated in order, unmatched files are placed last, and path ordering makes ties deterministic.
Fixes#3975
When a multi-thread source ignores ranged reads, abort the partial
destination and retry the copy as a single stream. Reset accounting
before the fallback so transfer progress remains accurate.
Validate ranged GET responses before returning their bodies. Close invalid
responses, retry malformed partial responses through the pacer, and return
without retrying when a server deterministically ignores Range.
Fixes#6980
Add response validation for calls made with Range open options. Verify
Content-Range, Content-Length, response status, and the complete
representation size before a backend accepts the response body.
Return a shared sentinel when a server ignores a partial range so callers
can avoid retrying the same unsupported request.
Before this change, when no_data_encryption was set, uploads from
local disk advertised the hash of the encrypted data even though the
data was uploaded unencrypted.
On backends which check upload hashes (eg b2) this made uploads of
small files fail with errors like "Checksum did not match data
received", and made chunked uploads store an incorrect hash so the
files failed their checksum on download with "corrupted on transfer:
SHA1 hashes differ".
See: https://forum.rclone.org/t/sha1-mismatches-on-b2-with-no-data-encryption-true/54121
Before this change the environment variable getters in fs/configmap.go
logged the option value with %q, so a password set via
RCLONE_CONFIG_remote_pass (or RCLONE_remote_pass) was printed in full
to the debug log. Values from the config file were already redacted,
which made the leak easy to miss.
This change routes both getters through fs.RedactOptionValue, which
looks up the option in the backend's option list: options marked
IsPassword or Sensitive log as XXX, unknown options are conservatively
redacted, and --dump auth still shows the value for debugging.
Fixes#5794
The NFS section under Mounting on macOS talked about serve nfs without
pointing at rclone nfsmount, which is the command that actually does the
NFS-based mount on macOS.
Fixes#7869
Signed-off-by: Dean Chen <862469039@qq.com>
rclone checksum and rclone check --checkfile hashed one file at a time
unless --download was in use. The non-download branch of CheckSum called
Object.Hash synchronously from inside the listing callback, so --checkers
had no effect there; only the --download branch used the checker token
pool.
Both branches now do their work in the same goroutine bounded by
--checkers, so --checkers means the same thing with and without
--download. Checking a sum file against 2.6 GiB of local files with
--checkers 8 went from 1.70s to 0.31s here.
As a result the "hashing" checking transfer now covers the hashing work
in both branches, rather than being finished as soon as the download
goroutine had been started.
RcatSize streams the body straight into Put behind an ObjectInfo with no
hashes, so a known-size upload had its size checked but its checksum
never verified, unlike the unknown-size path through Rcat which hashes as
it goes.
The stream can only be read once, so hash it on the way past and compare
with the destination after the upload, as operations.Copy does. A
destination which reports no usable hash is still checked by size, and one
which fails either check has the failed copy removed.
Writing any file into a third-party app container - the Obsidian, Pages or
Shortcuts folders that iCloud Drive shows alongside your own - failed with
HTTP error 412 (412 Precondition Failed) returned body:
"{ ... \"error_code\" : \"VALIDATING_REFERENCE_ERROR\", \"reason\" :
\"Request has out of order children to be chained but the parents were
missing\" }"
Reading from those paths worked, and so did creating directories in them, so
the failure looked like a missing parent when the parent was plainly there.
Items in an app container live in a different zone from ordinary iCloud Drive
folders: a folder under Documents has a drive ID like
FOLDER::com.apple.CloudDocs::<uuid>, while the Obsidian container has
FOLDER::iCloud.md.obsidian::documents#o2v. DownloadFile already accounts for
this - it deconstructs the item's own ID and addresses the zone it finds - but
CreateUpload and UpdateFile hardcoded defaultZone, and UpdateFile built the
resulting Drivewsid with a hardcoded com.apple.CloudDocs as well.
So rclone asked Apple to chain the new document to a parent in
com.apple.CloudDocs while the parent lived in iCloud.md.obsidian. The parent
really was missing from the zone being addressed, which is what the error said.
Take the zone from the parent's drive ID instead, the same way the download
path does, and build the new item's ID with ConstructDriveID. Uploads outside
an app container are unaffected: their parents are in com.apple.CloudDocs, so
the derived zone is the value that was previously hardcoded.
Verified against a real remote: files now upload into an Obsidian vault inside
the container and read back correctly with an unpatched binary afterwards.
Before this change, `NewStats` stored the context it was created from on the
`StatsInfo`. Stats groups are never freed -- they are only evicted once there are
`--max-stats-groups` of them -- so each one kept its context, and everything reachable
from it, alive for the life of the process. As the rc creates a group per call,
that included the call's filters and their compiled regexps.
The context was only ever used to get `ci.StatsFileNameLength` from the config.
`StatsInfo` already stores that same `*fs.ConfigInfo`, read from the same
context in `NewStats`.
This change fixes the issue by passing the stored ci to `transferMap.String` and
dropping the context from `StatsInfo`.
Before this change, Transfer.Done closed the account of a completed transfer but
never released it, because it assigned nil to only a local copy of the pointer.
As a result, every completed transfer continued to reference its account.
An Account holds the transfer context and the source reader, and the stats keep
completed transfers around up to `MaxCompletedTransfers` per group, with groups
discarded only at --max-stats-groups. On a long-running `rclone rcd`, this adds up.
It was noticeable when running bisync repeatedly via the rc, where the transfer
context carries `b.WriteResults` (bisync's `LoggerFn`). A `*bisyncRun` holds
Path1 and Path2 listings, which can be quite large, and are not supposed to be
retained between runs. (Naturally they aren't, when running bisync on the
command line -- which is probably why we didn't notice this issue sooner.)
This change fixes the issue by releasing `tr.acc` in Done, instead of the local
copy. `tr.Snapshot` reads the byte counts off the account, so the progress is
recorded on the transfer first. That read happens before taking `tr.mu`, because
`acc.progress()` locks `acc.values.mu`, `checkReadBefore` holds that lock while
calling `StatsInfo.GetBytes`, and `StatsInfo` locks back into `Transfer` in
`Transferred` and `_removeTransfer`.
A client which started a multipart upload and vanished without either
completing or aborting it used to hold on to its resources forever.
Incomplete multipart uploads which have had no activity for
--multipart-expiry (default 24h) are now aborted and cleaned up
exactly as if the client had called AbortMultipartUpload, with a
NOTICE logged.
An upload with a part still being received is never expired, and each
completed part restarts the clock. Late operations on an expired
upload fail with NoSuchUpload, as they do on real S3 when a lifecycle
rule has aborted the upload.
Set --multipart-expiry 0 to keep incomplete uploads forever.
Multipart uploads used to be streamed directly to the remote with their
own PutStream machinery, bypassing the VFS, a design left over from
before the VFS could abandon a streaming write.
They are now written through the VFS exactly like plain object PUTs in
every cache mode. The parts are written, in part-number order, to a
temporary object which is renamed into place server-side on
completion.
With the default --vfs-cache-mode off the parts stream through the VFS
to the remote as they arrive. With --vfs-cache-mode writes or above
they are buffered in the VFS cache and uploaded by its write-back.
User visible changes:
- Multipart uploads now show in rclone's transfer stats and obey
--bwlimit (previously they bypassed both).
- Remotes without streaming upload support now spool the upload to a
temporary file on local disk instead of buffering it in memory.
- Multipart uploads are never buffered in memory because of missing
remote capabilities - only --disable-multipart-streaming does that.
- Remotes that upload atomically now also write to a temporary object
renamed into place, so an in-progress multipart upload is no longer
briefly visible under its final key.
- On the few remotes with no server-side move or copy the parts are
written straight to the final object in all cache modes.
- With --vfs-cache-mode writes, plain PUTs and multipart uploads to the
same key go through the same cache entry, so an earlier PUT still in
the write-back window can no longer be written back over a newer
multipart upload.
- Failed write-backs are retried by the VFS without the client having
to restart the upload, and completed objects are served from the
cache for read-after-write.
The temporary objects that uploads are written to before being renamed
into place are now named .rclone_temp_put_* and .rclone_temp_multipart_*,
and the whole .rclone_temp_ prefix is reserved: any object whose name
starts with it is hidden from S3 listings. This gives a single pattern
for cleaning up leftovers from killed servers:
rclone delete --min-age 24h --include ".rclone_temp_*" remote:path
The .rclone_multipart_upload_* objects rclone v1.75 used are still
hidden from listings so leftovers from an older server stay invisible
to S3 clients.
The mtime metadata fallback was nested inside the X-Amz-Meta-Mtime
branch, so it only ran when X-Amz-Meta-Mtime was present but invalid -
and then set the modtime from the invalid value's failed parse rather
than parsing mtime. An object PUT with only mtime metadata kept the
upload time as its modtime.
Now the two keys are checked independently, as TouchObject already
does.
streamPart did not check whether the upload had been torn down, but
AbortMultipartUpload sets the reorder buffer map to nil, so an abort
arriving while a part body was still being received panicked with an
assignment to a nil map once the part was buffered.
Now a part whose upload has been aborted or completed under it is
rejected with NoSuchUpload.
A PUT which failed part way through removed the object at the
destination key. As well as differing from real S3 (where a failed PUT
never affects the stored object), this raced with the client's
automatic retry of the same PUT: the retry stored the object and
returned 200 OK, then the failed first attempt's cleanup deleted it,
silently losing an acknowledged upload. The interrupted upload could
also be committed as a truncated object, since closing the write handle
gave the streaming upload a clean end of stream.
Now a failed or interrupted PUT never disturbs the object at the key:
- The object at the key is never removed on error.
- On backends where a partial upload is visible at its final name
(PartialUploads), and when the VFS cache mode is writes or above, the
upload is written to a temporary object which is renamed into place
on success and removed on failure, as streamed multipart uploads
already do. Backends which upload atomically are still streamed
straight to the destination.
- An interrupted or short body fails the upload via
WriteFileHandle.CloseWithError instead of committing truncated data,
and a body which ends cleanly short of its declared size is rejected
with IncompleteBody.
Closing a streaming write handle sends a clean EOF to the backend
upload, so a writer which knows its data source failed part way through
had no way to stop the truncated file being stored as if it were
complete.
CloseWithError closes the handle failing the upload with the given
reason instead. EOF-like reasons are rewritten so the layers reading
the pipe can't mistake them for a clean end of stream and commit the
partial file.
Needed by serve s3 to abort interrupted PUTs - see #9718
If the source supplied fewer bytes than its declared size, the upload
request failed but a retry could report success even though the stored
file was truncated, because the retry re-sent an already exhausted
reader.
Count the bytes actually read from the source and if they do not match
the declared size return an error.
This was found by the new FsPutShortEOF integration test.
If the source supplied fewer bytes than its declared size, the upload
request failed but a retry could report success even though the stored
file was truncated, because the retry re-sent an already exhausted
reader.
Count the bytes actually read from the source and if they do not match
the declared size, remove the partially uploaded file and return an
error.
This was found by the new FsPutShortEOF integration test.
If the source supplied fewer bytes than its declared size, the
multipart upload was completed anyway, storing a truncated file and
reporting a successful upload.
Check the number of bytes read from the source against the declared
size before finalising and abort the upload with an error if they do
not match.
This was found by the new FsPutShortEOF integration test.
If the source supplied fewer bytes than its declared size, the
multipart upload was completed anyway, storing a truncated object and
reporting a successful upload.
Check the number of bytes read from the source against the declared
size before finalising and abort the upload with an error if they do
not match.
This affects all backends which use lib/multipart for chunked uploads:
azureblob, b2, drime, internxt, oracleobjectstorage, s3 and shade.
This was found by the new FsPutShortEOF and TestRcatSizeShortEOF
integration tests.
If the source supplied fewer bytes than its declared size, the
truncated file was stored and the upload reported success with the
object claiming the declared size.
Count the bytes actually read from the source and if they do not match
the declared size, remove the truncated file and return an error.
This was found by the new TestRcatSizeShortEOF integration test.
The file is created at the declared size and the data then written
with ranged writes, so if the source supplied fewer bytes than
declared, the remainder of the file was left as zeroes and the upload
reported success.
Count the bytes actually read from the source and if they do not match
the declared size, delete the partially uploaded file (if newly
created) and return an error.
This was found by the new FsPutShortEOF and TestRcatSizeShortEOF
integration tests.
Before this change, `fastCopy` created a cancellable context for the sync and
stored its cancel func on the `bisyncRun`, but only ever called it when
gracefully shutting down. On a normal run, it was never called, and until it is
cancelled, a context from `context.WithCancel` stays registered with its nearest
cancellable ancestor.
For an rc job, that ancestor is the job's own context, which the job registry
retains for `--rc-job-expire-duration`. The sync context carries bisync's
`LoggerOpt`, whose `LoggerFn` is a method value on `*bisyncRun`, so a finished
run was kept alive -- including the Path1 and Path2 listings -- for as long as the
job was.
This change fixes the issue by cancelling the sync context when `fastCopy`
returns. The cancel func is still stored on the `bisyncRun`, so a graceful
shutdown can still interrupt a sync that is in progress.
When using Microsoft Entra ID credentials, Azure Blob server-side copy
uses a user delegation SAS URL for the private copy source.
The SAS start time was set to the current local time. Azure Storage
validates the copy source from the service side, and small clock
differences can make that SAS appear not yet valid. The service then
returns 403 CannotVerifyCopySource with AuthenticationFailed.
Start the copy-source SAS 15 minutes in the past, matching Microsoft SAS
guidance for clock skew.
The precision field in the backend overview YAML files can hold
fs.ModTimeNotSupported (100 years in nanoseconds) which overflows int
on 32 bit platforms, making the YAML for those backends fail to parse
and causing rclone to log 18 internal errors on every invocation.
Use int64 for the precision field and add a test that parses every
embedded backend YAML file so this is caught on 32 bit test runs.
When an upload failed part way through with a retryable error (eg a
502 from the block storage servers) the pacer retried the whole upload
call with the same input stream. The stream had already been partially
consumed, so the retry re-created the upload draft and committed just
the remainder of the stream as a complete file, silently truncating
it. With restic over serve restic this corrupted the repository as the
truncated pack was reported as successfully uploaded.
This fixes it by using CallNoRetry for the upload, as the other
backends do, so retryable errors are returned wrapped in a RetryError
for the caller to retry the upload with a fresh stream.
Uploads through RcatSize with a known size - used by rcat --size, the
rc operations/uploadfile and the serve backends, eg serve restic - did
not check the size of the uploaded object. If the source stream ended
before the declared size worth of data had been read, the truncated
object was reported as a successful upload. This could corrupt data
for callers which trust the result, eg a restic repository accessed
via serve restic (see #9722).
This adds the same size check operations.Copy performs after a copy,
respecting --ignore-size and backends which do not report sizes.
The SFTP page said `--dump-auth`, which no longer exists: it became a
value of `--dump`, so the docs asked for a flag rclone would reject.
The same line, and a line in the Swift troubleshooting section, also
used `--dump-headers` and `--dump-bodies`. Those still parse, but
SetFlags logs "--dump-headers is obsolete - please use --dump headers
instead", so the docs were steering readers onto a deprecated form.
The generated flag listings in docs/content/flags.md and
docs/content/commands/rclone.md are left alone: those flags do still
exist, so `--help` output should keep showing them.
When the source supplied fewer bytes than its declared size, the
compressed data file was stored under a name containing the declared
size while the metadata recorded the actual number of bytes read.
NewObject looks the data file up by the size in the metadata, so the
resulting object could never be read again, and the upload reported
success.
Check that the number of bytes read matches the declared size after
uploading the data and before writing the metadata, and remove the
data file and return an error if it does not.
This was found by the new FsPutShortEOF integration test.
The append loop retries everything once the upload session has
started, so a cancelled context error was retried through all the low
level retries with exponential backoff before the upload gave up.
A source which returned EOF before supplying as many bytes as it
declared would either commit a truncated file (if the shortfall was
within the final chunk) or loop forever appending empty chunks to the
upload session. Return an error wrapping io.ErrUnexpectedEOF instead.
Note that all dropbox uploads use the chunked upload path with the
default batch_mode of sync, so this affected uploads of every size.
A source reader which returns EOF before supplying as many bytes as it
declared should not cause a backend Put to hang, panic or create an
object claiming the declared size.
The layers above detect a truncated transfer by comparing sizes after
the upload, so a backend may either return an error or create a
shorter object.
See #9704
Object.Update held its connection until the deferred putConnection ran at
function exit, so the SetModTime it does at the end of every upload had to take
a second connection from the pool, dialling a whole new SMB session when the
pool was empty. With N transfers in flight the pool grew to roughly 2N sessions
for no reason.
Return the connection as soon as the file is closed. At that point the upload
has succeeded and remove() can no longer be reached, so nothing else needs it,
and SetModTime picks the same connection straight back out of the pool.
putConnection nils the pointer, so the deferred putConnection becomes a no-op
and the connection is not returned twice.
purgeCheck listed a directory's children with includeAll set, so the Drive API
returned trashed children as well as live ones. The only thing that came of
that was the trashedFiles flag, which is used solely to decide whether to trash
the directory or delete it permanently, and when use_trash is on the directory
gets trashed either way.
That made the common case pay for nothing: after trashing N files in a
directory, removing the now empty directory paged through all N trashed entries,
about one API call and pacer delay per 1000, instead of running a single query
that returns nothing.
Ask the server to filter out trashed children when the directory is going to be
trashed anyway. Hard deletes still enumerate them, since there the distinction
decides whether the directory can be removed permanently (#1040), and
--drive-trashed-only is left alone because it deliberately lists trashed items.
Write the git configuration for each test's fake home directory as a
file instead of running three "git config --global" commands, and drop
the "git annex version" invocation from repository setup. This removes
four subprocess launches from each of the fifteen test cases.
The subtests within each end to end test function already run in
parallel, but the three test functions themselves ran one after
another.
Marking the functions parallel lets all their subtests overlap.
The end to end tests exercise rclone via a separate subprocess spawned
by git-annex, and that subprocess is not built with race
instrumentation.
Running them in the race test therefore adds several minutes to CI
without providing any race coverage. The unit tests in this package
still run under the race detector.
Run the full "git annex testremote" suite for a single layout mode and
use "testremote --fast" for the rest.
The full suite repeats the same protocol operations across a matrix of
key sizes and chunk configurations, which exercises client side
git-annex behaviour rather than rclone.
The migration test ran the full "git annex testremote" suite for every
layout mode after verifying the migration with "git annex fsck".
The fsck calls already prove the migrated data is accessible via the
builtin special remote, and TestEndToEnd covers the special remote
protocol with testremote, so the extra five full testremote runs
duplicated coverage at a cost of tens of seconds each on CI.
The cmd/gitannex end to end tests routinely take 230-530s and on slow
CI runners the package exceeds the go test default timeout of 10m.
Raise the per-package timeout to 20m in quicktest and racequicktest to
give slow runners headroom.
Direct lookups of exported Dropbox Paper files retained the
caller-visible extension before export metadata processing appended it
again. Track when metadata was resolved through an export path so the
object keeps the requested remote name while listing behavior remains
unchanged.
The auth proxy was only given the user and their password or public
key, so a proxy program had no way to restrict logins to particular
networks, or to record where an authentication attempt came from.
The JSON sent to the program now has a client_ip key holding the bare
IP the client connected from, with the port stripped so IPv6 arrives
as 2001:db8::1 rather than [2001:db8::1]:52344. An IPv4-mapped IPv6
address is reported as plain IPv4 so that a client arriving over a
dual-stack listener still matches IPv4 networks. The key is omitted
when the client has no IP address.
The IP is also mixed into the backend cache key. That is needed as the
program is only run on a cache miss, so a client from a
non-allowlisted address presenting valid credentials within the 5
minute cache lifetime would get a cache hit and be let in without the
program being consulted at all.
The macOS NFS client sends SETATTR after SYMLINK, which arrives as
Chmod/Chown on the link path. These opened the target with vfs.Open,
which follows symlinks - a freshly created symlink usually dangles, so
the open failed with ENOENT, surfaced to the client as NFS3ERR_IO even
though the link was created.
Add path-based VFS.Chmod and VFS.Chown mirroring VFS.Chtimes. They do
not follow symlinks (lstat semantics, matching VFS.Stat) and return
ENOSYS when the node exists, since the VFS stores neither permissions
nor ownership; serve nfs calls them and masks ENOSYS as before.
Fixes#9627
@@ -20,6 +20,8 @@ The core parts of rclone under `fs` and `vfs` need to work with all backends and
Make sure added tests **actually test the code you have written** and test the intention behind the change. If you are fixing a problem, write the tests first to reproduce the problem before starting on the fix.
Tests in `fs/operations`, `fs/sync`, `cmd/bisync`, `cmd/gitannex` and `vfs` (the definitive list is in `fstest/test_all/config.yaml`) are run on the integration test machine against **all** the backends, so they **must be prepared to run on backends with limited functionality**. Ideally tests, or parts of tests, should be skipped using the `Fs.Features()` flags on backends which can't support them.
## Build and Test Commands
```bash
@@ -132,3 +134,16 @@ Uses golangci-lint v2 with config in `.golangci.yml`. Enabled linters: errcheck,
- Command docs are in the command source code (e.g., `cmd/ls/ls.go`).
- Don't commit autogenerated doc changes from `make backenddocs` or `make commanddocs`.
- Website docs are in `docs/content/` as markdown, built with Hugo (`make serve` to preview).
## Fork Workflow
This fork carries agent tooling that upstream does not. Never include `docs/specs/`, `docs/plans/`, `docs/{ARCHITECTURE,DECISIONS,TODO,PERMISSIONS}.md`, `GEMINI.md`, `.claude/`, `.codex/`, `.gemini/` or this section in a PR to rclone/rclone.
- Required skill: `superpowers` - check at session start, install if missing. There is no product UI here, so `impeccable` is not needed.
- Specs go in `docs/specs/NNN-slug.md` (permanent). Plans go in `docs/plans/NNN-slug.md` and are removed with `git rm` before merge. This overrides the superpowers default paths.
- Paused or in-flight work is listed under "Now" in `docs/TODO.md` with the handoff to read first.
- Non-trivial work: brainstorm → spec → plan → TDD → review. For trivial fixes say "trivial, skipping spec" and proceed.
- Log things noticed but deliberately not fixed in `docs/TODO.md`, record decisions in `docs/DECISIONS.md`, and keep `docs/ARCHITECTURE.md` in step with structural changes - in the same commit as the code.
-`docs/PERMISSIONS.md` is the source for the `.claude/`, `.codex/` and `.gemini/` permission files - change them together.
- Never push, force-push or commit directly to `master`. Ask before adding or upgrading a dependency (`go get`, `go mod tidy`).
- Done means run in this session and seen passing: `go build`, `go test` on the touched packages, and `golangci-lint run` on them.
//lint:ignore SA1019 since we need to use jwt.StandardClaims even if deprecated in jwt-go v4 until a more permanent solution is ready in time before jwt-go v5 where it is removed entirely
//nolint:staticcheck // Don't include staticcheck when running golangci-lint to avoid SA1019
// Read the chunk into memory from the global pool, hashing
// it for the whole file hash (must be done sequentially)
rw:=multipart.NewRW().Reserve(reqSize)
_,err=io.CopyN(rw,io.TeeReader(in,hash),reqSize)
iferr!=nil{
_=rw.Close()
err=fmt.Errorf("multipart upload failed to read source: %w",err)
breakouter
}
// Make the global hash (must be done sequentially)
_,_=hash.Write(buf)
// Transfer the chunk
wg.Add(1)
o.fs.uploadToken.Get()
gofunc(partint,positionint64){
deferwg.Done()
defero.fs.uploadToken.Put()
deferfunc(){_=rw.Close()}()
fs.Debugf(o,"Uploading part %d/%d offset %v/%v part size %v",part+1,session.TotalParts,fs.SizeSuffix(position),fs.SizeSuffix(size),fs.SizeSuffix(chunkSize))
fs.Logf(nil,"crypt: directory %q has a legacy encrypted name - rename %q on the underlying remote to %q to make it accessible",out,in,c.encryptFileName(out,false))
returnout,nil
}
}
returnout,err
}
// NameEncryptionMode returns the encryption mode in use for names
constconcurrentLimitBody=`{"ok":false,"error":"You have 5 uploads in progress. Wait for them to finish before starting more.","error_code":"concurrent_upload_limit","max_concurrent_uploads":5}`
Loaded 100 of 913 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.