This is an automated email from the ASF dual-hosted git repository.

adoroszlai pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ozone.git


The following commit(s) were added to refs/heads/master by this push:
     new e81f5c09d2 HDDS-10645. Support x-amz-metadata-directive in CopyObject 
(#6490)
e81f5c09d2 is described below

commit e81f5c09d216ca0d401977f69a2b6c0728eb3f55
Author: Ivan Andika <[email protected]>
AuthorDate: Wed Apr 10 13:54:52 2024 +0800

    HDDS-10645. Support x-amz-metadata-directive in CopyObject (#6490)
---
 .../dist/src/main/smoketest/s3/objectcopy.robot    | 32 +++++++++--
 .../hadoop/ozone/s3/endpoint/ObjectEndpoint.java   | 22 +++++++-
 .../org/apache/hadoop/ozone/s3/util/S3Consts.java  | 10 ++++
 .../hadoop/ozone/s3/endpoint/TestObjectPut.java    | 63 +++++++++++++++++++++-
 4 files changed, 122 insertions(+), 5 deletions(-)

diff --git a/hadoop-ozone/dist/src/main/smoketest/s3/objectcopy.robot 
b/hadoop-ozone/dist/src/main/smoketest/s3/objectcopy.robot
index af7571d35b..e2bca772bc 100644
--- a/hadoop-ozone/dist/src/main/smoketest/s3/objectcopy.robot
+++ b/hadoop-ozone/dist/src/main/smoketest/s3/objectcopy.robot
@@ -39,26 +39,42 @@ Copy Object Happy Scenario
                         Execute                    date > /tmp/copyfile
     ${file_checksum} =  Execute                    md5sum /tmp/copyfile | awk 
'{print $1}'
 
-    ${result} =         Execute AWSS3ApiCli        put-object --bucket 
${BUCKET} --key ${PREFIX}/copyobject/key=value/f1 --body /tmp/copyfile
+    ${result} =         Execute AWSS3ApiCli        put-object --bucket 
${BUCKET} --key ${PREFIX}/copyobject/key=value/f1 --body /tmp/copyfile 
--metadata="custom-key1=custom-value1,custom-key2=custom-value2,gdprEnabled=true"
     ${eTag} =           Execute and checkrc        echo '${result}' | jq -r 
'.ETag'  0
                         Should Be Equal            ${eTag}           
\"${file_checksum}\"
 
     ${result} =         Execute AWSS3ApiCli        list-objects --bucket 
${BUCKET} --prefix ${PREFIX}/copyobject/key=value/
                         Should contain             ${result}         f1
 
-    ${result} =         Execute AWSS3ApiCli        copy-object --bucket 
${DESTBUCKET} --key ${PREFIX}/copyobject/key=value/f1 --copy-source 
${BUCKET}/${PREFIX}/copyobject/key=value/f1
+    ${result} =         Execute AWSS3ApiCli        copy-object --bucket 
${DESTBUCKET} --key ${PREFIX}/copyobject/key=value/f1 --copy-source 
${BUCKET}/${PREFIX}/copyobject/key=value/f1 
--metadata="custom-key3=custom-value3,custom-key4=custom-value4"
     ${eTag} =           Execute and checkrc        echo '${result}' | jq -r 
'.CopyObjectResult.ETag'  0
                         Should Be Equal            ${eTag}           
\"${file_checksum}\"
 
     ${result} =         Execute AWSS3ApiCli        list-objects --bucket 
${DESTBUCKET} --prefix ${PREFIX}/copyobject/key=value/
                         Should contain             ${result}         f1
+
+    #check that the custom metadata of the source key has been copied to the 
destination key (default copy directive is COPY)
+    ${result} =         Execute AWSS3ApiCli        head-object --bucket 
${BUCKET} --key ${PREFIX}/copyobject/key=value/f1
+                        Should contain             ${result}    
\"custom-key1\": \"custom-value1\"
+                        Should contain             ${result}    
\"custom-key2\": \"custom-value2\"
+                        # COPY directive ignores any metadata specified in the 
copy object request
+                        Should Not contain         ${result}    
\"custom-key3\": \"custom-value3\"
+                        Should Not contain         ${result}    
\"custom-key4\": \"custom-value4\"
+
     #copying again will not throw error
-    ${result} =         Execute AWSS3ApiCli        copy-object --bucket 
${DESTBUCKET} --key ${PREFIX}/copyobject/key=value/f1 --copy-source 
${BUCKET}/${PREFIX}/copyobject/key=value/f1
+    #also uses the REPLACE copy directive
+    ${result} =         Execute AWSS3ApiCli        copy-object --bucket 
${DESTBUCKET} --key ${PREFIX}/copyobject/key=value/f1 --copy-source 
${BUCKET}/${PREFIX}/copyobject/key=value/f1 
--metadata="custom-key3=custom-value3,custom-key4=custom-value4" 
--metadata-directive REPLACE
     ${eTag} =           Execute and checkrc        echo '${result}' | jq -r 
'.CopyObjectResult.ETag'  0
                         Should Be Equal            ${eTag}           
\"${file_checksum}\"
 
     ${result} =         Execute AWSS3ApiCli        list-objects --bucket 
${DESTBUCKET} --prefix ${PREFIX}/copyobject/key=value/
                         Should contain             ${result}         f1
+    ${result} =         Execute AWSS3ApiCli        head-object --bucket 
${DESTBUCKET} --key ${PREFIX}/copyobject/key=value/f1
+                        Should contain             ${result}    
\"custom-key3\": \"custom-value3\"
+                        Should contain             ${result}    
\"custom-key4\": \"custom-value4\"
+                        # REPLACE directive uses the custom metadata specified 
in the request instead of the source key's custom metadata
+                        Should Not contain         ${result}    
\"custom-key1\": \"custom-value1\"
+                        Should Not contain         ${result}    
\"custom-key2\": \"custom-value2\"
 
 Copy Object Where Bucket is not available
     ${result} =         Execute AWSS3APICli and checkrc        copy-object 
--bucket dfdfdfdfdfnonexistent --key ${PREFIX}/copyobject/key=value/f1 
--copy-source ${BUCKET}/${PREFIX}/copyobject/key=value/f1      255
@@ -76,3 +92,13 @@ Copy Object Where both source and dest are same with change 
to storageclass
 Copy Object Where Key not available
     ${result} =         Execute AWSS3APICli and checkrc        copy-object 
--bucket ${DESTBUCKET} --key ${PREFIX}/copyobject/key=value/f1 --copy-source 
${BUCKET}/nonnonexistentkey       255
                         Should contain             ${result}        NoSuchKey
+
+Copy Object using an invalid copy directive
+    ${result} =         Execute AWSS3ApiCli and checkrc        copy-object 
--bucket ${DESTBUCKET} --key ${PREFIX}/copyobject/key=value/f1 --copy-source 
${BUCKET}/${PREFIX}/copyobject/key=value/f1 --metadata-directive INVALID       
255
+                        Should contain             ${result}        
InvalidArgument
+
+Copy Object with user defined metadata size larger than 2 KB
+                                Execute                    echo "Randomtext" > 
/tmp/testfile2
+    ${custom_metadata_value} =  Execute                    printf 'v%.0s' 
{1..3000}
+    ${result} =                 Execute AWSS3ApiCli and checkrc       
copy-object --bucket ${DESTBUCKET} --key ${PREFIX}/copyobject/key=value/f1 
--copy-source ${BUCKET}/${PREFIX}/copyobject/key=value/f1 
--metadata="custom-key1=${custom_metadata_value}" --metadata-directive REPLACE  
     255
+                                Should contain                        
${result}   MetadataTooLarge
\ No newline at end of file
diff --git 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectEndpoint.java
 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectEndpoint.java
index 8b7db9f061..26e51a6d66 100644
--- 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectEndpoint.java
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectEndpoint.java
@@ -126,6 +126,7 @@ import static 
org.apache.hadoop.ozone.s3.exception.S3ErrorTable.NO_SUCH_UPLOAD;
 import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.PRECOND_FAILED;
 import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.newError;
 import static org.apache.hadoop.ozone.s3.util.S3Consts.ACCEPT_RANGE_HEADER;
+import static 
org.apache.hadoop.ozone.s3.util.S3Consts.CUSTOM_METADATA_COPY_DIRECTIVE_HEADER;
 import static 
org.apache.hadoop.ozone.s3.util.S3Consts.DECODED_CONTENT_LENGTH_HEADER;
 import static org.apache.hadoop.ozone.s3.util.S3Consts.CONTENT_RANGE_HEADER;
 import static org.apache.hadoop.ozone.s3.util.S3Consts.COPY_SOURCE_HEADER;
@@ -135,6 +136,7 @@ import static 
org.apache.hadoop.ozone.s3.util.S3Consts.COPY_SOURCE_IF_UNMODIFIED
 import static org.apache.hadoop.ozone.s3.util.S3Consts.RANGE_HEADER;
 import static 
org.apache.hadoop.ozone.s3.util.S3Consts.RANGE_HEADER_SUPPORTED_UNIT;
 import static org.apache.hadoop.ozone.s3.util.S3Consts.STORAGE_CLASS_HEADER;
+import static org.apache.hadoop.ozone.s3.util.S3Consts.CopyDirective;
 import static org.apache.hadoop.ozone.s3.util.S3Utils.urlDecode;
 
 /**
@@ -1208,12 +1210,30 @@ public class ObjectEndpoint extends EndpointBase {
         }
       }
       long sourceKeyLen = sourceKeyDetails.getDataSize();
+
+      // Custom metadata in copyObject with metadata directive
+      Map<String, String> customMetadata;
+      String metadataCopyDirective = 
headers.getHeaderString(CUSTOM_METADATA_COPY_DIRECTIVE_HEADER);
+      if (StringUtils.isEmpty(metadataCopyDirective) || 
metadataCopyDirective.equals(CopyDirective.COPY.name())) {
+        // The custom metadata will be copied from the source key
+        customMetadata = sourceKeyDetails.getMetadata();
+      } else if (metadataCopyDirective.equals(CopyDirective.REPLACE.name())) {
+        // Replace the metadata with the metadata form the request headers
+        customMetadata = 
getCustomMetadataFromHeaders(headers.getRequestHeaders());
+      } else {
+        OS3Exception ex = newError(INVALID_ARGUMENT, metadataCopyDirective);
+        ex.setErrorMessage("An error occurred (InvalidArgument) " +
+            "when calling the CopyObject operation: " +
+            "The metadata directive specified is invalid. Valid values are 
COPY or REPLACE.");
+        throw ex;
+      }
+
       try (OzoneInputStream src = getClientProtocol().getKey(volume.getName(),
           sourceBucket, sourceKey)) {
         getMetrics().updateCopyKeyMetadataStats(startNanos);
         sourceDigestInputStream = new DigestInputStream(src, 
getMessageDigestInstance());
         copy(volume, sourceDigestInputStream, sourceKeyLen, destkey, 
destBucket, replicationConfig,
-                sourceKeyDetails.getMetadata(), perf, startNanos);
+                customMetadata, perf, startNanos);
       }
 
       final OzoneKeyDetails destKeyDetails = getClientProtocol().getKeyDetails(
diff --git 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/S3Consts.java
 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/S3Consts.java
index df3d01936b..3b38ff03c4 100644
--- 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/S3Consts.java
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/S3Consts.java
@@ -62,10 +62,20 @@ public final class S3Consts {
   public static final String S3_XML_NAMESPACE = "http://s3.amazonaws"; +
       ".com/doc/2006-03-01/";
 
+  // Constants related to custom metadata
   public static final String CUSTOM_METADATA_HEADER_PREFIX = "x-amz-meta-";
+  public static final String CUSTOM_METADATA_COPY_DIRECTIVE_HEADER = 
"x-amz-metadata-directive";
 
 
   public static final String DECODED_CONTENT_LENGTH_HEADER =
       "x-amz-decoded-content-length";
 
+  /**
+   * Copy directive for metadata and tags.
+   */
+  public enum CopyDirective {
+    COPY, // Default directive
+    REPLACE
+  }
+
 }
diff --git 
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestObjectPut.java
 
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestObjectPut.java
index 17c3cba304..abae489b41 100644
--- 
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestObjectPut.java
+++ 
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestObjectPut.java
@@ -27,6 +27,8 @@ import java.util.stream.Stream;
 import java.io.OutputStream;
 import java.security.MessageDigest;
 import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MultivaluedHashMap;
+import javax.ws.rs.core.MultivaluedMap;
 import javax.ws.rs.core.Response;
 import org.apache.commons.io.IOUtils;
 import org.apache.commons.lang3.RandomStringUtils;
@@ -56,6 +58,8 @@ import org.junit.jupiter.params.provider.MethodSource;
 import org.mockito.MockedStatic;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static 
org.apache.hadoop.ozone.s3.util.S3Consts.CUSTOM_METADATA_COPY_DIRECTIVE_HEADER;
+import static 
org.apache.hadoop.ozone.s3.util.S3Consts.CUSTOM_METADATA_HEADER_PREFIX;
 import static 
org.apache.hadoop.ozone.s3.util.S3Consts.DECODED_CONTENT_LENGTH_HEADER;
 import static org.apache.hadoop.ozone.s3.util.S3Consts.COPY_SOURCE_HEADER;
 import static org.apache.hadoop.ozone.s3.util.S3Consts.STORAGE_CLASS_HEADER;
@@ -254,6 +258,14 @@ class TestObjectPut {
     ByteArrayInputStream body =
         new ByteArrayInputStream(CONTENT.getBytes(UTF_8));
 
+    // Add some custom metadata
+    MultivaluedMap<String, String> metadataHeaders = new 
MultivaluedHashMap<>();
+    metadataHeaders.putSingle(CUSTOM_METADATA_HEADER_PREFIX + "custom-key-1", 
"custom-value-1");
+    metadataHeaders.putSingle(CUSTOM_METADATA_HEADER_PREFIX + "custom-key-2", 
"custom-value-2");
+    when(headers.getRequestHeaders()).thenReturn(metadataHeaders);
+    // Add COPY metadata directive (default)
+    
when(headers.getHeaderString(CUSTOM_METADATA_COPY_DIRECTIVE_HEADER)).thenReturn("COPY");
+
     Response response = objectEndpoint.put(BUCKET_NAME, KEY_NAME,
         CONTENT.length(), 1, null, body);
 
@@ -268,9 +280,14 @@ class TestObjectPut {
     assertEquals(CONTENT, keyContent);
     assertNotNull(keyDetails.getMetadata());
     assertThat(keyDetails.getMetadata().get(OzoneConsts.ETAG)).isNotEmpty();
+    
assertThat(keyDetails.getMetadata().get("custom-key-1")).isEqualTo("custom-value-1");
+    
assertThat(keyDetails.getMetadata().get("custom-key-2")).isEqualTo("custom-value-2");
 
     String sourceETag = keyDetails.getMetadata().get(OzoneConsts.ETAG);
 
+    // This will be ignored since the copy directive is COPY
+    metadataHeaders.putSingle(CUSTOM_METADATA_HEADER_PREFIX + "custom-key-3", 
"custom-value-3");
+
     // Add copy header, and then call put
     when(headers.getHeaderString(COPY_SOURCE_HEADER)).thenReturn(
         BUCKET_NAME + "/" + urlEncode(KEY_NAME));
@@ -296,9 +313,53 @@ class TestObjectPut {
     // the same Etag since the key content is the same
     assertEquals(sourceETag, 
sourceKeyDetails.getMetadata().get(OzoneConsts.ETAG));
     assertEquals(sourceETag, 
destKeyDetails.getMetadata().get(OzoneConsts.ETAG));
+    
assertThat(destKeyDetails.getMetadata().get("custom-key-1")).isEqualTo("custom-value-1");
+    
assertThat(destKeyDetails.getMetadata().get("custom-key-2")).isEqualTo("custom-value-2");
+    
assertThat(destKeyDetails.getMetadata().containsKey("custom-key-3")).isFalse();
 
-    // source and dest same
+    // Now use REPLACE metadata directive (default) and remove some custom 
metadata used in the source key
+    
when(headers.getHeaderString(CUSTOM_METADATA_COPY_DIRECTIVE_HEADER)).thenReturn("REPLACE");
+    metadataHeaders.remove(CUSTOM_METADATA_HEADER_PREFIX + "custom-key-1");
+    metadataHeaders.remove(CUSTOM_METADATA_HEADER_PREFIX + "custom-key-2");
+
+    response = objectEndpoint.put(DEST_BUCKET_NAME, DEST_KEY, 
CONTENT.length(), 1,
+        null, body);
+
+    ozoneInputStream = 
clientStub.getObjectStore().getS3Bucket(DEST_BUCKET_NAME)
+        .readKey(DEST_KEY);
+
+    keyContent = IOUtils.toString(ozoneInputStream, UTF_8);
+    sourceKeyDetails = clientStub.getObjectStore()
+        .getS3Bucket(BUCKET_NAME).getKey(KEY_NAME);
+    destKeyDetails = clientStub.getObjectStore()
+        .getS3Bucket(DEST_BUCKET_NAME).getKey(DEST_KEY);
+
+    assertEquals(200, response.getStatus());
+    assertEquals(CONTENT, keyContent);
+    assertNotNull(keyDetails.getMetadata());
+    assertThat(keyDetails.getMetadata().get(OzoneConsts.ETAG)).isNotEmpty();
+    // Source key eTag should remain unchanged and the dest key should have
+    // the same Etag since the key content is the same
+    assertEquals(sourceETag, 
sourceKeyDetails.getMetadata().get(OzoneConsts.ETAG));
+    assertEquals(sourceETag, 
destKeyDetails.getMetadata().get(OzoneConsts.ETAG));
+    
assertThat(destKeyDetails.getMetadata().containsKey("custom-key-1")).isFalse();
+    
assertThat(destKeyDetails.getMetadata().containsKey("custom-key-2")).isFalse();
+    
assertThat(destKeyDetails.getMetadata().get("custom-key-3")).isEqualTo("custom-value-3");
+
+
+    // wrong copy metadata directive
+    
when(headers.getHeaderString(CUSTOM_METADATA_COPY_DIRECTIVE_HEADER)).thenReturn("INVALID");
     OS3Exception e = assertThrows(OS3Exception.class, () -> objectEndpoint.put(
+            DEST_BUCKET_NAME, DEST_KEY, CONTENT.length(), 1, null, body),
+        "test copy object failed");
+    assertThat(e.getHttpCode()).isEqualTo(400);
+    assertThat(e.getCode()).isEqualTo("InvalidArgument");
+    assertThat(e.getErrorMessage()).contains("The metadata directive specified 
is invalid");
+
+    
when(headers.getHeaderString(CUSTOM_METADATA_COPY_DIRECTIVE_HEADER)).thenReturn("COPY");
+
+    // source and dest same
+    e = assertThrows(OS3Exception.class, () -> objectEndpoint.put(
             BUCKET_NAME, KEY_NAME, CONTENT.length(), 1, null, body),
         "test copy object failed");
     assertThat(e.getErrorMessage()).contains("This copy request is illegal");


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to