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,