diff --git a/cmd/gateway/s3/gateway-s3.go b/cmd/gateway/s3/gateway-s3.go index 97417f57ee2b5..94793ee65b4db 100644 --- a/cmd/gateway/s3/gateway-s3.go +++ b/cmd/gateway/s3/gateway-s3.go @@ -17,7 +17,10 @@ package s3 import ( + "bytes" "context" + "crypto/md5" + "encoding/base64" "encoding/json" "io" "math/rand" @@ -520,7 +523,25 @@ func (l *s3Objects) PutObject(ctx context.Context, bucket string, object string, // we can set md5sum to be calculated always. SendContentMd5: true, } - ui, err := l.Client.PutObject(ctx, bucket, object, data, data.Size(), data.MD5Base64String(), data.SHA256HexString(), putOpts) + + // Core.PutObject passes md5Base64 directly to AWS without computing it. + // When the caller uses streaming SigV4 (no Content-MD5 header, e.g. Java + // AWS SDK / OpenSearch), MD5Base64String() returns "". Object Lock buckets + // require Content-MD5, so buffer the data and compute MD5 when missing. + md5Base64 := data.MD5Base64String() + sha256Hex := data.SHA256HexString() + var reader io.Reader = data + if md5Base64 == "" { + buf, err := io.ReadAll(data) + if err != nil { + return objInfo, minio.ErrorRespToObjectError(err, bucket, object) + } + h := md5.New() + h.Write(buf) + md5Base64 = base64.StdEncoding.EncodeToString(h.Sum(nil)) + reader = bytes.NewReader(buf) + } + ui, err := l.Client.PutObject(ctx, bucket, object, reader, data.Size(), md5Base64, sha256Hex, putOpts) if err != nil { return objInfo, minio.ErrorRespToObjectError(err, bucket, object) } @@ -629,12 +650,31 @@ func (l *s3Objects) NewMultipartUpload(ctx context.Context, bucket string, objec // PutObjectPart puts a part of object in bucket func (l *s3Objects) PutObjectPart(ctx context.Context, bucket string, object string, uploadID string, partID int, r *minio.PutObjReader, opts minio.ObjectOptions) (pi minio.PartInfo, e error) { data := r.Reader + + md5Base64 := data.MD5Base64String() + sha256Hex := data.SHA256HexString() + var reader io.Reader = data + + // AWS S3 requires Content-MD5 on every UploadPart request for Object Lock + // buckets. When the caller uses streaming SigV4 (no Content-MD5 header), + // MD5Base64String() returns "". Buffer the part and compute MD5 ourselves. + if md5Base64 == "" { + buf, err := io.ReadAll(data) + if err != nil { + return pi, minio.ErrorRespToObjectError(err, bucket, object) + } + h := md5.New() + h.Write(buf) + md5Base64 = base64.StdEncoding.EncodeToString(h.Sum(nil)) + reader = bytes.NewReader(buf) + } + putOpts := miniogo.PutObjectPartOptions{ SSE: opts.ServerSideEncryption, - Md5Base64: data.MD5Base64String(), - Sha256Hex: data.SHA256HexString(), + Md5Base64: md5Base64, + Sha256Hex: sha256Hex, } - info, err := l.Client.PutObjectPart(ctx, bucket, object, uploadID, partID, data, data.Size(), putOpts) + info, err := l.Client.PutObjectPart(ctx, bucket, object, uploadID, partID, reader, data.Size(), putOpts) if err != nil { return pi, minio.ErrorRespToObjectError(err, bucket, object) } @@ -718,7 +758,7 @@ func (l *s3Objects) AbortMultipartUpload(ctx context.Context, bucket string, obj // CompleteMultipartUpload completes ongoing multipart upload and finalizes object func (l *s3Objects) CompleteMultipartUpload(ctx context.Context, bucket string, object string, uploadID string, uploadedParts []minio.CompletePart, opts minio.ObjectOptions) (oi minio.ObjectInfo, e error) { - uploadInfo, err := l.Client.CompleteMultipartUpload(ctx, bucket, object, uploadID, minio.ToMinioClientCompleteParts(uploadedParts), miniogo.PutObjectOptions{}) + uploadInfo, err := l.Client.CompleteMultipartUpload(ctx, bucket, object, uploadID, minio.ToMinioClientCompleteParts(uploadedParts), miniogo.PutObjectOptions{SendContentMd5: true}) if err != nil { return oi, minio.ErrorRespToObjectError(err, bucket, object) }