fix: return checksums from CopyObject

Signed-off-by: Feng Ruohang <rh@vonng.com>
This commit is contained in:
Feng Ruohang
2026-08-24 20:37:36 +08:00
parent 05df6e70d7
commit f2520f3346
4 changed files with 62 additions and 6 deletions
@@ -1832,6 +1832,12 @@
"cmd:cmd:field:CopyObjectPartResponse.ETag", "cmd:cmd:field:CopyObjectPartResponse.ETag",
"cmd:cmd:field:CopyObjectPartResponse.LastModified", "cmd:cmd:field:CopyObjectPartResponse.LastModified",
"cmd:cmd:field:CopyObjectPartResponse.XMLName", "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.ETag",
"cmd:cmd:field:CopyObjectResponse.LastModified", "cmd:cmd:field:CopyObjectResponse.LastModified",
"cmd:cmd:field:CopyObjectResponse.XMLName", "cmd:cmd:field:CopyObjectResponse.XMLName",
+18 -5
View File
@@ -27,7 +27,6 @@ import (
"path" "path"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/minio/minio/internal/amztime" "github.com/minio/minio/internal/amztime"
"github.com/minio/minio/internal/crypto" "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:"-"` 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" LastModified string // time string of format "2006-01-02T15:04:05.000Z"
ETag string // md5sum of the copied object. 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 // 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) type metaCheckFn = func(name string, action policy.Action) (s3Err APIErrorCode)
// generates CopyObjectResponse from etag and lastModified time. // generates CopyObjectResponse from the committed object information.
func generateCopyObjectResponse(etag string, lastModified time.Time) CopyObjectResponse { func generateCopyObjectResponse(oi ObjectInfo, h http.Header) CopyObjectResponse {
cs, _ := oi.decryptChecksums(0, h)
return CopyObjectResponse{ return CopyObjectResponse{
ETag: "\"" + etag + "\"", ETag: "\"" + oi.ETag + "\"",
LastModified: amztime.ISO8601Format(lastModified.UTC()), 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],
} }
} }
+37
View File
@@ -22,6 +22,7 @@ import (
"crypto/md5" "crypto/md5"
"encoding/base64" "encoding/base64"
"encoding/hex" "encoding/hex"
"encoding/xml"
"io" "io"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
@@ -117,6 +118,33 @@ func assertCopyChecksum(t *testing.T, obj ObjectLayer, bucket, object string, ty
return oi 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 // TestAPICopyObjectServerSideChecksum verifies that server-computed checksums
// cover the logical object, never the compressed storage stream. // cover the logical object, never the compressed storage stream.
func TestAPICopyObjectServerSideChecksum(t *testing.T) { func TestAPICopyObjectServerSideChecksum(t *testing.T) {
@@ -187,6 +215,7 @@ func testAPICopyObjectServerSideChecksum(obj ObjectLayer, instanceType, bucketNa
if rec.Code != http.StatusOK { if rec.Code != http.StatusOK {
t.Fatalf("%s: CopyObject failed: %d %s", instanceType, rec.Code, rec.Body.String()) 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) info := assertCopyChecksum(t, obj, bucketName, destination, tc.typ, data, tc.compressed, nil)
md5sum := md5.Sum(data) md5sum := md5.Sum(data)
@@ -280,6 +309,7 @@ func testAPICopyObjectServerSideChecksumEncryption(obj ObjectLayer, instanceType
if rec.Code != http.StatusOK { if rec.Code != http.StatusOK {
t.Fatalf("%s: SSE-S3 CopyObject failed: %d %s", instanceType, rec.Code, rec.Body.String()) 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) assertCopyChecksum(t, obj, bucketName, destination, hash.ChecksumCRC32, data, variant.compressed, nil)
if got := readCopyChecksumObject(t, obj, bucketName, destination, ObjectOptions{}); !bytes.Equal(got, data) { if got := readCopyChecksumObject(t, obj, bucketName, destination, ObjectOptions{}); !bytes.Equal(got, data) {
t.Fatalf("%s: SSE-S3 round-trip body differs", instanceType) t.Fatalf("%s: SSE-S3 round-trip body differs", instanceType)
@@ -296,6 +326,7 @@ func testAPICopyObjectServerSideChecksumEncryption(obj ObjectLayer, instanceType
if rec.Code != http.StatusOK { if rec.Code != http.StatusOK {
t.Fatalf("%s: SSE-S3 source CopyObject failed: %d %s", instanceType, rec.Code, rec.Body.String()) 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) assertCopyChecksum(t, obj, bucketName, destination, hash.ChecksumCRC32, data, true, nil)
if got := readCopyChecksumObject(t, obj, bucketName, destination, ObjectOptions{}); !bytes.Equal(got, data) { if got := readCopyChecksumObject(t, obj, bucketName, destination, ObjectOptions{}); !bytes.Equal(got, data) {
t.Fatalf("%s: SSE-S3 source round-trip body differs", instanceType) 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 { if rec.Code != http.StatusOK {
t.Fatalf("%s: SSE-C CopyObject failed: %d %s", instanceType, rec.Code, rec.Body.String()) 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) assertCopyChecksum(t, obj, bucketName, destination, hash.ChecksumCRC32, data, variant.compressed, decryptHeaders)
req, err := newTestSignedRequestV4(http.MethodGet, getGetObjectURL("", bucketName, destination), req, err := newTestSignedRequestV4(http.MethodGet, getGetObjectURL("", bucketName, destination),
@@ -390,6 +422,7 @@ func testAPICopyObjectServerSideChecksumSourceVariants(obj ObjectLayer, instance
if rec.Code != http.StatusOK { if rec.Code != http.StatusOK {
t.Fatalf("%s: CopyObject failed: %d %s", instanceType, rec.Code, rec.Body.String()) 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) assertCopyChecksum(t, obj, bucketName, destination, hash.ChecksumCRC32, data, true, nil)
rec = copyChecksumRequest(t, apiRouter, credentials, bucketName, source, source, map[string]string{ 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 { if rec.Code != http.StatusOK {
t.Fatalf("%s: in-place CopyObject failed: %d %s", instanceType, rec.Code, rec.Body.String()) 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) assertCopyChecksum(t, obj, bucketName, source, hash.ChecksumSHA256, data, true, nil)
if got := readCopyChecksumObject(t, obj, bucketName, source, ObjectOptions{}); !bytes.Equal(got, data) { if got := readCopyChecksumObject(t, obj, bucketName, source, ObjectOptions{}); !bytes.Equal(got, data) {
t.Fatalf("%s: in-place CopyObject body differs", instanceType) t.Fatalf("%s: in-place CopyObject body differs", instanceType)
@@ -415,6 +449,7 @@ func testAPICopyObjectServerSideChecksumSourceVariants(obj ObjectLayer, instance
if rec.Code != http.StatusOK { if rec.Code != http.StatusOK {
t.Fatalf("%s: CopyObject failed: %d %s", instanceType, rec.Code, rec.Body.String()) 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) assertCopyChecksum(t, obj, bucketName, destination, hash.ChecksumCRC32, data, true, nil)
}) })
@@ -447,6 +482,7 @@ func testAPICopyObjectServerSideChecksumSourceVariants(obj ObjectLayer, instance
if rec.Code != http.StatusOK { if rec.Code != http.StatusOK {
t.Fatalf("%s: CopyObject failed: %d %s", instanceType, rec.Code, rec.Body.String()) 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) assertCopyChecksum(t, obj, bucketName, destination, typ, full, true, nil)
if got := readCopyChecksumObject(t, obj, bucketName, destination, ObjectOptions{}); !bytes.Equal(got, full) { if got := readCopyChecksumObject(t, obj, bucketName, destination, ObjectOptions{}); !bytes.Equal(got, full) {
t.Fatalf("%s: multipart source round-trip body differs", instanceType) t.Fatalf("%s: multipart source round-trip body differs", instanceType)
@@ -475,6 +511,7 @@ func testAPICopyObjectServerSideChecksumSourceVariants(obj ObjectLayer, instance
if rec.Code != http.StatusOK { if rec.Code != http.StatusOK {
t.Fatalf("%s: CopyObject failed: %d %s", instanceType, rec.Code, rec.Body.String()) 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) assertCopyChecksum(t, obj, bucketName, destination, hash.ChecksumCRC32, boundary.data, boundary.compressed, nil)
}) })
} }
+1 -1
View File
@@ -1810,7 +1810,7 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
origETag := objInfo.ETag origETag := objInfo.ETag
objInfo.ETag = getDecryptedETag(r.Header, objInfo, false) objInfo.ETag = getDecryptedETag(r.Header, objInfo, false)
response := generateCopyObjectResponse(objInfo.ETag, objInfo.ModTime) response := generateCopyObjectResponse(objInfo, r.Header)
encodedSuccessResponse := encodeResponse(response) encodedSuccessResponse := encodeResponse(response)
if dsc := mustReplicate(ctx, dstBucket, dstObject, objInfo.getMustReplicateOptions(replication.ObjectReplicationType, dstOpts)); dsc.ReplicateAny() { if dsc := mustReplicate(ctx, dstBucket, dstObject, objInfo.getMustReplicateOptions(replication.ObjectReplicationType, dstOpts)); dsc.ReplicateAny() {