diff --git a/backend/azure/azure.go b/backend/azure/azure.go index 1eb4d92..c05fae4 100644 --- a/backend/azure/azure.go +++ b/backend/azure/azure.go @@ -20,6 +20,7 @@ import ( "encoding/base64" "encoding/binary" "encoding/json" + "errors" "fmt" "io" "math" @@ -317,6 +318,61 @@ func (az *Azure) HeadObject(ctx context.Context, input *s3.HeadObjectInput) (*s3 }, nil } +func (az *Azure) GetObjectAttributes(ctx context.Context, input *s3.GetObjectAttributesInput) (s3response.GetObjectAttributesResult, error) { + data, err := az.HeadObject(ctx, &s3.HeadObjectInput{ + Bucket: input.Bucket, + Key: input.Key, + }) + if err == nil { + return s3response.GetObjectAttributesResult{ + ETag: data.ETag, + LastModified: data.LastModified, + ObjectSize: data.ContentLength, + StorageClass: &data.StorageClass, + VersionId: data.VersionId, + }, nil + } + if !errors.Is(err, s3err.GetAPIError(s3err.ErrNoSuchKey)) { + return s3response.GetObjectAttributesResult{}, err + } + + resp, err := az.ListParts(ctx, &s3.ListPartsInput{ + Bucket: input.Bucket, + Key: input.Key, + PartNumberMarker: input.PartNumberMarker, + MaxParts: input.MaxParts, + }) + if errors.Is(err, s3err.GetAPIError(s3err.ErrNoSuchUpload)) { + return s3response.GetObjectAttributesResult{}, s3err.GetAPIError(s3err.ErrNoSuchKey) + } + if err != nil { + return s3response.GetObjectAttributesResult{}, err + } + + parts := []types.ObjectPart{} + + for _, p := range resp.Parts { + partNumber := int32(p.PartNumber) + size := p.Size + + parts = append(parts, types.ObjectPart{ + Size: &size, + PartNumber: &partNumber, + }) + } + + //TODO: handle PartsCount prop + return s3response.GetObjectAttributesResult{ + ObjectParts: &s3response.ObjectParts{ + IsTruncated: resp.IsTruncated, + MaxParts: resp.MaxParts, + PartNumberMarker: resp.PartNumberMarker, + NextPartNumberMarker: resp.PartNumberMarker, + Parts: parts, + }, + }, nil +} + func (az *Azure) ListObjects(ctx context.Context, input *s3.ListObjectsInput) (*s3.ListObjectsOutput, error) { pager := az.client.NewListBlobsFlatPager(*input.Bucket, &azblob.ListBlobsFlatOptions{ Marker: input.Marker, diff --git a/backend/backend.go b/backend/backend.go index fa060d5..3e74231 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -58,7 +58,7 @@ type Backend interface { HeadObject(context.Context, *s3.HeadObjectInput) (*s3.HeadObjectOutput, error) GetObject(context.Context, *s3.GetObjectInput, io.Writer) (*s3.GetObjectOutput, error) GetObjectAcl(context.Context, *s3.GetObjectAclInput) (*s3.GetObjectAclOutput, error) - GetObjectAttributes(context.Context, *s3.GetObjectAttributesInput) (*s3.GetObjectAttributesOutput, error) + GetObjectAttributes(context.Context, *s3.GetObjectAttributesInput) (s3response.GetObjectAttributesResult, error) CopyObject(context.Context, *s3.CopyObjectInput) (*s3.CopyObjectOutput, error) ListObjects(context.Context, *s3.ListObjectsInput) (*s3.ListObjectsOutput, error) ListObjectsV2(context.Context, *s3.ListObjectsV2Input) (*s3.ListObjectsV2Output, error) @@ -173,8 +173,8 @@ func (BackendUnsupported) GetObject(context.Context, *s3.GetObjectInput, io.Writ func (BackendUnsupported) GetObjectAcl(context.Context, *s3.GetObjectAclInput) (*s3.GetObjectAclOutput, error) { return nil, s3err.GetAPIError(s3err.ErrNotImplemented) } -func (BackendUnsupported) GetObjectAttributes(context.Context, *s3.GetObjectAttributesInput) (*s3.GetObjectAttributesOutput, error) { - return nil, s3err.GetAPIError(s3err.ErrNotImplemented) +func (BackendUnsupported) GetObjectAttributes(context.Context, *s3.GetObjectAttributesInput) (s3response.GetObjectAttributesResult, error) { + return s3response.GetObjectAttributesResult{}, s3err.GetAPIError(s3err.ErrNotImplemented) } func (BackendUnsupported) CopyObject(context.Context, *s3.CopyObjectInput) (*s3.CopyObjectOutput, error) { return nil, s3err.GetAPIError(s3err.ErrNotImplemented) diff --git a/backend/posix/posix.go b/backend/posix/posix.go index fd17035..e4192b4 100644 --- a/backend/posix/posix.go +++ b/backend/posix/posix.go @@ -521,6 +521,18 @@ func (p *Posix) checkUploadIDExists(bucket, object, uploadID string) ([32]byte, return sum, nil } +func (p *Posix) retrieveUploadId(bucket, object string) (string, error) { + sum := sha256.Sum256([]byte(object)) + objdir := filepath.Join(bucket, metaTmpMultipartDir, fmt.Sprintf("%x", sum)) + + entries, err := os.ReadDir(objdir) + if err != nil || len(entries) == 0 { + return "", s3err.GetAPIError(s3err.ErrNoSuchKey) + } + + return entries[0].Name(), nil +} + // fll out the user metadata map with the metadata for the object // and return the content type and encoding func (p *Posix) loadUserMetaData(bucket, object string, m map[string]string) (string, string) { @@ -1505,6 +1517,9 @@ func (p *Posix) HeadObject(_ context.Context, input *s3.HeadObjectInput) (*s3.He size := fi.Size() + //TODO: Add object lock status properties + //TODO: the method must handle multipart upload case + return &s3.HeadObjectOutput{ ContentLength: &size, ContentType: &contentType, @@ -1515,6 +1530,65 @@ func (p *Posix) HeadObject(_ context.Context, input *s3.HeadObjectInput) (*s3.He }, nil } +func (p *Posix) GetObjectAttributes(ctx context.Context, input *s3.GetObjectAttributesInput) (s3response.GetObjectAttributesResult, error) { + data, err := p.HeadObject(ctx, &s3.HeadObjectInput{ + Bucket: input.Bucket, + Key: input.Key, + }) + if err == nil { + return s3response.GetObjectAttributesResult{ + ETag: data.ETag, + LastModified: data.LastModified, + ObjectSize: data.ContentLength, + StorageClass: &data.StorageClass, + VersionId: data.VersionId, + }, nil + } + if !errors.Is(err, s3err.GetAPIError(s3err.ErrNoSuchKey)) { + return s3response.GetObjectAttributesResult{}, err + } + + uploadId, err := p.retrieveUploadId(*input.Bucket, *input.Key) + if err != nil { + return s3response.GetObjectAttributesResult{}, err + } + + resp, err := p.ListParts(ctx, &s3.ListPartsInput{ + Bucket: input.Bucket, + Key: input.Key, + UploadId: &uploadId, + PartNumberMarker: input.PartNumberMarker, + MaxParts: input.MaxParts, + }) + if err != nil { + return s3response.GetObjectAttributesResult{}, err + } + + parts := []types.ObjectPart{} + + for _, p := range resp.Parts { + partNumber := int32(p.PartNumber) + size := p.Size + + parts = append(parts, types.ObjectPart{ + Size: &size, + PartNumber: &partNumber, + }) + } + + //TODO: handle PartsCount prop + //TODO: Maybe simply calling ListParts isn't a good option + return s3response.GetObjectAttributesResult{ + ObjectParts: &s3response.ObjectParts{ + IsTruncated: resp.IsTruncated, + MaxParts: resp.MaxParts, + PartNumberMarker: resp.PartNumberMarker, + NextPartNumberMarker: resp.NextPartNumberMarker, + Parts: parts, + }, + }, nil +} + func (p *Posix) CopyObject(ctx context.Context, input *s3.CopyObjectInput) (*s3.CopyObjectOutput, error) { if input.Bucket == nil { return nil, s3err.GetAPIError(s3err.ErrInvalidBucketName) diff --git a/backend/s3proxy/s3.go b/backend/s3proxy/s3.go index a00811a..5871a99 100644 --- a/backend/s3proxy/s3.go +++ b/backend/s3proxy/s3.go @@ -296,9 +296,41 @@ func (s *S3Proxy) GetObject(ctx context.Context, input *s3.GetObjectInput, w io. return output, nil } -func (s *S3Proxy) GetObjectAttributes(ctx context.Context, input *s3.GetObjectAttributesInput) (*s3.GetObjectAttributesOutput, error) { +func (s *S3Proxy) GetObjectAttributes(ctx context.Context, input *s3.GetObjectAttributesInput) (s3response.GetObjectAttributesResult, error) { out, err := s.client.GetObjectAttributes(ctx, input) - return out, handleError(err) + + parts := s3response.ObjectParts{} + objParts := out.ObjectParts + if objParts != nil { + 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 + } + } + + return s3response.GetObjectAttributesResult{ + ETag: out.ETag, + LastModified: out.LastModified, + ObjectSize: out.ObjectSize, + StorageClass: &out.StorageClass, + VersionId: out.VersionId, + ObjectParts: &parts, + }, handleError(err) } func (s *S3Proxy) CopyObject(ctx context.Context, input *s3.CopyObjectInput) (*s3.CopyObjectOutput, error) { diff --git a/s3api/controllers/backend_moq_test.go b/s3api/controllers/backend_moq_test.go index b943fed..dc42bdc 100644 --- a/s3api/controllers/backend_moq_test.go +++ b/s3api/controllers/backend_moq_test.go @@ -77,7 +77,7 @@ var _ backend.Backend = &BackendMock{} // GetObjectAclFunc: func(contextMoqParam context.Context, getObjectAclInput *s3.GetObjectAclInput) (*s3.GetObjectAclOutput, error) { // panic("mock out the GetObjectAcl method") // }, -// GetObjectAttributesFunc: func(contextMoqParam context.Context, getObjectAttributesInput *s3.GetObjectAttributesInput) (*s3.GetObjectAttributesOutput, error) { +// GetObjectAttributesFunc: func(contextMoqParam context.Context, getObjectAttributesInput *s3.GetObjectAttributesInput) (s3response.GetObjectAttributesResult, error) { // panic("mock out the GetObjectAttributes method") // }, // GetObjectLegalHoldFunc: func(contextMoqParam context.Context, bucket string, object string, versionId string) (*bool, error) { @@ -229,7 +229,7 @@ type BackendMock struct { GetObjectAclFunc func(contextMoqParam context.Context, getObjectAclInput *s3.GetObjectAclInput) (*s3.GetObjectAclOutput, error) // GetObjectAttributesFunc mocks the GetObjectAttributes method. - GetObjectAttributesFunc func(contextMoqParam context.Context, getObjectAttributesInput *s3.GetObjectAttributesInput) (*s3.GetObjectAttributesOutput, error) + GetObjectAttributesFunc func(contextMoqParam context.Context, getObjectAttributesInput *s3.GetObjectAttributesInput) (s3response.GetObjectAttributesResult, error) // GetObjectLegalHoldFunc mocks the GetObjectLegalHold method. GetObjectLegalHoldFunc func(contextMoqParam context.Context, bucket string, object string, versionId string) (*bool, error) @@ -1406,7 +1406,7 @@ func (mock *BackendMock) GetObjectAclCalls() []struct { } // GetObjectAttributes calls GetObjectAttributesFunc. -func (mock *BackendMock) GetObjectAttributes(contextMoqParam context.Context, getObjectAttributesInput *s3.GetObjectAttributesInput) (*s3.GetObjectAttributesOutput, error) { +func (mock *BackendMock) GetObjectAttributes(contextMoqParam context.Context, getObjectAttributesInput *s3.GetObjectAttributesInput) (s3response.GetObjectAttributesResult, error) { if mock.GetObjectAttributesFunc == nil { panic("BackendMock.GetObjectAttributesFunc: method is nil but Backend.GetObjectAttributes was just called") } diff --git a/s3api/controllers/base.go b/s3api/controllers/base.go index 96f7194..851a890 100644 --- a/s3api/controllers/base.go +++ b/s3api/controllers/base.go @@ -291,7 +291,7 @@ func (c S3ApiController) GetActions(ctx *fiber.Ctx) error { }) } - if attrs := ctx.Get("X-Amz-Object-Attributes"); attrs != "" { + if ctx.Request().URI().QueryArgs().Has("attributes") { err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ Acl: parsedAcl, AclPermission: types.PermissionRead, @@ -309,17 +309,36 @@ func (c S3ApiController) GetActions(ctx *fiber.Ctx) error { BucketOwner: parsedAcl.Owner, }) } - var oattrs []types.ObjectAttributes - for _, a := range strings.Split(attrs, ",") { - oattrs = append(oattrs, types.ObjectAttributes(a)) + maxParts := ctx.Get("X-Amz-Max-Parts") + partNumberMarker := ctx.Get("X-Amz-Part-Number-Marker") + maxPartsParsed, err := utils.ParseUint(maxParts) + if err != nil { + return SendXMLResponse(ctx, nil, err, + &MetaOpts{ + Logger: c.logger, + Action: "GetObjectAttributes", + BucketOwner: parsedAcl.Owner, + }) } + attrs := utils.ParseObjectAttributes(ctx) + res, err := c.be.GetObjectAttributes(ctx.Context(), &s3.GetObjectAttributesInput{ Bucket: &bucket, Key: &key, - ObjectAttributes: oattrs, + PartNumberMarker: &partNumberMarker, + MaxParts: &maxPartsParsed, + VersionId: &versionId, }) - return SendXMLResponse(ctx, res, err, + if err != nil { + return SendXMLResponse(ctx, nil, err, + &MetaOpts{ + Logger: c.logger, + Action: "GetObjectAttributes", + BucketOwner: parsedAcl.Owner, + }) + } + return SendXMLResponse(ctx, utils.FilterObjectAttributes(attrs, res), err, &MetaOpts{ Logger: c.logger, Action: "GetObjectAttributes", diff --git a/s3api/controllers/base_test.go b/s3api/controllers/base_test.go index b522e38..8eec037 100644 --- a/s3api/controllers/base_test.go +++ b/s3api/controllers/base_test.go @@ -188,8 +188,8 @@ func TestS3ApiController_GetActions(t *testing.T) { GetObjectAclFunc: func(context.Context, *s3.GetObjectAclInput) (*s3.GetObjectAclOutput, error) { return &s3.GetObjectAclOutput{}, nil }, - GetObjectAttributesFunc: func(context.Context, *s3.GetObjectAttributesInput) (*s3.GetObjectAttributesOutput, error) { - return &s3.GetObjectAttributesOutput{}, nil + GetObjectAttributesFunc: func(context.Context, *s3.GetObjectAttributesInput) (s3response.GetObjectAttributesResult, error) { + return s3response.GetObjectAttributesResult{}, nil }, GetObjectFunc: func(context.Context, *s3.GetObjectInput, io.Writer) (*s3.GetObjectOutput, error) { return &s3.GetObjectOutput{ diff --git a/s3api/utils/utils.go b/s3api/utils/utils.go index 17e9be5..77723f3 100644 --- a/s3api/utils/utils.go +++ b/s3api/utils/utils.go @@ -31,6 +31,7 @@ import ( "github.com/gofiber/fiber/v2" "github.com/valyala/fasthttp" "github.com/versity/versitygw/s3err" + "github.com/versity/versitygw/s3response" ) var ( @@ -253,3 +254,34 @@ func ParseDeleteObjects(objs []types.ObjectIdentifier) (result []string) { return } + +func FilterObjectAttributes(attrs map[types.ObjectAttributes]struct{}, output s3response.GetObjectAttributesResult) s3response.GetObjectAttributesResult { + if _, ok := attrs[types.ObjectAttributesEtag]; !ok { + output.ETag = nil + } + if _, ok := attrs[types.ObjectAttributesObjectParts]; !ok { + output.ObjectParts = nil + } + if _, ok := attrs[types.ObjectAttributesObjectSize]; !ok { + output.ObjectSize = nil + } + if _, ok := attrs[types.ObjectAttributesStorageClass]; !ok { + output.StorageClass = nil + } + + return output +} + +func ParseObjectAttributes(ctx *fiber.Ctx) map[types.ObjectAttributes]struct{} { + attrs := map[types.ObjectAttributes]struct{}{} + ctx.Request().Header.VisitAll(func(key, value []byte) { + if string(key) == "X-Amz-Object-Attributes" { + oattrs := strings.Split(string(value), ",") + for _, a := range oattrs { + attrs[types.ObjectAttributes(a)] = struct{}{} + } + } + }) + + return attrs +} diff --git a/s3api/utils/utils_test.go b/s3api/utils/utils_test.go index 5d8ef5f..e0c409f 100644 --- a/s3api/utils/utils_test.go +++ b/s3api/utils/utils_test.go @@ -6,8 +6,10 @@ import ( "reflect" "testing" + "github.com/aws/aws-sdk-go-v2/service/s3/types" "github.com/gofiber/fiber/v2" "github.com/valyala/fasthttp" + "github.com/versity/versitygw/s3response" ) func TestCreateHttpRequestFromCtx(t *testing.T) { @@ -264,3 +266,58 @@ func TestParseUint(t *testing.T) { }) } } + +func TestFilterObjectAttributes(t *testing.T) { + type args struct { + attrs map[types.ObjectAttributes]struct{} + output s3response.GetObjectAttributesResult + } + etag, objSize := "etag", int64(3222) + tests := []struct { + name string + args args + want s3response.GetObjectAttributesResult + }{ + { + name: "keep only ETag", + args: args{ + attrs: map[types.ObjectAttributes]struct{}{ + types.ObjectAttributesEtag: {}, + }, + output: s3response.GetObjectAttributesResult{ + ObjectSize: &objSize, + ETag: &etag, + }, + }, + want: s3response.GetObjectAttributesResult{ETag: &etag}, + }, + { + name: "keep multiple props", + args: args{ + attrs: map[types.ObjectAttributes]struct{}{ + types.ObjectAttributesEtag: {}, + types.ObjectAttributesObjectSize: {}, + types.ObjectAttributesStorageClass: {}, + }, + output: s3response.GetObjectAttributesResult{ + ObjectSize: &objSize, + ETag: &etag, + ObjectParts: &s3response.ObjectParts{}, + VersionId: &etag, + }, + }, + want: s3response.GetObjectAttributesResult{ + ETag: &etag, + ObjectSize: &objSize, + VersionId: &etag, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := FilterObjectAttributes(tt.args.attrs, tt.args.output); !reflect.DeepEqual(got, tt.want) { + t.Errorf("FilterObjectAttributes() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/s3response/s3response.go b/s3response/s3response.go index 48cce3e..4540133 100644 --- a/s3response/s3response.go +++ b/s3response/s3response.go @@ -52,6 +52,23 @@ type ListPartsResult struct { Parts []Part `xml:"Part"` } +type GetObjectAttributesResult struct { + ETag *string + LastModified *time.Time + ObjectSize *int64 + StorageClass *types.StorageClass + VersionId *string + ObjectParts *ObjectParts +} + +type ObjectParts struct { + PartNumberMarker int + NextPartNumberMarker int + MaxParts int + IsTruncated bool + Parts []types.ObjectPart `xml:"Part"` +} + // ListMultipartUploadsResponse - s3 api list multipart uploads response. type ListMultipartUploadsResult struct { XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ ListMultipartUploadsResult" json:"-"` diff --git a/tests/integration/group-tests.go b/tests/integration/group-tests.go index a8ad351..d568978 100644 --- a/tests/integration/group-tests.go +++ b/tests/integration/group-tests.go @@ -103,6 +103,14 @@ func TestHeadObject(s *S3Conf) { HeadObject_success(s) } +func TestGetObjectAttributes(s *S3Conf) { + GetObjectAttributes_non_existing_bucket(s) + GetObjectAttributes_non_existing_object(s) + GetObjectAttributes_existing_object(s) + GetObjectAttributes_multipart_upload(s) + GetObjectAttributes_multipart_upload_truncated(s) +} + func TestGetObject(s *S3Conf) { GetObject_non_existing_key(s) GetObject_invalid_ranges(s) @@ -344,6 +352,7 @@ func TestFullFlow(s *S3Conf) { TestDeleteBucketTagging(s) TestPutObject(s) TestHeadObject(s) + TestGetObjectAttributes(s) TestGetObject(s) TestListObjects(s) TestListObjectsV2(s) @@ -471,6 +480,11 @@ func GetIntTests() IntTests { "PutObject_success": PutObject_success, "HeadObject_non_existing_object": HeadObject_non_existing_object, "HeadObject_success": HeadObject_success, + "GetObjectAttributes_non_existing_bucket": GetObjectAttributes_non_existing_bucket, + "GetObjectAttributes_non_existing_object": GetObjectAttributes_non_existing_object, + "GetObjectAttributes_existing_object": GetObjectAttributes_existing_object, + "GetObjectAttributes_multipart_upload": GetObjectAttributes_multipart_upload, + "GetObjectAttributes_multipart_upload_truncated": GetObjectAttributes_multipart_upload_truncated, "GetObject_non_existing_key": GetObject_non_existing_key, "GetObject_invalid_ranges": GetObject_invalid_ranges, "GetObject_with_meta": GetObject_with_meta, diff --git a/tests/integration/tests.go b/tests/integration/tests.go index 0b54add..56093e4 100644 --- a/tests/integration/tests.go +++ b/tests/integration/tests.go @@ -1,7 +1,9 @@ package integration import ( + "bytes" "context" + "crypto/rand" "crypto/sha256" "encoding/xml" "errors" @@ -2452,6 +2454,247 @@ func HeadObject_success(s *S3Conf) error { }) } +func GetObjectAttributes_non_existing_bucket(s *S3Conf) error { + testName := "GetObjectAttributes_non_existing_bucket" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := s3client.GetObjectAttributes(ctx, &s3.GetObjectAttributesInput{ + Bucket: getPtr(getBucketName()), + Key: getPtr("my-obj"), + ObjectAttributes: []types.ObjectAttributes{}, + }) + cancel() + if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrNoSuchBucket)); err != nil { + return err + } + + return nil + }) +} + +func GetObjectAttributes_non_existing_object(s *S3Conf) error { + testName := "GetObjectAttributes_non_existing_object" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := s3client.GetObjectAttributes(ctx, &s3.GetObjectAttributesInput{ + Bucket: &bucket, + Key: getPtr("my-obj"), + ObjectAttributes: []types.ObjectAttributes{}, + }) + cancel() + if err := checkSdkApiErr(err, "NoSuchKey"); err != nil { + return err + } + + return nil + }) +} + +func GetObjectAttributes_existing_object(s *S3Conf) error { + testName := "GetObjectAttributes_existing_object" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + obj, data_len := "my-obj", int64(45679) + data := make([]byte, data_len) + + _, err := rand.Read(data) + if err != nil { + return err + } + + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + resp, err := s3client.PutObject(ctx, &s3.PutObjectInput{ + Bucket: &bucket, + Key: &obj, + Body: bytes.NewReader(data), + }) + cancel() + if err != nil { + return err + } + + ctx, cancel = context.WithTimeout(context.Background(), shortTimeout) + out, err := s3client.GetObjectAttributes(ctx, &s3.GetObjectAttributesInput{ + Bucket: &bucket, + Key: &obj, + ObjectAttributes: []types.ObjectAttributes{ + types.ObjectAttributesEtag, + types.ObjectAttributesObjectSize, + }, + }) + cancel() + if err != nil { + return err + } + + if resp.ETag == nil || out.ETag == nil { + return fmt.Errorf("nil ETag output") + } + if *resp.ETag != *out.ETag { + return fmt.Errorf("expected ETag to be %v, instead got %v", *resp.ETag, *out.ETag) + } + if out.ObjectSize == nil { + return fmt.Errorf("nil object size output") + } + if *out.ObjectSize != data_len { + return fmt.Errorf("expected object size to be %v, instead got %v", data_len, *out.ObjectSize) + } + if out.Checksum != nil { + return fmt.Errorf("expected checksum do be nil, instead got %v", *out.Checksum) + } + + return nil + }) +} + +func GetObjectAttributes_multipart_upload(s *S3Conf) error { + testName := "GetObjectAttributes_multipart_upload" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + obj := "my-obj" + out, err := createMp(s3client, bucket, obj) + if err != nil { + return err + } + + parts, err := uploadParts(s3client, 5*1024*1024, 5, bucket, obj, *out.UploadId) + if err != nil { + return err + } + + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + resp, err := s3client.GetObjectAttributes(ctx, &s3.GetObjectAttributesInput{ + Bucket: &bucket, + Key: &obj, + ObjectAttributes: []types.ObjectAttributes{ + types.ObjectAttributesObjectParts, + }, + }) + cancel() + if err != nil { + return err + } + + if resp.ObjectParts == nil { + return fmt.Errorf("expected non nil object parts") + } + + for i, p := range resp.ObjectParts.Parts { + if *p.PartNumber != *parts[i].PartNumber { + return fmt.Errorf("expected part number to be %v, instead got %v", *parts[i].PartNumber, *p.PartNumber) + } + if *p.Size != *parts[i].Size { + return fmt.Errorf("expected part size to be %v, instead got %v", *parts[i].Size, *p.Size) + } + } + + return nil + }) +} + +func GetObjectAttributes_multipart_upload_truncated(s *S3Conf) error { + testName := "GetObjectAttributes_multipart_upload_truncated" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + obj := "my-obj" + out, err := createMp(s3client, bucket, obj) + if err != nil { + return err + } + + parts, err := uploadParts(s3client, 5*1024*1024, 5, bucket, obj, *out.UploadId) + if err != nil { + return err + } + + maxParts := int32(3) + + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + resp, err := s3client.GetObjectAttributes(ctx, &s3.GetObjectAttributesInput{ + Bucket: &bucket, + Key: &obj, + ObjectAttributes: []types.ObjectAttributes{ + types.ObjectAttributesObjectParts, + }, + MaxParts: &maxParts, + }) + cancel() + if err != nil { + return err + } + + if resp.ObjectParts == nil { + return fmt.Errorf("expected non nil object parts") + } + if resp.ObjectParts.IsTruncated == nil { + return fmt.Errorf("expected non nil isTruncated") + } + if !*resp.ObjectParts.IsTruncated { + return fmt.Errorf("expected object parts to be truncated") + } + if resp.ObjectParts.MaxParts == nil { + return fmt.Errorf("expected non nil max-parts") + } + if *resp.ObjectParts.MaxParts != maxParts { + return fmt.Errorf("expected max-parts to be %v, instead got %v", maxParts, *resp.ObjectParts.MaxParts) + } + if resp.ObjectParts.NextPartNumberMarker == nil { + return fmt.Errorf("expected non nil NextPartNumberMarker") + } + if *resp.ObjectParts.NextPartNumberMarker != fmt.Sprint(*parts[2].PartNumber) { + return fmt.Errorf("expected NextPartNumberMarker to be %v, instead got %v", fmt.Sprint(*parts[2].PartNumber), *resp.ObjectParts.NextPartNumberMarker) + } + if len(resp.ObjectParts.Parts) != int(maxParts) { + return fmt.Errorf("expected length of parts to be %v, instead got %v", maxParts, len(resp.ObjectParts.Parts)) + } + + for i, p := range resp.ObjectParts.Parts { + if *p.PartNumber != *parts[i].PartNumber { + return fmt.Errorf("expected part number to be %v, instead got %v", *parts[i].PartNumber, *p.PartNumber) + } + if *p.Size != *parts[i].Size { + return fmt.Errorf("expected part size to be %v, instead got %v", *parts[i].Size, *p.Size) + } + } + + ctx, cancel = context.WithTimeout(context.Background(), shortTimeout) + resp, err = s3client.GetObjectAttributes(ctx, &s3.GetObjectAttributesInput{ + Bucket: &bucket, + Key: &obj, + ObjectAttributes: []types.ObjectAttributes{ + types.ObjectAttributesObjectParts, + }, + PartNumberMarker: resp.ObjectParts.NextPartNumberMarker, + }) + cancel() + if err != nil { + return err + } + + if resp.ObjectParts == nil { + return fmt.Errorf("expected non nil object parts") + } + if resp.ObjectParts.IsTruncated == nil { + return fmt.Errorf("expected non nil isTruncated") + } + if *resp.ObjectParts.IsTruncated { + return fmt.Errorf("expected object parts to not be truncated") + } + + if len(resp.ObjectParts.Parts) != len(parts)-int(maxParts) { + return fmt.Errorf("expected length of parts to be %v, instead got %v", len(parts)-int(maxParts), len(resp.ObjectParts.Parts)) + } + + for i, p := range resp.ObjectParts.Parts { + if *p.PartNumber != *parts[i+int(maxParts)].PartNumber { + return fmt.Errorf("expected part number to be %v, instead got %v", *parts[i+int(maxParts)].PartNumber, *p.PartNumber) + } + if *p.Size != *parts[i+int(maxParts)].Size { + return fmt.Errorf("expected part size to be %v, instead got %v", *parts[i+int(maxParts)].Size, *p.Size) + } + } + + return nil + }) +} + func GetObject_non_existing_key(s *S3Conf) error { testName := "GetObject_non_existing_key" return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { diff --git a/tests/integration/utils.go b/tests/integration/utils.go index 7130558..bfc4f60 100644 --- a/tests/integration/utils.go +++ b/tests/integration/utils.go @@ -522,6 +522,7 @@ func uploadParts(client *s3.Client, size, partCount int, bucket, key, uploadId s parts = append(parts, types.Part{ ETag: out.ETag, PartNumber: &pn, + Size: &partSize, }) offset += partSize }