Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ Running S3Mock in unit tests is still supported by using [TestContainers](https:
* Features and fixes
* TBD
* Refactorings
* Removal of legacy-style Spring properties in favor of current environment variables.
* AWS has deprecated SDK for Java v1, and will remove support EOY 2025.
* S3Mock will remove bundled support for Java SDK v1 in late 2025.
* JUnit 4.x deprecation
Expand Down Expand Up @@ -164,11 +165,17 @@ Version 4.x is JDK17 LTS bytecode compatible, with Docker and JUnit / direct Jav
* Refactorings
* Add JSpecify annotations to S3Mock code
* Migrate unit tests in "testsupport" modules to Kotlin
* Deprecation of legacy-style Spring properties in favor of current environment variables.
* Various fixes and clarifications in README.md
* Version updates (deliverable dependencies)
* Bump alpine from 3.21.3 to 3.22.0 in /docker
* Bump aws.version from 1.12.783 to 1.12.785
* Bump testcontainers.version from 1.21.0 to 1.21.1
* Bump aws-v2.version from 2.31.50 to 2.31.67
* Bump aws.version from 1.12.783 to 1.12.787
* Bump spring-boot.version from 3.5.0 to 3.5.3
* Bump testcontainers.version from 1.21.0 to 1.21.2
* Version updates (build dependencies)
* Bump aws.sdk.kotlin:s3-jvm from 1.4.91 to 1.4.109
* Bump org.xmlunit:xmlunit-assertj3 from 2.10.2 to 2.10.3
* Bump org.codehaus.mojo:exec-maven-plugin from 3.5.0 to 3.5.1
* Bump org.apache.maven.plugins:maven-clean-plugin from 3.4.1 to 3.5.0
* Bump com.puppycrawl.tools:checkstyle from 10.24.0 to 10.25.0
Expand Down
373 changes: 273 additions & 100 deletions README.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion build-config/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<parent>
<groupId>com.adobe.testing</groupId>
<artifactId>s3mock-parent</artifactId>
<version>4.4.1-SNAPSHOT</version>
<version>4.5.0-SNAPSHOT</version>
</parent>

<artifactId>s3mock-build-config</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion docker/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
<parent>
<groupId>com.adobe.testing</groupId>
<artifactId>s3mock-parent</artifactId>
<version>4.4.1-SNAPSHOT</version>
<version>4.5.0-SNAPSHOT</version>
</parent>

<artifactId>s3mock-docker</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion integration-tests/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
<parent>
<groupId>com.adobe.testing</groupId>
<artifactId>s3mock-parent</artifactId>
<version>4.4.1-SNAPSHOT</version>
<version>4.5.0-SNAPSHOT</version>
</parent>

<artifactId>s3mock-integration-tests</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

<groupId>com.adobe.testing</groupId>
<artifactId>s3mock-parent</artifactId>
<version>4.4.1-SNAPSHOT</version>
<version>4.5.0-SNAPSHOT</version>
<packaging>pom</packaging>

<name>S3Mock - Parent</name>
Expand Down
2 changes: 1 addition & 1 deletion server/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
<parent>
<groupId>com.adobe.testing</groupId>
<artifactId>s3mock-parent</artifactId>
<version>4.4.1-SNAPSHOT</version>
<version>4.5.0-SNAPSHOT</version>
</parent>

<artifactId>s3mock</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,9 @@
@RequestMapping("${com.adobe.testing.s3mock.contextPath:}")
public class BucketController {
private final BucketService bucketService;
private final Region region;

public BucketController(BucketService bucketService, Region region) {
public BucketController(BucketService bucketService) {
this.bucketService = bucketService;
this.region = region;
}

//================================================================================================
Expand Down Expand Up @@ -376,8 +374,9 @@ public ResponseEntity<LocationConstraint> getBucketLocation(@PathVariable String

/**
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjects.html">API Reference</a>.
* @deprecated Long since replaced by listObjectsV2
*
* @see #listObjectsV2
* @deprecated Long since replaced by listObjectsV2
*/
@GetMapping(
value = {
Expand Down Expand Up @@ -486,12 +485,12 @@ public ResponseEntity<ListVersionsResult> listObjectVersions(
return ResponseEntity.ok(listVersionsResult);
}

@Nullable
private String regionFrom(@Nullable CreateBucketConfiguration createBucketRequest) {
if (createBucketRequest != null
&& createBucketRequest.locationConstraint() != null
&& createBucketRequest.locationConstraint().region() != null) {
return createBucketRequest.locationConstraint().region().toString();
if (createBucketRequest != null) {
return createBucketRequest.regionFrom();
} else {
return null;
}
return this.region.toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;

import com.adobe.testing.s3mock.dto.ErrorResponse;
import com.adobe.testing.s3mock.dto.Region;
import com.adobe.testing.s3mock.service.BucketService;
import com.adobe.testing.s3mock.service.MultipartService;
import com.adobe.testing.s3mock.service.ObjectService;
Expand Down Expand Up @@ -159,8 +158,8 @@ ObjectController fileStoreController(ObjectService objectService, BucketService
}

@Bean
BucketController bucketController(BucketService bucketService, S3MockProperties properties) {
return new BucketController(bucketService, properties.region());
BucketController bucketController(BucketService bucketService) {
return new BucketController(bucketService);
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,7 @@ public record S3MockProperties(
// For example if set to `s3-mock` all endpoints will be available at
// `http://host:port/s3-mock` instead of `http://host:port/`
@DefaultValue("")
String contextPath,

// Region is S3Mock is supposed to mock.
// Must be an official AWS region string like "us-east-1"
Region region
String contextPath
) {

}
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,12 @@ public CreateBucketConfiguration(BucketInfo bucket,
LocationConstraint locationConstraint) {
this(bucket, location, locationConstraint, null);
}

public String regionFrom() {
if (this.locationConstraint() != null
&& this.locationConstraint().region() != null) {
return this.locationConstraint().region().toString();
}
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ public Bucket createBucket(
String bucketName,
boolean objectLockEnabled,
ObjectOwnership objectOwnership,
String bucketRegion,
@Nullable String bucketRegion,
@Nullable BucketInfo bucketInfo,
@Nullable LocationInfo locationInfo) {
return Bucket.from(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,8 @@ public BucketMetadata withObjectLockConfiguration(
}

public BucketMetadata withBucketLifecycleConfiguration(
BucketLifecycleConfiguration bucketLifecycleConfiguration) {
@Nullable BucketLifecycleConfiguration bucketLifecycleConfiguration
) {
return new BucketMetadata(name(),
creationDate(),
versioningConfiguration(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,17 @@ public class BucketStore {
private final Map<String, Object> lockStore = new ConcurrentHashMap<>();
private final File rootFolder;
private final DateTimeFormatter s3ObjectDateFormat;
private final String region;
private final ObjectMapper objectMapper;

public BucketStore(File rootFolder,
public BucketStore(
File rootFolder,
DateTimeFormatter s3ObjectDateFormat,
String region,
ObjectMapper objectMapper) {
this.rootFolder = rootFolder;
this.s3ObjectDateFormat = s3ObjectDateFormat;
this.region = region;
this.objectMapper = objectMapper;
}

Expand Down Expand Up @@ -136,7 +140,7 @@ public BucketMetadata createBucket(
String bucketName,
boolean objectLockEnabled,
ObjectOwnership objectOwnership,
String bucketRegion,
@Nullable String bucketRegion,
@Nullable BucketInfo bucketInfo,
@Nullable LocationInfo locationInfo) {
if (doesBucketExist(bucketName)) {
Expand All @@ -145,6 +149,7 @@ public BucketMetadata createBucket(
lockStore.putIfAbsent(bucketName, new Object());
synchronized (lockStore.get(bucketName)) {
var bucketFolder = createBucketFolder(bucketName);
var region = bucketRegion == null ? this.region : bucketRegion;

var newBucketMetadata = new BucketMetadata(
bucketName,
Expand All @@ -155,7 +160,7 @@ public BucketMetadata createBucket(
null,
objectOwnership,
bucketFolder.toPath(),
bucketRegion,
region,
bucketInfo,
locationInfo
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright 2017-2025 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.store;

import java.util.List;
import java.util.Set;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.DefaultValue;

@Deprecated(forRemoval = true, since = "4.5.0")
@ConfigurationProperties("com.adobe.testing.s3mock.domain")
public record LegacyStoreProperties(
// True if files should be retained when S3Mock exits gracefully.
// False to let S3Mock delete all files when S3Mock exits gracefully.
boolean retainFilesOnExit,
// The root directory to use. If omitted a default temp-dir will be used.
@Nullable String root,
@DefaultValue
Set<String> validKmsKeys,
// A comma separated list of buckets that are to be created at startup.
@DefaultValue
List<String> initialBuckets
) {

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import org.apache.commons.io.FileUtils;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
Expand All @@ -37,7 +39,7 @@
import software.amazon.awssdk.regions.Region;

@Configuration
@EnableConfigurationProperties(StoreProperties.class)
@EnableConfigurationProperties({StoreProperties.class, LegacyStoreProperties.class})
public class StoreConfiguration {

private static final Logger LOG = LoggerFactory.getLogger(StoreConfiguration.class);
Expand All @@ -47,7 +49,6 @@ public class StoreConfiguration {

@Bean
ObjectStore objectStore(
StoreProperties properties,
List<String> bucketNames,
BucketStore bucketStore,
ObjectMapper objectMapper) {
Expand All @@ -62,18 +63,28 @@ ObjectStore objectStore(
}

@Bean
BucketStore bucketStore(StoreProperties properties,
File rootFolder,
List<String> bucketNames,
ObjectMapper objectMapper,
@Value("com.adobe.testing.s3mock.region") Region region) {
var bucketStore = new BucketStore(rootFolder, S3_OBJECT_DATE_FORMAT, objectMapper);
BucketStore bucketStore(
StoreProperties properties,
LegacyStoreProperties legacyProperties,
File rootFolder,
List<String> bucketNames,
ObjectMapper objectMapper,
@Nullable @Value("${com.adobe.testing.s3mock.region}") Region region) {
Region mockRegion = region == null ? properties.region() : region;

var bucketStore = new BucketStore(rootFolder, S3_OBJECT_DATE_FORMAT, mockRegion.id(), objectMapper);
//load existing buckets first
bucketStore.loadBuckets(bucketNames);

//load initialBuckets if not part of existing buckets
properties
.initialBuckets()
List<String> initialBuckets = List.of();
if (!legacyProperties.initialBuckets().isEmpty()) {
initialBuckets = legacyProperties.initialBuckets();
} else if (!properties.initialBuckets().isEmpty()) {
initialBuckets = properties.initialBuckets();
}

initialBuckets
.stream()
.filter(name -> {
boolean partOfExistingBuckets = bucketNames.contains(name);
Expand Down Expand Up @@ -118,21 +129,38 @@ List<String> bucketNames(File rootFolder) {
}

@Bean
MultipartStore multipartStore(StoreProperties properties,
MultipartStore multipartStore(
ObjectStore objectStore,
ObjectMapper objectMapper) {
return new MultipartStore(objectStore, objectMapper);
}

@Bean
KmsKeyStore kmsKeyStore(StoreProperties properties) {
return new KmsKeyStore(properties.validKmsKeys());
KmsKeyStore kmsKeyStore(
StoreProperties properties,
LegacyStoreProperties legacyProperties) {
if (!properties.validKmsKeys().isEmpty()) {
return new KmsKeyStore(properties.validKmsKeys());
} else if (!legacyProperties.validKmsKeys().isEmpty()) {
return new KmsKeyStore(legacyProperties.validKmsKeys());
}

return new KmsKeyStore(new HashSet<>());
}

@Bean
File rootFolder(StoreProperties properties) {
File rootFolder(
StoreProperties properties,
LegacyStoreProperties legacyProperties) {
File root;
var createTempDir = properties.root() == null || properties.root().isEmpty();
String rootPath = null;
if (legacyProperties.root() != null && !legacyProperties.root().isEmpty()) {
rootPath = legacyProperties.root();
} else if (properties.root() != null && !properties.root().isEmpty()) {
rootPath = properties.root();
}

var createTempDir = rootPath == null;

if (createTempDir) {
var baseTempDir = FileUtils.getTempDirectory().toPath();
Expand All @@ -146,7 +174,7 @@ File rootFolder(StoreProperties properties) {
LOG.info("Successfully created \"{}\" as root folder. Will retain files on exit: {}",
root.getAbsolutePath(), properties.retainFilesOnExit());
} else {
root = new File(properties.root());
root = new File(rootPath);

if (root.exists()) {
LOG.info("Using existing folder \"{}\" as root folder. Will retain files on exit: {}",
Expand All @@ -164,7 +192,14 @@ File rootFolder(StoreProperties properties) {
}

@Bean
StoreCleaner storeCleaner(File rootFolder, StoreProperties storeProperties) {
return new StoreCleaner(rootFolder, storeProperties.retainFilesOnExit());
StoreCleaner storeCleaner(
File rootFolder,
StoreProperties properties,
LegacyStoreProperties legacyProperties) {
if (legacyProperties.retainFilesOnExit() || properties.retainFilesOnExit()) {
return new StoreCleaner(rootFolder, true);
} else {
return new StoreCleaner(rootFolder, false);
}
}
}
Loading