feat: Implements object meta properties for CopyObject in azure and posix backends.

Fixes #998
Closes #1125
Closes #1126
Closes #1127

Implements objects meta properties(Content-Disposition, Content-Language, Content-Encoding, Cache-Control, Expires) and tagging besed on the directives(metadata, tagging) in CopyObject in posix and azure backends. The properties/tagging should be coppied from the source object if "COPY" directive is provided and it should be replaced otherwise.

Changes the object copy principle in azure: instead of using the `CopyFromURL` method from azure sdk, it first loads the object then creates one, to be able to compare and store the meta properties.
This commit is contained in:
niksis02
2025-03-17 09:37:05 -07:00
committed by Ben McClelland
parent 38768a92b8
commit cfb2d6d87d
13 changed files with 819 additions and 61 deletions
+12
View File
@@ -266,9 +266,15 @@ func TestCopyObject(s *S3Conf) {
CopyObject_not_owned_source_bucket(s)
CopyObject_copy_to_itself(s)
CopyObject_copy_to_itself_invalid_directive(s)
CopyObject_should_copy_tagging(s)
CopyObject_invalid_tagging_directive(s)
CopyObject_to_itself_with_new_metadata(s)
CopyObject_CopySource_starting_with_slash(s)
CopyObject_non_existing_dir_object(s)
CopyObject_should_copy_meta_props(s)
CopyObject_should_replace_meta_props(s)
CopyObject_with_legal_hold(s)
CopyObject_with_retention_lock(s)
//TODO: remove the condition after implementing checksums in azure
if !s.azureTests {
CopyObject_invalid_checksum_algorithm(s)
@@ -909,9 +915,15 @@ func GetIntTests() IntTests {
"CopyObject_not_owned_source_bucket": CopyObject_not_owned_source_bucket,
"CopyObject_copy_to_itself": CopyObject_copy_to_itself,
"CopyObject_copy_to_itself_invalid_directive": CopyObject_copy_to_itself_invalid_directive,
"CopyObject_should_copy_tagging": CopyObject_should_copy_tagging,
"CopyObject_invalid_tagging_directive": CopyObject_invalid_tagging_directive,
"CopyObject_to_itself_with_new_metadata": CopyObject_to_itself_with_new_metadata,
"CopyObject_CopySource_starting_with_slash": CopyObject_CopySource_starting_with_slash,
"CopyObject_non_existing_dir_object": CopyObject_non_existing_dir_object,
"CopyObject_should_copy_meta_props": CopyObject_should_copy_meta_props,
"CopyObject_should_replace_meta_props": CopyObject_should_replace_meta_props,
"CopyObject_with_legal_hold": CopyObject_with_legal_hold,
"CopyObject_with_retention_lock": CopyObject_with_retention_lock,
"CopyObject_invalid_checksum_algorithm": CopyObject_invalid_checksum_algorithm,
"CopyObject_create_checksum_on_copy": CopyObject_create_checksum_on_copy,
"CopyObject_should_copy_the_existing_checksum": CopyObject_should_copy_the_existing_checksum,
+339
View File
@@ -5880,6 +5880,131 @@ func CopyObject_copy_to_itself_invalid_directive(s *S3Conf) error {
})
}
func CopyObject_invalid_tagging_directive(s *S3Conf) error {
testName := "CopyObject_invalid_tagging_directive"
return 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)),
TaggingDirective: types.TaggingDirective("invalid"),
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidTaggingDirective)); err != nil {
return err
}
return nil
})
}
func CopyObject_should_copy_tagging(s *S3Conf) error {
testName := "CopyObject_should_copy_tagging"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
srcObj, dstObj := "source-object", "dest-object"
tagging := "foo=bar&baz=quxx"
_, err := putObjectWithData(100, &s3.PutObjectInput{
Bucket: &bucket,
Key: &srcObj,
Tagging: &tagging,
}, s3client)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: &bucket,
Key: &dstObj,
CopySource: getPtr(fmt.Sprintf("%v/%v", bucket, srcObj)),
})
cancel()
if err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.GetObjectTagging(ctx, &s3.GetObjectTaggingInput{
Bucket: &bucket,
Key: &dstObj,
})
cancel()
if err != nil {
return err
}
expectedTagSet := []types.Tag{
{Key: getPtr("foo"), Value: getPtr("bar")},
{Key: getPtr("baz"), Value: getPtr("quxx")},
}
if !areTagsSame(res.TagSet, expectedTagSet) {
return fmt.Errorf("expected the tag set to be %v, instead got %v", expectedTagSet, res.TagSet)
}
return nil
})
}
func CopyObject_should_reaplace_tagging(s *S3Conf) error {
testName := "CopyObject_should_reaplace_tagging"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
srcObj, dstObj := "source-object", "dest-object"
tagging := "foo=bar&baz=quxx"
_, err := putObjectWithData(100, &s3.PutObjectInput{
Bucket: &bucket,
Key: &srcObj,
Tagging: &tagging,
}, s3client)
if err != nil {
return err
}
copyTagging := "key1=val1&key2=val2"
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: &bucket,
Key: &dstObj,
CopySource: getPtr(fmt.Sprintf("%v/%v", bucket, srcObj)),
TaggingDirective: types.TaggingDirectiveReplace,
Tagging: &copyTagging,
})
cancel()
if err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.GetObjectTagging(ctx, &s3.GetObjectTaggingInput{
Bucket: &bucket,
Key: &dstObj,
})
cancel()
if err != nil {
return err
}
expectedTagSet := []types.Tag{
{Key: getPtr("key1"), Value: getPtr("val1")},
{Key: getPtr("key2"), Value: getPtr("val2")},
}
if !areTagsSame(res.TagSet, expectedTagSet) {
return fmt.Errorf("expected the tag set to be %v, instead got %v", expectedTagSet, res.TagSet)
}
return nil
})
}
func CopyObject_to_itself_with_new_metadata(s *S3Conf) error {
testName := "CopyObject_to_itself_with_new_metadata"
@@ -6055,6 +6180,220 @@ func CopyObject_non_existing_dir_object(s *S3Conf) error {
})
}
func CopyObject_should_copy_meta_props(s *S3Conf) error {
testName := "CopyObject_should_copy_meta_props"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
srcObj, dstObj := "source-object", "dest-object"
cType, cEnc, cDesp, cLang, cLength := "application/json", "base64", "test-desp", "us", int64(100)
cacheControl, expires := "no-cache", time.Now().Add(time.Hour*10)
meta := map[string]string{
"foo": "bar",
"baz": "quxx",
}
_, err := putObjectWithData(cLength, &s3.PutObjectInput{
Bucket: &bucket,
Key: &srcObj,
ContentDisposition: &cDesp,
ContentEncoding: &cEnc,
ContentLanguage: &cLang,
ContentType: &cType,
CacheControl: &cacheControl,
Expires: &expires,
Metadata: meta,
}, s3client)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: &bucket,
Key: &dstObj,
CopySource: getPtr(bucket + "/" + srcObj),
})
cancel()
if err != nil {
return err
}
return checkObjectMetaProps(s3client, bucket, dstObj, ObjectMetaProps{
ContentLength: cLength,
ContentType: cType,
ContentEncoding: cEnc,
ContentDisposition: cDesp,
ContentLanguage: cLang,
CacheControl: cacheControl,
ExpiresString: expires.UTC().Format(timefmt),
Metadata: meta,
})
})
}
func CopyObject_should_replace_meta_props(s *S3Conf) error {
testName := "CopyObject_should_replace_meta_props"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
srcObj, dstObj := "source-object", "dest-object"
expire := time.Now().Add(time.Minute * 10)
contentLength := int64(200)
_, err := putObjectWithData(contentLength, &s3.PutObjectInput{
Bucket: &bucket,
Key: &srcObj,
ContentDisposition: getPtr("test"),
ContentEncoding: getPtr("test"),
ContentLanguage: getPtr("test"),
ContentType: getPtr("test"),
CacheControl: getPtr("test"),
Expires: &expire,
Metadata: map[string]string{
"key": "val",
},
}, s3client)
if err != nil {
return err
}
cType, cEnc, cDesp, cLang := "application/binary", "hex", "desp", "mex"
cacheControl, expires := "no-cache", time.Now().Add(time.Hour*10)
meta := map[string]string{
"foo": "bar",
"baz": "quxx",
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: &bucket,
Key: &dstObj,
CopySource: getPtr(bucket + "/" + srcObj),
MetadataDirective: types.MetadataDirectiveReplace,
ContentDisposition: &cDesp,
ContentEncoding: &cEnc,
ContentLanguage: &cLang,
ContentType: &cType,
CacheControl: &cacheControl,
Expires: &expires,
Metadata: meta,
})
cancel()
if err != nil {
return err
}
return checkObjectMetaProps(s3client, bucket, dstObj, ObjectMetaProps{
ContentLength: contentLength,
ContentType: cType,
ContentEncoding: cEnc,
ContentDisposition: cDesp,
ContentLanguage: cLang,
CacheControl: cacheControl,
ExpiresString: expires.UTC().Format(timefmt),
Metadata: meta,
})
})
}
func CopyObject_with_legal_hold(s *S3Conf) error {
testName := "CopyObject_with_legal_hold"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
srcObj, dstObj := "source-object", "dst-object"
_, err := putObjectWithData(100, &s3.PutObjectInput{
Bucket: &bucket,
Key: &srcObj,
}, s3client)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: &bucket,
Key: &dstObj,
CopySource: getPtr(fmt.Sprintf("%v/%v", bucket, srcObj)),
ObjectLockLegalHoldStatus: types.ObjectLockLegalHoldStatusOn,
})
cancel()
if err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.GetObjectLegalHold(ctx, &s3.GetObjectLegalHoldInput{
Bucket: &bucket,
Key: &dstObj,
})
cancel()
if err != nil {
return err
}
if res.LegalHold.Status != types.ObjectLockLegalHoldStatusOn {
return fmt.Errorf("expected the copied object legal hold status to be %v, instead got %v", types.ObjectLockLegalHoldStatusOn, res.LegalHold.Status)
}
err = changeBucketObjectLockStatus(s3client, bucket, false)
if err != nil {
return err
}
return nil
}, withLock())
}
func CopyObject_with_retention_lock(s *S3Conf) error {
testName := "CopyObject_with_retention_lock"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
srcObj, dstObj := "source-object", "dst-object"
_, err := putObjectWithData(200, &s3.PutObjectInput{
Bucket: &bucket,
Key: &srcObj,
}, s3client)
if err != nil {
return err
}
retDate := time.Now().Add(time.Hour * 7)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: &bucket,
Key: &dstObj,
CopySource: getPtr(fmt.Sprintf("%v/%v", bucket, srcObj)),
ObjectLockMode: types.ObjectLockModeGovernance,
ObjectLockRetainUntilDate: &retDate,
})
cancel()
if err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.GetObjectRetention(ctx, &s3.GetObjectRetentionInput{
Bucket: &bucket,
Key: &dstObj,
})
cancel()
if err != nil {
return err
}
if res.Retention.Mode != types.ObjectLockRetentionModeGovernance {
return fmt.Errorf("expected the copied object retention mode to be %v, instead got %v", types.ObjectLockRetentionModeGovernance, res.Retention.Mode)
}
if res.Retention.RetainUntilDate.UTC().Unix() != retDate.UTC().Unix() {
return fmt.Errorf("expected the retention date to be %v, instead got %v", retDate.Format(time.RFC1123), res.Retention.RetainUntilDate.Format(time.RFC1123))
}
err = changeBucketObjectLockStatus(s3client, bucket, false)
if err != nil {
return err
}
return nil
}, withLock())
}
func CopyObject_invalid_checksum_algorithm(s *S3Conf) error {
testName := "CopyObject_invalid_checksum_algorithm"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
+58
View File
@@ -1286,6 +1286,64 @@ func compareDelMarkers(d1, d2 []types.DeleteMarkerEntry) bool {
return true
}
type ObjectMetaProps struct {
ContentLength int64
ContentType string
ContentEncoding string
ContentDisposition string
ContentLanguage string
CacheControl string
ExpiresString string
Metadata map[string]string
}
func checkObjectMetaProps(client *s3.Client, bucket, object string, o ObjectMetaProps) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
out, err := client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &object,
})
cancel()
if err != nil {
return err
}
if o.Metadata != nil {
if !areMapsSame(out.Metadata, o.Metadata) {
return fmt.Errorf("expected the object metadata to be %v, instead got %v", o.Metadata, out.Metadata)
}
}
if out.ContentLength == nil {
return fmt.Errorf("expected Content-Length %v, instead got nil", o.ContentLength)
}
if *out.ContentLength != o.ContentLength {
return fmt.Errorf("expected Content-Length %v, instead got %v", o.ContentLength, *out.ContentLength)
}
if o.ContentType != "" && getString(out.ContentType) != o.ContentType {
return fmt.Errorf("expected Content-Type %v, instead got %v", o.ContentType, getString(out.ContentType))
}
if o.ContentDisposition != "" && getString(out.ContentDisposition) != o.ContentDisposition {
return fmt.Errorf("expected Content-Disposition %v, instead got %v", o.ContentDisposition, getString(out.ContentDisposition))
}
if o.ContentEncoding != "" && getString(out.ContentEncoding) != o.ContentEncoding {
return fmt.Errorf("expected Content-Encoding %v, instead got %v", o.ContentEncoding, getString(out.ContentEncoding))
}
if o.ContentLanguage != "" && getString(out.ContentLanguage) != o.ContentLanguage {
return fmt.Errorf("expected Content-Language %v, instead got %v", o.ContentLanguage, getString(out.ContentLanguage))
}
if o.CacheControl != "" && getString(out.CacheControl) != o.CacheControl {
return fmt.Errorf("expected Cache-Control %v, instead got %v", o.CacheControl, getString(out.CacheControl))
}
if o.ExpiresString != "" && getString(out.ExpiresString) != o.ExpiresString {
return fmt.Errorf("expected Expires %v, instead got %v", o.ExpiresString, getString(out.ExpiresString))
}
if out.StorageClass != types.StorageClassStandard {
return fmt.Errorf("expected the storage class to be %v, instead got %v", types.StorageClassStandard, out.StorageClass)
}
return nil
}
func getBoolPtr(b bool) *bool {
return &b
}