From 818e91ebde1739a8efbc575e898bf535c7bc9970 Mon Sep 17 00:00:00 2001 From: niksis02 Date: Fri, 5 Sep 2025 21:40:46 +0400 Subject: [PATCH] feat: adds x-amz-object-size in PutObject response headers Closes #1518 Adds the `x-amz-object-size` header to the `PutObject` response, indicating the size of the uploaded object. This change is applied to the POSIX, Azure, and S3 proxy backends. --- backend/azure/azure.go | 1 + backend/posix/posix.go | 5 ++++- backend/s3proxy/s3.go | 1 + s3api/controllers/object-put.go | 1 + s3api/controllers/object-put_test.go | 4 ++++ s3response/s3response.go | 1 + tests/integration/tests.go | 25 ++++++++++++++++++++++++- tests/integration/utils.go | 11 +++++++++++ 8 files changed, 47 insertions(+), 2 deletions(-) diff --git a/backend/azure/azure.go b/backend/azure/azure.go index c12e2ba1..0c3286d2 100644 --- a/backend/azure/azure.go +++ b/backend/azure/azure.go @@ -367,6 +367,7 @@ func (az *Azure) PutObject(ctx context.Context, po s3response.PutObjectInput) (s return s3response.PutObjectOutput{ ETag: convertAzureEtag(uploadResp.ETag), + Size: po.ContentLength, }, nil } diff --git a/backend/posix/posix.go b/backend/posix/posix.go index 82370387..91b9a9d6 100644 --- a/backend/posix/posix.go +++ b/backend/posix/posix.go @@ -2796,6 +2796,7 @@ func (p *Posix) PutObject(ctx context.Context, po s3response.PutObjectInput) (s3 // for directory object no version is created return s3response.PutObjectOutput{ ETag: emptyMD5, + Size: &contentLength, }, nil } @@ -2848,6 +2849,8 @@ func (p *Posix) PutObject(ctx context.Context, po s3response.PutObjectInput) (s3 } defer f.cleanup() + objsize := f.size + hash := md5.New() rdr := io.TeeReader(po.Body, hash) @@ -2984,7 +2987,6 @@ func (p *Posix) PutObject(ctx context.Context, po s3response.PutObjectInput) (s3 return s3response.PutObjectOutput{}, fmt.Errorf("set versionId attr: %w", err) } } - err = f.link() if errors.Is(err, syscall.EEXIST) { return s3response.PutObjectOutput{ @@ -3042,6 +3044,7 @@ func (p *Posix) PutObject(ctx context.Context, po s3response.PutObjectInput) (s3 ChecksumSHA1: checksum.SHA1, ChecksumSHA256: checksum.SHA256, ChecksumCRC64NVME: checksum.CRC64NVME, + Size: &objsize, ChecksumType: checksum.Type, }, nil } diff --git a/backend/s3proxy/s3.go b/backend/s3proxy/s3.go index f420104f..071c2d91 100644 --- a/backend/s3proxy/s3.go +++ b/backend/s3proxy/s3.go @@ -938,6 +938,7 @@ func (s *S3Proxy) PutObject(ctx context.Context, input s3response.PutObjectInput ChecksumCRC64NVME: output.ChecksumCRC64NVME, ChecksumSHA1: output.ChecksumSHA1, ChecksumSHA256: output.ChecksumSHA256, + Size: output.Size, }, nil } diff --git a/s3api/controllers/object-put.go b/s3api/controllers/object-put.go index ba9f6446..bd9997ee 100644 --- a/s3api/controllers/object-put.go +++ b/s3api/controllers/object-put.go @@ -725,6 +725,7 @@ func (c S3ApiController) PutObject(ctx *fiber.Ctx) (*Response, error) { "x-amz-checksum-sha256": res.ChecksumSHA256, "x-amz-checksum-type": utils.ConvertToStringPtr(res.ChecksumType), "x-amz-version-id": &res.VersionID, + "x-amz-object-size": utils.ConvertPtrToStringPtr(res.Size), }, MetaOpts: &MetaOptions{ ContentLength: contentLength, diff --git a/s3api/controllers/object-put_test.go b/s3api/controllers/object-put_test.go index ed075640..2a9b298d 100644 --- a/s3api/controllers/object-put_test.go +++ b/s3api/controllers/object-put_test.go @@ -1036,6 +1036,7 @@ func TestS3ApiController_CopyObject(t *testing.T) { func TestS3ApiController_PutObject(t *testing.T) { str := "" emptyStringPtr := &str + objSize := int64(120) tests := []struct { name string @@ -1148,6 +1149,7 @@ func TestS3ApiController_PutObject(t *testing.T) { "x-amz-checksum-sha256": nil, "x-amz-checksum-type": nil, "x-amz-version-id": emptyStringPtr, + "x-amz-object-size": nil, }, MetaOpts: &MetaOptions{ BucketOwner: "root", @@ -1188,6 +1190,7 @@ func TestS3ApiController_PutObject(t *testing.T) { ChecksumCRC64NVME: utils.GetStringPtr("crc64nvme"), ChecksumType: types.ChecksumTypeComposite, VersionID: "versionId", + Size: &objSize, }, }, output: testOutput{ @@ -1201,6 +1204,7 @@ func TestS3ApiController_PutObject(t *testing.T) { "x-amz-checksum-sha256": utils.GetStringPtr("sha256"), "x-amz-checksum-type": utils.GetStringPtr(string(types.ChecksumTypeComposite)), "x-amz-version-id": utils.GetStringPtr("versionId"), + "x-amz-object-size": utils.ConvertToStringPtr(objSize), }, MetaOpts: &MetaOptions{ BucketOwner: "root", diff --git a/s3response/s3response.go b/s3response/s3response.go index 28c076b0..506996bf 100644 --- a/s3response/s3response.go +++ b/s3response/s3response.go @@ -37,6 +37,7 @@ type PutObjectOutput struct { ChecksumSHA1 *string ChecksumSHA256 *string ChecksumCRC64NVME *string + Size *int64 ChecksumType types.ChecksumType } diff --git a/tests/integration/tests.go b/tests/integration/tests.go index c110fd02..d9552453 100644 --- a/tests/integration/tests.go +++ b/tests/integration/tests.go @@ -3418,10 +3418,33 @@ func PutObject_racey_success(s *S3Conf) error { func PutObject_success(s *S3Conf) error { testName := "PutObject_success" return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { - _, err := putObjects(s3client, []string{"my-obj"}, bucket) + lgth := int64(100) + res, err := putObjectWithData(lgth, &s3.PutObjectInput{ + Bucket: &bucket, + Key: getPtr("my-obj"), + }, s3client) if err != nil { return err } + + // skip the ETag check for azure tests + if !s.azureTests { + etag, err := calculateEtag(res.data) + if err != nil { + return err + } + + if getString(res.res.ETag) != etag { + return fmt.Errorf("expected ETag to be %s, intead got %s", getString(res.res.ETag), etag) + } + } + if res.res.Size == nil { + return fmt.Errorf("unexpected nil object Size") + } + if *res.res.Size != lgth { + return fmt.Errorf("expected the object size to be %v, instead got %v", lgth, *res.res.Size) + } + return nil }) } diff --git a/tests/integration/utils.go b/tests/integration/utils.go index 66718638..b57311e9 100644 --- a/tests/integration/utils.go +++ b/tests/integration/utils.go @@ -17,6 +17,7 @@ package integration import ( "bytes" "context" + "crypto/md5" "crypto/rand" "crypto/sha1" "crypto/sha256" @@ -1798,3 +1799,13 @@ func testOPTIONSEdnpoint(s *S3Conf, bucket, origin, method string, headers strin return comparePreflightResult(expected, result) } + +func calculateEtag(data []byte) (string, error) { + h := md5.New() + _, err := h.Write(data) + if err != nil { + return "", err + } + dataSum := h.Sum(nil) + return fmt.Sprintf("\"%s\"", hex.EncodeToString(dataSum[:])), nil +}