Skip to content

Commit 2ab091e

Browse files
committed
Add basic version support
Needs extensive testing, delete marker implementation. Fixes #64
1 parent eb29318 commit 2ab091e

31 files changed

+706
-290
lines changed

build-config/src/main/resources/build-config/checkstyle.xml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?xml version="1.0"?>
22
<!--
33
4-
Copyright 2017-2021 Adobe.
4+
Copyright 2017-2025 Adobe.
55
66
Licensed under the Apache License, Version 2.0 (the "License");
77
you may not use this file except in compliance with the License.
@@ -201,7 +201,9 @@
201201
<property name="allowedAbbreviationLength" value="4"/>
202202
</module>
203203
<module name="OverloadMethodsDeclarationOrder"/>
204-
<module name="VariableDeclarationUsageDistance"/>
204+
<module name="VariableDeclarationUsageDistance">
205+
<property name="allowedDistance" value="4"/>
206+
</module>
205207
<module name="CustomImportOrder">
206208
<property name="sortImportsInGroupAlphabetically" value="true"/>
207209
<property name="separateLineBetweenGroups" value="true"/>

integration-tests/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@
167167
<COM_ADOBE_TESTING_S3MOCK_DOMAIN_INITIAL_BUCKETS>bucket-a, bucket-b</COM_ADOBE_TESTING_S3MOCK_DOMAIN_INITIAL_BUCKETS>
168168
<COM_ADOBE_TESTING_S3MOCK_REGION>eu-west-1</COM_ADOBE_TESTING_S3MOCK_REGION>
169169
</env>
170-
<memory>192000000</memory>
170+
<memory>256000000</memory>
171171
</run>
172172
</image>
173173
</images>

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

Lines changed: 65 additions & 31 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.
@@ -33,7 +33,6 @@ import software.amazon.awssdk.services.s3.model.DeleteBucketRequest
3333
import software.amazon.awssdk.services.s3.model.ExpirationStatus
3434
import software.amazon.awssdk.services.s3.model.GetBucketLifecycleConfigurationRequest
3535
import software.amazon.awssdk.services.s3.model.GetBucketLocationRequest
36-
import software.amazon.awssdk.services.s3.model.GetBucketVersioningRequest
3736
import software.amazon.awssdk.services.s3.model.HeadBucketRequest
3837
import software.amazon.awssdk.services.s3.model.LifecycleExpiration
3938
import software.amazon.awssdk.services.s3.model.LifecycleRule
@@ -42,8 +41,6 @@ import software.amazon.awssdk.services.s3.model.MFADelete
4241
import software.amazon.awssdk.services.s3.model.MFADeleteStatus
4342
import software.amazon.awssdk.services.s3.model.NoSuchBucketException
4443
import software.amazon.awssdk.services.s3.model.PutBucketLifecycleConfigurationRequest
45-
import software.amazon.awssdk.services.s3.model.PutBucketVersioningRequest
46-
import software.amazon.awssdk.services.s3.model.VersioningConfiguration
4744
import java.util.concurrent.TimeUnit
4845

4946
/**
@@ -90,42 +87,79 @@ internal class BucketV2IT : S3TestBase() {
9087
fun getDefaultBucketVersioning(testInfo: TestInfo) {
9188
val bucketName = givenBucketV2(testInfo)
9289

93-
s3ClientV2.getBucketVersioning(
94-
GetBucketVersioningRequest
95-
.builder()
96-
.bucket(bucketName)
97-
.build()
98-
).also {
90+
s3ClientV2.getBucketVersioning {
91+
it.bucket(bucketName)
92+
}.also {
9993
assertThat(it.status()).isNull()
10094
assertThat(it.mfaDelete()).isNull()
10195
}
10296
}
10397

10498
@Test
105-
@S3VerifiedFailure(year = 2024, reason = "No real Mfa value")
99+
@S3VerifiedTodo
106100
fun putAndGetBucketVersioning(testInfo: TestInfo) {
107101
val bucketName = givenBucketV2(testInfo)
108-
s3ClientV2.putBucketVersioning(
109-
PutBucketVersioningRequest
110-
.builder()
111-
.bucket(bucketName)
112-
.mfa("fakeMfaValue")
113-
.versioningConfiguration(
114-
VersioningConfiguration
115-
.builder()
116-
.status(BucketVersioningStatus.ENABLED)
117-
.mfaDelete(MFADelete.ENABLED)
118-
.build()
119-
)
120-
.build()
121-
)
102+
s3ClientV2.putBucketVersioning {
103+
it.bucket(bucketName)
104+
it.versioningConfiguration {
105+
it.status(BucketVersioningStatus.ENABLED)
106+
}
107+
}
122108

123-
s3ClientV2.getBucketVersioning(
124-
GetBucketVersioningRequest
125-
.builder()
126-
.bucket(bucketName)
127-
.build()
128-
).also {
109+
s3ClientV2.getBucketVersioning {
110+
it.bucket(bucketName)
111+
}.also {
112+
assertThat(it.status()).isEqualTo(BucketVersioningStatus.ENABLED)
113+
}
114+
}
115+
116+
@Test
117+
@S3VerifiedTodo
118+
fun putAndGetBucketVersioning_suspended(testInfo: TestInfo) {
119+
val bucketName = givenBucketV2(testInfo)
120+
s3ClientV2.putBucketVersioning {
121+
it.bucket(bucketName)
122+
it.versioningConfiguration {
123+
it.status(BucketVersioningStatus.ENABLED)
124+
}
125+
}
126+
127+
s3ClientV2.getBucketVersioning {
128+
it.bucket(bucketName)
129+
}.also {
130+
assertThat(it.status()).isEqualTo(BucketVersioningStatus.ENABLED)
131+
}
132+
133+
s3ClientV2.putBucketVersioning {
134+
it.bucket(bucketName)
135+
it.versioningConfiguration {
136+
it.status(BucketVersioningStatus.SUSPENDED)
137+
}
138+
}
139+
140+
s3ClientV2.getBucketVersioning {
141+
it.bucket(bucketName)
142+
}.also {
143+
assertThat(it.status()).isEqualTo(BucketVersioningStatus.SUSPENDED)
144+
}
145+
}
146+
147+
@Test
148+
@S3VerifiedFailure(year = 2024, reason = "No real Mfa value")
149+
fun putAndGetBucketVersioning_mfa(testInfo: TestInfo) {
150+
val bucketName = givenBucketV2(testInfo)
151+
s3ClientV2.putBucketVersioning {
152+
it.bucket(bucketName)
153+
it.mfa("fakeMfaValue")
154+
it.versioningConfiguration {
155+
it.status(BucketVersioningStatus.ENABLED)
156+
it.mfaDelete(MFADelete.ENABLED)
157+
}
158+
}
159+
160+
s3ClientV2.getBucketVersioning {
161+
it.bucket(bucketName)
162+
}.also {
129163
assertThat(it.status()).isEqualTo(BucketVersioningStatus.ENABLED)
130164
assertThat(it.mfaDelete()).isEqualTo(MFADeleteStatus.ENABLED)
131165
}

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

Lines changed: 102 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,10 +16,111 @@
1616

1717
package com.adobe.testing.s3mock.its
1818

19+
import com.adobe.testing.s3mock.util.DigestUtil
20+
import org.assertj.core.api.Assertions.assertThat
21+
import org.junit.jupiter.api.Test
22+
import org.junit.jupiter.api.TestInfo
23+
import software.amazon.awssdk.core.checksums.Algorithm
24+
import software.amazon.awssdk.core.sync.RequestBody
1925
import software.amazon.awssdk.services.s3.S3Client
26+
import software.amazon.awssdk.services.s3.model.BucketVersioningStatus
27+
import software.amazon.awssdk.services.s3.model.ChecksumAlgorithm
28+
import software.amazon.awssdk.services.s3.model.ObjectAttributes
29+
import software.amazon.awssdk.services.s3.model.StorageClass
30+
import java.io.File
2031

2132
internal class VersionsV2IT : S3TestBase() {
2233
private val s3ClientV2: S3Client = createS3ClientV2()
2334

35+
@Test
36+
@S3VerifiedSuccess(year = 2024)
37+
fun testPutGetObject_withVersion(testInfo: TestInfo) {
38+
val uploadFile = File(UPLOAD_FILE_NAME)
39+
val expectedChecksum = DigestUtil.checksumFor(uploadFile.toPath(), Algorithm.SHA1)
40+
val bucketName = givenBucketV2(testInfo)
2441

42+
s3ClientV2.putBucketVersioning {
43+
it.bucket(bucketName)
44+
it.versioningConfiguration {
45+
it.status(BucketVersioningStatus.ENABLED)
46+
}
47+
}
48+
49+
val versionId = s3ClientV2.putObject(
50+
{
51+
it.bucket(bucketName).key(UPLOAD_FILE_NAME)
52+
it.checksumAlgorithm(ChecksumAlgorithm.SHA1)
53+
}, RequestBody.fromFile(uploadFile)
54+
).versionId()
55+
56+
s3ClientV2.getObjectAttributes {
57+
it.bucket(bucketName)
58+
it.key(UPLOAD_FILE_NAME)
59+
it.versionId(versionId)
60+
it.objectAttributes(
61+
ObjectAttributes.OBJECT_SIZE,
62+
ObjectAttributes.STORAGE_CLASS,
63+
ObjectAttributes.E_TAG,
64+
ObjectAttributes.CHECKSUM
65+
)
66+
}.also {
67+
//
68+
assertThat(it.versionId()).isEqualTo(versionId)
69+
//default storageClass is STANDARD, which is never returned from APIs
70+
assertThat(it.storageClass()).isEqualTo(StorageClass.STANDARD)
71+
assertThat(it.objectSize()).isEqualTo(File(UPLOAD_FILE_NAME).length())
72+
assertThat(it.checksum().checksumSHA1()).isEqualTo(expectedChecksum)
73+
}
74+
}
75+
76+
@Test
77+
@S3VerifiedSuccess(year = 2024)
78+
fun testPutGetObject_withMultipleVersions(testInfo: TestInfo) {
79+
val uploadFile = File(UPLOAD_FILE_NAME)
80+
val bucketName = givenBucketV2(testInfo)
81+
82+
s3ClientV2.putBucketVersioning {
83+
it.bucket(bucketName)
84+
it.versioningConfiguration {
85+
it.status(BucketVersioningStatus.ENABLED)
86+
}
87+
}
88+
89+
val versionId1 = s3ClientV2.putObject(
90+
{
91+
it.bucket(bucketName).key(UPLOAD_FILE_NAME)
92+
it.checksumAlgorithm(ChecksumAlgorithm.SHA1)
93+
}, RequestBody.fromFile(uploadFile)
94+
).versionId()
95+
96+
val versionId2 = s3ClientV2.putObject(
97+
{
98+
it.bucket(bucketName).key(UPLOAD_FILE_NAME)
99+
it.checksumAlgorithm(ChecksumAlgorithm.SHA1)
100+
}, RequestBody.fromFile(uploadFile)
101+
).versionId()
102+
103+
s3ClientV2.getObject {
104+
it.bucket(bucketName)
105+
it.key(UPLOAD_FILE_NAME)
106+
it.versionId(versionId2)
107+
}.also {
108+
assertThat(it.response().versionId()).isEqualTo(versionId2)
109+
}
110+
111+
s3ClientV2.getObject {
112+
it.bucket(bucketName)
113+
it.key(UPLOAD_FILE_NAME)
114+
it.versionId(versionId1)
115+
}.also {
116+
assertThat(it.response().versionId()).isEqualTo(versionId1)
117+
}
118+
119+
s3ClientV2.getObject {
120+
it.bucket(bucketName)
121+
it.key(UPLOAD_FILE_NAME)
122+
}.also {
123+
assertThat(it.response().versionId()).isEqualTo(versionId2)
124+
}
125+
}
25126
}

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

Lines changed: 25 additions & 12 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.
@@ -27,6 +27,7 @@
2727
import static com.adobe.testing.s3mock.util.AwsHttpHeaders.X_AMZ_COPY_SOURCE_IF_UNMODIFIED_SINCE;
2828
import static com.adobe.testing.s3mock.util.AwsHttpHeaders.X_AMZ_COPY_SOURCE_RANGE;
2929
import static com.adobe.testing.s3mock.util.AwsHttpHeaders.X_AMZ_STORAGE_CLASS;
30+
import static com.adobe.testing.s3mock.util.AwsHttpHeaders.X_AMZ_VERSION_ID;
3031
import static com.adobe.testing.s3mock.util.AwsHttpParameters.NOT_LIFECYCLE;
3132
import static com.adobe.testing.s3mock.util.AwsHttpParameters.PART_NUMBER;
3233
import static com.adobe.testing.s3mock.util.AwsHttpParameters.UPLOADS;
@@ -223,12 +224,12 @@ public ResponseEntity<Void> uploadPart(@PathVariable String bucketName,
223224

224225
String checksum = null;
225226
ChecksumAlgorithm checksumAlgorithm = null;
226-
ChecksumAlgorithm algorithmFromSdk = checksumAlgorithmFromSdk(httpHeaders);
227+
var algorithmFromSdk = checksumAlgorithmFromSdk(httpHeaders);
227228
if (algorithmFromSdk != null) {
228229
checksum = tempFileAndChecksum.getRight();
229230
checksumAlgorithm = algorithmFromSdk;
230231
}
231-
ChecksumAlgorithm algorithmFromHeader = checksumAlgorithmFromHeader(httpHeaders);
232+
var algorithmFromHeader = checksumAlgorithmFromHeader(httpHeaders);
232233
if (algorithmFromHeader != null) {
233234
checksum = checksumFrom(httpHeaders);
234235
checksumAlgorithm = algorithmFromHeader;
@@ -295,9 +296,10 @@ public ResponseEntity<CopyPartResult> uploadPartCopy(
295296
@RequestParam String uploadId,
296297
@RequestParam String partNumber,
297298
@RequestHeader HttpHeaders httpHeaders) {
298-
bucketService.verifyBucketExists(bucketName);
299+
var bucket = bucketService.verifyBucketExists(bucketName);
299300
multipartService.verifyPartNumberLimits(partNumber);
300-
var s3ObjectMetadata = objectService.verifyObjectExists(copySource.bucket(), copySource.key());
301+
var s3ObjectMetadata = objectService.verifyObjectExists(copySource.bucket(), copySource.key(),
302+
copySource.versionId());
301303
objectService.verifyObjectMatchingForCopy(match, noneMatch,
302304
ifModifiedSince, ifUnmodifiedSince, s3ObjectMetadata);
303305

@@ -308,12 +310,19 @@ public ResponseEntity<CopyPartResult> uploadPartCopy(
308310
bucketName,
309311
key.key(),
310312
uploadId,
311-
encryptionHeadersFrom(httpHeaders)
313+
encryptionHeadersFrom(httpHeaders),
314+
copySource.versionId()
312315
);
313316

314317
//return encryption headers
315-
//return source version id
316-
return ResponseEntity.ok(result);
318+
return ResponseEntity
319+
.ok()
320+
.headers(h -> {
321+
if (bucket.isVersioningEnabled() && s3ObjectMetadata.versionId() != null) {
322+
h.set(X_AMZ_VERSION_ID, s3ObjectMetadata.versionId());
323+
}
324+
})
325+
.body(result);
317326
}
318327

319328
/**
@@ -385,7 +394,7 @@ public ResponseEntity<CompleteMultipartUploadResult> completeMultipartUpload(
385394
@RequestBody CompleteMultipartUpload upload,
386395
HttpServletRequest request,
387396
@RequestHeader HttpHeaders httpHeaders) {
388-
bucketService.verifyBucketExists(bucketName);
397+
var bucket = bucketService.verifyBucketExists(bucketName);
389398
multipartService.verifyMultipartUploadExists(bucketName, uploadId);
390399
multipartService.verifyMultipartParts(bucketName, key.key(), uploadId, upload.parts());
391400
var objectName = key.key();
@@ -401,14 +410,18 @@ public ResponseEntity<CompleteMultipartUploadResult> completeMultipartUpload(
401410
encryptionHeadersFrom(httpHeaders),
402411
locationWithEncodedKey);
403412

404-
String checksum = result.checksum();
405-
ChecksumAlgorithm checksumAlgorithm = result.multipartUploadInfo().checksumAlgorithm();
413+
var checksum = result.checksum();
414+
var checksumAlgorithm = result.multipartUploadInfo().checksumAlgorithm();
406415

407416
//return encryption headers.
408-
//return version id
409417
return ResponseEntity
410418
.ok()
411419
.headers(h -> h.setAll(checksumHeaderFrom(checksum, checksumAlgorithm)))
420+
.headers(h -> {
421+
if (bucket.isVersioningEnabled() && result.versionId() != null) {
422+
h.set(X_AMZ_VERSION_ID, result.versionId());
423+
}
424+
})
412425
.body(result);
413426
}
414427
}

0 commit comments

Comments
 (0)