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 96c43b3354b HDDS-16362. GetObjectAttributes ObjectParts should return
Part entries for FSO buckets (#11242).
96c43b3354b is described below
commit 96c43b3354bad6659eb1f7d0c9a35cdc65f3dd41
Author: Gargi Jaiswal <[email protected]>
AuthorDate: Tue Oct 6 10:04:36 2026 +0530
HDDS-16362. GetObjectAttributes ObjectParts should return Part entries for
FSO buckets (#11242).
---
.../ozone/client/S3HeadObjectAttributes.java | 10 +-
.../apache/hadoop/ozone/client/rpc/RpcClient.java | 16 ++-
.../ozone/om/helpers/KeyInfoWithVolumeContext.java | 25 +++-
.../ozone/s3/awssdk/v2/AbstractS3SDKV2Tests.java | 98 +++++++++++---
.../src/main/proto/OmClientProtocol.proto | 1 +
.../apache/hadoop/ozone/om/OmMetadataReader.java | 6 +-
.../org/apache/hadoop/ozone/om/OmSnapshot.java | 1 +
.../ozone/s3/endpoint/ObjectAttributesHandler.java | 38 ++++--
.../hadoop/ozone/client/ClientProtocolStub.java | 2 +-
.../ozone/s3/endpoint/TestObjectAttributesGet.java | 146 ++++++++++++++++++++-
10 files changed, 304 insertions(+), 39 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
index 91a5ae70193..fd2a8e214ea 100644
---
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
@@ -20,6 +20,7 @@
import java.util.Collections;
import java.util.NavigableMap;
import java.util.Objects;
+import org.apache.hadoop.ozone.om.helpers.BucketLayout;
/**
* Head metadata and completed multipart part sizes from a single S3 {@code
GetKeyInfo} call.
@@ -28,13 +29,16 @@ public final class S3HeadObjectAttributes {
private final OzoneKey key;
private final NavigableMap<Integer, Long> completedMultipartPartSizes;
+ private final BucketLayout bucketLayout;
public S3HeadObjectAttributes(OzoneKey key,
- NavigableMap<Integer, Long> completedMultipartPartSizes) {
+ NavigableMap<Integer, Long> completedMultipartPartSizes,
+ BucketLayout bucketLayout) {
this.key = Objects.requireNonNull(key, "key == null");
this.completedMultipartPartSizes = completedMultipartPartSizes == null
? Collections.emptyNavigableMap()
: completedMultipartPartSizes;
+ this.bucketLayout = bucketLayout == null ? BucketLayout.DEFAULT :
bucketLayout;
}
public OzoneKey getKey() {
@@ -44,4 +48,8 @@ public OzoneKey getKey() {
public NavigableMap<Integer, Long> getCompletedMultipartPartSizes() {
return completedMultipartPartSizes;
}
+
+ public BucketLayout getBucketLayout() {
+ return bucketLayout;
+ }
}
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 899cf921e4c..522fb17e1fb 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
@@ -1955,7 +1955,9 @@ public OzoneKey headS3Object(String bucketName, String
keyName,
@Override
public S3HeadObjectAttributes headS3ObjectAttributes(String bucketName,
String keyName)
throws IOException {
- OmKeyInfo keyInfo = getS3KeyInfo(bucketName, keyName, true);
+ KeyInfoWithVolumeContext keyInfoWithS3Context =
+ getS3KeyInfoWithVolumeContext(bucketName, keyName, true);
+ OmKeyInfo keyInfo = keyInfoWithS3Context.getKeyInfo();
OmKeyLocationInfoGroup locationGroup = keyInfo.getLatestVersionLocations();
NavigableMap<Integer, Long> partSizes = Collections.emptyNavigableMap();
if (locationGroup != null && locationGroup.isMultipartKey()) {
@@ -1967,7 +1969,9 @@ public S3HeadObjectAttributes
headS3ObjectAttributes(String bucketName, String k
}
}
}
- return new S3HeadObjectAttributes(OzoneKey.fromKeyInfo(keyInfo),
partSizes);
+ BucketLayout bucketLayout = keyInfoWithS3Context.getBucketLayout()
+ .orElse(BucketLayout.DEFAULT);
+ return new S3HeadObjectAttributes(OzoneKey.fromKeyInfo(keyInfo),
partSizes, bucketLayout);
}
private OmKeyInfo getS3PartOmKeyInfo(String bucketName, String keyName,
@@ -1993,6 +1997,12 @@ private OmKeyInfo getS3PartOmKeyInfo(String bucketName,
String keyName,
@Nonnull
private OmKeyInfo getS3KeyInfo(
String bucketName, String keyName, boolean isHeadOp) throws IOException {
+ return getS3KeyInfoWithVolumeContext(bucketName, keyName,
isHeadOp).getKeyInfo();
+ }
+
+ @Nonnull
+ private KeyInfoWithVolumeContext getS3KeyInfoWithVolumeContext(
+ String bucketName, String keyName, boolean isHeadOp) throws IOException {
verifyBucketName(bucketName);
Objects.requireNonNull(keyName, "keyName == null");
@@ -2010,7 +2020,7 @@ private OmKeyInfo getS3KeyInfo(
KeyInfoWithVolumeContext keyInfoWithS3Context =
ozoneManagerClient.getKeyInfo(keyArgs, true);
keyInfoWithS3Context.getUserPrincipal().ifPresent(this::updateS3Principal);
- return keyInfoWithS3Context.getKeyInfo();
+ return keyInfoWithS3Context;
}
@Nonnull
diff --git
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/KeyInfoWithVolumeContext.java
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/KeyInfoWithVolumeContext.java
index d6d54d3c174..bc2a70942ac 100644
---
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/KeyInfoWithVolumeContext.java
+++
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/KeyInfoWithVolumeContext.java
@@ -37,12 +37,22 @@ public class KeyInfoWithVolumeContext {
private final OmKeyInfo keyInfo;
+ private final Optional<BucketLayout> bucketLayout;
+
public KeyInfoWithVolumeContext(OmVolumeArgs volumeArgs,
String userPrincipal,
OmKeyInfo keyInfo) {
+ this(volumeArgs, userPrincipal, keyInfo, null);
+ }
+
+ public KeyInfoWithVolumeContext(OmVolumeArgs volumeArgs,
+ String userPrincipal,
+ OmKeyInfo keyInfo,
+ BucketLayout bucketLayout) {
this.volumeArgs = Optional.ofNullable(volumeArgs);
this.userPrincipal = Optional.ofNullable(userPrincipal);
this.keyInfo = keyInfo;
+ this.bucketLayout = Optional.ofNullable(bucketLayout);
}
public static KeyInfoWithVolumeContext fromProtobuf(
@@ -52,6 +62,8 @@ public static KeyInfoWithVolumeContext fromProtobuf(
OmVolumeArgs.getFromProtobuf(proto.getVolumeInfo()) : null)
.setUserPrincipal(proto.getUserPrincipal())
.setKeyInfo(OmKeyInfo.getFromProtobuf(proto.getKeyInfo()))
+ .setBucketLayout(proto.hasBucketLayout()
+ ? BucketLayout.fromProto(proto.getBucketLayout()) : null)
.build();
}
@@ -60,6 +72,7 @@ public GetKeyInfoResponse toProtobuf(int clientVersion) {
volumeArgs.ifPresent(v -> builder.setVolumeInfo(v.getProtobuf()));
userPrincipal.ifPresent(builder::setUserPrincipal);
builder.setKeyInfo(keyInfo.getProtobuf(clientVersion));
+ bucketLayout.ifPresent(layout ->
builder.setBucketLayout(layout.toProto()));
return builder.build();
}
@@ -75,6 +88,10 @@ public Optional<String> getUserPrincipal() {
return userPrincipal;
}
+ public Optional<BucketLayout> getBucketLayout() {
+ return bucketLayout;
+ }
+
public static Builder newBuilder() {
return new Builder();
}
@@ -86,6 +103,7 @@ public static class Builder {
private OmVolumeArgs volumeArgs;
private String userPrincipal;
private OmKeyInfo keyInfo;
+ private BucketLayout bucketLayout;
public Builder setVolumeArgs(OmVolumeArgs volumeArgs) {
this.volumeArgs = volumeArgs;
@@ -102,8 +120,13 @@ public Builder setKeyInfo(OmKeyInfo keyInfo) {
return this;
}
+ public Builder setBucketLayout(BucketLayout layout) {
+ this.bucketLayout = layout;
+ return this;
+ }
+
public KeyInfoWithVolumeContext build() {
- return new KeyInfoWithVolumeContext(volumeArgs, userPrincipal, keyInfo);
+ return new KeyInfoWithVolumeContext(volumeArgs, userPrincipal, keyInfo,
bucketLayout);
}
}
}
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 bb3efb1d304..6e88f707672 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
@@ -2243,6 +2243,72 @@ public void
testGetObjectAttributesMultipartObjectParts(@TempDir Path tempDir) t
assertTrue(secondPage.objectParts().parts().isEmpty());
}
+ /**
+ * Directory (FSO layout) buckets return per-part {@code Part} elements even
when no additional
+ * checksum was stored at upload time, matching AWS S3 directory-bucket
behavior.
+ */
+ @Test
+ public void testGetObjectAttrFsoMpuNoChecksum(@TempDir Path tempDir)
+ throws Exception {
+ final String bucketName = uniqueObjectName();
+ final String keyName = getKeyName();
+ final int partSize = (int) (5 * MB);
+
+ createFsoBucket(bucketName);
+ try {
+ File multipartUploadFile =
+
Files.createFile(tempDir.resolve("fso-get-object-attributes-mpu.txt")).toFile();
+ createFile(multipartUploadFile, (int) (15 * MB));
+ multipartUpload(bucketName, keyName, multipartUploadFile, partSize, new
HashMap<>(),
+ Collections.emptyList());
+
+ GetObjectAttributesResponse attributesResponse =
s3Client.getObjectAttributes(
+ GetObjectAttributesRequest.builder()
+ .bucket(bucketName)
+ .key(keyName)
+ .objectAttributes(ObjectAttributes.OBJECT_PARTS,
ObjectAttributes.OBJECT_SIZE)
+ .build());
+
+ assertNotNull(attributesResponse.objectParts());
+ assertEquals(3, attributesResponse.objectParts().totalPartsCount());
+ assertFalse(attributesResponse.objectParts().isTruncated());
+ assertEquals(multipartUploadFile.length(),
attributesResponse.objectSize());
+ assertEquals(3, attributesResponse.objectParts().parts().size());
+ for (int i = 0; i < 3; i++) {
+ assertEquals(i + 1,
attributesResponse.objectParts().parts().get(i).partNumber());
+ assertEquals((long) partSize,
attributesResponse.objectParts().parts().get(i).size());
+ }
+
+ 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().parts().size());
+ assertEquals(1, firstPage.objectParts().parts().get(0).partNumber());
+ assertEquals(2, firstPage.objectParts().parts().get(1).partNumber());
+
+ GetObjectAttributesResponse secondPage = s3Client.getObjectAttributes(
+ GetObjectAttributesRequest.builder()
+ .bucket(bucketName)
+ .key(keyName)
+ .objectAttributes(ObjectAttributes.OBJECT_PARTS)
+ .maxParts(2)
+ .partNumberMarker(2)
+ .build());
+ assertFalse(secondPage.objectParts().isTruncated());
+ assertEquals(1, secondPage.objectParts().parts().size());
+ assertEquals(3, secondPage.objectParts().parts().get(0).partNumber());
+ } finally {
+ s3Client.deleteObject(b -> b.bucket(bucketName).key(keyName));
+ deleteFsoBucket(bucketName);
+ }
+ }
+
@Test
public void testGetObjectAttributesNonContiguousMultipartObjectParts()
throws Exception {
final String bucketName = getBucketName();
@@ -3443,6 +3509,22 @@ private static void assertNotImplemented(Executable
request) {
assertEquals(S3ErrorTable.NOT_IMPLEMENTED.getErrorMessage(),
exception.awsErrorDetails().errorMessage());
}
+ private void createFsoBucket(String bucketName) throws Exception {
+ try (OzoneClient ozoneClient = cluster.newClient()) {
+ OzoneVolume volume = ozoneClient.getObjectStore().getS3Volume();
+ volume.createBucket(bucketName, BucketArgs.newBuilder()
+ .setBucketLayout(BucketLayout.FILE_SYSTEM_OPTIMIZED)
+ .build());
+ }
+ }
+
+ private void deleteFsoBucket(String bucketName) throws Exception {
+ try (OzoneClient ozoneClient = cluster.newClient()) {
+ OzoneVolume volume = ozoneClient.getObjectStore().getS3Volume();
+ volume.deleteBucket(bucketName);
+ }
+ }
+
private String multipartUpload(String bucketName, String key, File file, int
partSize,
Map<String, String> userMetadata, List<Tag>
tags) throws Exception {
String uploadId = initiateMultipartUpload(bucketName, key, userMetadata,
tags);
@@ -4591,21 +4673,5 @@ public void testListBucketsIncludesFSOBuckets() throws
Exception {
deleteFsoBucket(fsoBucketName);
}
}
-
- private void createFsoBucket(String bucketName) throws Exception {
- try (OzoneClient ozoneClient = cluster.newClient()) {
- OzoneVolume volume = ozoneClient.getObjectStore().getS3Volume();
- volume.createBucket(bucketName, BucketArgs.newBuilder()
- .setBucketLayout(BucketLayout.FILE_SYSTEM_OPTIMIZED)
- .build());
- }
- }
-
- private void deleteFsoBucket(String bucketName) throws Exception {
- try (OzoneClient ozoneClient = cluster.newClient()) {
- OzoneVolume volume = ozoneClient.getObjectStore().getS3Volume();
- volume.deleteBucket(bucketName);
- }
- }
}
}
diff --git
a/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
b/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
index ddc4cd80aab..43f7c6aae51 100644
--- a/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
+++ b/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
@@ -1419,6 +1419,7 @@ message GetKeyInfoResponse {
optional KeyInfo keyInfo = 1;
optional VolumeInfo volumeInfo = 2;
optional string UserPrincipal = 3;
+ optional BucketLayoutProto bucketLayout = 4;
}
message RenameKeysRequest {
diff --git
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataReader.java
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataReader.java
index d6db1cb35d8..c3121599566 100644
---
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataReader.java
+++
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataReader.java
@@ -31,6 +31,7 @@
import java.net.InetAddress;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
@@ -46,6 +47,7 @@
import org.apache.hadoop.ozone.audit.OMAction;
import org.apache.hadoop.ozone.om.exceptions.OMException;
import org.apache.hadoop.ozone.om.helpers.BasicOmKeyInfo;
+import org.apache.hadoop.ozone.om.helpers.BucketLayout;
import org.apache.hadoop.ozone.om.helpers.KeyInfoWithVolumeContext;
import org.apache.hadoop.ozone.om.helpers.ListKeysLightResult;
import org.apache.hadoop.ozone.om.helpers.ListKeysResult;
@@ -199,7 +201,9 @@ public KeyInfoWithVolumeContext getKeyInfo(final OmKeyArgs
args,
OmMetadataReader.getClientAddress());
KeyInfoWithVolumeContext.Builder builder = KeyInfoWithVolumeContext
.newBuilder()
- .setKeyInfo(keyInfo);
+ .setKeyInfo(keyInfo)
+ .setBucketLayout(Objects.requireNonNullElse(
+ bucket.bucketLayout(), BucketLayout.DEFAULT));
s3VolumeContext.ifPresent(context -> {
builder.setVolumeArgs(context.getOmVolumeArgs());
builder.setUserPrincipal(context.getUserPrincipal());
diff --git
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmSnapshot.java
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmSnapshot.java
index 3e59092c74f..acba22ce721 100644
---
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmSnapshot.java
+++
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmSnapshot.java
@@ -323,6 +323,7 @@ private KeyInfoWithVolumeContext
denormalizeKeyInfoWithVolumeContext(
.setKeyInfo(denormalizeOmKeyInfo(k.getKeyInfo()))
.setVolumeArgs(k.getVolumeArgs().orElse(null))
.setUserPrincipal(k.getUserPrincipal().orElse(null))
+ .setBucketLayout(k.getBucketLayout().orElse(null))
.build();
}
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 171fabde56b..45755f22de4 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
@@ -57,10 +57,10 @@
* <p>Returns selected metadata about an object without transferring the
object body.
* 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. 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
+ * <p>For general-purpose buckets, individual {@code Part} elements under
{@code ObjectParts}
+ * are omitted unless an additional checksum is stored on the object, matching
AWS S3 behavior.
+ * For FSO layout buckets (Ozone directory buckets), {@code Part} elements are
always returned,
+ * matching AWS S3 directory-bucket 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
@@ -101,6 +101,15 @@ static boolean hasStoredAdditionalChecksum(OzoneKey key) {
return false;
}
+ /**
+ * Whether {@code ObjectParts} should include per-part {@code Part} elements
in the response.
+ * Directory (FSO layout) buckets always include them; general-purpose
buckets include them
+ * only when an additional checksum was stored at upload time.
+ */
+ static boolean shouldIncludePartElements(OzoneKey key, boolean
directoryBucketLayout) {
+ return directoryBucketLayout || hasStoredAdditionalChecksum(key);
+ }
+
@Override
S3GAction getAction() {
return queryParams().get(QueryParams.ATTRIBUTES) == null ? null :
S3GAction.NOT_IMPLEMENTED;
@@ -123,12 +132,15 @@ Response handleGetRequest(ObjectRequestContext context,
String keyPath)
OzoneKey key;
NavigableMap<Integer, Long> completedPartSizes = null;
+ boolean directoryBucketLayout = false;
try {
if (requestedAttributes.contains(ATTR_OBJECT_PARTS)) {
S3HeadObjectAttributes headAttributes =
getClientProtocol().headS3ObjectAttributes(bucketName, keyPath);
key = headAttributes.getKey();
completedPartSizes = headAttributes.getCompletedMultipartPartSizes();
+ directoryBucketLayout =
+ headAttributes.getBucketLayout().isFileSystemOptimized();
} else {
key = getClientProtocol().headS3Object(bucketName, keyPath);
}
@@ -143,7 +155,8 @@ Response handleGetRequest(ObjectRequestContext context,
String keyPath)
}
GetObjectAttributesResponse response =
- buildResponse(keyPath, key, requestedAttributes, completedPartSizes);
+ buildResponse(keyPath, key, requestedAttributes, completedPartSizes,
+ directoryBucketLayout);
Response.ResponseBuilder rb = Response.ok(response,
MediaType.APPLICATION_XML_TYPE);
ObjectEndpoint.addLastModifiedDate(rb, key);
@@ -182,7 +195,8 @@ private Set<String> parseAttributesHeader(String keyPath)
throws OS3Exception {
}
private GetObjectAttributesResponse buildResponse(String keyPath, OzoneKey
key,
- Set<String> requested, NavigableMap<Integer, Long> completedPartSizes)
+ Set<String> requested, NavigableMap<Integer, Long> completedPartSizes,
+ boolean directoryBucketLayout)
throws IOException, OS3Exception {
GetObjectAttributesResponse resp = new GetObjectAttributesResponse();
@@ -210,7 +224,7 @@ private GetObjectAttributesResponse buildResponse(String
keyPath, OzoneKey key,
String partsCountStr = extractPartsCount(eTag);
if (partsCountStr != null && completedPartSizes != null) {
resp.setObjectParts(buildObjectParts(keyPath,
Integer.parseInt(partsCountStr),
- completedPartSizes, key));
+ completedPartSizes, key, directoryBucketLayout));
}
}
}
@@ -233,10 +247,12 @@ private GetObjectAttributesResponse buildResponse(String
keyPath, OzoneKey key,
*
* <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.
+ * summary and pagination fields are returned. For FSO layout (directory)
buckets, {@code Part}
+ * elements are always returned.
*/
private GetObjectAttributesResponse.ObjectParts buildObjectParts(String
keyPath,
- int totalPartsCount, NavigableMap<Integer, Long> partSizes, OzoneKey key)
+ int totalPartsCount, NavigableMap<Integer, Long> partSizes, OzoneKey key,
+ boolean directoryBucketLayout)
throws OS3Exception {
int maxParts = parseMaxPartsHeader(keyPath);
int marker = parsePartNumberMarkerHeader(keyPath);
@@ -257,9 +273,7 @@ private GetObjectAttributesResponse.ObjectParts
buildObjectParts(String keyPath,
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);
+ boolean includePartEntries = shouldIncludePartElements(key,
directoryBucketLayout);
Integer lastPartReturned = null;
int partsOnPage = 0;
while (partIterator.hasNext() && partsOnPage < maxParts) {
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 6e57ae2f604..c8c326eb736 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
@@ -149,7 +149,7 @@ public S3HeadObjectAttributes headS3ObjectAttributes(String
bucketName, String k
NavigableMap<Integer, Long> partSizes = bucket instanceof OzoneBucketStub
? ((OzoneBucketStub) bucket).getCompletedMultipartPartSizes(keyName)
: Collections.emptyNavigableMap();
- return new S3HeadObjectAttributes(key, partSizes);
+ return new S3HeadObjectAttributes(key, partSizes,
bucket.getBucketLayout());
}
@Override
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 6c39ac89e1a..343924f29ff 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
@@ -35,17 +35,38 @@
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 static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
+import java.util.NavigableMap;
+import java.util.TreeMap;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
+import org.apache.hadoop.hdds.client.RatisReplicationConfig;
import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
+import org.apache.hadoop.ozone.OzoneConfigKeys;
+import org.apache.hadoop.ozone.OzoneConsts;
+import org.apache.hadoop.ozone.client.BucketArgs;
+import org.apache.hadoop.ozone.client.ObjectStore;
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.client.OzoneKey;
+import org.apache.hadoop.ozone.client.OzoneVolume;
+import org.apache.hadoop.ozone.client.S3HeadObjectAttributes;
+import org.apache.hadoop.ozone.client.protocol.ClientProtocol;
+import org.apache.hadoop.ozone.om.exceptions.OMException;
+import org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes;
+import org.apache.hadoop.ozone.om.helpers.BucketLayout;
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;
@@ -60,6 +81,7 @@ public class TestObjectAttributesGet {
private static final String CONTENT = "0123456789";
private static final String BUCKET_NAME = "b1";
+ private static final String FSO_BUCKET_NAME = "fso-b1";
private static final String KEY_NAME = "key1";
private ObjectEndpoint rest;
private OzoneBucket bucket;
@@ -299,13 +321,129 @@ public void
testGetObjectAttributesNonContiguousMultipartParts() throws IOExcept
assertEquals(partThreeContent.length(),
paginatedParts.getParts().get(0).getSize());
}
- private void completeMultipartUploadWithParts(String key, String...
partContents)
+ @Test
+ public void testGetObjectAttrFsoMPUPartsWithoutChecksum()
throws IOException, OS3Exception {
- String uploadID = initiateMultipartUpload(rest, BUCKET_NAME, key);
+ OzoneClient client = rest.getClient();
+ String volumeName =
rest.getOzoneConfiguration().get(OzoneConfigKeys.OZONE_S3_VOLUME_NAME,
+ OzoneConfigKeys.OZONE_S3_VOLUME_NAME_DEFAULT);
+ OzoneVolume volume = client.getObjectStore().getVolume(volumeName);
+ volume.createBucket(FSO_BUCKET_NAME, BucketArgs.newBuilder()
+ .setBucketLayout(BucketLayout.FILE_SYSTEM_OPTIMIZED)
+ .build());
+
+ final String key = "fso-mpu-key";
+ completeMultipartUploadWithPartsInBucket(FSO_BUCKET_NAME, key, "part-one",
"part-two");
+
+ Response response = getObjectAttributes(rest, FSO_BUCKET_NAME, key,
"ObjectParts");
+
+ assertEquals(HTTP_OK, response.getStatus());
+ GetObjectAttributesResponse.ObjectParts objectParts =
+ ((GetObjectAttributesResponse) response.getEntity()).getObjectParts();
+ assertNotNull(objectParts);
+ 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());
+ }
+
+ /**
+ * Regression: multipart {@code ObjectParts} must not call {@code
context.getBucket()}
+ * (bucket READ). Layout and part sizes come from {@code
headS3ObjectAttributes} only.
+ */
+ @Test
+ public void testGetObjectAttributesObjectPartsWithoutBucketRead()
+ throws IOException, OS3Exception {
+ OzoneConfiguration ozoneConfiguration = new OzoneConfiguration();
+ ozoneConfiguration.set(OzoneConfigKeys.OZONE_S3_VOLUME_NAME,
+ OzoneConfigKeys.OZONE_S3_VOLUME_NAME_DEFAULT);
+
+ ClientProtocol clientProtocol = mock(ClientProtocol.class);
+ OzoneClient client = mock(OzoneClient.class);
+ ObjectStore objectStore = mock(ObjectStore.class);
+ OzoneVolume volume = mock(OzoneVolume.class);
+
+ when(client.getConfiguration()).thenReturn(ozoneConfiguration);
+ when(client.getObjectStore()).thenReturn(objectStore);
+ when(client.getProxy()).thenReturn(clientProtocol);
+ when(objectStore.getClientProxy()).thenReturn(clientProtocol);
+ when(objectStore.getS3Volume()).thenReturn(volume);
+ when(volume.getBucket(anyString())).thenThrow(
+ new OMException("bucket read denied", ResultCodes.ACCESS_DENIED));
+
+ final String mpuKey = "mpu-key";
+ OzoneKey ozoneKey = new OzoneKey("s3v", BUCKET_NAME, mpuKey, 10, 0, 0,
+ RatisReplicationConfig.getInstance(HddsProtos.ReplicationFactor.ONE),
+ true, "owner");
+ ozoneKey.setMetadata(Collections.singletonMap(OzoneConsts.ETAG,
"\"abc-2\""));
+
+ NavigableMap<Integer, Long> partSizes = new TreeMap<>();
+ partSizes.put(1, 5L);
+ partSizes.put(2, 5L);
+ S3HeadObjectAttributes headAttributes = new
S3HeadObjectAttributes(ozoneKey, partSizes,
+ BucketLayout.OBJECT_STORE);
+ when(clientProtocol.headS3ObjectAttributes(BUCKET_NAME,
mpuKey)).thenReturn(headAttributes);
+
+ HttpHeaders headers = Mockito.mock(HttpHeaders.class);
+
when(headers.getHeaderString(X_AMZ_CONTENT_SHA256)).thenReturn("UNSIGNED-PAYLOAD");
+
+ ObjectEndpoint endpoint = EndpointBuilder.newObjectEndpointBuilder()
+ .setClient(client)
+ .setHeaders(headers)
+ .build();
+
+ Response response = getObjectAttributes(endpoint, BUCKET_NAME, mpuKey,
"ObjectParts");
+
+ assertEquals(HTTP_OK, response.getStatus());
+ GetObjectAttributesResponse.ObjectParts objectParts =
+ ((GetObjectAttributesResponse) response.getEntity()).getObjectParts();
+ assertNotNull(objectParts);
+ assertEquals(2, objectParts.getPartsCount().intValue());
+ assertTrue(objectParts.getParts().isEmpty());
+
+ verify(volume, never()).getBucket(anyString());
+ verify(clientProtocol).headS3ObjectAttributes(BUCKET_NAME, mpuKey);
+ }
+
+ @Test
+ public void testShouldIncludePartElements() {
+ assertFalse(ObjectAttributesHandler.shouldIncludePartElements(null,
false));
+ assertTrue(ObjectAttributesHandler.shouldIncludePartElements(null, true));
+
+ OzoneKey obsKeyNoChecksum = testOzoneKey(false);
+
assertFalse(ObjectAttributesHandler.shouldIncludePartElements(obsKeyNoChecksum,
false));
+
+ OzoneKey obsKeyWithChecksum = testOzoneKey(true);
+
assertTrue(ObjectAttributesHandler.shouldIncludePartElements(obsKeyWithChecksum,
false));
+
+
assertTrue(ObjectAttributesHandler.shouldIncludePartElements(obsKeyNoChecksum,
true));
+
assertTrue(ObjectAttributesHandler.shouldIncludePartElements(obsKeyWithChecksum,
true));
+ }
+
+ private static OzoneKey testOzoneKey(boolean withAdditionalChecksum) {
+ OzoneKey key = new OzoneKey("vol", "b", "k", 0, 0, 0,
+ RatisReplicationConfig.getInstance(HddsProtos.ReplicationFactor.ONE),
+ true, "owner");
+ if (withAdditionalChecksum) {
+ key.setMetadata(Collections.singletonMap("x-amz-checksum-crc32", "abc"));
+ }
+ return key;
+ }
+
+ private void completeMultipartUploadWithPartsInBucket(String bucketName,
String key,
+ String... partContents) throws IOException, OS3Exception {
+ String uploadID = initiateMultipartUpload(rest, bucketName, 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]));
+ partsList.add(uploadPart(rest, bucketName, key, i + 1, uploadID,
partContents[i]));
}
- completeMultipartUpload(rest, BUCKET_NAME, key, uploadID, partsList);
+ completeMultipartUpload(rest, bucketName, key, uploadID, partsList);
+ }
+
+ private void completeMultipartUploadWithParts(String key, String...
partContents)
+ throws IOException, OS3Exception {
+ completeMultipartUploadWithPartsInBucket(BUCKET_NAME, key, partContents);
}
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]