From 9cb357ecc52f70a1128e5b01dad61ee530de81a1 Mon Sep 17 00:00:00 2001 From: Jon Austin <62040422+jonaustin09@users.noreply.github.com> Date: Tue, 26 Sep 2023 18:09:09 -0700 Subject: [PATCH] CopyObject metadata (#265) * fix: Object tag actions cleanup * fix: Fixes #249, Changed ListObjects default max-keys from -1 to 1000 * fix: Fixes #250, Added support to provide a marker not from the objects list and list the objects after the provided marker in ListObjects(V2) actions * feat: Closes #256, Addded a check step, to compare object metadatas and allow the copying to itself, if the metadata has been changed * fix: Simplified range assignment in CopyObject posix function --- backend/posix/posix.go | 39 +++++++++++++++++++++++++++++++------ integration/action-tests.go | 1 + integration/tests.go | 26 +++++++++++++++++++++++++ s3api/controllers/base.go | 4 ++++ 4 files changed, 64 insertions(+), 6 deletions(-) diff --git a/backend/posix/posix.go b/backend/posix/posix.go index c7f1d86..729816c 100644 --- a/backend/posix/posix.go +++ b/backend/posix/posix.go @@ -449,6 +449,20 @@ func loadUserMetaData(path string, m map[string]string) (contentType, contentEnc return } +func compareUserMetadata(meta1, meta2 map[string]string) bool { + if len(meta1) != len(meta2) { + return false + } + + for key, val := range meta1 { + if meta2[key] != val { + return false + } + } + + return true +} + func isValidMeta(val string) bool { if strings.HasPrefix(val, "user."+metaHdr) { return true @@ -1243,10 +1257,6 @@ func (p *Posix) CopyObject(ctx context.Context, input *s3.CopyObjectInput) (*s3. dstObject := *input.Key owner := *input.ExpectedBucketOwner - if fmt.Sprintf("%v/%v", srcBucket, srcObject) == fmt.Sprintf("%v/%v", dstBucket, dstObject) { - return &s3.CopyObjectOutput{}, s3err.GetAPIError(s3err.ErrInvalidCopyDest) - } - _, err := os.Stat(srcBucket) if errors.Is(err, fs.ErrNotExist) { return nil, s3err.GetAPIError(s3err.ErrNoSuchBucket) @@ -1294,12 +1304,29 @@ func (p *Posix) CopyObject(ctx context.Context, input *s3.CopyObjectInput) (*s3. return nil, fmt.Errorf("stat object: %w", err) } - etag, err := p.PutObject(ctx, &s3.PutObjectInput{Bucket: &dstBucket, Key: &dstObject, Body: f, ContentLength: fInfo.Size()}) + meta := make(map[string]string) + loadUserMetaData(objPath, meta) + + dstObjdPath := filepath.Join(dstBucket, dstObject) + if dstObjdPath == objPath { + if compareUserMetadata(meta, input.Metadata) { + return &s3.CopyObjectOutput{}, s3err.GetAPIError(s3err.ErrInvalidCopyDest) + } else { + for key := range meta { + xattr.Remove(dstObjdPath, key) + } + for k, v := range input.Metadata { + xattr.Set(dstObjdPath, fmt.Sprintf("user.%v.%v", metaHdr, k), []byte(v)) + } + } + } + + etag, err := p.PutObject(ctx, &s3.PutObjectInput{Bucket: &dstBucket, Key: &dstObject, Body: f, ContentLength: fInfo.Size(), Metadata: meta}) if err != nil { return nil, err } - fi, err := os.Stat(filepath.Join(dstBucket, dstObject)) + fi, err := os.Stat(dstObjdPath) if err != nil { return nil, fmt.Errorf("stat dst object: %w", err) } diff --git a/integration/action-tests.go b/integration/action-tests.go index ab74520..8a0694f 100644 --- a/integration/action-tests.go +++ b/integration/action-tests.go @@ -95,6 +95,7 @@ func TestCopyObject(s *S3Conf) { CopyObject_non_existing_dst_bucket(s) CopyObject_not_owned_source_bucket(s) CopyObject_copy_to_itself(s) + CopyObject_to_itself_with_new_metadata(s) CopyObject_success(s) } diff --git a/integration/tests.go b/integration/tests.go index c561cbb..da6b70a 100644 --- a/integration/tests.go +++ b/integration/tests.go @@ -1904,6 +1904,32 @@ func CopyObject_copy_to_itself(s *S3Conf) { }) } +func CopyObject_to_itself_with_new_metadata(s *S3Conf) { + testName := "CopyObject_to_itself_with_new_metadata" + actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + obj := "my-obj" + err := putObjects(s3client, []string{obj}, bucket) + if err != nil { + return err + } + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err = s3client.CopyObject(ctx, &s3.CopyObjectInput{ + Bucket: &bucket, + Key: &obj, + CopySource: getPtr(fmt.Sprintf("%v/%v", bucket, obj)), + Metadata: map[string]string{ + "Hello": "World", + }, + }) + cancel() + if err != nil { + return err + } + + return nil + }) +} + func CopyObject_success(s *S3Conf) { testName := "CopyObject_success" actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { diff --git a/s3api/controllers/base.go b/s3api/controllers/base.go index 4026d9c..f64820b 100644 --- a/s3api/controllers/base.go +++ b/s3api/controllers/base.go @@ -593,6 +593,9 @@ func (c S3ApiController) PutActions(ctx *fiber.Ctx) error { return SendXMLResponse(ctx, nil, s3err.GetAPIError(s3err.ErrInvalidCopySource), &MetaOpts{Logger: c.logger, Action: "CopyObject", BucketOwner: parsedAcl.Owner}) } } + + metadata := utils.GetUserMetaData(&ctx.Request().Header) + res, err := c.be.CopyObject(ctx.Context(), &s3.CopyObjectInput{ Bucket: &bucket, Key: &keyStart, @@ -602,6 +605,7 @@ func (c S3ApiController) PutActions(ctx *fiber.Ctx) error { CopySourceIfModifiedSince: &mtime, CopySourceIfUnmodifiedSince: &umtime, ExpectedBucketOwner: &access, + Metadata: metadata, }) if err == nil { return SendXMLResponse(ctx, res, err, &MetaOpts{