diff --git a/README.md b/README.md index cee8318..cd48769 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,6 @@ S3Proxy has broad compatibility with the S3 API, however, it does not support: * bucket logging * [CORS bucket operations](https://docs.aws.amazon.com/AmazonS3/latest/dev/cors.html#how-do-i-enable-cors) like getting or setting the CORS configuration for a bucket. S3Proxy only supports a static configuration (see below). * hosting static websites -* list objects v2, see [#168](https://github.com/gaul/s3proxy/issues/168) * object server-side encryption * object tagging * object versioning, see [#74](https://github.com/gaul/s3proxy/issues/74) diff --git a/src/main/java/org/gaul/s3proxy/S3ProxyHandler.java b/src/main/java/org/gaul/s3proxy/S3ProxyHandler.java index f6ec5a3..af15419 100644 --- a/src/main/java/org/gaul/s3proxy/S3ProxyHandler.java +++ b/src/main/java/org/gaul/s3proxy/S3ProxyHandler.java @@ -143,7 +143,6 @@ public class S3ProxyHandler { "cors", "inventory", "lifecycle", - "list-type", "logging", "metrics", "notification", @@ -1316,7 +1315,27 @@ public class S3ProxyHandler { if (prefix != null && !prefix.isEmpty()) { options.prefix(prefix); } - String marker = request.getParameter("marker"); + + boolean isListV2 = false; + String marker; + String listType = request.getParameter("list-type"); + String continuationToken = request.getParameter("continuation-token"); + String startAfter = request.getParameter("start-after"); + if (listType == null) { + marker = request.getParameter("marker"); + } else if (listType.equals("2")) { + isListV2 = true; + if (continuationToken != null && startAfter != null) { + throw new S3Exception(S3ErrorCode.INVALID_ARGUMENT); + } + if (continuationToken != null) { + marker = continuationToken; + } else { + marker = startAfter; + } + } else { + throw new S3Exception(S3ErrorCode.NOT_IMPLEMENTED); + } if (marker != null) { if (Quirks.OPAQUE_MARKERS.contains(blobStoreType)) { String realMarker = lastKeyToMarker.getIfPresent( @@ -1327,6 +1346,12 @@ public class S3ProxyHandler { } options.afterMarker(marker); } + + String fetchOwner = request.getParameter("fetch-owner"); + if (fetchOwner != null && !fetchOwner.equals("false")) { + throw new S3Exception(S3ErrorCode.NOT_IMPLEMENTED); + } + int maxKeys = 1000; String maxKeysString = request.getParameter("max-keys"); if (maxKeysString != null) { @@ -1372,11 +1397,26 @@ public class S3ProxyHandler { writeSimpleElement(xml, "MaxKeys", String.valueOf(maxKeys)); - if (marker == null) { - xml.writeEmptyElement("Marker"); + if (!isListV2) { + if (marker == null) { + xml.writeEmptyElement("Marker"); + } else { + writeSimpleElement(xml, "Marker", encodeBlob( + encodingType, marker)); + } } else { - writeSimpleElement(xml, "Marker", encodeBlob( - encodingType, marker)); + if (continuationToken == null) { + xml.writeEmptyElement("ContinuationToken"); + } else { + writeSimpleElement(xml, "ContinuationToken", encodeBlob( + encodingType, continuationToken)); + } + if (startAfter == null) { + xml.writeEmptyElement("StartAfter"); + } else { + writeSimpleElement(xml, "StartAfter", encodeBlob( + encodingType, startAfter)); + } } if (delimiter != null) { @@ -1391,8 +1431,9 @@ public class S3ProxyHandler { String nextMarker = set.getNextMarker(); if (nextMarker != null) { writeSimpleElement(xml, "IsTruncated", "true"); - writeSimpleElement(xml, "NextMarker", encodeBlob( - encodingType, nextMarker)); + writeSimpleElement(xml, + isListV2 ? "NextContinuationToken" : "NextMarker", + encodeBlob(encodingType, nextMarker)); if (Quirks.OPAQUE_MARKERS.contains(blobStoreType)) { lastKeyToMarker.put(Maps.immutableEntry(containerName, Iterables.getLast(set).getName()), nextMarker); diff --git a/src/test/java/org/gaul/s3proxy/AwsSdkTest.java b/src/test/java/org/gaul/s3proxy/AwsSdkTest.java index 9b84d1b..5573059 100644 --- a/src/test/java/org/gaul/s3proxy/AwsSdkTest.java +++ b/src/test/java/org/gaul/s3proxy/AwsSdkTest.java @@ -70,6 +70,8 @@ import com.amazonaws.services.s3.model.InitiateMultipartUploadRequest; import com.amazonaws.services.s3.model.InitiateMultipartUploadResult; import com.amazonaws.services.s3.model.ListMultipartUploadsRequest; import com.amazonaws.services.s3.model.ListObjectsRequest; +import com.amazonaws.services.s3.model.ListObjectsV2Request; +import com.amazonaws.services.s3.model.ListObjectsV2Result; import com.amazonaws.services.s3.model.ListPartsRequest; import com.amazonaws.services.s3.model.MultipartUploadListing; import com.amazonaws.services.s3.model.ObjectListing; @@ -904,6 +906,52 @@ public final class AwsSdkTest { .isEqualTo("blob2"); } + @Test + public void testBlobListV2() throws Exception { + ObjectMetadata metadata = new ObjectMetadata(); + metadata.setContentLength(BYTE_SOURCE.size()); + for (int i = 1; i < 5; ++i) { + client.putObject(containerName, String.valueOf(i), + BYTE_SOURCE.openStream(), metadata); + } + + ListObjectsV2Result result = client.listObjectsV2( + new ListObjectsV2Request() + .withBucketName(containerName) + .withMaxKeys(1) + .withStartAfter("1")); + assertThat(result.getContinuationToken()).isEmpty(); + assertThat(result.getStartAfter()).isEqualTo("1"); + assertThat(result.getNextContinuationToken()).isEqualTo("2"); + assertThat(result.isTruncated()).isTrue(); + assertThat(result.getObjectSummaries()).hasSize(1); + assertThat(result.getObjectSummaries().get(0).getKey()).isEqualTo("2"); + + result = client.listObjectsV2( + new ListObjectsV2Request() + .withBucketName(containerName) + .withMaxKeys(1) + .withContinuationToken(result.getNextContinuationToken())); + assertThat(result.getContinuationToken()).isEqualTo("2"); + assertThat(result.getStartAfter()).isEmpty(); + assertThat(result.getNextContinuationToken()).isEqualTo("3"); + assertThat(result.isTruncated()).isTrue(); + assertThat(result.getObjectSummaries()).hasSize(1); + assertThat(result.getObjectSummaries().get(0).getKey()).isEqualTo("3"); + + result = client.listObjectsV2( + new ListObjectsV2Request() + .withBucketName(containerName) + .withMaxKeys(1) + .withContinuationToken(result.getNextContinuationToken())); + assertThat(result.getContinuationToken()).isEqualTo("3"); + assertThat(result.getStartAfter()).isEmpty(); + assertThat(result.getNextContinuationToken()).isNull(); + assertThat(result.isTruncated()).isFalse(); + assertThat(result.getObjectSummaries()).hasSize(1); + assertThat(result.getObjectSummaries().get(0).getKey()).isEqualTo("4"); + } + @Test public void testBlobMetadata() throws Exception { String blobName = "blob";