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 0ec5ee2fa69 HDDS-16073. Implement ObjectParts listing and pagination 
for completed multipart objects in GetObjectAttributes (#11020).
0ec5ee2fa69 is described below

commit 0ec5ee2fa69df4a922c5ecc7cba33cf87ba9603c
Author: Gargi Jaiswal <[email protected]>
AuthorDate: Wed Sep 2 21:41:20 2026 +0530

    HDDS-16073. Implement ObjectParts listing and pagination for completed 
multipart objects in GetObjectAttributes (#11020).
---
 .../ozone/client/S3HeadObjectAttributes.java       |  47 ++++++
 .../ozone/client/protocol/ClientProtocol.java      |  13 ++
 .../apache/hadoop/ozone/client/rpc/RpcClient.java  |  21 +++
 .../ozone/s3/awssdk/v2/AbstractS3SDKV2Tests.java   | 107 +++++++++++++
 .../s3/endpoint/GetObjectAttributesResponse.java   |  10 +-
 .../ozone/s3/endpoint/ObjectAttributesHandler.java | 166 +++++++++++++++++++--
 .../org/apache/hadoop/ozone/s3/util/S3Consts.java  |   9 +-
 .../hadoop/ozone/client/ClientProtocolStub.java    |  23 ++-
 .../hadoop/ozone/client/OzoneBucketStub.java       |  95 ++++++++++--
 .../ozone/s3/endpoint/EndpointTestUtils.java       |  20 +++
 .../ozone/s3/endpoint/TestObjectAttributesGet.java | 135 +++++++++++++++++
 11 files changed, 608 insertions(+), 38 deletions(-)

diff --git 
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/S3HeadObjectAttributes.java
 
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/S3HeadObjectAttributes.java
new file mode 100644
index 00000000000..91a5ae70193
--- /dev/null
+++ 
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/S3HeadObjectAttributes.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.client;
+
+import java.util.Collections;
+import java.util.NavigableMap;
+import java.util.Objects;
+
+/**
+ * Head metadata and completed multipart part sizes from a single S3 {@code 
GetKeyInfo} call.
+ */
+public final class S3HeadObjectAttributes {
+
+  private final OzoneKey key;
+  private final NavigableMap<Integer, Long> completedMultipartPartSizes;
+
+  public S3HeadObjectAttributes(OzoneKey key,
+      NavigableMap<Integer, Long> completedMultipartPartSizes) {
+    this.key = Objects.requireNonNull(key, "key == null");
+    this.completedMultipartPartSizes = completedMultipartPartSizes == null
+        ? Collections.emptyNavigableMap()
+        : completedMultipartPartSizes;
+  }
+
+  public OzoneKey getKey() {
+    return key;
+  }
+
+  public NavigableMap<Integer, Long> getCompletedMultipartPartSizes() {
+    return completedMultipartPartSizes;
+  }
+}
diff --git 
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java
 
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java
index a8e75eed746..0c115fe8f61 100644
--- 
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java
+++ 
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java
@@ -40,6 +40,7 @@
 import org.apache.hadoop.ozone.client.OzoneMultipartUploadPartListParts;
 import org.apache.hadoop.ozone.client.OzoneSnapshot;
 import org.apache.hadoop.ozone.client.OzoneVolume;
+import org.apache.hadoop.ozone.client.S3HeadObjectAttributes;
 import org.apache.hadoop.ozone.client.TenantArgs;
 import org.apache.hadoop.ozone.client.VolumeArgs;
 import org.apache.hadoop.ozone.client.io.OzoneDataStreamOutput;
@@ -172,6 +173,18 @@ OzoneVolume getVolumeDetails(String volumeName)
   OzoneKey headS3Object(String bucketName, String keyName, int partNumber)
       throws IOException;
 
+  /**
+   * Returns S3 head metadata and completed multipart part sizes from a single
+   * {@code GetKeyInfo} OM call.
+   *
+   * @param bucketName Name of the Bucket
+   * @param keyName Key name
+   * @return head key metadata and sorted part-number to size map (empty when 
not MPU)
+   * @throws IOException
+   */
+  S3HeadObjectAttributes headS3ObjectAttributes(String bucketName, String 
keyName)
+      throws IOException;
+
   /**
    * Get OzoneKey in S3 context.
    * @param bucketName Name of the Bucket
diff --git 
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java
 
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java
index 01b7b8d9796..9be0128dea6 100644
--- 
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java
+++ 
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java
@@ -49,7 +49,9 @@
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.NavigableMap;
 import java.util.Objects;
+import java.util.TreeMap;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.SynchronousQueue;
@@ -109,6 +111,7 @@
 import org.apache.hadoop.ozone.client.OzoneMultipartUploadPartListParts;
 import org.apache.hadoop.ozone.client.OzoneSnapshot;
 import org.apache.hadoop.ozone.client.OzoneVolume;
+import org.apache.hadoop.ozone.client.S3HeadObjectAttributes;
 import org.apache.hadoop.ozone.client.TenantArgs;
 import org.apache.hadoop.ozone.client.VolumeArgs;
 import org.apache.hadoop.ozone.client.io.BlockInputStreamFactory;
@@ -1938,6 +1941,24 @@ public OzoneKey headS3Object(String bucketName, String 
keyName,
         getS3PartOmKeyInfo(bucketName, keyName, partNumber, true));
   }
 
+  @Override
+  public S3HeadObjectAttributes headS3ObjectAttributes(String bucketName, 
String keyName)
+      throws IOException {
+    OmKeyInfo keyInfo = getS3KeyInfo(bucketName, keyName, true);
+    OmKeyLocationInfoGroup locationGroup = keyInfo.getLatestVersionLocations();
+    NavigableMap<Integer, Long> partSizes = Collections.emptyNavigableMap();
+    if (locationGroup != null && locationGroup.isMultipartKey()) {
+      partSizes = new TreeMap<>();
+      for (OmKeyLocationInfo location : 
locationGroup.getBlocksLatestVersionOnly()) {
+        int partNumber = location.getPartNumber();
+        if (partNumber > 0) {
+          partSizes.merge(partNumber, location.getLength(), Long::sum);
+        }
+      }
+    }
+    return new S3HeadObjectAttributes(OzoneKey.fromKeyInfo(keyInfo), 
partSizes);
+  }
+
   private OmKeyInfo getS3PartOmKeyInfo(String bucketName, String keyName,
       int partNumber, boolean isHeadOp) throws IOException {
     OmKeyInfo keyInfo;
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 63c0607f0d6..f56676d37f6 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
@@ -1789,6 +1789,113 @@ public void 
testGetObjectAttributesMultipartObjectParts(@TempDir Path tempDir) t
     assertNotNull(attributesResponse.objectParts());
     assertEquals(3, attributesResponse.objectParts().totalPartsCount());
     assertFalse(attributesResponse.objectParts().isTruncated());
+    assertEquals(1000, attributesResponse.objectParts().maxParts());
+    assertTrue(attributesResponse.objectParts().parts().isEmpty());
+
+    GetObjectAttributesResponse firstPage = s3Client.getObjectAttributes(
+        GetObjectAttributesRequest.builder()
+            .bucket(bucketName)
+            .key(keyName)
+            .objectAttributes(ObjectAttributes.OBJECT_PARTS)
+            .maxParts(2)
+            .partNumberMarker(0)
+            .build());
+    assertTrue(firstPage.objectParts().isTruncated());
+    assertEquals(2, firstPage.objectParts().maxParts());
+    assertEquals(0, firstPage.objectParts().partNumberMarker());
+    assertEquals(2, firstPage.objectParts().nextPartNumberMarker());
+    assertTrue(firstPage.objectParts().parts().isEmpty());
+
+    GetObjectAttributesResponse secondPage = s3Client.getObjectAttributes(
+        GetObjectAttributesRequest.builder()
+            .bucket(bucketName)
+            .key(keyName)
+            .objectAttributes(ObjectAttributes.OBJECT_PARTS)
+            .maxParts(2)
+            .partNumberMarker(2)
+            .build());
+    assertFalse(secondPage.objectParts().isTruncated());
+    assertTrue(secondPage.objectParts().parts().isEmpty());
+  }
+
+  @Test
+  public void testGetObjectAttributesNonContiguousMultipartObjectParts() 
throws Exception {
+    final String bucketName = getBucketName();
+    final String keyName = getKeyName();
+    final int partOneSize = (int) (5 * MB);
+    final int partThreeSize = 4096;
+    byte[] partOneBytes = new byte[partOneSize];
+    byte[] partThreeBytes = new byte[partThreeSize];
+
+    s3Client.createBucket(b -> b.bucket(bucketName));
+
+    String uploadId = initiateMultipartUpload(bucketName, keyName, new 
HashMap<>(),
+        Collections.emptyList());
+
+    UploadPartResponse partOneResponse = 
s3Client.uploadPart(UploadPartRequest.builder()
+        .bucket(bucketName)
+        .key(keyName)
+        .uploadId(uploadId)
+        .partNumber(1)
+        .build(), RequestBody.fromBytes(partOneBytes));
+
+    UploadPartResponse partThreeResponse = 
s3Client.uploadPart(UploadPartRequest.builder()
+        .bucket(bucketName)
+        .key(keyName)
+        .uploadId(uploadId)
+        .partNumber(3)
+        .build(), RequestBody.fromBytes(partThreeBytes));
+
+    completeMultipartUpload(bucketName, keyName, uploadId, Arrays.asList(
+        CompletedPart.builder()
+            .partNumber(1)
+            .eTag(stripQuotes(partOneResponse.eTag()))
+            .build(),
+        CompletedPart.builder()
+            .partNumber(3)
+            .eTag(stripQuotes(partThreeResponse.eTag()))
+            .build()));
+
+    GetObjectAttributesResponse attributesResponse = 
s3Client.getObjectAttributes(
+        GetObjectAttributesRequest.builder()
+            .bucket(bucketName)
+            .key(keyName)
+            .objectAttributes(ObjectAttributes.OBJECT_PARTS, 
ObjectAttributes.OBJECT_SIZE)
+            .build());
+
+    assertNotNull(attributesResponse.objectParts());
+    assertEquals(2, attributesResponse.objectParts().totalPartsCount());
+    assertFalse(attributesResponse.objectParts().isTruncated());
+    assertTrue(attributesResponse.objectParts().parts().isEmpty());
+    assertEquals((long) partOneSize + partThreeSize, 
attributesResponse.objectSize());
+
+    GetObjectAttributesResponse firstPage = s3Client.getObjectAttributes(
+        GetObjectAttributesRequest.builder()
+            .bucket(bucketName)
+            .key(keyName)
+            .objectAttributes(ObjectAttributes.OBJECT_PARTS)
+            .maxParts(1)
+            .partNumberMarker(0)
+            .build());
+    assertTrue(firstPage.objectParts().isTruncated());
+    assertEquals(0, firstPage.objectParts().partNumberMarker());
+    assertEquals(1, firstPage.objectParts().nextPartNumberMarker());
+    assertEquals(1, firstPage.objectParts().maxParts());
+    assertTrue(firstPage.objectParts().parts().isEmpty());
+
+    GetObjectAttributesResponse secondPage = s3Client.getObjectAttributes(
+        GetObjectAttributesRequest.builder()
+            .bucket(bucketName)
+            .key(keyName)
+            .objectAttributes(ObjectAttributes.OBJECT_PARTS)
+            .maxParts(1)
+            .partNumberMarker(1)
+            .build());
+    assertFalse(secondPage.objectParts().isTruncated());
+    assertEquals(1, secondPage.objectParts().partNumberMarker());
+    assertNull(secondPage.objectParts().nextPartNumberMarker());
+    assertEquals(1, secondPage.objectParts().maxParts());
+    assertTrue(secondPage.objectParts().parts().isEmpty());
   }
 
   @Test
diff --git 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/GetObjectAttributesResponse.java
 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/GetObjectAttributesResponse.java
index 612ade64a8a..188e2bc4e8a 100644
--- 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/GetObjectAttributesResponse.java
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/GetObjectAttributesResponse.java
@@ -84,12 +84,6 @@ public void setObjectParts(ObjectParts objectParts) {
 
   /**
    * Represents the ObjectParts element in the GetObjectAttributes response.
-   *
-   * <p>For completed multipart-uploaded objects, {@code partsCount} is 
derived from
-   * the composite ETag suffix (e.g. {@code "hash-15"} → 15 parts). Per-part 
sizes
-   * are not stored for completed multipart uploads in Ozone and are therefore 
omitted
-   * from the part list in this response.
-   * TODO: Will support completed multipart uploads in this ticket: HDDS-16073
    */
   @XmlAccessorType(XmlAccessType.FIELD)
   @XmlRootElement(name = "ObjectParts")
@@ -168,6 +162,10 @@ public void addPart(Part part) {
 
   /**
    * A single part entry within {@link ObjectParts}.
+   *
+   * <p>AWS returns {@code Part} elements for general-purpose buckets only 
when an additional
+   * checksum was stored at upload time. {@code PartNumber} and {@code Size} 
are included when
+   * present; per-part checksum fields are omitted until Ozone stores them.
    */
   @XmlAccessorType(XmlAccessType.FIELD)
   @XmlRootElement(name = "Part")
diff --git 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectAttributesHandler.java
 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectAttributesHandler.java
index ab4934b3d67..1a1319bc13a 100644
--- 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectAttributesHandler.java
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectAttributesHandler.java
@@ -17,16 +17,24 @@
 
 package org.apache.hadoop.ozone.s3.endpoint;
 
+import static 
org.apache.hadoop.ozone.OzoneConsts.MAXIMUM_NUMBER_OF_PARTS_PER_UPLOAD;
 import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.ACCESS_DENIED;
 import static 
org.apache.hadoop.ozone.s3.exception.S3ErrorTable.INVALID_ARGUMENT;
 import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.NO_SUCH_KEY;
 import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.newError;
+import static 
org.apache.hadoop.ozone.s3.util.S3Consts.GET_OBJECT_ATTRIBUTES_MAX_PARTS_LIMIT;
+import static org.apache.hadoop.ozone.s3.util.S3Consts.MAX_PARTS_HEADER;
 import static 
org.apache.hadoop.ozone.s3.util.S3Consts.OBJECT_ATTRIBUTES_HEADER;
+import static 
org.apache.hadoop.ozone.s3.util.S3Consts.PART_NUMBER_MARKER_HEADER;
 import static org.apache.hadoop.ozone.s3.util.S3Consts.QueryParams;
 
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Locale;
+import java.util.Map;
+import java.util.NavigableMap;
 import java.util.Set;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
@@ -34,11 +42,14 @@
 import org.apache.hadoop.ozone.OzoneConsts;
 import org.apache.hadoop.ozone.audit.S3GAction;
 import org.apache.hadoop.ozone.client.OzoneKey;
+import org.apache.hadoop.ozone.client.S3HeadObjectAttributes;
 import org.apache.hadoop.ozone.om.exceptions.OMException;
 import org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes;
 import org.apache.hadoop.ozone.s3.endpoint.ObjectEndpoint.ObjectRequestContext;
 import org.apache.hadoop.ozone.s3.exception.OS3Exception;
 import org.apache.hadoop.ozone.s3.util.S3StorageType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * Handles the GetObjectAttributes S3 API ({@code GET 
/{bucket}/{key}?attributes}).
@@ -47,13 +58,17 @@
  * Supported attributes: {@code ETag}, {@code ObjectSize}, {@code 
StorageClass}, {@code ObjectParts}.
  *
  * <p>The {@code Checksum} attribute is not yet supported because Ozone does 
not store
- * non-MD5 checksum algorithms in key metadata. Object versioning ({@code 
versionId}) and
+ * non-MD5 checksum algorithms in key metadata. For general-purpose buckets, 
{@code Part}
+ * elements under {@code ObjectParts} are omitted unless an additional 
checksum is stored
+ * on the object, matching AWS S3 behavior. Object versioning ({@code 
versionId}) and
  * SSE-C encryption headers are also not supported and are silently ignored.
  *
  * <p>See 
https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObjectAttributes.html
  */
 class ObjectAttributesHandler extends ObjectOperationHandler {
 
+  private static final Logger LOG = 
LoggerFactory.getLogger(ObjectAttributesHandler.class);
+
   /** Valid values for the x-amz-object-attributes request header. */
   static final String ATTR_ETAG = "ETag";
   static final String ATTR_CHECKSUM = "Checksum";
@@ -64,6 +79,28 @@ class ObjectAttributesHandler extends ObjectOperationHandler 
{
   private static final Set<String> KNOWN_ATTRIBUTES = new 
HashSet<>(Arrays.asList(
       ATTR_ETAG, ATTR_CHECKSUM, ATTR_OBJECT_PARTS, ATTR_STORAGE_CLASS, 
ATTR_OBJECT_SIZE));
 
+  private static final String ADDITIONAL_CHECKSUM_METADATA_PREFIX = 
"x-amz-checksum-";
+
+  /**
+   * Returns whether key metadata contains a stored AWS additional checksum.
+   * Ozone does not persist {@code x-amz-checksum-*} on upload today, so this 
is
+   * false for normal objects until checksum storage is implemented.
+   */
+  static boolean hasStoredAdditionalChecksum(OzoneKey key) {
+    if (key == null || key.getMetadata() == null) {
+      return false;
+    }
+    for (Map.Entry<String, String> entry : key.getMetadata().entrySet()) {
+      String name = entry.getKey();
+      if (name != null
+          && 
name.toLowerCase(Locale.ROOT).startsWith(ADDITIONAL_CHECKSUM_METADATA_PREFIX)
+          && StringUtils.isNotBlank(entry.getValue())) {
+        return true;
+      }
+    }
+    return false;
+  }
+
   @Override
   Response handleGetRequest(ObjectRequestContext context, String keyPath)
       throws IOException, OS3Exception {
@@ -80,8 +117,16 @@ Response handleGetRequest(ObjectRequestContext context, 
String keyPath)
       String bucketName = context.getBucketName();
 
       OzoneKey key;
+      NavigableMap<Integer, Long> completedPartSizes = null;
       try {
-        key = getClientProtocol().headS3Object(bucketName, keyPath);
+        if (requestedAttributes.contains(ATTR_OBJECT_PARTS)) {
+          S3HeadObjectAttributes headAttributes =
+              getClientProtocol().headS3ObjectAttributes(bucketName, keyPath);
+          key = headAttributes.getKey();
+          completedPartSizes = headAttributes.getCompletedMultipartPartSizes();
+        } else {
+          key = getClientProtocol().headS3Object(bucketName, keyPath);
+        }
         validateFileKey(keyPath, key);
       } catch (OMException ex) {
         if (ex.getResult() == ResultCodes.KEY_NOT_FOUND) {
@@ -92,7 +137,8 @@ Response handleGetRequest(ObjectRequestContext context, 
String keyPath)
         throw ex;
       }
 
-      GetObjectAttributesResponse response = buildResponse(key, 
requestedAttributes);
+      GetObjectAttributesResponse response =
+          buildResponse(keyPath, key, requestedAttributes, completedPartSizes);
 
       Response.ResponseBuilder rb = Response.ok(response, 
MediaType.APPLICATION_XML_TYPE);
       ObjectEndpoint.addLastModifiedDate(rb, key);
@@ -130,7 +176,9 @@ private Set<String> parseAttributesHeader(String keyPath) 
throws OS3Exception {
     return requested;
   }
 
-  private GetObjectAttributesResponse buildResponse(OzoneKey key, Set<String> 
requested) {
+  private GetObjectAttributesResponse buildResponse(String keyPath, OzoneKey 
key,
+      Set<String> requested, NavigableMap<Integer, Long> completedPartSizes)
+      throws IOException, OS3Exception {
     GetObjectAttributesResponse resp = new GetObjectAttributesResponse();
 
     if (requested.contains(ATTR_ETAG)) {
@@ -155,11 +203,9 @@ private GetObjectAttributesResponse buildResponse(OzoneKey 
key, Set<String> requ
       String eTag = key.getMetadata().get(OzoneConsts.ETAG);
       if (eTag != null) {
         String partsCountStr = extractPartsCount(eTag);
-        if (partsCountStr != null) {
-          GetObjectAttributesResponse.ObjectParts parts = new 
GetObjectAttributesResponse.ObjectParts();
-          parts.setPartsCount(Integer.parseInt(partsCountStr));
-          parts.setTruncated(false);
-          resp.setObjectParts(parts);
+        if (partsCountStr != null && completedPartSizes != null) {
+          resp.setObjectParts(buildObjectParts(keyPath, 
Integer.parseInt(partsCountStr),
+              completedPartSizes, key));
         }
       }
     }
@@ -170,4 +216,106 @@ private GetObjectAttributesResponse 
buildResponse(OzoneKey key, Set<String> requ
 
     return resp;
   }
+
+  /**
+   * Builds the {@link GetObjectAttributesResponse.ObjectParts} element for a 
completed
+   * multipart object, including per-part sizes and optional pagination.
+   *
+   * <p>When {@code x-amz-max-parts} is omitted, the page size defaults to 
1000, matching ListParts.
+   * Part numbers and sizes are paginated over the actual part numbers from 
OM, which may be
+   * non-contiguous. {@code TotalPartsCount} follows the block-derived part 
count when it differs
+   * from the ETag suffix.
+   *
+   * <p>For general-purpose buckets, individual {@code Part} elements are 
returned only when the
+   * object has a stored AWS additional checksum in key metadata; otherwise 
only {@code ObjectParts}
+   * summary and pagination fields are returned.
+   */
+  private GetObjectAttributesResponse.ObjectParts buildObjectParts(String 
keyPath,
+      int totalPartsCount, NavigableMap<Integer, Long> partSizes, OzoneKey key)
+      throws OS3Exception {
+    int maxParts = parseMaxPartsHeader(keyPath);
+    int marker = parsePartNumberMarkerHeader(keyPath);
+    boolean partNumberMarkerSet = isPartNumberMarkerHeaderSet();
+
+    GetObjectAttributesResponse.ObjectParts parts = new 
GetObjectAttributesResponse.ObjectParts();
+    int partsCount = totalPartsCount;
+    if (partSizes.size() != totalPartsCount) {
+      LOG.debug("ETag parts count {} differs from block-derived part count {} 
for key {}",
+          totalPartsCount, partSizes.size(), keyPath);
+      partsCount = partSizes.size();
+    }
+    parts.setPartsCount(partsCount);
+    parts.setMaxParts(maxParts);
+    if (partNumberMarkerSet) {
+      parts.setPartNumberMarker(marker);
+    }
+
+    Iterator<Map.Entry<Integer, Long>> partIterator =
+        partSizes.tailMap(marker, false).entrySet().iterator();
+    // TODO: For FSO (directory) buckets, always include Part entries per AWS
+    // directory-bucket GetObjectAttributes behavior, regardless of checksum 
metadata.
+    boolean includePartEntries = hasStoredAdditionalChecksum(key);
+    Integer lastPartReturned = null;
+    int partsOnPage = 0;
+    while (partIterator.hasNext() && partsOnPage < maxParts) {
+      Map.Entry<Integer, Long> partEntry = partIterator.next();
+      if (includePartEntries) {
+        parts.addPart(new GetObjectAttributesResponse.Part(
+            partEntry.getKey(), partEntry.getValue()));
+      }
+      lastPartReturned = partEntry.getKey();
+      partsOnPage++;
+    }
+
+    boolean truncated = partIterator.hasNext();
+    parts.setTruncated(truncated);
+    if (truncated && lastPartReturned != null) {
+      parts.setNextPartNumberMarker(lastPartReturned);
+    }
+    return parts;
+  }
+
+  private int parseMaxPartsHeader(String resource) throws OS3Exception {
+    return parseMaxPartsHeader(resource, 
GET_OBJECT_ATTRIBUTES_MAX_PARTS_LIMIT);
+  }
+
+  private int parseMaxPartsHeader(String resource, int defaultValue) throws 
OS3Exception {
+    String headerValue = getHeaders().getHeaderString(MAX_PARTS_HEADER);
+    if (StringUtils.isBlank(headerValue)) {
+      return defaultValue;
+    }
+    try {
+      int maxParts = Integer.parseInt(headerValue.trim());
+      if (maxParts <= 0 || maxParts > GET_OBJECT_ATTRIBUTES_MAX_PARTS_LIMIT) {
+        throw newError(INVALID_ARGUMENT, resource,
+            new IllegalArgumentException("max-parts must be between 1 and "
+                + GET_OBJECT_ATTRIBUTES_MAX_PARTS_LIMIT));
+      }
+      return maxParts;
+    } catch (NumberFormatException ex) {
+      throw newError(INVALID_ARGUMENT, resource, ex);
+    }
+  }
+
+  private boolean isPartNumberMarkerHeaderSet() {
+    return 
StringUtils.isNotBlank(getHeaders().getHeaderString(PART_NUMBER_MARKER_HEADER));
+  }
+
+  private int parsePartNumberMarkerHeader(String resource) throws OS3Exception 
{
+    String headerValue = 
getHeaders().getHeaderString(PART_NUMBER_MARKER_HEADER);
+    if (StringUtils.isBlank(headerValue)) {
+      return 0;
+    }
+    try {
+      int marker = Integer.parseInt(headerValue.trim());
+      if (marker < 0 || marker > MAXIMUM_NUMBER_OF_PARTS_PER_UPLOAD) {
+        throw newError(INVALID_ARGUMENT, resource,
+            new IllegalArgumentException("part-number-marker must be between 0 
and "
+                + MAXIMUM_NUMBER_OF_PARTS_PER_UPLOAD));
+      }
+      return marker;
+    } catch (NumberFormatException ex) {
+      throw newError(INVALID_ARGUMENT, resource, ex);
+    }
+  }
 }
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 8477d8c64a1..f75653ad098 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
@@ -128,9 +128,16 @@ public final class S3Consts {
   // tag limit for bucket
   public static final int TAG_BUCKET_NUM_LIMIT = 50;
 
-  /** Request header carrying the list of object attributes to return. */
+  // Request header carrying the list of object attributes to return.
   public static final String OBJECT_ATTRIBUTES_HEADER = 
"x-amz-object-attributes";
 
+  // Pagination headers for GetObjectAttributes ObjectParts.
+  public static final String MAX_PARTS_HEADER = "x-amz-max-parts";
+  public static final String PART_NUMBER_MARKER_HEADER = 
"x-amz-part-number-marker";
+
+  // Maximum number of parts returned in one GetObjectAttributes response.
+  public static final int GET_OBJECT_ATTRIBUTES_MAX_PARTS_LIMIT = 1000;
+
   //Never Constructed
   private S3Consts() {
 
diff --git 
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/ClientProtocolStub.java
 
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/ClientProtocolStub.java
index 122158262fa..a59aed4c7fd 100644
--- 
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/ClientProtocolStub.java
+++ 
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/ClientProtocolStub.java
@@ -24,6 +24,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.NavigableMap;
 import org.apache.hadoop.crypto.key.KeyProvider;
 import org.apache.hadoop.hdds.client.ReplicationConfig;
 import org.apache.hadoop.hdds.client.ReplicationFactor;
@@ -124,12 +125,22 @@ public OzoneKey headS3Object(String bucketName, String 
keyName)
   @Override
   public OzoneKey headS3Object(String bucketName, String keyName,
                                int partNumber) throws IOException {
-    // The stub does not model individual multipart parts, so it returns
-    // whole-object metadata (consistent with getS3KeyDetails). Real 
part-number
-    // semantics (InvalidPart, per-part size) are covered by the SDK-based
-    // integration tests against a live cluster.
-    return objectStoreStub.getS3Volume().getBucket(bucketName)
-        .headObject(keyName);
+    OzoneBucket bucket = objectStoreStub.getS3Volume().getBucket(bucketName);
+    if (bucket instanceof OzoneBucketStub) {
+      return ((OzoneBucketStub) bucket).headObject(keyName, partNumber);
+    }
+    return bucket.headObject(keyName);
+  }
+
+  @Override
+  public S3HeadObjectAttributes headS3ObjectAttributes(String bucketName, 
String keyName)
+      throws IOException {
+    OzoneBucket bucket = objectStoreStub.getS3Volume().getBucket(bucketName);
+    OzoneKey key = bucket.headObject(keyName);
+    NavigableMap<Integer, Long> partSizes = bucket instanceof OzoneBucketStub
+        ? ((OzoneBucketStub) bucket).getCompletedMultipartPartSizes(keyName)
+        : Collections.emptyNavigableMap();
+    return new S3HeadObjectAttributes(key, partSizes);
   }
 
   @Override
diff --git 
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/OzoneBucketStub.java
 
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/OzoneBucketStub.java
index 09ae82564a5..5c8c7e0b448 100644
--- 
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/OzoneBucketStub.java
+++ 
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/OzoneBucketStub.java
@@ -35,6 +35,7 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.NavigableMap;
 import java.util.TreeMap;
 import java.util.UUID;
 import java.util.stream.Collectors;
@@ -400,22 +401,81 @@ public OzoneKeyDetails getKey(String key) throws 
IOException {
 
   @Override
   public OzoneKey headObject(String key) throws IOException {
-    if (keyDetails.containsKey(key)) {
-      OzoneKeyDetails ozoneKeyDetails = keyDetails.get(key);
-      return new OzoneKey(ozoneKeyDetails.getVolumeName(),
-          ozoneKeyDetails.getBucketName(),
-          ozoneKeyDetails.getName(),
-          ozoneKeyDetails.getDataSize(),
-          ozoneKeyDetails.getCreationTime().toEpochMilli(),
-          ozoneKeyDetails.getModificationTime().toEpochMilli(),
-          ozoneKeyDetails.getReplicationConfig(),
-          ozoneKeyDetails.getMetadata(),
-          ozoneKeyDetails.isFile(),
-          ozoneKeyDetails.getOwner(),
-          ozoneKeyDetails.getTags());
-    } else {
-      throw new OMException(ResultCodes.KEY_NOT_FOUND);
+    return headObject(key, 0);
+  }
+
+  /**
+   * Returns metadata for a completed multipart part when {@code partNumber > 
0}.
+   */
+  public OzoneKey headObject(String key, int partNumber) throws IOException {
+    OzoneKeyDetails ozoneKeyDetails = getKey(key);
+    if (partNumber <= 0) {
+      return toHeadOzoneKey(ozoneKeyDetails, ozoneKeyDetails.getDataSize());
+    }
+
+    Map<Integer, Part> parts = partList.get(key);
+    if (parts == null || !parts.containsKey(partNumber)) {
+      throw new OMException("Invalid part number " + partNumber,
+          ResultCodes.INVALID_PART);
+    }
+    return toHeadOzoneKey(ozoneKeyDetails, 
parts.get(partNumber).getContent().length);
+  }
+
+  /**
+   * Returns part numbers and sizes for a completed multipart object from stub 
state.
+   */
+  public NavigableMap<Integer, Long> getCompletedMultipartPartSizes(String key)
+      throws IOException {
+    getKey(key);
+    Map<Integer, Part> parts = partList.get(key);
+    if (parts == null || parts.isEmpty()) {
+      return Collections.emptyNavigableMap();
     }
+    NavigableMap<Integer, Long> partSizes = new TreeMap<>();
+    for (Map.Entry<Integer, Part> partEntry : parts.entrySet()) {
+      partSizes.put(partEntry.getKey(), (long) 
partEntry.getValue().getContent().length);
+    }
+    return partSizes;
+  }
+
+  /**
+   * Test-only helper to add key metadata for compatibility tests.
+   */
+  public void putKeyMetadataForTest(String key, String metadataKey, String 
metadataValue)
+      throws IOException {
+    OzoneKeyDetails details = getKey(key);
+    Map<String, String> metadata = new HashMap<>(details.getMetadata());
+    metadata.put(metadataKey, metadataValue);
+    keyDetails.put(key, new OzoneKeyDetails(
+        details.getVolumeName(),
+        details.getBucketName(),
+        details.getName(),
+        details.getDataSize(),
+        details.getCreationTime().toEpochMilli(),
+        details.getModificationTime().toEpochMilli(),
+        details.getOzoneKeyLocations(),
+        details.getReplicationConfig(),
+        metadata,
+        details.getFileEncryptionInfo(),
+        () -> readKey(key),
+        details.isFile(),
+        details.getOwner(),
+        details.getTags(),
+        details.getGeneration()));
+  }
+
+  private static OzoneKey toHeadOzoneKey(OzoneKeyDetails details, long 
dataSize) {
+    return new OzoneKey(details.getVolumeName(),
+        details.getBucketName(),
+        details.getName(),
+        dataSize,
+        details.getCreationTime().toEpochMilli(),
+        details.getModificationTime().toEpochMilli(),
+        details.getReplicationConfig(),
+        details.getMetadata(),
+        details.isFile(),
+        details.getOwner(),
+        details.getTags());
   }
 
   @Override
@@ -609,6 +669,9 @@ public OmMultipartUploadCompleteInfo 
completeMultipartUpload(String key,
         keyContents.put(key, output.toByteArray());
       }
 
+      Map<String, String> metadata = new 
HashMap<>(keyToMultipartUpload.get(key).getMetadata());
+      metadata.put(ETAG, DigestUtils.sha256Hex(output.toByteArray()) + "-" + 
partsMap.size());
+
       keyDetails.put(key, new OzoneKeyDetails(
           getVolumeName(),
           getName(),
@@ -617,7 +680,7 @@ public OmMultipartUploadCompleteInfo 
completeMultipartUpload(String key,
           System.currentTimeMillis(),
           System.currentTimeMillis(),
           new ArrayList<>(), getReplicationConfig(),
-          keyToMultipartUpload.get(key).getMetadata(), null,
+          metadata, null,
           () -> readKey(key), true,
           UserGroupInformation.getCurrentUser().getShortUserName(),
           keyToMultipartUpload.get(key).getTags()
diff --git 
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/EndpointTestUtils.java
 
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/EndpointTestUtils.java
index 0bbbf4eb18e..c1210ceb8b0 100644
--- 
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/EndpointTestUtils.java
+++ 
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/EndpointTestUtils.java
@@ -68,11 +68,31 @@ public static Response getObjectAttributes(
       String bucket,
       String key,
       String attributesHeader
+  ) throws IOException, OS3Exception {
+    return getObjectAttributes(subject, bucket, key, attributesHeader, null, 
null);
+  }
+
+  /** Get object attributes (?attributes) with optional ObjectParts pagination 
headers. */
+  public static Response getObjectAttributes(
+      ObjectEndpoint subject,
+      String bucket,
+      String key,
+      String attributesHeader,
+      Integer maxParts,
+      Integer partNumberMarker
   ) throws IOException, OS3Exception {
     subject.queryParamsForTest().set(S3Consts.QueryParams.ATTRIBUTES, "");
     when(subject.getContext().getMethod()).thenReturn(HttpMethod.GET);
     
when(subject.getHeaders().getHeaderString(S3Consts.OBJECT_ATTRIBUTES_HEADER))
         .thenReturn(attributesHeader);
+    if (maxParts != null) {
+      when(subject.getHeaders().getHeaderString(S3Consts.MAX_PARTS_HEADER))
+          .thenReturn(String.valueOf(maxParts));
+    }
+    if (partNumberMarker != null) {
+      
when(subject.getHeaders().getHeaderString(S3Consts.PART_NUMBER_MARKER_HEADER))
+          .thenReturn(String.valueOf(partNumberMarker));
+    }
     return subject.get(bucket, key);
   }
 
diff --git 
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestObjectAttributesGet.java
 
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestObjectAttributesGet.java
index 0f2cca58b3f..6c39ac89e1a 100644
--- 
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestObjectAttributesGet.java
+++ 
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestObjectAttributesGet.java
@@ -21,8 +21,11 @@
 import static 
org.apache.hadoop.ozone.s3.S3GatewayConfigKeys.OZONE_S3G_FSO_DIRECTORY_CREATION_ENABLED;
 import static 
org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.assertErrorResponse;
 import static 
org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.assertSucceeds;
+import static 
org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.completeMultipartUpload;
 import static 
org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.getObjectAttributes;
+import static 
org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.initiateMultipartUpload;
 import static org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.put;
+import static org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.uploadPart;
 import static 
org.apache.hadoop.ozone.s3.exception.S3ErrorTable.INVALID_ARGUMENT;
 import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.NO_SUCH_BUCKET;
 import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.NO_SUCH_KEY;
@@ -31,15 +34,21 @@
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
 import javax.ws.rs.core.HttpHeaders;
 import javax.ws.rs.core.Response;
 import org.apache.hadoop.hdds.conf.OzoneConfiguration;
 import org.apache.hadoop.ozone.client.OzoneBucket;
+import org.apache.hadoop.ozone.client.OzoneBucketStub;
 import org.apache.hadoop.ozone.client.OzoneClient;
 import org.apache.hadoop.ozone.client.OzoneClientStub;
+import org.apache.hadoop.ozone.s3.endpoint.CompleteMultipartUploadRequest.Part;
 import org.apache.hadoop.ozone.s3.exception.OS3Exception;
+import org.apache.hadoop.ozone.s3.util.S3Consts;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.mockito.Mockito;
@@ -173,4 +182,130 @@ public void 
testWhenKeyIsDirectoryAndKeyPathEndsWithASlash() throws Exception {
 
     assertEquals(HTTP_OK, response.getStatus());
   }
+
+  @Test
+  public void testGetObjectAttributesMultipartObjectPartsListsAllParts() 
throws IOException, OS3Exception {
+    final String key = "mpu-key";
+    completeMultipartUploadWithParts(key, "part-one", "part-two", 
"part-three");
+
+    Response response = getObjectAttributes(rest, BUCKET_NAME, key, 
"ObjectParts");
+
+    assertEquals(HTTP_OK, response.getStatus());
+    GetObjectAttributesResponse attributes = (GetObjectAttributesResponse) 
response.getEntity();
+    GetObjectAttributesResponse.ObjectParts objectParts = 
attributes.getObjectParts();
+    assertNotNull(objectParts);
+    assertEquals(3, objectParts.getPartsCount().intValue());
+    assertFalse(objectParts.isTruncated());
+    assertEquals(S3Consts.GET_OBJECT_ATTRIBUTES_MAX_PARTS_LIMIT, 
objectParts.getMaxParts().intValue());
+    assertNull(objectParts.getPartNumberMarker());
+    assertNull(objectParts.getNextPartNumberMarker());
+    assertTrue(objectParts.getParts().isEmpty());
+  }
+
+  @Test
+  public void 
testGetObjectAttributesMultipartObjectPartsReturnsPartsWhenChecksumStored()
+      throws IOException, OS3Exception {
+    final String key = "mpu-with-checksum";
+    completeMultipartUploadWithParts(key, "part-one", "part-two");
+    ((OzoneBucketStub) bucket).putKeyMetadataForTest(key, 
"x-amz-checksum-crc32", "dummy");
+
+    Response response = getObjectAttributes(rest, BUCKET_NAME, key, 
"ObjectParts");
+
+    assertEquals(HTTP_OK, response.getStatus());
+    GetObjectAttributesResponse.ObjectParts objectParts =
+        ((GetObjectAttributesResponse) response.getEntity()).getObjectParts();
+    assertEquals(2, objectParts.getPartsCount().intValue());
+    assertEquals(2, objectParts.getParts().size());
+    assertEquals(1, objectParts.getParts().get(0).getPartNumber());
+    assertEquals("part-one".length(), objectParts.getParts().get(0).getSize());
+    assertEquals(2, objectParts.getParts().get(1).getPartNumber());
+    assertEquals("part-two".length(), objectParts.getParts().get(1).getSize());
+  }
+
+  @Test
+  public void testGetObjectAttributesMultipartObjectPartsPagination() throws 
IOException, OS3Exception {
+    final String key = "mpu-paginated";
+    completeMultipartUploadWithParts(key, "part-one", "part-two", 
"part-three");
+
+    Response response = getObjectAttributes(rest, BUCKET_NAME, key, 
"ObjectParts", 2, 0);
+
+    assertEquals(HTTP_OK, response.getStatus());
+    GetObjectAttributesResponse.ObjectParts objectParts =
+        ((GetObjectAttributesResponse) response.getEntity()).getObjectParts();
+    assertEquals(3, objectParts.getPartsCount().intValue());
+    assertTrue(objectParts.isTruncated());
+    assertEquals(2, objectParts.getMaxParts().intValue());
+    assertEquals(0, objectParts.getPartNumberMarker().intValue());
+    assertEquals(2, objectParts.getNextPartNumberMarker().intValue());
+    assertTrue(objectParts.getParts().isEmpty());
+  }
+
+  @Test
+  public void testGetObjectAttributesInvalidMaxParts() throws IOException, 
OS3Exception {
+    final String key = "mpu-invalid-max-parts";
+    completeMultipartUploadWithParts(key, "part-one", "part-two");
+
+    assertErrorResponse(INVALID_ARGUMENT,
+        () -> getObjectAttributes(rest, BUCKET_NAME, key, "ObjectParts",
+            S3Consts.GET_OBJECT_ATTRIBUTES_MAX_PARTS_LIMIT + 1, null));
+  }
+
+  @Test
+  public void testGetObjectAttributesNonContiguousMultipartParts() throws 
IOException, OS3Exception {
+    final String key = "mpu-non-contiguous";
+    final String partOneContent = "part-one";
+    final String partThreeContent = "part-three";
+    String uploadID = initiateMultipartUpload(rest, BUCKET_NAME, key);
+    List<Part> partsList = new ArrayList<>();
+    partsList.add(uploadPart(rest, BUCKET_NAME, key, 1, uploadID, 
partOneContent));
+    partsList.add(uploadPart(rest, BUCKET_NAME, key, 3, uploadID, 
partThreeContent));
+    completeMultipartUpload(rest, BUCKET_NAME, key, uploadID, partsList);
+
+    Response responseWithoutChecksum = getObjectAttributes(rest, BUCKET_NAME, 
key, "ObjectParts");
+
+    assertEquals(HTTP_OK, responseWithoutChecksum.getStatus());
+    GetObjectAttributesResponse.ObjectParts objectPartsWithoutChecksum =
+        ((GetObjectAttributesResponse) 
responseWithoutChecksum.getEntity()).getObjectParts();
+    assertEquals(2, objectPartsWithoutChecksum.getPartsCount().intValue());
+    assertFalse(objectPartsWithoutChecksum.isTruncated());
+    assertTrue(objectPartsWithoutChecksum.getParts().isEmpty());
+
+    ((OzoneBucketStub) bucket).putKeyMetadataForTest(key, 
"x-amz-checksum-crc32", "dummy");
+
+    Response response = getObjectAttributes(rest, BUCKET_NAME, key, 
"ObjectParts");
+
+    assertEquals(HTTP_OK, response.getStatus());
+    GetObjectAttributesResponse.ObjectParts objectParts =
+        ((GetObjectAttributesResponse) response.getEntity()).getObjectParts();
+    assertEquals(2, objectParts.getPartsCount().intValue());
+    assertFalse(objectParts.isTruncated());
+    assertEquals(2, objectParts.getParts().size());
+    assertEquals(1, objectParts.getParts().get(0).getPartNumber());
+    assertEquals(partOneContent.length(), 
objectParts.getParts().get(0).getSize());
+    assertEquals(3, objectParts.getParts().get(1).getPartNumber());
+    assertEquals(partThreeContent.length(), 
objectParts.getParts().get(1).getSize());
+
+    Response paginatedResponse = getObjectAttributes(rest, BUCKET_NAME, key, 
"ObjectParts", 1, 1);
+
+    assertEquals(HTTP_OK, paginatedResponse.getStatus());
+    GetObjectAttributesResponse.ObjectParts paginatedParts =
+        ((GetObjectAttributesResponse) 
paginatedResponse.getEntity()).getObjectParts();
+    assertEquals(2, paginatedParts.getPartsCount().intValue());
+    assertFalse(paginatedParts.isTruncated());
+    assertEquals(1, paginatedParts.getPartNumberMarker().intValue());
+    assertNull(paginatedParts.getNextPartNumberMarker());
+    assertEquals(1, paginatedParts.getParts().size());
+    assertEquals(3, paginatedParts.getParts().get(0).getPartNumber());
+    assertEquals(partThreeContent.length(), 
paginatedParts.getParts().get(0).getSize());
+  }
+
+  private void completeMultipartUploadWithParts(String key, String... 
partContents)
+      throws IOException, OS3Exception {
+    String uploadID = initiateMultipartUpload(rest, BUCKET_NAME, key);
+    List<Part> partsList = new ArrayList<>();
+    for (int i = 0; i < partContents.length; i++) {
+      partsList.add(uploadPart(rest, BUCKET_NAME, key, i + 1, uploadID, 
partContents[i]));
+    }
+    completeMultipartUpload(rest, BUCKET_NAME, key, uploadID, partsList);
+  }
 }


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

Reply via email to