diff --git a/buildscripts/rebrand-guard/compat-baseline.json b/buildscripts/rebrand-guard/compat-baseline.json index 63859b1fcdc87..afa12e505681c 100644 --- a/buildscripts/rebrand-guard/compat-baseline.json +++ b/buildscripts/rebrand-guard/compat-baseline.json @@ -1811,6 +1811,7 @@ "cmd:cmd:field:CompleteMultipartUploadResponse.ChecksumCRC64NVME", "cmd:cmd:field:CompleteMultipartUploadResponse.ChecksumSHA1", "cmd:cmd:field:CompleteMultipartUploadResponse.ChecksumSHA256", + "cmd:cmd:field:CompleteMultipartUploadResponse.ChecksumType", "cmd:cmd:field:CompleteMultipartUploadResponse.ETag", "cmd:cmd:field:CompleteMultipartUploadResponse.Key", "cmd:cmd:field:CompleteMultipartUploadResponse.Location", diff --git a/cmd/api-response.go b/cmd/api-response.go index cf25fd9803ef4..9ed17245867a3 100644 --- a/cmd/api-response.go +++ b/cmd/api-response.go @@ -416,6 +416,7 @@ type CompleteMultipartUploadResponse struct { Key string ETag string + ChecksumType string `xml:"ChecksumType,omitempty"` ChecksumCRC32 string `xml:"ChecksumCRC32,omitempty"` ChecksumCRC32C string `xml:"ChecksumCRC32C,omitempty"` ChecksumSHA1 string `xml:"ChecksumSHA1,omitempty"` @@ -797,6 +798,7 @@ func generateCompleteMultipartUploadResponse(bucket, key, location string, oi Ob Key: key, // AWS S3 quotes the ETag in XML, make sure we are compatible here. ETag: "\"" + oi.ETag + "\"", + ChecksumType: cs[xhttp.AmzChecksumType], ChecksumSHA1: cs[hash.ChecksumSHA1.String()], ChecksumSHA256: cs[hash.ChecksumSHA256.String()], ChecksumCRC32: cs[hash.ChecksumCRC32.String()], diff --git a/cmd/object-handlers_test.go b/cmd/object-handlers_test.go index a182d9e6fb95d..4d6c14d0ecd7f 100644 --- a/cmd/object-handlers_test.go +++ b/cmd/object-handlers_test.go @@ -43,6 +43,7 @@ import ( "github.com/dustin/go-humanize" "github.com/minio/minio/internal/auth" "github.com/minio/minio/internal/crypto" + minhash "github.com/minio/minio/internal/hash" "github.com/minio/minio/internal/hash/sha256" xhttp "github.com/minio/minio/internal/http" ioutilx "github.com/minio/minio/internal/ioutil" @@ -3386,6 +3387,62 @@ func testAPICompleteMultipartHandler(obj ObjectLayer, instanceType, bucketName s ExecObjectLayerAPINilTest(t, nilBucket, nilObject, instanceType, apiRouter, nilReq) } +// TestGenerateCompleteMultipartUploadResponseChecksumType verifies that +// ChecksumType is populated as FULL_OBJECT/COMPOSITE when the object carries +// a checksum, and omitted from the XML entirely when it doesn't. +func TestGenerateCompleteMultipartUploadResponseChecksumType(t *testing.T) { + bucket, key := "test-bucket", "test-object" + + testCases := []struct { + name string + checksum *minhash.Checksum + wantChecksumType string + }{ + { + name: "no checksum", + checksum: nil, + wantChecksumType: "", + }, + { + name: "full object checksum", + checksum: minhash.NewChecksumFromData(minhash.ChecksumCRC32, []byte("full-object-data")), + wantChecksumType: xhttp.AmzChecksumTypeFullObject, + }, + { + name: "composite multipart checksum", + checksum: func() *minhash.Checksum { + c := minhash.NewChecksumFromData(minhash.ChecksumCRC32C|minhash.ChecksumMultipart, []byte("combined-part-checksums")) + c.WantParts = 2 + return c + }(), + wantChecksumType: xhttp.AmzChecksumTypeComposite, + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + oi := ObjectInfo{ETag: "d41d8cd98f00b204e9800998ecf8427e"} + if tt.checksum != nil { + oi.Checksum = tt.checksum.AppendTo(nil, nil) + } + + resp := generateCompleteMultipartUploadResponse(bucket, key, getGetObjectURL("", bucket, key), oi, nil) + if resp.ChecksumType != tt.wantChecksumType { + t.Fatalf("ChecksumType: got %q, want %q", resp.ChecksumType, tt.wantChecksumType) + } + + encoded, err := xml.Marshal(resp) + if err != nil { + t.Fatalf("failed to marshal response: %v", err) + } + gotTag := strings.Contains(string(encoded), "") + if wantTag := tt.wantChecksumType != ""; gotTag != wantTag { + t.Fatalf("ChecksumType tag presence: got %v, want %v (xml: %s)", gotTag, wantTag, encoded) + } + }) + } +} + // The UploadID from the response body is parsed and its existence is asserted with an attempt to ListParts using it. func TestAPIAbortMultipartHandler(t *testing.T) { defer DetectTestLeak(t)()