Skip to content

Commit 31e1b8a

Browse files
committed
Support ListObjectVersions
Not all aspects have been tested yet. Fixes #64
1 parent 30fd0ac commit 31e1b8a

File tree

12 files changed

+178
-88
lines changed

12 files changed

+178
-88
lines changed
Lines changed: 81 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2017-2024 Adobe.
2+
* Copyright 2017-2025 Adobe.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -22,49 +22,102 @@ import org.junit.jupiter.api.Test
2222
import org.junit.jupiter.api.TestInfo
2323
import software.amazon.awssdk.core.sync.RequestBody
2424
import software.amazon.awssdk.services.s3.S3Client
25-
import software.amazon.awssdk.services.s3.model.ChecksumAlgorithm
26-
import software.amazon.awssdk.services.s3.model.ListObjectVersionsRequest
27-
import software.amazon.awssdk.services.s3.model.ObjectVersion
28-
import software.amazon.awssdk.services.s3.model.PutObjectRequest
25+
import software.amazon.awssdk.services.s3.model.BucketVersioningStatus
2926
import java.io.File
3027

3128
internal class ListObjectVersionsV2IT : S3TestBase() {
3229
private val s3ClientV2: S3Client = createS3ClientV2()
3330

3431
@Test
3532
@S3VerifiedSuccess(year = 2024)
36-
fun testPutObjects_listObjectVersions(testInfo: TestInfo) {
33+
fun testListObjectVersions(testInfo: TestInfo) {
3734
val uploadFile = File(UPLOAD_FILE_NAME)
3835
val bucketName = givenBucketV2(testInfo)
36+
s3ClientV2.putBucketVersioning {
37+
it.bucket(bucketName)
38+
it.versioningConfiguration {
39+
it.status(BucketVersioningStatus.ENABLED)
40+
}
41+
}
3942

40-
s3ClientV2.putObject(
41-
PutObjectRequest.builder()
42-
.bucket(bucketName).key("$UPLOAD_FILE_NAME-1")
43-
.checksumAlgorithm(ChecksumAlgorithm.SHA256)
44-
.build(),
43+
val version1 = s3ClientV2.putObject(
44+
{
45+
it.bucket(bucketName)
46+
it.key("$UPLOAD_FILE_NAME-1")
47+
},
4548
RequestBody.fromFile(uploadFile)
46-
)
49+
).versionId()
4750

48-
s3ClientV2.putObject(
49-
PutObjectRequest.builder()
50-
.bucket(bucketName).key("$UPLOAD_FILE_NAME-2")
51-
.checksumAlgorithm(ChecksumAlgorithm.SHA256)
52-
.build(),
51+
val version2 = s3ClientV2.putObject(
52+
{
53+
it.bucket(bucketName)
54+
it.key("$UPLOAD_FILE_NAME-2")
55+
},
5356
RequestBody.fromFile(uploadFile)
54-
)
57+
).versionId()
5558

56-
s3ClientV2.listObjectVersions(
57-
ListObjectVersionsRequest.builder()
58-
.bucket(bucketName)
59-
.build()
60-
).also {
59+
s3ClientV2.listObjectVersions {
60+
it.bucket(bucketName)
61+
}.also {
6162
assertThat(it.versions())
6263
.hasSize(2)
63-
.extracting(ObjectVersion::checksumAlgorithm)
64-
.containsOnly(
65-
Tuple(arrayListOf(ChecksumAlgorithm.SHA256)),
66-
Tuple(arrayListOf(ChecksumAlgorithm.SHA256))
67-
)
64+
.extracting("versionId", "isLatest")
65+
.containsExactlyInAnyOrder(Tuple(version1, true), Tuple(version2, true))
66+
}
67+
}
68+
69+
@Test
70+
@S3VerifiedTodo
71+
fun testListObjectVersions_deleteMarker(testInfo: TestInfo) {
72+
val uploadFile = File(UPLOAD_FILE_NAME)
73+
val bucketName = givenBucketV2(testInfo)
74+
s3ClientV2.putBucketVersioning {
75+
it.bucket(bucketName)
76+
it.versioningConfiguration {
77+
it.status(BucketVersioningStatus.ENABLED)
78+
}
79+
}
80+
81+
val version1 = s3ClientV2.putObject(
82+
{
83+
it.bucket(bucketName)
84+
it.key("$UPLOAD_FILE_NAME-1")
85+
},
86+
RequestBody.fromFile(uploadFile)
87+
).versionId()
88+
89+
val version2 = s3ClientV2.putObject(
90+
{
91+
it.bucket(bucketName)
92+
it.key("$UPLOAD_FILE_NAME-2")
93+
},
94+
RequestBody.fromFile(uploadFile)
95+
).versionId()
96+
97+
val version3 = s3ClientV2.putObject(
98+
{
99+
it.bucket(bucketName)
100+
it.key("$UPLOAD_FILE_NAME-3")
101+
},
102+
RequestBody.fromFile(uploadFile)
103+
).versionId()
104+
105+
s3ClientV2.deleteObject {
106+
it.bucket(bucketName)
107+
it.key("$UPLOAD_FILE_NAME-3")
108+
}
109+
110+
s3ClientV2.listObjectVersions {
111+
it.bucket(bucketName)
112+
}.also {
113+
assertThat(it.versions())
114+
.hasSize(3)
115+
.extracting("versionId", "isLatest")
116+
.containsExactlyInAnyOrder(Tuple(version1, true), Tuple(version2, true), Tuple(version3, false))
117+
assertThat(it.deleteMarkers())
118+
.hasSize(1)
119+
.extracting("key")
120+
.containsExactlyInAnyOrder("$UPLOAD_FILE_NAME-3")
68121
}
69122
}
70123
}

integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/VersionsV2IT.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -182,17 +182,15 @@ internal class VersionsV2IT : S3TestBase() {
182182
}
183183
}
184184

185-
val versionId1 = s3ClientV2.putObject(
185+
s3ClientV2.putObject(
186186
{
187187
it.bucket(bucketName).key(UPLOAD_FILE_NAME)
188-
it.checksumAlgorithm(ChecksumAlgorithm.SHA1)
189188
}, RequestBody.fromFile(uploadFile)
190189
).versionId()
191190

192191
s3ClientV2.putObject(
193192
{
194193
it.bucket(bucketName).key(UPLOAD_FILE_NAME)
195-
it.checksumAlgorithm(ChecksumAlgorithm.SHA1)
196194
}, RequestBody.fromFile(uploadFile)
197195
).versionId()
198196

server/src/main/java/com/adobe/testing/s3mock/BucketController.java

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2017-2024 Adobe.
2+
* Copyright 2017-2025 Adobe.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
1616

1717
package com.adobe.testing.s3mock;
1818

19+
import static com.adobe.testing.s3mock.S3Exception.NOT_FOUND_BUCKET_VERSIONING_CONFIGURATION;
1920
import static com.adobe.testing.s3mock.util.AwsHttpHeaders.X_AMZ_BUCKET_OBJECT_LOCK_ENABLED;
2021
import static com.adobe.testing.s3mock.util.AwsHttpHeaders.X_AMZ_OBJECT_OWNERSHIP;
2122
import static com.adobe.testing.s3mock.util.AwsHttpParameters.CONTINUATION_TOKEN;
@@ -48,6 +49,7 @@
4849
import com.adobe.testing.s3mock.dto.ObjectLockConfiguration;
4950
import com.adobe.testing.s3mock.dto.VersioningConfiguration;
5051
import com.adobe.testing.s3mock.service.BucketService;
52+
import com.adobe.testing.s3mock.store.BucketMetadata;
5153
import org.springframework.http.ResponseEntity;
5254
import org.springframework.stereotype.Controller;
5355
import org.springframework.web.bind.annotation.CrossOrigin;
@@ -490,10 +492,7 @@ public ResponseEntity<ListBucketResultV2> listObjectsV2(
490492
*
491493
* @param bucketName {@link String} set bucket name
492494
* @param prefix {@link String} find object names they start with prefix
493-
* @param startAfter {@link String} return key names after a specific object key in your key
494-
* space
495495
* @param maxKeys {@link Integer} set maximum number of keys to be returned
496-
* @param continuationToken {@link String} pagination token returned by previous request
497496
*
498497
* @return {@link ListVersionsResult} a list of objects in Bucket
499498
*/
@@ -516,15 +515,17 @@ public ResponseEntity<ListVersionsResult> listObjectVersions(
516515
@RequestParam(name = KEY_MARKER, required = false) String keyMarker,
517516
@RequestParam(name = VERSION_ID_MARKER, required = false) String versionIdMarker,
518517
@RequestParam(name = ENCODING_TYPE, required = false) String encodingType,
519-
@RequestParam(name = START_AFTER, required = false) String startAfter,
520-
@RequestParam(name = MAX_KEYS, defaultValue = "1000", required = false) Integer maxKeys,
521-
@RequestParam(name = CONTINUATION_TOKEN, required = false) String continuationToken) {
522-
bucketService.verifyBucketExists(bucketName);
518+
@RequestParam(name = MAX_KEYS, defaultValue = "1000", required = false) Integer maxKeys) {
519+
BucketMetadata bucketMetadata = bucketService.verifyBucketExists(bucketName);
520+
if (!bucketMetadata.isVersioningEnabled()) {
521+
//TODO: find correct exception.
522+
throw NOT_FOUND_BUCKET_VERSIONING_CONFIGURATION;
523+
}
523524
bucketService.verifyMaxKeys(maxKeys);
524525
bucketService.verifyEncodingType(encodingType);
525526
var listVersionsResult =
526-
bucketService.listVersions(bucketName, prefix, delimiter, encodingType, startAfter,
527-
maxKeys, continuationToken, keyMarker, versionIdMarker);
527+
bucketService.listVersions(bucketName, prefix, delimiter, encodingType, maxKeys, keyMarker,
528+
versionIdMarker);
528529

529530
return ResponseEntity.ok(listVersionsResult);
530531
}

server/src/main/java/com/adobe/testing/s3mock/S3Exception.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ public class S3Exception extends RuntimeException {
5959
"The lifecycle configuration does not exist.");
6060
public static final S3Exception NO_SUCH_KEY =
6161
new S3Exception(NOT_FOUND.value(), "NoSuchKey", "The specified key does not exist.");
62+
public static final S3Exception NO_SUCH_VERSION =
63+
new S3Exception(NOT_FOUND.value(), "NoSuchVersion", "The version ID specified in the "
64+
+ "request does not match an existing version..");
6265
public static final S3Exception NO_SUCH_KEY_DELETE_MARKER =
6366
new S3Exception(NOT_FOUND.value(), "NoSuchKey", "The specified key does not exist.");
6467
public static final S3Exception NOT_MODIFIED =
@@ -83,16 +86,19 @@ public class S3Exception extends RuntimeException {
8386
public static final S3Exception NOT_FOUND_BUCKET_OBJECT_LOCK =
8487
new S3Exception(BAD_REQUEST.value(), INVALID_REQUEST_CODE,
8588
"Bucket is missing Object Lock Configuration");
89+
public static final S3Exception NOT_FOUND_BUCKET_VERSIONING_CONFIGURATION =
90+
new S3Exception(BAD_REQUEST.value(), INVALID_REQUEST_CODE,
91+
"Bucket is missing Versioning Configuration");
8692
public static final S3Exception NOT_FOUND_OBJECT_LOCK =
8793
new S3Exception(NOT_FOUND.value(), "NotFound",
8894
"The specified object does not have a ObjectLock configuration");
89-
public static final S3Exception INVALID_REQUEST_RETAINDATE =
95+
public static final S3Exception INVALID_REQUEST_RETAIN_DATE =
9096
new S3Exception(BAD_REQUEST.value(), INVALID_REQUEST_CODE,
9197
"The retain until date must be in the future!");
92-
public static final S3Exception INVALID_REQUEST_MAXKEYS =
98+
public static final S3Exception INVALID_REQUEST_MAX_KEYS =
9399
new S3Exception(BAD_REQUEST.value(), INVALID_REQUEST_CODE,
94100
"maxKeys should be non-negative");
95-
public static final S3Exception INVALID_REQUEST_ENCODINGTYPE =
101+
public static final S3Exception INVALID_REQUEST_ENCODING_TYPE =
96102
new S3Exception(BAD_REQUEST.value(), INVALID_REQUEST_CODE,
97103
"encodingtype can only be none or 'url'");
98104
public static final S3Exception INVALID_COPY_REQUEST_SAME_KEY =

server/src/main/java/com/adobe/testing/s3mock/dto/DeleteMarkerEntry.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2017-2024 Adobe.
2+
* Copyright 2017-2025 Adobe.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
1616

1717
package com.adobe.testing.s3mock.dto;
1818

19+
import com.adobe.testing.s3mock.store.S3ObjectMetadata;
1920
import com.fasterxml.jackson.annotation.JsonProperty;
2021

2122
/**
@@ -34,4 +35,11 @@ public record DeleteMarkerEntry(
3435
String versionId
3536
) {
3637

38+
public static DeleteMarkerEntry from(S3ObjectMetadata s3ObjectMetadata, boolean isLatest) {
39+
return new DeleteMarkerEntry(isLatest,
40+
s3ObjectMetadata.key(),
41+
s3ObjectMetadata.modificationDate(),
42+
s3ObjectMetadata.owner(),
43+
s3ObjectMetadata.versionId());
44+
}
3745
}

server/src/main/java/com/adobe/testing/s3mock/dto/ObjectVersion.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,27 +49,27 @@ public record ObjectVersion(
4949
etag = normalizeEtag(etag);
5050
}
5151

52-
public static ObjectVersion from(S3ObjectMetadata s3ObjectMetadata) {
52+
public static ObjectVersion from(S3ObjectMetadata s3ObjectMetadata, boolean isLatest) {
5353
return new ObjectVersion(s3ObjectMetadata.key(),
5454
s3ObjectMetadata.modificationDate(),
5555
s3ObjectMetadata.etag(),
5656
s3ObjectMetadata.size(),
5757
s3ObjectMetadata.storageClass(),
5858
s3ObjectMetadata.owner(),
5959
s3ObjectMetadata.checksumAlgorithm(),
60-
true,
60+
isLatest,
6161
s3ObjectMetadata.versionId());
6262
}
6363

64-
public static ObjectVersion from(S3Object s3Object) {
64+
public static ObjectVersion from(S3Object s3Object, boolean isLatest, String versionId) {
6565
return new ObjectVersion(s3Object.key(),
6666
s3Object.lastModified(),
6767
s3Object.etag(),
6868
s3Object.size(),
6969
s3Object.storageClass(),
7070
s3Object.owner(),
7171
s3Object.checksumAlgorithm(),
72-
true,
73-
"staticVersion");
72+
isLatest,
73+
versionId);
7474
}
7575
}

0 commit comments

Comments
 (0)