mirror of
https://github.com/pgsty/minio.git
synced 2026-09-05 18:16:16 +03:00
fix: align multipart completion checksum errors
Return AWS-compatible errors for CompleteMultipartUpload checksum failures without changing the global streaming checksum mapping. Compare explicit multipart checksum types symmetrically, distinguish missing composite part checksums, and preserve the CRC64NVME canonicalization pending a direct AWS probe. Signed-off-by: Feng Ruohang <rh@vonng.com>
This commit is contained in:
@@ -2169,6 +2169,10 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
|
|||||||
err = unwrapAll(err)
|
err = unwrapAll(err)
|
||||||
|
|
||||||
switch err {
|
switch err {
|
||||||
|
case errCompleteMultipartChecksumMismatch, errCompleteMultipartChecksumTypeMismatch:
|
||||||
|
apiErr = ErrBadDigest
|
||||||
|
case errMissingPartChecksum:
|
||||||
|
apiErr = ErrInvalidRequest
|
||||||
case errInvalidArgument:
|
case errInvalidArgument:
|
||||||
apiErr = ErrAdminInvalidArgument
|
apiErr = ErrAdminInvalidArgument
|
||||||
case errNoSuchPolicy:
|
case errNoSuchPolicy:
|
||||||
@@ -2465,6 +2469,14 @@ func toAPIError(ctx context.Context, err error) APIError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
apiErr := errorCodes.ToAPIErr(toAPIErrorCode(ctx, err))
|
apiErr := errorCodes.ToAPIErr(toAPIErrorCode(ctx, err))
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, errCompleteMultipartChecksumMismatch):
|
||||||
|
apiErr.Description = strings.TrimPrefix(err.Error(), errCompleteMultipartChecksumMismatch.Error()+": ")
|
||||||
|
case errors.Is(err, errCompleteMultipartChecksumTypeMismatch):
|
||||||
|
apiErr.Description = strings.TrimPrefix(err.Error(), errCompleteMultipartChecksumTypeMismatch.Error()+": ")
|
||||||
|
case errors.Is(err, errMissingPartChecksum):
|
||||||
|
apiErr.Description = strings.TrimPrefix(err.Error(), errMissingPartChecksum.Error()+": ")
|
||||||
|
}
|
||||||
switch apiErr.Code {
|
switch apiErr.Code {
|
||||||
case "NotImplemented":
|
case "NotImplemented":
|
||||||
apiErr = APIError{
|
apiErr = APIError{
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ var toAPIErrorTests = []struct {
|
|||||||
{err: ObjectNameInvalid{}, errCode: ErrInvalidObjectName},
|
{err: ObjectNameInvalid{}, errCode: ErrInvalidObjectName},
|
||||||
{err: InvalidUploadID{}, errCode: ErrNoSuchUpload},
|
{err: InvalidUploadID{}, errCode: ErrNoSuchUpload},
|
||||||
{err: InvalidPart{}, errCode: ErrInvalidPart},
|
{err: InvalidPart{}, errCode: ErrInvalidPart},
|
||||||
|
{err: errCompleteMultipartChecksumMismatch, errCode: ErrBadDigest},
|
||||||
|
{err: errCompleteMultipartChecksumTypeMismatch, errCode: ErrBadDigest},
|
||||||
|
{err: errMissingPartChecksum, errCode: ErrInvalidRequest},
|
||||||
|
{err: hash.ChecksumMismatch{}, errCode: ErrContentChecksumMismatch},
|
||||||
{err: InsufficientReadQuorum{}, errCode: ErrSlowDownRead},
|
{err: InsufficientReadQuorum{}, errCode: ErrSlowDownRead},
|
||||||
{err: InsufficientWriteQuorum{}, errCode: ErrSlowDownWrite},
|
{err: InsufficientWriteQuorum{}, errCode: ErrSlowDownWrite},
|
||||||
{err: InvalidUploadIDKeyCombination{}, errCode: ErrNotImplemented},
|
{err: InvalidUploadIDKeyCombination{}, errCode: ErrNotImplemented},
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/dustin/go-humanize"
|
"github.com/dustin/go-humanize"
|
||||||
@@ -139,13 +140,18 @@ func completeMultipartUploadHTTP(t *testing.T, apiRouter http.Handler, creds aut
|
|||||||
return rec
|
return rec
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiErrorCode(t *testing.T, rec *httptest.ResponseRecorder) string {
|
func apiError(t *testing.T, rec *httptest.ResponseRecorder) APIErrorResponse {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
var e APIErrorResponse
|
var e APIErrorResponse
|
||||||
if err := xml.Unmarshal(rec.Body.Bytes(), &e); err != nil {
|
if err := xml.Unmarshal(rec.Body.Bytes(), &e); err != nil {
|
||||||
t.Fatalf("unable to decode error response %q: %v", rec.Body.String(), err)
|
t.Fatalf("unable to decode error response %q: %v", rec.Body.String(), err)
|
||||||
}
|
}
|
||||||
return e.Code
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func apiErrorCode(t *testing.T, rec *httptest.ResponseRecorder) string {
|
||||||
|
t.Helper()
|
||||||
|
return apiError(t, rec).Code
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestAPICompleteMultipartFullObjectChecksum covers pgsty/silo#31.
|
// TestAPICompleteMultipartFullObjectChecksum covers pgsty/silo#31.
|
||||||
@@ -243,12 +249,12 @@ func testAPICompleteMultipartFullObjectChecksumMismatch(obj ObjectLayer, instanc
|
|||||||
t.Fatalf("%s: CompleteMultipartUpload with a bad full object checksum returned %d, want 400",
|
t.Fatalf("%s: CompleteMultipartUpload with a bad full object checksum returned %d, want 400",
|
||||||
instanceType, rec.Code)
|
instanceType, rec.Code)
|
||||||
}
|
}
|
||||||
// NOTE: AWS S3 documents BadDigest for a full object checksum mismatch on
|
apiErr := apiError(t, rec)
|
||||||
// CompleteMultipartUpload. MinIO reports XAmzContentChecksumMismatch. That
|
if apiErr.Code != "BadDigest" {
|
||||||
// deviation is tracked separately; assert the current code so a future
|
t.Fatalf("%s: expected BadDigest, got %q", instanceType, apiErr.Code)
|
||||||
// change to it is a deliberate one.
|
}
|
||||||
if got := apiErrorCode(t, rec); got != "XAmzContentChecksumMismatch" {
|
if want := "The CRC32 checksum you specified did not match the calculated checksum."; apiErr.Message != want {
|
||||||
t.Fatalf("%s: expected XAmzContentChecksumMismatch, got %q", instanceType, got)
|
t.Fatalf("%s: expected message %q, got %q", instanceType, want, apiErr.Message)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := obj.GetObjectInfo(t.Context(), bucketName, objectName, ObjectOptions{}); err == nil {
|
if _, err := obj.GetObjectInfo(t.Context(), bucketName, objectName, ObjectOptions{}); err == nil {
|
||||||
@@ -288,8 +294,33 @@ func testAPICompleteMultipartCompositeStillRequiresPartChecksums(obj ObjectLayer
|
|||||||
t.Fatalf("%s/%s: composite CompleteMultipartUpload without part checksums returned %d, want 400",
|
t.Fatalf("%s/%s: composite CompleteMultipartUpload without part checksums returned %d, want 400",
|
||||||
instanceType, typ.String(), rec.Code)
|
instanceType, typ.String(), rec.Code)
|
||||||
}
|
}
|
||||||
if got := apiErrorCode(t, rec); got != "InvalidPart" {
|
apiErr := apiError(t, rec)
|
||||||
t.Fatalf("%s/%s: expected InvalidPart, got %q", instanceType, typ.String(), got)
|
if apiErr.Code != "InvalidRequest" {
|
||||||
|
t.Fatalf("%s/%s: expected InvalidRequest, got %q", instanceType, typ.String(), apiErr.Code)
|
||||||
|
}
|
||||||
|
wantMessage := fmt.Sprintf("The upload was created using a %s checksum. The complete request must include the checksum for each part. It was missing for part 1 in the request.", strings.ToLower(typ.String()))
|
||||||
|
if apiErr.Message != wantMessage {
|
||||||
|
t.Fatalf("%s/%s: expected message %q, got %q", instanceType, typ.String(), wantMessage, apiErr.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A retry that supplies part 1 but omits part 2 must name the actual
|
||||||
|
// missing part, not merely the first part in the upload.
|
||||||
|
completedParts := []CompletePart{
|
||||||
|
completePartWithChecksum(typ, 1, etags[0], mustChecksum(t, typ, partData[0])),
|
||||||
|
{PartNumber: 2, ETag: etags[1]},
|
||||||
|
}
|
||||||
|
rec = completePartsHTTP(t, apiRouter, credentials, bucketName, objectName, uploadID, completedParts, nil)
|
||||||
|
if rec.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("%s/%s: composite completion missing part 2 checksum returned %d, want 400",
|
||||||
|
instanceType, typ.String(), rec.Code)
|
||||||
|
}
|
||||||
|
apiErr = apiError(t, rec)
|
||||||
|
if apiErr.Code != "InvalidRequest" {
|
||||||
|
t.Fatalf("%s/%s: expected InvalidRequest, got %q", instanceType, typ.String(), apiErr.Code)
|
||||||
|
}
|
||||||
|
wantMessage = fmt.Sprintf("The upload was created using a %s checksum. The complete request must include the checksum for each part. It was missing for part 2 in the request.", strings.ToLower(typ.String()))
|
||||||
|
if apiErr.Message != wantMessage {
|
||||||
|
t.Fatalf("%s/%s: expected message %q, got %q", instanceType, typ.String(), wantMessage, apiErr.Message)
|
||||||
}
|
}
|
||||||
if _, err := obj.GetObjectInfo(t.Context(), bucketName, objectName, ObjectOptions{}); err == nil {
|
if _, err := obj.GetObjectInfo(t.Context(), bucketName, objectName, ObjectOptions{}); err == nil {
|
||||||
t.Fatalf("%s/%s: object was created despite a rejected completion", instanceType, typ.String())
|
t.Fatalf("%s/%s: object was created despite a rejected completion", instanceType, typ.String())
|
||||||
@@ -297,6 +328,145 @@ func testAPICompleteMultipartCompositeStillRequiresPartChecksums(obj ObjectLayer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestAPICompleteMultipartCompositeChecksumMismatch covers the composite
|
||||||
|
// object-checksum path independently from full object checksum merging.
|
||||||
|
func TestAPICompleteMultipartCompositeChecksumMismatch(t *testing.T) {
|
||||||
|
defer DetectTestLeak(t)()
|
||||||
|
ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{
|
||||||
|
t: t,
|
||||||
|
objAPITest: testAPICompleteMultipartCompositeChecksumMismatch,
|
||||||
|
endpoints: []string{"NewMultipart", "PutObjectPart", "CompleteMultipart"},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAPICompleteMultipartCompositeChecksumMismatch(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||||
|
credentials auth.Credentials, t *testing.T,
|
||||||
|
) {
|
||||||
|
typ := hash.ChecksumCRC32
|
||||||
|
partData, _ := multipartChecksumTestData()
|
||||||
|
objectName := "uploads/composite-object-mismatch"
|
||||||
|
uploadID := newMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName,
|
||||||
|
typ.String(), xhttp.AmzChecksumTypeComposite)
|
||||||
|
etags := uploadPartsHTTP(t, apiRouter, credentials, bucketName, objectName, uploadID, typ, partData)
|
||||||
|
partCS := []string{mustChecksum(t, typ, partData[0]), mustChecksum(t, typ, partData[1])}
|
||||||
|
rec := completeMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName, uploadID, etags, partCS,
|
||||||
|
map[string]string{
|
||||||
|
typ.Key(): mustChecksum(t, typ, []byte("wrong composite checksum")) + "-2",
|
||||||
|
xhttp.AmzChecksumType: xhttp.AmzChecksumTypeComposite,
|
||||||
|
})
|
||||||
|
if rec.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("%s: composite checksum mismatch returned %d, want 400", instanceType, rec.Code)
|
||||||
|
}
|
||||||
|
apiErr := apiError(t, rec)
|
||||||
|
if apiErr.Code != "BadDigest" {
|
||||||
|
t.Fatalf("%s: expected BadDigest, got %q", instanceType, apiErr.Code)
|
||||||
|
}
|
||||||
|
if want := "The CRC32 checksum you specified did not match the calculated checksum."; apiErr.Message != want {
|
||||||
|
t.Fatalf("%s: expected message %q, got %q", instanceType, want, apiErr.Message)
|
||||||
|
}
|
||||||
|
if _, err := obj.GetObjectInfo(t.Context(), bucketName, objectName, ObjectOptions{}); err == nil {
|
||||||
|
t.Fatalf("%s: object was created despite a failed composite checksum validation", instanceType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAPICompleteMultipartChecksumTypeMismatch verifies the type comparison in
|
||||||
|
// both directions. ChecksumType is a bitmask, so a containment check alone
|
||||||
|
// incorrectly accepts COMPOSITE uploads completed as FULL_OBJECT.
|
||||||
|
func TestAPICompleteMultipartChecksumTypeMismatch(t *testing.T) {
|
||||||
|
defer DetectTestLeak(t)()
|
||||||
|
ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{
|
||||||
|
t: t,
|
||||||
|
objAPITest: testAPICompleteMultipartChecksumTypeMismatch,
|
||||||
|
endpoints: []string{"NewMultipart", "PutObjectPart", "CompleteMultipart"},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAPICompleteMultipartChecksumTypeMismatch(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||||
|
credentials auth.Credentials, t *testing.T,
|
||||||
|
) {
|
||||||
|
typ := hash.ChecksumCRC32
|
||||||
|
partData, full := multipartChecksumTestData()
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
createdType string
|
||||||
|
providedType string
|
||||||
|
}{
|
||||||
|
{name: "full-to-composite", createdType: xhttp.AmzChecksumTypeFullObject, providedType: xhttp.AmzChecksumTypeComposite},
|
||||||
|
{name: "composite-to-full", createdType: xhttp.AmzChecksumTypeComposite, providedType: xhttp.AmzChecksumTypeFullObject},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
objectName := "type-mismatch/" + test.name
|
||||||
|
uploadID := newMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName,
|
||||||
|
typ.String(), test.createdType)
|
||||||
|
etags := uploadPartsHTTP(t, apiRouter, credentials, bucketName, objectName, uploadID, typ, partData)
|
||||||
|
partCS := []string{mustChecksum(t, typ, partData[0]), mustChecksum(t, typ, partData[1])}
|
||||||
|
rec := completeMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName, uploadID, etags, partCS,
|
||||||
|
map[string]string{
|
||||||
|
typ.Key(): mustChecksum(t, typ, full),
|
||||||
|
xhttp.AmzChecksumType: test.providedType,
|
||||||
|
})
|
||||||
|
if rec.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("%s: checksum type mismatch returned %d, want 400", instanceType, rec.Code)
|
||||||
|
}
|
||||||
|
apiErr := apiError(t, rec)
|
||||||
|
if apiErr.Code != "BadDigest" {
|
||||||
|
t.Fatalf("%s: expected BadDigest, got %q", instanceType, apiErr.Code)
|
||||||
|
}
|
||||||
|
wantMessage := fmt.Sprintf("The checksum type %s does not match the multipart upload checksum type %s.", test.providedType, test.createdType)
|
||||||
|
if apiErr.Message != wantMessage {
|
||||||
|
t.Fatalf("%s: expected message %q, got %q", instanceType, wantMessage, apiErr.Message)
|
||||||
|
}
|
||||||
|
if _, err := obj.GetObjectInfo(t.Context(), bucketName, objectName, ObjectOptions{}); err == nil {
|
||||||
|
t.Fatalf("%s: object was created despite a rejected checksum type", instanceType)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("omitted-type-is-not-composite", func(t *testing.T) {
|
||||||
|
objectName := "type-mismatch/omitted-type"
|
||||||
|
uploadID := newMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName,
|
||||||
|
typ.String(), xhttp.AmzChecksumTypeFullObject)
|
||||||
|
etags := uploadPartsHTTP(t, apiRouter, credentials, bucketName, objectName, uploadID, typ, partData)
|
||||||
|
rec := completeMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName, uploadID, etags, nil,
|
||||||
|
map[string]string{typ.Key(): mustChecksum(t, typ, full)})
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("%s: completion without an explicit checksum type returned %d %s",
|
||||||
|
instanceType, rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("algorithm-mismatch-remains-invalid-argument", func(t *testing.T) {
|
||||||
|
objectName := "type-mismatch/algorithm"
|
||||||
|
uploadID := newMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName,
|
||||||
|
typ.String(), xhttp.AmzChecksumTypeFullObject)
|
||||||
|
etags := uploadPartsHTTP(t, apiRouter, credentials, bucketName, objectName, uploadID, typ, partData)
|
||||||
|
rec := completeMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName, uploadID, etags, nil,
|
||||||
|
map[string]string{
|
||||||
|
hash.ChecksumCRC32C.Key(): mustChecksum(t, hash.ChecksumCRC32C, full),
|
||||||
|
xhttp.AmzChecksumType: xhttp.AmzChecksumTypeFullObject,
|
||||||
|
})
|
||||||
|
if rec.Code != http.StatusBadRequest || apiErrorCode(t, rec) != "InvalidArgument" {
|
||||||
|
t.Fatalf("%s: algorithm mismatch returned %d %s", instanceType, rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("crc64nvme-composite-remains-canonicalized", func(t *testing.T) {
|
||||||
|
crc64Type := hash.ChecksumCRC64NVME
|
||||||
|
objectName := "type-mismatch/crc64nvme-composite"
|
||||||
|
uploadID := newMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName,
|
||||||
|
crc64Type.String(), xhttp.AmzChecksumTypeComposite)
|
||||||
|
etags := uploadPartsHTTP(t, apiRouter, credentials, bucketName, objectName, uploadID, crc64Type, partData)
|
||||||
|
rec := completeMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName, uploadID, etags, nil,
|
||||||
|
map[string]string{
|
||||||
|
crc64Type.Key(): mustChecksum(t, crc64Type, full),
|
||||||
|
xhttp.AmzChecksumType: xhttp.AmzChecksumTypeComposite,
|
||||||
|
})
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("%s: CRC64NVME canonicalization changed: %d %s", instanceType, rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// TestAPICompleteMultipartFullObjectVariants pins down the surrounding
|
// TestAPICompleteMultipartFullObjectVariants pins down the surrounding
|
||||||
// behavior of the relaxation: what may be omitted, what must still match, and
|
// behavior of the relaxation: what may be omitted, what must still match, and
|
||||||
// that a zero length object is handled like any other.
|
// that a zero length object is handled like any other.
|
||||||
|
|||||||
+26
-21
@@ -1173,11 +1173,18 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
|
|||||||
var checksumType hash.ChecksumType
|
var checksumType hash.ChecksumType
|
||||||
if cs := fi.Metadata[hash.MinIOMultipartChecksum]; cs != "" {
|
if cs := fi.Metadata[hash.MinIOMultipartChecksum]; cs != "" {
|
||||||
checksumType = hash.NewChecksumType(cs, fi.Metadata[hash.MinIOMultipartChecksumType])
|
checksumType = hash.NewChecksumType(cs, fi.Metadata[hash.MinIOMultipartChecksumType])
|
||||||
if opts.WantChecksum != nil && !opts.WantChecksum.Type.Is(checksumType) {
|
if opts.WantChecksum != nil {
|
||||||
return oi, InvalidArgument{
|
providedType := opts.WantChecksum.Type | hash.ChecksumMultipart | hash.ChecksumIncludesMultipart
|
||||||
Bucket: bucket,
|
expectedType := checksumType | hash.ChecksumMultipart | hash.ChecksumIncludesMultipart
|
||||||
Object: fi.Name,
|
if providedType.Base() != expectedType.Base() {
|
||||||
Err: fmt.Errorf("checksum type mismatch. got %q (%s) expected %q (%s)", checksumType.String(), checksumType.ObjType(), opts.WantChecksum.Type.String(), opts.WantChecksum.Type.ObjType()),
|
return oi, InvalidArgument{
|
||||||
|
Bucket: bucket,
|
||||||
|
Object: fi.Name,
|
||||||
|
Err: fmt.Errorf("checksum algorithm mismatch. got %q expected %q", providedType.String(), expectedType.String()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if opts.wantChecksumTypeSet && providedType.ObjType() != expectedType.ObjType() {
|
||||||
|
return oi, completeMultipartChecksumTypeMismatch(providedType.ObjType(), expectedType.ObjType())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
checksumType |= hash.ChecksumMultipart | hash.ChecksumIncludesMultipart
|
checksumType |= hash.ChecksumMultipart | hash.ChecksumIncludesMultipart
|
||||||
@@ -1308,19 +1315,18 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Part checksums are optional in the CompleteMultipartUpload body when
|
// Full object completions may omit part checksums. Composite
|
||||||
// the upload was created with a full object checksum type: clients send
|
// completions may not. Any checksum that is supplied is still
|
||||||
// the object level checksum instead and do not retain part checksums.
|
// validated, including one sent under the wrong algorithm.
|
||||||
// A part that carries any checksum at all is still validated against
|
if !suppliedAnyCS {
|
||||||
// what we stored - including one sent under the wrong algorithm, which
|
if !checksumType.FullObjectRequested() {
|
||||||
// cannot match and is rejected. The object level checksum, if supplied,
|
return oi, missingPartChecksum(checksumType.String(), part.PartNumber)
|
||||||
// is verified against the merged part checksums below.
|
}
|
||||||
allowMissingPartCS := checksumType.FullObjectRequested() && !suppliedAnyCS
|
} else if gotCS != crc {
|
||||||
if !allowMissingPartCS && gotCS != crc {
|
|
||||||
return oi, InvalidPart{
|
return oi, InvalidPart{
|
||||||
PartNumber: part.PartNumber,
|
PartNumber: part.PartNumber,
|
||||||
ExpETag: gotCS,
|
ExpETag: crc,
|
||||||
GotETag: crc,
|
GotETag: gotCS,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cs := hash.NewChecksumString(checksumType.String(), crc)
|
cs := hash.NewChecksumString(checksumType.String(), crc)
|
||||||
@@ -1370,15 +1376,14 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
|
|||||||
if opts.WantChecksum != nil {
|
if opts.WantChecksum != nil {
|
||||||
if checksumType.FullObjectRequested() {
|
if checksumType.FullObjectRequested() {
|
||||||
if opts.WantChecksum.Encoded != checksum.Encoded {
|
if opts.WantChecksum.Encoded != checksum.Encoded {
|
||||||
err := hash.ChecksumMismatch{
|
return oi, completeMultipartChecksumMismatch(checksumType.String())
|
||||||
Want: opts.WantChecksum.Encoded,
|
|
||||||
Got: checksum.Encoded,
|
|
||||||
}
|
|
||||||
return oi, err
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
err := opts.WantChecksum.Matches(checksumCombined, len(parts))
|
err := opts.WantChecksum.Matches(checksumCombined, len(parts))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if hash.IsChecksumMismatch(err) {
|
||||||
|
return oi, completeMultipartChecksumMismatch(checksumType.String())
|
||||||
|
}
|
||||||
return oi, err
|
return oi, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Converts underlying storage error. Convenience function written to
|
// Converts underlying storage error. Convenience function written to
|
||||||
@@ -653,6 +654,41 @@ func (e InvalidPart) Error() string {
|
|||||||
e.PartNumber, e.ExpETag, e.GotETag)
|
e.PartNumber, e.ExpETag, e.GotETag)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
errCompleteMultipartChecksumMismatch = errors.New("complete multipart checksum mismatch")
|
||||||
|
errCompleteMultipartChecksumTypeMismatch = errors.New("complete multipart checksum type mismatch")
|
||||||
|
errMissingPartChecksum = errors.New("missing multipart part checksum")
|
||||||
|
)
|
||||||
|
|
||||||
|
// completeMultipartChecksumMismatch reports an object checksum mismatch
|
||||||
|
// detected while completing a multipart upload. It is distinct from
|
||||||
|
// hash.ChecksumMismatch because AWS maps completion failures to BadDigest,
|
||||||
|
// while streaming PutObject and UploadPart failures keep using
|
||||||
|
// XAmzContentChecksumMismatch.
|
||||||
|
func completeMultipartChecksumMismatch(algorithm string) error {
|
||||||
|
description := "The checksum you specified did not match the calculated checksum."
|
||||||
|
if algorithm != "" {
|
||||||
|
description = fmt.Sprintf("The %s checksum you specified did not match the calculated checksum.", algorithm)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("%w: %s", errCompleteMultipartChecksumMismatch, description)
|
||||||
|
}
|
||||||
|
|
||||||
|
// completeMultipartChecksumTypeMismatch reports an explicit checksum type that
|
||||||
|
// differs from the type selected when the multipart upload was initiated.
|
||||||
|
func completeMultipartChecksumTypeMismatch(providedType, expectedType string) error {
|
||||||
|
description := fmt.Sprintf("The checksum type %s does not match the multipart upload checksum type %s.",
|
||||||
|
providedType, expectedType)
|
||||||
|
return fmt.Errorf("%w: %s", errCompleteMultipartChecksumTypeMismatch, description)
|
||||||
|
}
|
||||||
|
|
||||||
|
// missingPartChecksum reports a part whose checksum is absent from a
|
||||||
|
// composite CompleteMultipartUpload request.
|
||||||
|
func missingPartChecksum(algorithm string, partNumber int) error {
|
||||||
|
description := fmt.Sprintf("The upload was created using a %s checksum. The complete request must include the checksum for each part. It was missing for part %d in the request.",
|
||||||
|
strings.ToLower(algorithm), partNumber)
|
||||||
|
return fmt.Errorf("%w: %s", errMissingPartChecksum, description)
|
||||||
|
}
|
||||||
|
|
||||||
// PartTooSmall - error if part size is less than 5MB.
|
// PartTooSmall - error if part size is less than 5MB.
|
||||||
type PartTooSmall struct {
|
type PartTooSmall struct {
|
||||||
PartSize int64
|
PartSize int64
|
||||||
|
|||||||
@@ -84,7 +84,8 @@ type ObjectOptions struct {
|
|||||||
Expiration ExpirationOptions
|
Expiration ExpirationOptions
|
||||||
LifecycleAuditEvent lcAuditEvent
|
LifecycleAuditEvent lcAuditEvent
|
||||||
|
|
||||||
WantChecksum *hash.Checksum // x-amz-checksum-XXX checksum sent to PutObject/ CompleteMultipartUpload.
|
WantChecksum *hash.Checksum // x-amz-checksum-XXX checksum sent to PutObject/ CompleteMultipartUpload.
|
||||||
|
wantChecksumTypeSet bool // x-amz-checksum-type was explicitly set on CompleteMultipartUpload.
|
||||||
|
|
||||||
WantServerSideChecksumType hash.ChecksumType // if set, we compute a server-side checksum of this type
|
WantServerSideChecksumType hash.ChecksumType // if set, we compute a server-side checksum of this type
|
||||||
|
|
||||||
|
|||||||
@@ -473,6 +473,7 @@ func completeMultipartOpts(ctx context.Context, r *http.Request, bucket, object
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return opts, err
|
return opts, err
|
||||||
}
|
}
|
||||||
|
opts.wantChecksumTypeSet = r.Header.Get(xhttp.AmzChecksumType) != ""
|
||||||
opts.MTime = mtime
|
opts.MTime = mtime
|
||||||
opts.UserDefined = make(map[string]string)
|
opts.UserDefined = make(map[string]string)
|
||||||
// Transfer SSEC key in opts.EncryptFn
|
// Transfer SSEC key in opts.EncryptFn
|
||||||
|
|||||||
Reference in New Issue
Block a user