feat(transfer): align cross-alias copy, move, and diff on source identity - #354
Merged
Conversation
S3-to-S3 mirror uploads through a temporary file, so multipart completion stores a different ETag than the source. Auto compare now records the source ETag in x-amz-meta-rc-source-etag and HeadObject fills that identity when listed ETags differ, so unchanged objects are skipped on the next run. Co-authored-by: RustFS <hello@rustfs.com>
Same-alias copies keep using server-side CopyObject. When the source and destination aliases differ, rc cp now streams through a temporary file and uploads with the destination credentials so recursive and single object copies no longer fail with unsupported_feature. Co-authored-by: RustFS <hello@rustfs.com>
Add recursive download/upload coverage, same-alias client-cache behavior, and user-metadata forwarding. Mark the protected cp contract with a BREAKING migration note for the additive change. Co-authored-by: RustFS <hello@rustfs.com>
…pare Auto compare recopies when destination identity is absent or wrong, and size mismatches skip HeadObject. Mark the protected mirror contract with a BREAKING migration note for the additive change. Co-authored-by: RustFS <hello@rustfs.com>
The previous synchronize event ran before the PR body included BREAKING. This empty commit re-runs CI against the updated description. Co-authored-by: RustFS <hello@rustfs.com>
The previous synchronize event ran before the PR body included BREAKING. This empty commit re-runs CI against the updated description. Co-authored-by: RustFS <hello@rustfs.com>
…identity-fce3' and 'origin/cursor/feat-cross-alias-s3-copy-fce3' into cursor/feat-cross-alias-identity-parity-fce3 Co-authored-by: RustFS <hello@rustfs.com>
A remote-to-remote transfer that streams through the client cannot preserve the source ETag, so mirror records it in user metadata to recognize an unchanged object later. cp and diff need the same contract, and a second definition of the key or of how it is read would let one command re-copy what another already migrated. Move the key, the metadata reader, and the writer into a shared module with tests that pin the read/write round trip. No behavior change. Co-authored-by: RustFS <hello@rustfs.com>
…tity Cross-alias cp, mirror, and diff each decided independently whether two remote objects hold the same data, so a normal migration copied every object twice: cp --recursive across aliases did not record the source ETag, and the following mirror --compare auto could not tell a faithful copy from a changed object. - cp: record the source ETag on cross-alias uploads, the same key mirror writes. This is rc bookkeeping rather than user data, so it survives --metadata-directive replace. - mv: allow cross-alias remote moves by reusing the cp streaming path instead of rejecting them, and delete the source only after its copy succeeds. Same-alias moves keep server-side CopyObject. - diff: add --compare auto|etag|size matching mirror --compare, so the two commands cannot disagree about what is already synchronized. auto reads the recorded identity via HeadObject, and only for same-size pairs whose listed ETags differ, so an unchanged tree costs nothing. diff previously required both ETags to match, which is now --compare etag; auto is the new default. Unknown sizes are no longer treated as equal. Tests cover the read/write contract, each compare mode, the request shape of a cross-alias move, that a failed move keeps the source, and the end-to-end path where mirror skips what cp already migrated. Each new assertion was checked against a mutated implementation to confirm it fails when the behavior regresses. Co-authored-by: RustFS <hello@rustfs.com>
houseme
marked this pull request as ready for review
August 25, 2026 02:37
cxymds
reviewed
Aug 25, 2026
cxymds
reviewed
Aug 25, 2026
cxymds
reviewed
Aug 25, 2026
cxymds
reviewed
Aug 25, 2026
Member
Member
|
Addressed in a2133b5. In addition to the inline fixes, this branch now includes the inherited mirror source preflight and cross-alias post-download revalidation. Move deletion is version/ETag conditional, and diff detects LIST/HEAD snapshot conflicts. Workspace checks pass. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Related work
Builds on #351 (mirror incremental identity) and #353 (cross-alias
cp). Both are merged into this branch, so review this after those two land; the diff againstmainincludes them.Also resolves the
mvhalf of the cross-alias gap and thediffcomparison inconsistency.Background
#351 and #353 are each correct on their own, but together they leave a seam. A remote-to-remote transfer that streams through the client cannot preserve the source ETag — the destination computes its own, and multipart completion makes it differ even when the bytes are identical. #351 solved this for
mirrorby recording the source ETag inx-amz-meta-rc-source-etag. #353 added cross-aliascpwithout writing that key.The result breaks the most common migration shape:
The second command re-copies the entire tree, because it cannot tell a faithful cross-alias copy from a changed object.
Two related asymmetries came out of the same audit:
rc mvstill rejected cross-alias remote moves withunsupported_featureeven thoughrc cpnow supports them, so the two commands disagreed about what is possible. Its S3-to-S3 path also duplicated copy logic instead of reusingcp, unlike its local↔remote paths.rc diffcompared only size and the listed ETag, so it reported≠for exactly the pairsmirror --compare autocalls synchronized. Usingdiffto verify amirrorrun gave contradictory answers.Root cause
Three commands each decided independently whether two remote objects hold the same data, and only one of them knew about the recorded source identity.
Solution
A shared
object_identitymodule owns the metadata key, the reader, and the writer, so the three commands cannot drift apart again. The first commit is a pure refactor with no behavior change; the second changes behavior.cp: cross-alias uploads record the source ETag. This isrcbookkeeping rather than user data, so it is written even with--metadata-directive replace, while that flag still drops source user metadata. Same-alias copies use server-side CopyObject, which preserves the ETag, and are untouched.mv: cross-alias remote moves now work by reusing thecpstreaming path rather than reimplementing it, and inherit its constraints (SSE-C rejected,--storage-classrejected above the multipart threshold). The source is deleted only after its own copy succeeds, so a failed or partial move never destroys data that did not arrive. Same-alias moves keep server-side CopyObject.diff: adds--compare auto|etag|size, matchingrc mirror --compare.autoreads the recorded identity through HeadObject, and only for same-size pairs whose listed ETags differ, so an unchanged tree costs no extra requests andetag/sizenever issue HeadObject.Behavior changes
rc diffdefault moves from the old size-plus-ETag rule toauto. A target recording the source ETag is nowSameinstead ofDifferent, so the command exits0where it previously exited1. Pass--compare etagto keep the previous comparison.rc diffno longer treats entries with unknown size on either side as equal; they are reported as different.rc mvbetween different aliases succeeds instead of returning the unsupported-feature exit code. Automation relying on that rejection to detect an unsupported operation must be updated.This PR must be marked BREAKING because
docs/reference/rc/cp.md,mirror.md,mv.md, anddiff.mdare protected CLI behavior contracts. No JSON schema or configschema_versionbump applies.Tests
x-amz-meta-prefix and case variants, empty values rejected, unrelated metadata preservedcp: identity recorded, recorded under--metadata-directive replace, omitted when the source has no ETagdiff: each compare mode, mismatched identity, size mismatch and unknown size in all three modes, HeadObject lookup gating,--diff-onlycpupload carries the metadata header; cross-aliasmvdoes LIST/GET/PUT/DELETE and never sendsx-amz-copy-source; a failed move takes the cross-alias path and leaves the source intact;diff --compare autoreports same via HeadObject whileetagreports different with no HeadObjectcp --recursiveacross aliases, thenmirror --compare autoskips instead of recopying — the exact gap this PR closes, with a mock that persists what the upload wrotescripts/regression/cross-alias-identity.shEvery new assertion was verified against a deliberately mutated implementation (identity write disabled, cross-alias dispatch disabled, identity comparison disabled) to confirm it fails when the behavior regresses rather than passing unconditionally.
Validation
cargo fmt --all --check cargo clippy --workspace --all-targets -- -D warnings cargo test --workspace ./scripts/regression/cross-alias-identity.sh ./scripts/regression/mirror-identity.sh ./scripts/regression/cross-alias-copy.shAll 59 test binaries pass with zero failures, and the two prerequisite regression suites still pass unchanged.