From 5d215dc5a2cdc3606a6cad8263f169d4f1c926c5 Mon Sep 17 00:00:00 2001 From: Feng Ruohang Date: Mon, 24 Aug 2026 20:37:36 +0800 Subject: [PATCH] fix: return checksums from CopyObject Signed-off-by: Feng Ruohang --- .../rebrand-guard/compat-baseline.json | 6 +++ cmd/api-response.go | 23 +++++++++--- cmd/object-copy-checksum_test.go | 37 +++++++++++++++++++ cmd/object-handlers.go | 2 +- 4 files changed, 62 insertions(+), 6 deletions(-) diff --git a/buildscripts/rebrand-guard/compat-baseline.json b/buildscripts/rebrand-guard/compat-baseline.json index 6296c75f1e5f1..ccacdc50200e7 100644 --- a/buildscripts/rebrand-guard/compat-baseline.json +++ b/buildscripts/rebrand-guard/compat-baseline.json @@ -1832,6 +1832,12 @@ "cmd:cmd:field:CopyObjectPartResponse.ETag", "cmd:cmd:field:CopyObjectPartResponse.LastModified", "cmd:cmd:field:CopyObjectPartResponse.XMLName", + "cmd:cmd:field:CopyObjectResponse.ChecksumCRC32", + "cmd:cmd:field:CopyObjectResponse.ChecksumCRC32C", + "cmd:cmd:field:CopyObjectResponse.ChecksumCRC64NVME", + "cmd:cmd:field:CopyObjectResponse.ChecksumSHA1", + "cmd:cmd:field:CopyObjectResponse.ChecksumSHA256", + "cmd:cmd:field:CopyObjectResponse.ChecksumType", "cmd:cmd:field:CopyObjectResponse.ETag", "cmd:cmd:field:CopyObjectResponse.LastModified", "cmd:cmd:field:CopyObjectResponse.XMLName", diff --git a/cmd/api-response.go b/cmd/api-response.go index e7e22582c6ee9..ddd5d15e36a51 100644 --- a/cmd/api-response.go +++ b/cmd/api-response.go @@ -27,7 +27,6 @@ import ( "path" "strconv" "strings" - "time" "github.com/minio/minio/internal/amztime" "github.com/minio/minio/internal/crypto" @@ -380,6 +379,13 @@ type CopyObjectResponse struct { XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ CopyObjectResult" json:"-"` LastModified string // time string of format "2006-01-02T15:04:05.000Z" ETag string // md5sum of the copied object. + + ChecksumCRC32 string `xml:",omitempty"` + ChecksumCRC32C string `xml:",omitempty"` + ChecksumSHA1 string `xml:",omitempty"` + ChecksumSHA256 string `xml:",omitempty"` + ChecksumCRC64NVME string `xml:",omitempty"` + ChecksumType string `xml:",omitempty"` } // CopyObjectPartResponse container returns ETag and LastModified of the successfully copied object @@ -769,11 +775,18 @@ func generateListObjectsV2Response(ctx context.Context, bucket, prefix, token, n type metaCheckFn = func(name string, action policy.Action) (s3Err APIErrorCode) -// generates CopyObjectResponse from etag and lastModified time. -func generateCopyObjectResponse(etag string, lastModified time.Time) CopyObjectResponse { +// generates CopyObjectResponse from the committed object information. +func generateCopyObjectResponse(oi ObjectInfo, h http.Header) CopyObjectResponse { + cs, _ := oi.decryptChecksums(0, h) return CopyObjectResponse{ - ETag: "\"" + etag + "\"", - LastModified: amztime.ISO8601Format(lastModified.UTC()), + ETag: "\"" + oi.ETag + "\"", + LastModified: amztime.ISO8601Format(oi.ModTime.UTC()), + ChecksumCRC32: cs[hash.ChecksumCRC32.String()], + ChecksumCRC32C: cs[hash.ChecksumCRC32C.String()], + ChecksumSHA1: cs[hash.ChecksumSHA1.String()], + ChecksumSHA256: cs[hash.ChecksumSHA256.String()], + ChecksumCRC64NVME: cs[hash.ChecksumCRC64NVME.String()], + ChecksumType: cs[xhttp.AmzChecksumType], } } diff --git a/cmd/object-copy-checksum_test.go b/cmd/object-copy-checksum_test.go index 29f153c84f578..ad373f171f84e 100644 --- a/cmd/object-copy-checksum_test.go +++ b/cmd/object-copy-checksum_test.go @@ -22,6 +22,7 @@ import ( "crypto/md5" "encoding/base64" "encoding/hex" + "encoding/xml" "io" "net/http" "net/http/httptest" @@ -117,6 +118,33 @@ func assertCopyChecksum(t *testing.T, obj ObjectLayer, bucket, object string, ty return oi } +func assertCopyChecksumResponse(t *testing.T, rec *httptest.ResponseRecorder, typ hash.ChecksumType, data []byte) { + t.Helper() + var response CopyObjectResponse + if err := xml.Unmarshal(rec.Body.Bytes(), &response); err != nil { + t.Fatalf("unable to decode CopyObjectResult: %v", err) + } + var got string + switch typ.Base() { + case hash.ChecksumCRC32: + got = response.ChecksumCRC32 + case hash.ChecksumCRC32C: + got = response.ChecksumCRC32C + case hash.ChecksumSHA1: + got = response.ChecksumSHA1 + case hash.ChecksumSHA256: + got = response.ChecksumSHA256 + case hash.ChecksumCRC64NVME: + got = response.ChecksumCRC64NVME + } + if want := mustChecksum(t, typ, data); got != want { + t.Fatalf("CopyObjectResult %s checksum %q, want %q: %s", typ.String(), got, want, rec.Body.String()) + } + if response.ChecksumType != xhttp.AmzChecksumTypeFullObject { + t.Fatalf("CopyObjectResult checksum type %q, want %q", response.ChecksumType, xhttp.AmzChecksumTypeFullObject) + } +} + // TestAPICopyObjectServerSideChecksum verifies that server-computed checksums // cover the logical object, never the compressed storage stream. func TestAPICopyObjectServerSideChecksum(t *testing.T) { @@ -187,6 +215,7 @@ func testAPICopyObjectServerSideChecksum(obj ObjectLayer, instanceType, bucketNa if rec.Code != http.StatusOK { t.Fatalf("%s: CopyObject failed: %d %s", instanceType, rec.Code, rec.Body.String()) } + assertCopyChecksumResponse(t, rec, tc.typ, data) info := assertCopyChecksum(t, obj, bucketName, destination, tc.typ, data, tc.compressed, nil) md5sum := md5.Sum(data) @@ -280,6 +309,7 @@ func testAPICopyObjectServerSideChecksumEncryption(obj ObjectLayer, instanceType if rec.Code != http.StatusOK { t.Fatalf("%s: SSE-S3 CopyObject failed: %d %s", instanceType, rec.Code, rec.Body.String()) } + assertCopyChecksumResponse(t, rec, hash.ChecksumCRC32, data) assertCopyChecksum(t, obj, bucketName, destination, hash.ChecksumCRC32, data, variant.compressed, nil) if got := readCopyChecksumObject(t, obj, bucketName, destination, ObjectOptions{}); !bytes.Equal(got, data) { t.Fatalf("%s: SSE-S3 round-trip body differs", instanceType) @@ -296,6 +326,7 @@ func testAPICopyObjectServerSideChecksumEncryption(obj ObjectLayer, instanceType if rec.Code != http.StatusOK { t.Fatalf("%s: SSE-S3 source CopyObject failed: %d %s", instanceType, rec.Code, rec.Body.String()) } + assertCopyChecksumResponse(t, rec, hash.ChecksumCRC32, data) assertCopyChecksum(t, obj, bucketName, destination, hash.ChecksumCRC32, data, true, nil) if got := readCopyChecksumObject(t, obj, bucketName, destination, ObjectOptions{}); !bytes.Equal(got, data) { t.Fatalf("%s: SSE-S3 source round-trip body differs", instanceType) @@ -340,6 +371,7 @@ func testAPICopyObjectServerSideChecksumEncryption(obj ObjectLayer, instanceType if rec.Code != http.StatusOK { t.Fatalf("%s: SSE-C CopyObject failed: %d %s", instanceType, rec.Code, rec.Body.String()) } + assertCopyChecksumResponse(t, rec, hash.ChecksumCRC32, data) assertCopyChecksum(t, obj, bucketName, destination, hash.ChecksumCRC32, data, variant.compressed, decryptHeaders) req, err := newTestSignedRequestV4(http.MethodGet, getGetObjectURL("", bucketName, destination), @@ -390,6 +422,7 @@ func testAPICopyObjectServerSideChecksumSourceVariants(obj ObjectLayer, instance if rec.Code != http.StatusOK { t.Fatalf("%s: CopyObject failed: %d %s", instanceType, rec.Code, rec.Body.String()) } + assertCopyChecksumResponse(t, rec, hash.ChecksumCRC32, data) assertCopyChecksum(t, obj, bucketName, destination, hash.ChecksumCRC32, data, true, nil) rec = copyChecksumRequest(t, apiRouter, credentials, bucketName, source, source, map[string]string{ @@ -399,6 +432,7 @@ func testAPICopyObjectServerSideChecksumSourceVariants(obj ObjectLayer, instance if rec.Code != http.StatusOK { t.Fatalf("%s: in-place CopyObject failed: %d %s", instanceType, rec.Code, rec.Body.String()) } + assertCopyChecksumResponse(t, rec, hash.ChecksumSHA256, data) assertCopyChecksum(t, obj, bucketName, source, hash.ChecksumSHA256, data, true, nil) if got := readCopyChecksumObject(t, obj, bucketName, source, ObjectOptions{}); !bytes.Equal(got, data) { t.Fatalf("%s: in-place CopyObject body differs", instanceType) @@ -415,6 +449,7 @@ func testAPICopyObjectServerSideChecksumSourceVariants(obj ObjectLayer, instance if rec.Code != http.StatusOK { t.Fatalf("%s: CopyObject failed: %d %s", instanceType, rec.Code, rec.Body.String()) } + assertCopyChecksumResponse(t, rec, hash.ChecksumCRC32, data) assertCopyChecksum(t, obj, bucketName, destination, hash.ChecksumCRC32, data, true, nil) }) @@ -447,6 +482,7 @@ func testAPICopyObjectServerSideChecksumSourceVariants(obj ObjectLayer, instance if rec.Code != http.StatusOK { t.Fatalf("%s: CopyObject failed: %d %s", instanceType, rec.Code, rec.Body.String()) } + assertCopyChecksumResponse(t, rec, typ, full) assertCopyChecksum(t, obj, bucketName, destination, typ, full, true, nil) if got := readCopyChecksumObject(t, obj, bucketName, destination, ObjectOptions{}); !bytes.Equal(got, full) { t.Fatalf("%s: multipart source round-trip body differs", instanceType) @@ -475,6 +511,7 @@ func testAPICopyObjectServerSideChecksumSourceVariants(obj ObjectLayer, instance if rec.Code != http.StatusOK { t.Fatalf("%s: CopyObject failed: %d %s", instanceType, rec.Code, rec.Body.String()) } + assertCopyChecksumResponse(t, rec, hash.ChecksumCRC32, boundary.data) assertCopyChecksum(t, obj, bucketName, destination, hash.ChecksumCRC32, boundary.data, boundary.compressed, nil) }) } diff --git a/cmd/object-handlers.go b/cmd/object-handlers.go index 14b3e93f911e4..5ed554e62774b 100644 --- a/cmd/object-handlers.go +++ b/cmd/object-handlers.go @@ -1810,7 +1810,7 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re origETag := objInfo.ETag objInfo.ETag = getDecryptedETag(r.Header, objInfo, false) - response := generateCopyObjectResponse(objInfo.ETag, objInfo.ModTime) + response := generateCopyObjectResponse(objInfo, r.Header) encodedSuccessResponse := encodeResponse(response) if dsc := mustReplicate(ctx, dstBucket, dstObject, objInfo.getMustReplicateOptions(replication.ObjectReplicationType, dstOpts)); dsc.ReplicateAny() {