mirror of
https://github.com/versity/versitygw.git
synced 2026-04-19 04:05:02 +00:00
feat: Added GetObjectAttributes actions implementation in posix, azure and s3 backends. Added integration tests for GetObjectAttributes action
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:"-"`
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user