Commit Graph
2 Commits
Author SHA1 Message Date
f99c4a1f14 s3: make RenameObject idempotent for a retried request (#11178)
* feat(s3): make RenameObject idempotent for a retried request - #10661

A rename that succeeds but whose response is lost leaves the client with
no safe move: retrying returned 404, because the source is already gone,
so a retry was indistinguishable from a rename that never happened.

The destination now carries what the rename that created it was, under
x-seaweedfs-rename-token: the client's token, the source key and the
time. A retry that names the same token and the same source and
destination is answered 200 without touching anything. The same token
sent for a different rename is refused with 409 rather than silently
answered, and a token older than 24 hours is treated as unrelated so a
key cannot answer for a request indefinitely.

Requests without the header behave exactly as before.

* s3: answer a reused rename token with 409, not 400

The PR promised Conflict and the code returned Bad Request. 400 tells a
client its request was malformed and invites it to give up; this request
is well formed and resending it unchanged will not help, because what it
collides with is a rename the same token already stands for.

The status code is now asserted in a test, since it is the part of this
behaviour a client actually acts on.

* Update weed/s3api/s3err/s3api_errors.go

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* s3: fix rename token review notes

- ErrIdempotentParameterMismatch returns 409 Conflict, not 400. The
  comment and TestRenameTokenReuseAnswersConflict both expect 409; the
  code regressed to 400 in a later commit.
- stampRenameToken: clarify that markRenameToken mutates srcEntry in
  place, so the token reaches the destination via the move regardless
  of whether the UpdateEntry succeeds. The precondition only guards the
  pre-move write, not the move itself.
- Extract the handler retry branch into retryRenameDecision and add
  TestRetryRenameDecision, covering the source-still-exists fallthrough
  that was previously reasoned about but not tested.

* s3: IdempotentParameterMismatch returns 400, matching AWS docs

The AWS S3 RenameObject API documentation specifies HTTP Status Code: 400
for IdempotencyParameterMismatch. Revert the previous 409 change and align
the comment and test with the documented behavior.

---------

Co-authored-by: Chris Lu <chrislusf@users.noreply.github.com>
Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Chris Lu <chris.lu@gmail.com>
2026-09-05 11:48:42 -07:00
Chris Lu 2d9ea0285c s3: add the RenameObject endpoint (#10659)
* s3: add the RenameObject endpoint

PUT /{bucket}/{key}?renameObject with x-amz-rename-source moves an object
through the filer's AtomicRenameEntry, so no bytes are read or rewritten and
the ETag, tags and SSE keys travel with the entry.

Only unversioned buckets: a versioned rename would have to rebuild the
.versions chain, and AWS offers RenameObject on directory buckets, which
cannot be versioned. The source arrives in a header, so it is authorized
separately for read and delete; both keys are locked, in key order, across the
precondition checks and the move.

* s3: let a matched source ETag precondition settle its date precondition

RFC 7232 has an ETag precondition outrank the date precondition on its own
side, and AWS documents the same for CopyObject: a matching
x-amz-copy-source-if-match with a failing x-amz-copy-source-if-unmodified-since
copies rather than returning 412. The source check evaluated all four headers in
sequence, so the date header could still veto a decided ETag match.

validateConditionalHeadersForReads already applies this precedence; the source
path now matches it.

* s3: cover a rename source named as a directory without a trailing slash

Renaming a directory would move a whole subtree, so it has to stay a missing
key whether or not the caller wrote the trailing slash.

* s3: accept a bare object key as the RenameObject source

AWS spells x-amz-rename-source both ways. Its CLI, Java and Rust examples pass
the bare source key, and only a second CLI example and the boto3 conditional
example pass bucket/key; the API reference's own example is a bare key too. The
header was read as bucket/key only, so the form AWS leads with was rejected with
InvalidArgument and the endpoint was unusable as documented.

A value is now read as a literal key first — the only reading that can never
name the wrong object — and as bucket-qualified second, when it carries the
request's own bucket and the literal key does not exist. That costs one extra
lookup only for a source that starts with the bucket's own name.

Another bucket's name in the source is no longer a distinct error: RenameObject
moves within one bucket, so it is simply part of a key this bucket does not
hold, and it reports NoSuchKey.

* s3: only a proven absence picks the other reading of a rename source

A source that resolves to a directory is not a miss to fall through on: the
literal path is still what the caller named, so answering for it beats renaming
a different object under the bucket-qualified reading. With a directory at
bucket/source.txt and an object at source.txt, a rename naming the former moved
the latter.

A failed lookup is not a proof of absence either, so a blip can no longer
redirect a rename to the other reading.
2026-08-08 21:24:30 -07:00