From cd3e46a2b7dee00b64c323d35ebdd32aab0fe644 Mon Sep 17 00:00:00 2001 From: Feng Ruohang Date: Wed, 11 Nov 2026 15:49:35 +0800 Subject: [PATCH] fix(copy): preserve compression index on pooled copy rewrites Forward dstOpts.IndexCB into erasureServerPools.CopyObject's PutObject options so multi-block S2 compressed copies (> compMinIndexSize) rewritten across pools persist Parts[0].Index for O(1) block-level ranged GET seeking. Signed-off-by: Feng Ruohang --- cmd/erasure-server-pool.go | 1 + cmd/object-copy-pools_test.go | 23 ++++++++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/cmd/erasure-server-pool.go b/cmd/erasure-server-pool.go index 4c7f4f17f..85f497abe 100644 --- a/cmd/erasure-server-pool.go +++ b/cmd/erasure-server-pool.go @@ -1831,6 +1831,7 @@ func (z *erasureServerPools) CopyObject(ctx context.Context, srcBucket, srcObjec MTime: dstOpts.MTime, NoLock: true, EncryptFn: dstOpts.EncryptFn, + IndexCB: dstOpts.IndexCB, WantChecksum: dstOpts.WantChecksum, WantServerSideChecksumType: dstOpts.WantServerSideChecksumType, ReplicationRequest: dstOpts.ReplicationRequest, diff --git a/cmd/object-copy-pools_test.go b/cmd/object-copy-pools_test.go index bbec8a0a0..1a97ec849 100644 --- a/cmd/object-copy-pools_test.go +++ b/cmd/object-copy-pools_test.go @@ -84,7 +84,12 @@ func TestAPICopyHistoricalVersionAcrossPools(t *testing.T) { if mode == "sse-c" { maps.Copy(sourceHeaders, customer(0x11)) } - data := bytes.Repeat([]byte("historical encrypted compressed content-"), 8192) + pattern := []byte("historical encrypted compressed content-") + repeats := 8192 + if mode == "plain" { + repeats = compMinIndexSize/len(pattern) + 1 + } + data := bytes.Repeat(pattern, repeats) sourceHeaders[xhttp.AmzChecksumCRC32] = mustChecksum(t, hash.ChecksumCRC32, data) key := fmt.Sprintf("copy-%s-%s-%d.txt", mode, tc.movement, sourcePool) placed.uploadPool = sourcePool @@ -145,6 +150,22 @@ func TestAPICopyHistoricalVersionAcrossPools(t *testing.T) { if copied.IsCompressed() != (!compressed && mode != "sse-c") { t.Fatalf("copy did not use destination compression setting: %v", copied.IsCompressed()) } + if copied.IsCompressed() && len(data) > compMinIndexSize { + if len(copied.Parts) == 0 || len(copied.Parts[0].Index) == 0 { + t.Fatalf("compressed multi-block copy lost seek index: %+v", copied.Parts) + } + rangeHeaders := maps.Clone(getHeaders) + rangeHeaders["Range"] = "bytes=6000000-6000127" + req, err := newTestSignedRequestV4(http.MethodGet, getGetObjectURL("", bucket, key)+"?versionId="+copied.VersionID, 0, nil, globalActiveCred.AccessKey, globalActiveCred.SecretKey, rangeHeaders) + if err != nil { + t.Fatal(err) + } + rrec := httptest.NewRecorder() + router.ServeHTTP(rrec, req) + if rrec.Code != http.StatusPartialContent || !bytes.Equal(rrec.Body.Bytes(), data[6000000:6000128]) { + t.Fatalf("ranged GET on compressed copy: %d len=%d", rrec.Code, rrec.Body.Len()) + } + } if mode == "sse-kms" { assertFederationKMSContext(t, copied, headers[xhttp.AmzServerSideEncryptionKmsContext]) assertFederationKMSContext(t, original, sourceHeaders[xhttp.AmzServerSideEncryptionKmsContext])