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.
This commit is contained in:
LuisFigueroaG
2026-10-13 21:29:14 -03:00
parent f88fe63e50
commit 7edbd62cff
5 changed files with 78 additions and 58 deletions
+16 -15
View File
@@ -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,
}
}
}
+22 -6
View File
@@ -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)
+17 -16
View File
@@ -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,
}
}
}
+17 -21
View File
@@ -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
}
+6
View File
@@ -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,