This is an automated email from the ASF dual-hosted git repository.
Gargi-jais11 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 cb6f12408ec HDDS-15852. Allow CopyObject to the same key when the
metadata directive is REPLACE (#10752).
cb6f12408ec is described below
commit cb6f12408ec589eb89f92142a648892ece3fb886
Author: KUAN-HAO HUANG <[email protected]>
AuthorDate: Mon Aug 10 12:31:25 2026 +0800
HDDS-15852. Allow CopyObject to the same key when the metadata directive is
REPLACE (#10752).
---
.../ozone/s3/awssdk/v2/AbstractS3SDKV2Tests.java | 35 ++++++++++++++++++
.../hadoop/ozone/s3/endpoint/ObjectEndpoint.java | 17 +++++----
.../hadoop/ozone/s3/endpoint/TestObjectPut.java | 42 ++++++++++++++++++++++
3 files changed, 87 insertions(+), 7 deletions(-)
diff --git
a/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v2/AbstractS3SDKV2Tests.java
b/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v2/AbstractS3SDKV2Tests.java
index dfe570d23c4..ac54d20aa49 100644
---
a/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v2/AbstractS3SDKV2Tests.java
+++
b/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v2/AbstractS3SDKV2Tests.java
@@ -158,6 +158,7 @@
import software.amazon.awssdk.services.s3.model.ListObjectsV2Request;
import software.amazon.awssdk.services.s3.model.ListObjectsV2Response;
import software.amazon.awssdk.services.s3.model.ListPartsRequest;
+import software.amazon.awssdk.services.s3.model.MetadataDirective;
import software.amazon.awssdk.services.s3.model.NoSuchKeyException;
import software.amazon.awssdk.services.s3.model.ObjectIdentifier;
import software.amazon.awssdk.services.s3.model.PutBucketAclRequest;
@@ -1302,6 +1303,40 @@ public void testCopyObject() {
assertEquals("\"37b51d194a7513e45b56f6524f2d51f2\"",
copyObjectResponse.copyObjectResult().eTag());
}
+ @Test
+ public void testCopyObjectToSelfWithMetadataReplace() {
+ final String bucketName = getBucketName();
+ final String key = getKeyName();
+ final String content = "bar";
+ s3Client.createBucket(b -> b.bucket(bucketName));
+ s3Client.putObject(b ->
b.bucket(bucketName).key(key).metadata(Collections.singletonMap("meta1", "v1")),
+ RequestBody.fromString(content));
+
+ // Copying an object onto itself is allowed when the metadata is replaced.
+ CopyObjectRequest copyReq = CopyObjectRequest.builder()
+ .sourceBucket(bucketName)
+ .sourceKey(key)
+ .destinationBucket(bucketName)
+ .destinationKey(key)
+ .metadataDirective(MetadataDirective.REPLACE)
+ .metadata(Collections.singletonMap("meta2", "v2"))
+ .build();
+
+ CopyObjectResponse copyObjectResponse = assertDoesNotThrow(() ->
s3Client.copyObject(copyReq));
+ assertNotNull(copyObjectResponse.copyObjectResult().eTag());
+
+ // The metadata was replaced in place: the new entry is present and the
old one is gone.
+ HeadObjectResponse head = s3Client.headObject(b ->
b.bucket(bucketName).key(key));
+ assertThat(head.metadata())
+ .containsEntry("meta2", "v2")
+ .doesNotContainKey("meta1");
+
+ // The object is still readable with its original content after the
in-place copy.
+ ResponseBytes<GetObjectResponse> objectBytes = s3Client.getObjectAsBytes(
+ b -> b.bucket(bucketName).key(key));
+ assertEquals(content, objectBytes.asUtf8String());
+ }
+
@Test
public void testCopyObjectWithSourceIfMatch() {
final String sourceBucketName = getBucketName("source");
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 0fa629764e5..6e8260d5e49 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
@@ -1161,13 +1161,17 @@ private CopyObjectResponse copyObject(OzoneVolume
volume,
try {
OzoneKeyDetails sourceKeyDetails = getClientProtocol().getKeyDetails(
volume.getName(), sourceBucket, sourceKey);
+ // Metadata directive is read up front: a self-copy is legal when
metadata
+ // is being replaced (x-amz-metadata-directive: REPLACE).
+ String metadataCopyDirective =
getHeaders().getHeaderString(CUSTOM_METADATA_COPY_DIRECTIVE_HEADER);
+ boolean replacingMetadata =
CopyDirective.REPLACE.name().equals(metadataCopyDirective);
+
// Checking whether we trying to copying to it self.
- if (sourceBucket.equals(destBucket) && sourceKey
- .equals(destkey)) {
- // When copying to same storage type when storage type is provided,
- // we should not throw exception, as aws cli checks if any of the
- // options like storage type are provided or not when source and
- // dest are given same
+ if (sourceBucket.equals(destBucket) && sourceKey.equals(destkey)
+ && !replacingMetadata) {
+ // Self-copy without a metadata replacement. AWS still allows it
+ // when a storage class is provided (aws cli passes storage type), so
+ // only the default-storage-type case is rejected.
if (storageTypeDefault) {
OS3Exception ex = newError(S3ErrorTable.INVALID_REQUEST, copyHeader);
ex.setErrorMessage("This copy request is illegal because it is " +
@@ -1215,7 +1219,6 @@ private CopyObjectResponse copyObject(OzoneVolume volume,
// Custom metadata in copyObject with metadata directive
Map<String, String> customMetadata;
- String metadataCopyDirective =
getHeaders().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();
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 6b4927d1340..3db0722bc31 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
@@ -382,6 +382,48 @@ void testCopyObject() throws Exception {
() -> put(objectEndpoint, "nonexistent", KEY_NAME, CONTENT));
}
+ @Test
+ void testCopyObjectToSelfWithMetadataReplace() throws Exception {
+ // Put the source object with some custom metadata.
+ Map<String, String> sourceMetadata = ImmutableMap.of(
+ "custom-key-1", "custom-value-1",
+ "custom-key-2", "custom-value-2");
+ MultivaluedMap<String, String> metadataHeaders = new
MultivaluedHashMap<>();
+ sourceMetadata.forEach((k, v) ->
metadataHeaders.putSingle(CUSTOM_METADATA_HEADER_PREFIX + k, v));
+ when(headers.getRequestHeaders()).thenReturn(metadataHeaders);
+
when(headers.getHeaderString(CUSTOM_METADATA_COPY_DIRECTIVE_HEADER)).thenReturn("COPY");
+ assertSucceeds(() -> putObject(CONTENT));
+
assertThat(bucket.getKey(KEY_NAME).getMetadata()).containsAllEntriesOf(sourceMetadata);
+
+ // Copy the object onto itself with x-amz-metadata-directive: REPLACE and a
+ // new metadata set. AWS allows this as an in-place metadata update.
+ when(headers.getHeaderString(COPY_SOURCE_HEADER)).thenReturn(
+ BUCKET_NAME + "/" + urlEncode(KEY_NAME));
+
when(headers.getHeaderString(CUSTOM_METADATA_COPY_DIRECTIVE_HEADER)).thenReturn("REPLACE");
+ metadataHeaders.clear();
+ metadataHeaders.putSingle(CUSTOM_METADATA_HEADER_PREFIX + "custom-key-3",
"custom-value-3");
+
+ assertSucceeds(() -> putObject(CONTENT));
+
+ OzoneKeyDetails keyDetails = assertKeyContent(bucket, KEY_NAME, CONTENT);
+ assertThat(keyDetails.getMetadata())
+ .containsEntry("custom-key-3", "custom-value-3")
+ .doesNotContainKeys("custom-key-1", "custom-key-2");
+ }
+
+ @Test
+ void testCopyObjectToSelfWithoutMetadataReplaceRejected() throws Exception {
+ // Seed the source object.
+ assertSucceeds(() -> putObject(CONTENT));
+
+ // Self-copy without x-amz-metadata-directive: REPLACE (and no
storage-class
+ // change) is not a real update, so it stays InvalidRequest.
+ when(headers.getHeaderString(COPY_SOURCE_HEADER)).thenReturn(
+ BUCKET_NAME + "/" + urlEncode(KEY_NAME));
+
+ assertErrorResponse(INVALID_REQUEST, () -> putObject(CONTENT));
+ }
+
@Test
void testContentTypeStoredAndCopied() throws Exception {
// PUT with an explicit Content-Type (preserved by HeaderPreprocessor).
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]