From 7edbd62cff72320bb251577250d34ea42e39131c Mon Sep 17 00:00:00 2001 From: LuisFigueroaG Date: Tue, 13 Oct 2026 21:29:14 -0300 Subject: [PATCH] fix: only look up the parts count when ObjectParts is requested The controller now passes the requested attributes to the backend in ObjectAttributes, so posix and azure only do the extra HeadObject for the parts count when ObjectParts was asked for. This also lets the s3 backend reach the upstream service, which rejected the request before because the field was missing. The s3 backend now copies TotalPartsCount into PartsCount, leaves out ObjectParts when the upstream response has none, and sets the part number markers, which were dropped by inverted strconv.Atoi error checks. --- backend/azure/azure.go | 31 +++++++++-------- backend/posix/get_object_attributes_test.go | 28 +++++++++++---- backend/posix/posix.go | 33 +++++++++--------- backend/s3proxy/s3.go | 38 +++++++++------------ s3api/controllers/object-get.go | 6 ++++ 5 files changed, 78 insertions(+), 58 deletions(-) diff --git a/backend/azure/azure.go b/backend/azure/azure.go index 3d72cabd..54190683 100644 --- a/backend/azure/azure.go +++ b/backend/azure/azure.go @@ -764,22 +764,23 @@ func (az *Azure) GetObjectAttributes(ctx context.Context, input *s3.GetObjectAtt return s3response.GetObjectAttributesResponse{}, err } - // HeadObject reports the parts count only when a part is requested, and - // only for objects completed from a multipart upload - partNumber := int32(1) - part, err := az.HeadObject(ctx, &s3.HeadObjectInput{ - Bucket: input.Bucket, - Key: input.Key, - PartNumber: &partNumber, - }) - if err != nil { - return s3response.GetObjectAttributesResponse{}, err - } - var objParts *s3response.ObjectParts - if part.PartsCount != nil { - objParts = &s3response.ObjectParts{ - PartsCount: part.PartsCount, + if slices.Contains(input.ObjectAttributes, types.ObjectAttributesObjectParts) { + // HeadObject reports the parts count only when a part is requested, + // and only for objects completed from a multipart upload + partNumber := int32(1) + part, err := az.HeadObject(ctx, &s3.HeadObjectInput{ + Bucket: input.Bucket, + Key: input.Key, + PartNumber: &partNumber, + }) + if err != nil { + return s3response.GetObjectAttributesResponse{}, err + } + if part.PartsCount != nil { + objParts = &s3response.ObjectParts{ + PartsCount: part.PartsCount, + } } } diff --git a/backend/posix/get_object_attributes_test.go b/backend/posix/get_object_attributes_test.go index 45cee52b..daa3b00a 100644 --- a/backend/posix/get_object_attributes_test.go +++ b/backend/posix/get_object_attributes_test.go @@ -27,8 +27,8 @@ import ( ) // TestGetObjectAttributesPartsCount checks that GetObjectAttributes reports -// the parts count of an object completed from a multipart upload, and no -// object parts for an object that wasn't. +// the parts count of an object completed from a multipart upload when +// ObjectParts is requested, and no object parts for an object that wasn't. func TestGetObjectAttributesPartsCount(t *testing.T) { for name, mkMeta := range metaModes(t) { t.Run(name, func(t *testing.T) { @@ -79,9 +79,11 @@ func TestGetObjectAttributesPartsCount(t *testing.T) { t.Fatalf("complete multipart upload: %v", err) } + partsAttr := []types.ObjectAttributes{types.ObjectAttributesObjectParts} out, err := p.GetObjectAttributes(ctx, &s3.GetObjectAttributesInput{ - Bucket: &bucket, - Key: &mpKey, + Bucket: &bucket, + Key: &mpKey, + ObjectAttributes: partsAttr, }) if err != nil { t.Fatalf("get multipart object attributes: %v", err) @@ -96,6 +98,19 @@ func TestGetObjectAttributesPartsCount(t *testing.T) { t.Errorf("expected object size %d, got %d", totalSize, got) } + // the parts count is only looked up when ObjectParts is requested + out, err = p.GetObjectAttributes(ctx, &s3.GetObjectAttributesInput{ + Bucket: &bucket, + Key: &mpKey, + ObjectAttributes: []types.ObjectAttributes{types.ObjectAttributesObjectSize}, + }) + if err != nil { + t.Fatalf("get multipart object size: %v", err) + } + if out.ObjectParts != nil { + t.Errorf("expected no object parts when not requested, got %+v", *out.ObjectParts) + } + key := "obj" _, err = p.PutObject(ctx, s3response.PutObjectInput{ Bucket: &bucket, @@ -108,8 +123,9 @@ func TestGetObjectAttributesPartsCount(t *testing.T) { } out, err = p.GetObjectAttributes(ctx, &s3.GetObjectAttributesInput{ - Bucket: &bucket, - Key: &key, + Bucket: &bucket, + Key: &key, + ObjectAttributes: partsAttr, }) if err != nil { t.Fatalf("get object attributes: %v", err) diff --git a/backend/posix/posix.go b/backend/posix/posix.go index 68de5b5b..59e7ec03 100644 --- a/backend/posix/posix.go +++ b/backend/posix/posix.go @@ -6451,23 +6451,24 @@ func (p *Posix) GetObjectAttributes(ctx context.Context, input *s3.GetObjectAttr return s3response.GetObjectAttributesResponse{}, err } - // HeadObject reports the parts count only when a part is requested, and - // only for objects completed from a multipart upload - partNumber := int32(1) - part, err := p.HeadObject(withCtxNoSlot(ctx), &s3.HeadObjectInput{ - Bucket: input.Bucket, - Key: input.Key, - VersionId: input.VersionId, - PartNumber: &partNumber, - }) - if err != nil { - return s3response.GetObjectAttributesResponse{}, err - } - var objParts *s3response.ObjectParts - if part.PartsCount != nil { - objParts = &s3response.ObjectParts{ - PartsCount: part.PartsCount, + if slices.Contains(input.ObjectAttributes, types.ObjectAttributesObjectParts) { + // HeadObject reports the parts count only when a part is requested, + // and only for objects completed from a multipart upload + partNumber := int32(1) + part, err := p.HeadObject(withCtxNoSlot(ctx), &s3.HeadObjectInput{ + Bucket: input.Bucket, + Key: input.Key, + VersionId: input.VersionId, + PartNumber: &partNumber, + }) + if err != nil { + return s3response.GetObjectAttributesResponse{}, err + } + if part.PartsCount != nil { + objParts = &s3response.ObjectParts{ + PartsCount: part.PartsCount, + } } } diff --git a/backend/s3proxy/s3.go b/backend/s3proxy/s3.go index a6d9fdb3..14acd459 100644 --- a/backend/s3proxy/s3.go +++ b/backend/s3proxy/s3.go @@ -1222,27 +1222,23 @@ func (s *S3Proxy) GetObjectAttributes(ctx context.Context, input *s3.GetObjectAt return s3response.GetObjectAttributesResponse{}, handleError(err) } - parts := s3response.ObjectParts{} - objParts := out.ObjectParts - if objParts != nil { + var parts *s3response.ObjectParts + if objParts := out.ObjectParts; objParts != nil { + parts = &s3response.ObjectParts{ + Parts: objParts.Parts, + PartsCount: objParts.TotalPartsCount, + } if objParts.PartNumberMarker != nil { - partNumberMarker, err := strconv.Atoi(*objParts.PartNumberMarker) - if err != nil { - parts.PartNumberMarker = partNumberMarker - } - if objParts.NextPartNumberMarker != nil { - nextPartNumberMarker, err := strconv.Atoi(*objParts.NextPartNumberMarker) - if err != nil { - parts.NextPartNumberMarker = nextPartNumberMarker - } - } - if objParts.IsTruncated != nil { - parts.IsTruncated = *objParts.IsTruncated - } - if objParts.MaxParts != nil { - parts.MaxParts = int(*objParts.MaxParts) - } - parts.Parts = objParts.Parts + parts.PartNumberMarker, _ = strconv.Atoi(*objParts.PartNumberMarker) + } + if objParts.NextPartNumberMarker != nil { + parts.NextPartNumberMarker, _ = strconv.Atoi(*objParts.NextPartNumberMarker) + } + if objParts.MaxParts != nil { + parts.MaxParts = int(*objParts.MaxParts) + } + if objParts.IsTruncated != nil { + parts.IsTruncated = *objParts.IsTruncated } } @@ -1251,7 +1247,7 @@ func (s *S3Proxy) GetObjectAttributes(ctx context.Context, input *s3.GetObjectAt LastModified: out.LastModified, ObjectSize: out.ObjectSize, StorageClass: out.StorageClass, - ObjectParts: &parts, + ObjectParts: parts, Checksum: out.Checksum, }, nil } diff --git a/s3api/controllers/object-get.go b/s3api/controllers/object-get.go index 18db442e..5303a228 100644 --- a/s3api/controllers/object-get.go +++ b/s3api/controllers/object-get.go @@ -388,8 +388,14 @@ func (c S3ApiController) GetObjectAttributes(ctx fiber.Ctx) (*Response, error) { }, err } + objAttrs := make([]types.ObjectAttributes, 0, len(attrs)) + for attr := range attrs { + objAttrs = append(objAttrs, types.ObjectAttributes(attr)) + } + res, err := c.be.GetObjectAttributes(ctx.RequestCtx(), &s3.GetObjectAttributesInput{ + ObjectAttributes: objAttrs, SSECustomerAlgorithm: ssec.Algorithm, SSECustomerKey: ssec.Key, SSECustomerKeyMD5: ssec.KeyMD5,