Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions buildscripts/rebrand-guard/compat-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
23 changes: 18 additions & 5 deletions cmd/api-response.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import (
"path"
"strconv"
"strings"
"time"

"github.com/minio/minio/internal/amztime"
"github.com/minio/minio/internal/crypto"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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],
}
}

Expand Down
37 changes: 37 additions & 0 deletions cmd/object-copy-checksum_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"crypto/md5"
"encoding/base64"
"encoding/hex"
"encoding/xml"
"io"
"net/http"
"net/http/httptest"
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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{
Expand All @@ -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)
Expand All @@ -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)
})

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
})
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/object-handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down