Skip to content

Commit

Permalink
Support versionId in APIs
Browse files Browse the repository at this point in the history
  • Loading branch information
afranken committed May 30, 2024
1 parent d2e9458 commit 12efb2f
Show file tree
Hide file tree
Showing 9 changed files with 220 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
import static com.adobe.testing.s3mock.util.AwsHttpParameters.NOT_UPLOAD_ID;
import static com.adobe.testing.s3mock.util.AwsHttpParameters.RETENTION;
import static com.adobe.testing.s3mock.util.AwsHttpParameters.TAGGING;
import static com.adobe.testing.s3mock.util.AwsHttpParameters.VERSION_ID;
import static com.adobe.testing.s3mock.util.HeaderUtil.checksumAlgorithmFromHeader;
import static com.adobe.testing.s3mock.util.HeaderUtil.checksumAlgorithmFromSdk;
import static com.adobe.testing.s3mock.util.HeaderUtil.checksumFrom;
Expand Down Expand Up @@ -183,7 +184,8 @@ public ResponseEntity<DeleteResult> deleteObjects(
public ResponseEntity<Void> headObject(@PathVariable String bucketName,
@PathVariable ObjectKey key,
@RequestHeader(value = IF_MATCH, required = false) List<String> match,
@RequestHeader(value = IF_NONE_MATCH, required = false) List<String> noneMatch) {
@RequestHeader(value = IF_NONE_MATCH, required = false) List<String> noneMatch,
@RequestParam(value = VERSION_ID, required = false) String versionId) {
//TODO: needs modified-since handling, see API
bucketService.verifyBucketExists(bucketName);

Expand Down Expand Up @@ -224,7 +226,8 @@ public ResponseEntity<Void> headObject(@PathVariable String bucketName,
}
)
public ResponseEntity<Void> deleteObject(@PathVariable String bucketName,
@PathVariable ObjectKey key) {
@PathVariable ObjectKey key,
@RequestParam(value = VERSION_ID, required = false) String versionId) {
bucketService.verifyBucketExists(bucketName);

var deleted = objectService.deleteObject(bucketName, key.key());
Expand Down Expand Up @@ -260,7 +263,8 @@ public ResponseEntity<StreamingResponseBody> getObject(@PathVariable String buck
@RequestHeader(value = RANGE, required = false) HttpRange range,
@RequestHeader(value = IF_MATCH, required = false) List<String> match,
@RequestHeader(value = IF_NONE_MATCH, required = false) List<String> noneMatch,
@RequestParam Map<String, String> queryParams) {
@RequestParam Map<String, String> queryParams,
@RequestParam(value = VERSION_ID, required = false) String versionId) {
//TODO: needs modified-since handling, see API
bucketService.verifyBucketExists(bucketName);

Expand Down Expand Up @@ -313,6 +317,7 @@ public ResponseEntity<StreamingResponseBody> getObject(@PathVariable String buck
public ResponseEntity<Void> putObjectAcl(@PathVariable final String bucketName,
@PathVariable ObjectKey key,
@RequestHeader(value = X_AMZ_ACL, required = false) ObjectCannedACL cannedAcl,
@RequestParam(value = VERSION_ID, required = false) String versionId,
@RequestBody(required = false) AccessControlPolicy body) {
bucketService.verifyBucketExists(bucketName);
objectService.verifyObjectExists(bucketName, key.key());
Expand Down Expand Up @@ -351,7 +356,8 @@ public ResponseEntity<Void> putObjectAcl(@PathVariable final String bucketName,
produces = APPLICATION_XML_VALUE
)
public ResponseEntity<AccessControlPolicy> getObjectAcl(@PathVariable final String bucketName,
@PathVariable ObjectKey key) {
@PathVariable ObjectKey key,
@RequestParam(value = VERSION_ID, required = false) String versionId) {
bucketService.verifyBucketExists(bucketName);
objectService.verifyObjectExists(bucketName, key.key());
var acl = objectService.getAcl(bucketName, key.key());
Expand All @@ -375,7 +381,8 @@ public ResponseEntity<AccessControlPolicy> getObjectAcl(@PathVariable final Stri
}
)
public ResponseEntity<Tagging> getObjectTagging(@PathVariable String bucketName,
@PathVariable ObjectKey key) {
@PathVariable ObjectKey key,
@RequestParam(value = VERSION_ID, required = false) String versionId) {
bucketService.verifyBucketExists(bucketName);

var s3ObjectMetadata = objectService.verifyObjectExists(bucketName, key.key());
Expand Down Expand Up @@ -404,6 +411,7 @@ public ResponseEntity<Tagging> getObjectTagging(@PathVariable String bucketName,
)
public ResponseEntity<Void> putObjectTagging(@PathVariable String bucketName,
@PathVariable ObjectKey key,
@RequestParam(value = VERSION_ID, required = false) String versionId,
@RequestBody Tagging body) {
bucketService.verifyBucketExists(bucketName);

Expand Down Expand Up @@ -431,7 +439,8 @@ public ResponseEntity<Void> putObjectTagging(@PathVariable String bucketName,
produces = APPLICATION_XML_VALUE
)
public ResponseEntity<LegalHold> getLegalHold(@PathVariable String bucketName,
@PathVariable ObjectKey key) {
@PathVariable ObjectKey key,
@RequestParam(value = VERSION_ID, required = false) String versionId) {
bucketService.verifyBucketExists(bucketName);
bucketService.verifyBucketObjectLockEnabled(bucketName);
var s3ObjectMetadata = objectService.verifyObjectLockConfiguration(bucketName, key.key());
Expand All @@ -457,6 +466,7 @@ public ResponseEntity<LegalHold> getLegalHold(@PathVariable String bucketName,
)
public ResponseEntity<Void> putLegalHold(@PathVariable String bucketName,
@PathVariable ObjectKey key,
@RequestParam(value = VERSION_ID, required = false) String versionId,
@RequestBody LegalHold body) {
bucketService.verifyBucketExists(bucketName);
bucketService.verifyBucketObjectLockEnabled(bucketName);
Expand All @@ -483,7 +493,8 @@ public ResponseEntity<Void> putLegalHold(@PathVariable String bucketName,
produces = APPLICATION_XML_VALUE
)
public ResponseEntity<Retention> getObjectRetention(@PathVariable String bucketName,
@PathVariable ObjectKey key) {
@PathVariable ObjectKey key,
@RequestParam(value = VERSION_ID, required = false) String versionId) {
bucketService.verifyBucketExists(bucketName);
bucketService.verifyBucketObjectLockEnabled(bucketName);
var s3ObjectMetadata = objectService.verifyObjectLockConfiguration(bucketName, key.key());
Expand All @@ -509,6 +520,7 @@ public ResponseEntity<Retention> getObjectRetention(@PathVariable String bucketN
)
public ResponseEntity<Void> putObjectRetention(@PathVariable String bucketName,
@PathVariable ObjectKey key,
@RequestParam(value = VERSION_ID, required = false) String versionId,
@RequestBody Retention body) {
bucketService.verifyBucketExists(bucketName);
bucketService.verifyBucketObjectLockEnabled(bucketName);
Expand Down Expand Up @@ -539,7 +551,8 @@ public ResponseEntity<GetObjectAttributesOutput> getObjectAttributes(
@PathVariable ObjectKey key,
@RequestHeader(value = IF_MATCH, required = false) List<String> match,
@RequestHeader(value = IF_NONE_MATCH, required = false) List<String> noneMatch,
@RequestHeader(value = X_AMZ_OBJECT_ATTRIBUTES) List<String> objectAttributes) {
@RequestHeader(value = X_AMZ_OBJECT_ATTRIBUTES) List<String> objectAttributes,
@RequestParam(value = VERSION_ID, required = false) String versionId) {
//TODO: needs modified-since handling, see API
bucketService.verifyBucketExists(bucketName);

Expand Down
21 changes: 14 additions & 7 deletions server/src/main/java/com/adobe/testing/s3mock/dto/CopySource.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2017-2022 Adobe.
* Copyright 2017-2024 Adobe.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -25,13 +25,14 @@
*/
public record CopySource(
String bucket,
String key
String key,
String versionId
) {
public static final String DELIMITER = "/";
static final String DELIMITER = "/";

/**
* Creates a {@link CopySource} expecting the given String represents the source as {@code
* /{bucket}/{key}}.
* Creates a {@link CopySource} expecting the given String to represent the source as {@code
* /{bucket}/{key}[?versionId={versionId}]}.
*
* @param copySource The object references.
*
Expand All @@ -41,8 +42,14 @@ public record CopySource(
public CopySource(String copySource) {
//inefficient duplicate parsing of incoming String, call to default constructor must be the
//first statement...
this(extractBucketAndKeyArray(SdkHttpUtils.urlDecode(copySource))[0],
extractBucketAndKeyArray(SdkHttpUtils.urlDecode(copySource))[1]);
this(extractBucketAndKeyArray(
SdkHttpUtils.urlDecode(copySource)
)[0],
extractBucketAndKeyArray(
SdkHttpUtils.urlDecode(copySource)
)[1],
null //TODO: support versionId
);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Copyright 2017-2024 Adobe.
*
* Licensed 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 com.adobe.testing.s3mock.dto;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonRootName;
import com.fasterxml.jackson.annotation.JsonValue;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;

/**
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_VersioningConfiguration.html">API Reference</a>.
*/
@JsonRootName("VersioningConfiguration")
public record VersioningConfiguration(
@JsonProperty("MfaDelete")
MFADelete mfaDelete,
@JsonProperty("Status")
Status status,
//workaround for adding xmlns attribute to root element only.
@JacksonXmlProperty(isAttribute = true, localName = "xmlns")
String xmlns
) {

public VersioningConfiguration {
if (xmlns == null) {
xmlns = "http://s3.amazonaws.com/doc/2006-03-01/";
}
}

enum MFADelete {
ENABLED("Enabled"),
DISABLED("Disabled");

private final String value;

@JsonCreator
MFADelete(String value) {
this.value = value;
}

@Override
@JsonValue
public String toString() {
return value;
}
}

enum Status {
ENABLED("Enabled"),
SUSPENDED("Suspended");

private final String value;

@JsonCreator
Status(String value) {
this.value = value;
}

@Override
@JsonValue
public String toString() {
return value;
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public final class AwsHttpHeaders {
public static final String X_AMZ_TAGGING = "x-amz-tagging";
public static final String CONTENT_MD5 = "Content-MD5";

public static final String X_AMZ_VERSION_ID = "x-amz-version-id";
public static final String X_AMZ_DELETE_MARKER = "x-amz-delete-marker";

public static final String X_AMZ_BUCKET_OBJECT_LOCK_ENABLED = "x-amz-bucket-object-lock-enabled";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2017-2023 Adobe.
* Copyright 2017-2024 Adobe.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -58,6 +58,8 @@ public class AwsHttpParameters {
public static final String LOCATION = "location";
public static final String NOT_LOCATION = NOT + LOCATION;

public static final String VERSION_ID = "versionId";

private AwsHttpParameters() {
// private constructor for utility classes
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package com.adobe.testing.s3mock.dto

import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test
import java.util.UUID

Expand All @@ -26,7 +27,7 @@ import java.util.UUID
internal class CopySourceTest {
@Test
fun fromPrefixedCopySourceString() {
val copySource = CopySource(CopySource.DELIMITER + VALID_COPY_SOURCE)
val copySource = CopySource("/$VALID_COPY_SOURCE")

assertThat(copySource.bucket).isEqualTo(BUCKET)
assertThat(copySource.key).isEqualTo(KEY)
Expand Down Expand Up @@ -56,9 +57,19 @@ internal class CopySourceTest {
.isInstanceOf(NullPointerException::class.java)
}

@Test
@Disabled
fun fromCopySourceWithVersion() {
val copySource = CopySource(COPY_SOURCE_WITH_VERSION)

assertThat(copySource.bucket).isEqualTo(BUCKET)
assertThat(copySource.key).isEqualTo(KEY)
}

companion object {
private val BUCKET = UUID.randomUUID().toString()
private val KEY = UUID.randomUUID().toString()
private val VALID_COPY_SOURCE = BUCKET + CopySource.DELIMITER + KEY
private val VALID_COPY_SOURCE = "$BUCKET/$KEY"
private val COPY_SOURCE_WITH_VERSION = "$VALID_COPY_SOURCE?versionId=123"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright 2017-2024 Adobe.
*
* Licensed 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 com.adobe.testing.s3mock.dto

import com.adobe.testing.s3mock.dto.DtoTestUtil.deserialize
import com.adobe.testing.s3mock.dto.DtoTestUtil.serializeAndAssert
import com.adobe.testing.s3mock.dto.VersioningConfiguration.MFADelete
import com.adobe.testing.s3mock.dto.VersioningConfiguration.Status
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInfo
import java.io.IOException

internal class VersioningConfigurationTest {

@Test
@Throws(IOException::class)
fun testSerialization(testInfo: TestInfo) {
val iut = VersioningConfiguration(null, Status.SUSPENDED, null)
serializeAndAssert(iut, testInfo)
}

@Test
@Throws(IOException::class)
fun testDeserialization(testInfo: TestInfo) {
val iut = deserialize(VersioningConfiguration::class.java, testInfo)
assertThat(iut.status).isEqualTo(Status.ENABLED)
assertThat(iut.mfaDelete).isEqualTo(MFADelete.ENABLED)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2017-2024 Adobe.
Licensed 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.
-->
<VersioningConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<Status>Enabled</Status>
<MfaDelete>Enabled</MfaDelete>
</VersioningConfiguration>
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2017-2024 Adobe.
Licensed 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.
-->
<VersioningConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<Status>Suspended</Status>
</VersioningConfiguration>

0 comments on commit 12efb2f

Please sign in to comment.