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]