diff --git a/backend/posix/posix.go b/backend/posix/posix.go index 304c0716..7bca3f36 100644 --- a/backend/posix/posix.go +++ b/backend/posix/posix.go @@ -303,7 +303,7 @@ func (p *Posix) DeleteBucket(_ context.Context, input *s3.DeleteBucketInput) err return nil } -func (p *Posix) CreateMultipartUpload(_ context.Context, mpu *s3.CreateMultipartUploadInput) (*s3.CreateMultipartUploadOutput, error) { +func (p *Posix) CreateMultipartUpload(ctx context.Context, mpu *s3.CreateMultipartUploadInput) (*s3.CreateMultipartUploadOutput, error) { if mpu.Bucket == nil { return nil, s3err.GetAPIError(s3err.ErrInvalidBucketName) } @@ -328,6 +328,23 @@ func (p *Posix) CreateMultipartUpload(_ context.Context, mpu *s3.CreateMultipart return nil, s3err.GetAPIError(s3err.ErrDirectoryObjectContainsData) } + // parse object tags + tagsStr := getString(mpu.Tagging) + tags := make(map[string]string) + if tagsStr != "" { + tagParts := strings.Split(tagsStr, "&") + for _, prt := range tagParts { + p := strings.Split(prt, "=") + if len(p) != 2 { + return nil, s3err.GetAPIError(s3err.ErrInvalidTag) + } + if len(p[0]) > 128 || len(p[1]) > 256 { + return nil, s3err.GetAPIError(s3err.ErrInvalidTag) + } + tags[p[0]] = p[1] + } + } + // generate random uuid for upload id uploadID := uuid.New().String() // hash object name for multipart container @@ -355,10 +372,10 @@ func (p *Posix) CreateMultipartUpload(_ context.Context, mpu *s3.CreateMultipart return nil, fmt.Errorf("set name attr for upload: %w", err) } - // set user attrs + // set user metadata for k, v := range mpu.Metadata { err := p.meta.StoreAttribute(bucket, filepath.Join(objdir, uploadID), - k, []byte(v)) + fmt.Sprintf("%v.%v", metaHdr, k), []byte(v)) if err != nil { // cleanup object if returning error os.RemoveAll(filepath.Join(tmppath, uploadID)) @@ -367,6 +384,60 @@ func (p *Posix) CreateMultipartUpload(_ context.Context, mpu *s3.CreateMultipart } } + // set object tagging + if tagsStr != "" { + err := p.PutObjectTagging(ctx, bucket, filepath.Join(objdir, uploadID), tags) + if err != nil { + // cleanup object if returning error + os.RemoveAll(filepath.Join(tmppath, uploadID)) + os.Remove(tmppath) + return nil, err + } + } + + // set content-type + if *mpu.ContentType != "" { + err := p.meta.StoreAttribute(bucket, filepath.Join(objdir, uploadID), + contentTypeHdr, []byte(*mpu.ContentType)) + if err != nil { + // cleanup object if returning error + os.RemoveAll(filepath.Join(tmppath, uploadID)) + os.Remove(tmppath) + return nil, fmt.Errorf("set content-type: %w", err) + } + } + + // set object legal hold + if mpu.ObjectLockLegalHoldStatus == types.ObjectLockLegalHoldStatusOn { + if err := p.PutObjectLegalHold(ctx, bucket, filepath.Join(objdir, uploadID), "", true); err != nil { + // cleanup object if returning error + os.RemoveAll(filepath.Join(tmppath, uploadID)) + os.Remove(tmppath) + return nil, err + } + } + + // Set object retention + if mpu.ObjectLockMode != "" { + retention := types.ObjectLockRetention{ + Mode: types.ObjectLockRetentionMode(mpu.ObjectLockMode), + RetainUntilDate: mpu.ObjectLockRetainUntilDate, + } + retParsed, err := json.Marshal(retention) + if err != nil { + // cleanup object if returning error + os.RemoveAll(filepath.Join(tmppath, uploadID)) + os.Remove(tmppath) + return nil, fmt.Errorf("parse object lock retention: %w", err) + } + if err := p.PutObjectRetention(ctx, bucket, filepath.Join(objdir, uploadID), "", retParsed); err != nil { + // cleanup object if returning error + os.RemoveAll(filepath.Join(tmppath, uploadID)) + os.Remove(tmppath) + return nil, err + } + } + return &s3.CreateMultipartUploadOutput{ Bucket: &bucket, Key: &object, @@ -492,7 +563,7 @@ func (p *Posix) CompleteMultipartUpload(ctx context.Context, input *s3.CompleteM userMetaData := make(map[string]string) upiddir := filepath.Join(objdir, uploadID) - p.loadUserMetaData(bucket, objdir, userMetaData) + cType, _ := p.loadUserMetaData(bucket, upiddir, userMetaData) objname := filepath.Join(bucket, object) dir := filepath.Dir(objname) @@ -509,7 +580,7 @@ func (p *Posix) CompleteMultipartUpload(ctx context.Context, input *s3.CompleteM } for k, v := range userMetaData { - err = p.meta.StoreAttribute(bucket, object, k, []byte(v)) + err = p.meta.StoreAttribute(bucket, object, fmt.Sprintf("%v.%v", metaHdr, k), []byte(v)) if err != nil { // cleanup object if returning error os.Remove(objname) @@ -517,6 +588,54 @@ func (p *Posix) CompleteMultipartUpload(ctx context.Context, input *s3.CompleteM } } + // load and set tagging + tagging, err := p.meta.RetrieveAttribute(bucket, upiddir, tagHdr) + if err == nil { + if err := p.meta.StoreAttribute(bucket, object, tagHdr, tagging); err != nil { + // cleanup object + os.Remove(objname) + return nil, fmt.Errorf("set object tagging: %w", err) + } + } + if err != nil && !errors.Is(err, meta.ErrNoSuchKey) { + return nil, fmt.Errorf("get object tagging: %w", err) + } + + // set content-type + if cType != "" { + if err := p.meta.StoreAttribute(bucket, object, contentTypeHdr, []byte(cType)); err != nil { + // cleanup object + os.Remove(objname) + return nil, fmt.Errorf("set object content type: %w", err) + } + } + + // load and set legal hold + lHold, err := p.meta.RetrieveAttribute(bucket, upiddir, objectLegalHoldKey) + if err == nil { + if err := p.meta.StoreAttribute(bucket, object, objectLegalHoldKey, lHold); err != nil { + // cleanup object + os.Remove(objname) + return nil, fmt.Errorf("set object legal hold: %w", err) + } + } + if err != nil && !errors.Is(err, meta.ErrNoSuchKey) { + return nil, fmt.Errorf("get object legal hold: %w", err) + } + + // load and set retention + ret, err := p.meta.RetrieveAttribute(bucket, upiddir, objectRetentionKey) + if err == nil { + if err := p.meta.StoreAttribute(bucket, object, objectRetentionKey, ret); err != nil { + // cleanup object + os.Remove(objname) + return nil, fmt.Errorf("set object retention: %w", err) + } + } + if err != nil && !errors.Is(err, meta.ErrNoSuchKey) { + return nil, fmt.Errorf("get object retention: %w", err) + } + // Calculate s3 compatible md5sum for complete multipart. s3MD5 := backend.GetMultipartMD5(parts) diff --git a/s3api/controllers/base.go b/s3api/controllers/base.go index e467ecac..1b179f1f 100644 --- a/s3api/controllers/base.go +++ b/s3api/controllers/base.go @@ -1821,45 +1821,9 @@ func (c S3ApiController) PutActions(ctx *fiber.Ctx) error { }) } - legalHoldHdr := ctx.Get("X-Amz-Object-Lock-Legal-Hold") - objLockModeHdr := ctx.Get("X-Amz-Object-Lock-Mode") - objLockDate := ctx.Get("X-Amz-Object-Lock-Retain-Until-Date") - - if (objLockDate != "" && objLockModeHdr == "") || (objLockDate == "" && objLockModeHdr != "") { - return SendResponse(ctx, s3err.GetAPIError(s3err.ErrObjectLockInvalidHeaders), - &MetaOpts{ - Logger: c.logger, - Action: "PutObject", - BucketOwner: parsedAcl.Owner, - }) - } - - var retainUntilDate *time.Time - if objLockDate != "" { - rDate, err := time.Parse(time.RFC3339, objLockDate) - if err != nil { - return SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidRequest), - &MetaOpts{ - Logger: c.logger, - Action: "PutObject", - BucketOwner: parsedAcl.Owner, - }) - } - if rDate.Before(time.Now()) { - return SendResponse(ctx, s3err.GetAPIError(s3err.ErrPastObjectLockRetainDate), - &MetaOpts{ - Logger: c.logger, - Action: "PutObject", - BucketOwner: parsedAcl.Owner, - }) - } - retainUntilDate = &rDate - } - - if objLockModeHdr != "" && - objLockModeHdr != string(types.ObjectLockModeCompliance) && - objLockModeHdr != string(types.ObjectLockModeGovernance) { - return SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidRequest), + objLock, err := utils.ParsObjectLockHdrs(ctx) + if err != nil { + return SendResponse(ctx, err, &MetaOpts{ Logger: c.logger, Action: "PutObject", @@ -1884,9 +1848,9 @@ func (c S3ApiController) PutActions(ctx *fiber.Ctx) error { Metadata: metadata, Body: body, Tagging: &tagging, - ObjectLockRetainUntilDate: retainUntilDate, - ObjectLockMode: types.ObjectLockMode(objLockModeHdr), - ObjectLockLegalHoldStatus: types.ObjectLockLegalHoldStatus(legalHoldHdr), + ObjectLockRetainUntilDate: &objLock.RetainUntilDate, + ObjectLockMode: objLock.ObjectLockMode, + ObjectLockLegalHoldStatus: objLock.LegalHoldStatus, }) ctx.Response().Header.Set("ETag", etag) return SendResponse(ctx, err, @@ -2406,6 +2370,8 @@ func (c S3ApiController) CreateActions(ctx *fiber.Ctx) error { acct := ctx.Locals("account").(auth.Account) isRoot := ctx.Locals("isRoot").(bool) parsedAcl := ctx.Locals("parsedAcl").(auth.ACL) + contentType := ctx.Get("Content-Type") + tagging := ctx.Get("X-Amz-Tagging") if keyEnd != "" { key = strings.Join([]string{key, keyEnd}, "/") @@ -2607,10 +2573,28 @@ func (c S3ApiController) CreateActions(ctx *fiber.Ctx) error { }) } + objLockState, err := utils.ParsObjectLockHdrs(ctx) + if err != nil { + return SendXMLResponse(ctx, nil, err, + &MetaOpts{ + Logger: c.logger, + Action: "CreateMultipartUpload", + BucketOwner: parsedAcl.Owner, + }) + } + + metadata := utils.GetUserMetaData(&ctx.Request().Header) + res, err := c.be.CreateMultipartUpload(ctx.Context(), &s3.CreateMultipartUploadInput{ - Bucket: &bucket, - Key: &key, + Bucket: &bucket, + Key: &key, + Tagging: &tagging, + ContentType: &contentType, + ObjectLockRetainUntilDate: &objLockState.RetainUntilDate, + ObjectLockMode: objLockState.ObjectLockMode, + ObjectLockLegalHoldStatus: objLockState.LegalHoldStatus, + Metadata: metadata, }) return SendXMLResponse(ctx, res, err, &MetaOpts{ diff --git a/s3api/utils/utils.go b/s3api/utils/utils.go index 7c5fd590..1c6a22a9 100644 --- a/s3api/utils/utils.go +++ b/s3api/utils/utils.go @@ -278,3 +278,51 @@ func ParseObjectAttributes(ctx *fiber.Ctx) map[types.ObjectAttributes]struct{} { return attrs } + +type objLockCfg struct { + RetainUntilDate time.Time + ObjectLockMode types.ObjectLockMode + LegalHoldStatus types.ObjectLockLegalHoldStatus +} + +func ParsObjectLockHdrs(ctx *fiber.Ctx) (*objLockCfg, error) { + legalHoldHdr := ctx.Get("X-Amz-Object-Lock-Legal-Hold") + objLockModeHdr := ctx.Get("X-Amz-Object-Lock-Mode") + objLockDate := ctx.Get("X-Amz-Object-Lock-Retain-Until-Date") + + if (objLockDate != "" && objLockModeHdr == "") || (objLockDate == "" && objLockModeHdr != "") { + return nil, s3err.GetAPIError(s3err.ErrObjectLockInvalidHeaders) + } + + var retainUntilDate time.Time + if objLockDate != "" { + rDate, err := time.Parse(time.RFC3339, objLockDate) + if err != nil { + return nil, s3err.GetAPIError(s3err.ErrInvalidRequest) + } + if rDate.Before(time.Now()) { + return nil, s3err.GetAPIError(s3err.ErrPastObjectLockRetainDate) + } + retainUntilDate = rDate + } + + objLockMode := types.ObjectLockMode(objLockModeHdr) + + if objLockMode != "" && + objLockMode != types.ObjectLockModeCompliance && + objLockMode != types.ObjectLockModeGovernance { + return nil, s3err.GetAPIError(s3err.ErrInvalidRequest) + } + + legalHold := types.ObjectLockLegalHoldStatus(legalHoldHdr) + + if legalHold != "" && legalHold != types.ObjectLockLegalHoldStatusOff && legalHold != types.ObjectLockLegalHoldStatusOn { + return nil, s3err.GetAPIError(s3err.ErrInvalidRequest) + } + + return &objLockCfg{ + RetainUntilDate: retainUntilDate, + ObjectLockMode: objLockMode, + LegalHoldStatus: legalHold, + }, nil +} diff --git a/tests/integration/group-tests.go b/tests/integration/group-tests.go index 79876d87..6c267dd5 100644 --- a/tests/integration/group-tests.go +++ b/tests/integration/group-tests.go @@ -200,6 +200,14 @@ func TestDeleteObjectTagging(s *S3Conf) { func TestCreateMultipartUpload(s *S3Conf) { CreateMultipartUpload_non_existing_bucket(s) + CreateMultipartUpload_with_metadata(s) + CreateMultipartUpload_with_invalid_tagging(s) + CreateMultipartUpload_with_tagging(s) + CreateMultipartUpload_with_content_type(s) + CreateMultipartUpload_with_object_lock(s) + CreateMultipartUpload_with_object_lock_not_enabled(s) + CreateMultipartUpload_with_object_lock_invalid_retention(s) + CreateMultipartUpload_past_retain_until_date(s) CreateMultipartUpload_success(s) } @@ -564,6 +572,14 @@ func GetIntTests() IntTests { "DeleteObjectTagging_success_status": DeleteObjectTagging_success_status, "DeleteObjectTagging_success": DeleteObjectTagging_success, "CreateMultipartUpload_non_existing_bucket": CreateMultipartUpload_non_existing_bucket, + "CreateMultipartUpload_with_metadata": CreateMultipartUpload_with_metadata, + "CreateMultipartUpload_with_invalid_tagging": CreateMultipartUpload_with_invalid_tagging, + "CreateMultipartUpload_with_tagging": CreateMultipartUpload_with_tagging, + "CreateMultipartUpload_with_content_type": CreateMultipartUpload_with_content_type, + "CreateMultipartUpload_with_object_lock": CreateMultipartUpload_with_object_lock, + "CreateMultipartUpload_with_object_lock_not_enabled": CreateMultipartUpload_with_object_lock_not_enabled, + "CreateMultipartUpload_with_object_lock_invalid_retention": CreateMultipartUpload_with_object_lock_invalid_retention, + "CreateMultipartUpload_past_retain_until_date": CreateMultipartUpload_past_retain_until_date, "CreateMultipartUpload_success": CreateMultipartUpload_success, "UploadPart_non_existing_bucket": UploadPart_non_existing_bucket, "UploadPart_invalid_part_number": UploadPart_invalid_part_number, diff --git a/tests/integration/tests.go b/tests/integration/tests.go index c54ad40c..bdf0f105 100644 --- a/tests/integration/tests.go +++ b/tests/integration/tests.go @@ -4278,6 +4278,359 @@ func CreateMultipartUpload_non_existing_bucket(s *S3Conf) error { }) } +func CreateMultipartUpload_with_metadata(s *S3Conf) error { + testName := "CreateMultipartUpload_with_metadata" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + obj := "my-obj" + meta := map[string]string{ + "prop1": "val1", + "prop2": "val2", + } + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + out, err := s3client.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{ + Bucket: &bucket, + Key: &obj, + Metadata: meta, + }) + cancel() + if err != nil { + return err + } + + parts, err := uploadParts(s3client, 100, 1, bucket, obj, *out.UploadId) + if err != nil { + return err + } + + compParts := []types.CompletedPart{} + for _, el := range parts { + compParts = append(compParts, types.CompletedPart{ + ETag: el.ETag, + PartNumber: el.PartNumber, + }) + } + + ctx, cancel = context.WithTimeout(context.Background(), shortTimeout) + _, err = s3client.CompleteMultipartUpload(ctx, &s3.CompleteMultipartUploadInput{ + Bucket: &bucket, + Key: &obj, + UploadId: out.UploadId, + MultipartUpload: &types.CompletedMultipartUpload{ + Parts: compParts, + }, + }) + cancel() + if err != nil { + return err + } + + ctx, cancel = context.WithTimeout(context.Background(), shortTimeout) + resp, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{ + Bucket: &bucket, + Key: &obj, + }) + cancel() + if err != nil { + return err + } + + if !areMapsSame(resp.Metadata, meta) { + return fmt.Errorf("expected uploaded object metadata to be %v, instead got %v", meta, resp.Metadata) + } + + return nil + }) +} + +func CreateMultipartUpload_with_content_type(s *S3Conf) error { + testName := "CreateMultipartUpload_with_content_type" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + obj := "my-obj" + cType := "application/octet-stream" + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + out, err := s3client.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{ + Bucket: &bucket, + Key: &obj, + ContentType: &cType, + }) + cancel() + if err != nil { + return err + } + + parts, err := uploadParts(s3client, 100, 1, bucket, obj, *out.UploadId) + if err != nil { + return err + } + + compParts := []types.CompletedPart{} + for _, el := range parts { + compParts = append(compParts, types.CompletedPart{ + ETag: el.ETag, + PartNumber: el.PartNumber, + }) + } + + ctx, cancel = context.WithTimeout(context.Background(), shortTimeout) + _, err = s3client.CompleteMultipartUpload(ctx, &s3.CompleteMultipartUploadInput{ + Bucket: &bucket, + Key: &obj, + UploadId: out.UploadId, + MultipartUpload: &types.CompletedMultipartUpload{ + Parts: compParts, + }, + }) + cancel() + if err != nil { + return err + } + + ctx, cancel = context.WithTimeout(context.Background(), shortTimeout) + resp, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{ + Bucket: &bucket, + Key: &obj, + }) + cancel() + if err != nil { + return err + } + + if *resp.ContentType != cType { + return fmt.Errorf("expected uploaded object content-type to be %v, instead got %v", cType, *resp.ContentType) + } + + return nil + }) +} + +func CreateMultipartUpload_with_object_lock(s *S3Conf) error { + testName := "CreateMultipartUpload_with_object_lock" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + obj := "my-obj" + retainUntilDate := time.Now().Add(24 * time.Hour) + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + out, err := s3client.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{ + Bucket: &bucket, + Key: &obj, + ObjectLockLegalHoldStatus: types.ObjectLockLegalHoldStatusOn, + ObjectLockMode: types.ObjectLockModeGovernance, + ObjectLockRetainUntilDate: &retainUntilDate, + }) + cancel() + if err != nil { + return err + } + + parts, err := uploadParts(s3client, 100, 1, bucket, obj, *out.UploadId) + if err != nil { + return err + } + + compParts := []types.CompletedPart{} + for _, el := range parts { + compParts = append(compParts, types.CompletedPart{ + ETag: el.ETag, + PartNumber: el.PartNumber, + }) + } + + ctx, cancel = context.WithTimeout(context.Background(), shortTimeout) + _, err = s3client.CompleteMultipartUpload(ctx, &s3.CompleteMultipartUploadInput{ + Bucket: &bucket, + Key: &obj, + UploadId: out.UploadId, + MultipartUpload: &types.CompletedMultipartUpload{ + Parts: compParts, + }, + }) + cancel() + if err != nil { + return err + } + + ctx, cancel = context.WithTimeout(context.Background(), shortTimeout) + resp, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{ + Bucket: &bucket, + Key: &obj, + }) + cancel() + if err != nil { + return err + } + + if resp.ObjectLockLegalHoldStatus != types.ObjectLockLegalHoldStatusOn { + return fmt.Errorf("expected uploaded object legal hold status to be %v, instead got %v", types.ObjectLockLegalHoldStatusOn, resp.ObjectLockLegalHoldStatus) + } + if resp.ObjectLockMode != types.ObjectLockModeGovernance { + return fmt.Errorf("expected uploaded object lock mode to be %v, instead got %v", types.ObjectLockModeGovernance, resp.ObjectLockMode) + } + + return nil + }, withLock()) +} + +func CreateMultipartUpload_with_object_lock_not_enabled(s *S3Conf) error { + testName := "CreateMultipartUpload_with_object_lock_not_enabled" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + obj := "my-obj" + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := s3client.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{ + Bucket: &bucket, + Key: &obj, + ObjectLockLegalHoldStatus: types.ObjectLockLegalHoldStatusOn, + }) + cancel() + if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidBucketObjectLockConfiguration)); err != nil { + return err + } + + return nil + }) +} + +func CreateMultipartUpload_with_object_lock_invalid_retention(s *S3Conf) error { + testName := "CreateMultipartUpload_with_object_lock_invalid_retention" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + obj := "my-obj" + retentionDate := time.Now().Add(24 * time.Hour) + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := s3client.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{ + Bucket: &bucket, + Key: &obj, + ObjectLockMode: types.ObjectLockModeGovernance, + }) + cancel() + if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrObjectLockInvalidHeaders)); err != nil { + return err + } + + ctx, cancel = context.WithTimeout(context.Background(), shortTimeout) + _, err = s3client.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{ + Bucket: &bucket, + Key: &obj, + ObjectLockRetainUntilDate: &retentionDate, + }) + cancel() + if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrObjectLockInvalidHeaders)); err != nil { + return err + } + + return nil + }) +} + +func CreateMultipartUpload_past_retain_until_date(s *S3Conf) error { + testName := "CreateMultipartUpload_past_retain_until_date" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + obj := "my-obj" + rDate := time.Now().Add(-5 * time.Hour) + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := s3client.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{ + Bucket: &bucket, + Key: &obj, + ObjectLockMode: types.ObjectLockModeGovernance, + ObjectLockRetainUntilDate: &rDate, + }) + cancel() + if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrPastObjectLockRetainDate)); err != nil { + return err + } + + return nil + }) +} + +func CreateMultipartUpload_with_invalid_tagging(s *S3Conf) error { + testName := "CreateMultipartUpload_with_invalid_tagging" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + obj := "my-obj" + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := s3client.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{ + Bucket: &bucket, + Key: &obj, + Tagging: getPtr("invalid_tag"), + }) + cancel() + if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidTag)); err != nil { + return err + } + + return nil + }) +} + +func CreateMultipartUpload_with_tagging(s *S3Conf) error { + testName := "CreateMultipartUpload_with_tagging" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + obj := "my-obj" + tagging := "key1=val1&key2=val2" + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + out, err := s3client.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{ + Bucket: &bucket, + Key: &obj, + Tagging: &tagging, + }) + cancel() + if err != nil { + return err + } + + parts, err := uploadParts(s3client, 100, 1, bucket, obj, *out.UploadId) + if err != nil { + return err + } + + compParts := []types.CompletedPart{} + for _, el := range parts { + compParts = append(compParts, types.CompletedPart{ + ETag: el.ETag, + PartNumber: el.PartNumber, + }) + } + + ctx, cancel = context.WithTimeout(context.Background(), shortTimeout) + _, err = s3client.CompleteMultipartUpload(ctx, &s3.CompleteMultipartUploadInput{ + Bucket: &bucket, + Key: &obj, + UploadId: out.UploadId, + MultipartUpload: &types.CompletedMultipartUpload{ + Parts: compParts, + }, + }) + cancel() + if err != nil { + return err + } + + ctx, cancel = context.WithTimeout(context.Background(), shortTimeout) + resp, err := s3client.GetObjectTagging(ctx, &s3.GetObjectTaggingInput{ + Bucket: &bucket, + Key: &obj, + }) + cancel() + if err != nil { + return err + } + + expectedOutput := []types.Tag{ + { + Key: getPtr("key1"), + Value: getPtr("val1"), + }, + { + Key: getPtr("key2"), + Value: getPtr("val2"), + }, + } + + if !areTagsSame(resp.TagSet, expectedOutput) { + return fmt.Errorf("expected object tagging to be %v, instead got %v", expectedOutput, resp.TagSet) + } + + return nil + }) +} + func CreateMultipartUpload_success(s *S3Conf) error { testName := "CreateMultipartUpload_success" return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {