feat:增加内置oss

master
xiwa 2024-03-16 10:15:00 +08:00
parent 34bb73c18c
commit 21894f1f57
131 changed files with 9801 additions and 209 deletions

View File

@ -6,7 +6,7 @@
<parent>
<artifactId>iot-iita-core</artifactId>
<groupId>cc.iotkit</groupId>
<version>1.0.4</version>
<version>1.0.5</version>
</parent>
<artifactId>iot-common-core</artifactId>

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>iot-iita-core</artifactId>
<groupId>cc.iotkit</groupId>
<version>1.0.4</version>
<version>1.0.5</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>iot-iita-core</artifactId>
<groupId>cc.iotkit</groupId>
<version>1.0.4</version>
<version>1.0.5</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>iot-iita-core</artifactId>
<groupId>cc.iotkit</groupId>
<version>1.0.4</version>
<version>1.0.5</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>iot-iita-core</artifactId>
<groupId>cc.iotkit</groupId>
<version>1.0.4</version>
<version>1.0.5</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -89,7 +89,10 @@ public class OssClient {
client.createBucket(createBucketRequest);
client.setBucketPolicy(bucketName, getPolicy(bucketName, accessPolicy.getPolicyType()));
} catch (Exception e) {
throw new OssException("创建Bucket失败, 请核对配置信息:[" + e.getMessage() + "]");
if (e.getMessage().contains("Your previous request to create the named bucket succeeded and you already own it")) {
return;
}
throw new OssException("创建Bucket失败, 请核对配置信息:[" + e.getMessage() + "]", e);
}
}
@ -110,7 +113,7 @@ public class OssClient {
putObjectRequest.setCannedAcl(getAccessPolicy().getAcl());
client.putObject(putObjectRequest);
} catch (Exception e) {
throw new OssException("上传文件失败,请检查配置信息:[" + e.getMessage() + "]");
throw new OssException("上传文件失败,请检查配置信息:[" + e.getMessage() + "]", e);
}
return UploadResult.builder().url(getUrl() + "/" + path).filename(path).build();
}
@ -175,7 +178,11 @@ public class OssClient {
if (StringUtils.isNotBlank(prefix)) {
path = prefix + "/" + path;
}
return path + suffix;
path = path + suffix;
if (configKey.equals("oss-embed")) {
return path.replace("/", "_");
}
return path;
}

View File

@ -15,4 +15,7 @@ public class OssException extends RuntimeException {
super(msg);
}
public OssException(String msg, Exception e) {
super(msg, e);
}
}

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>iot-iita-core</artifactId>
<groupId>cc.iotkit</groupId>
<version>1.0.4</version>
<version>1.0.5</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>iot-iita-core</artifactId>
<groupId>cc.iotkit</groupId>
<version>1.0.4</version>
<version>1.0.5</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -46,7 +46,7 @@ public class SaTokenConfig implements WebMvcConfigurer {
List<String> swaggerUrls = List.of("/doc.html","/favicon.ico", "/webjars/**", "/resources/**"
, "/swagger-resources/**", "/swagger-ui.html/**");
List loginUrls = List.of("/code", "/auth/tenant/list", "/auth/login", "/auth/logout");
List loginUrls = List.of("/code", "/auth/tenant/list", "/auth/login", "/auth/logout","/iot-oss/**");
List<String> openApiUrls = List.of( "/openapi/v1/getToken");
List<String> excludeUrls = new ArrayList<>();

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>iot-iita-core</artifactId>
<groupId>cc.iotkit</groupId>
<version>1.0.4</version>
<version>1.0.5</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>iot-iita-core</artifactId>
<groupId>cc.iotkit</groupId>
<version>1.0.4</version>
<version>1.0.5</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>iot-iita-core</artifactId>
<groupId>cc.iotkit</groupId>
<version>1.0.4</version>
<version>1.0.5</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -21,11 +21,12 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
@Slf4j
@ControllerAdvice
@ControllerAdvice(annotations = RestController.class)
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)

View File

@ -14,6 +14,7 @@ import cn.dev33.satoken.util.SaResult;
import cn.hutool.core.util.IdUtil;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
@ -27,6 +28,9 @@ import java.util.Map;
public class ResponseResultHandler implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
if (returnType.getParameterType() == ResponseEntity.class) {
return false;
}
return !converterType.equals(StringHttpMessageConverter.class);
}

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>iot-iita-core</artifactId>
<groupId>cc.iotkit</groupId>
<version>1.0.4</version>
<version>1.0.5</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>iot-message-bus</artifactId>
<groupId>cc.iotkit</groupId>
<version>1.0.4</version>
<version>1.0.5</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>iot-message-bus</artifactId>
<groupId>cc.iotkit</groupId>
<version>1.0.4</version>
<version>1.0.5</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>iot-message-bus</artifactId>
<groupId>cc.iotkit</groupId>
<version>1.0.4</version>
<version>1.0.5</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>iot-iita-core</artifactId>
<groupId>cc.iotkit</groupId>
<version>1.0.4</version>
<version>1.0.5</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>pom</packaging>

95
iot-oss-embed/pom.xml Normal file
View File

@ -0,0 +1,95 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cc.iotkit</groupId>
<artifactId>iot-iita-core</artifactId>
<version>1.0.5</version>
</parent>
<artifactId>iot-oss-embed</artifactId>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>cc.iotkit</groupId>
<artifactId>iot-common-oss</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jdk8</artifactId>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>regions</artifactId>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>utils</artifactId>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-core</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,470 @@
/*
* 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 cc.iotkit.s3mock;
import static cc.iotkit.s3mock.util.AwsHttpHeaders.X_AMZ_BUCKET_OBJECT_LOCK_ENABLED;
import static cc.iotkit.s3mock.util.AwsHttpParameters.CONTINUATION_TOKEN;
import static cc.iotkit.s3mock.util.AwsHttpParameters.ENCODING_TYPE;
import static cc.iotkit.s3mock.util.AwsHttpParameters.KEY_MARKER;
import static cc.iotkit.s3mock.util.AwsHttpParameters.LIFECYCLE;
import static cc.iotkit.s3mock.util.AwsHttpParameters.LIST_TYPE_V2;
import static cc.iotkit.s3mock.util.AwsHttpParameters.LOCATION;
import static cc.iotkit.s3mock.util.AwsHttpParameters.MAX_KEYS;
import static cc.iotkit.s3mock.util.AwsHttpParameters.NOT_LIFECYCLE;
import static cc.iotkit.s3mock.util.AwsHttpParameters.NOT_LIST_TYPE;
import static cc.iotkit.s3mock.util.AwsHttpParameters.NOT_LOCATION;
import static cc.iotkit.s3mock.util.AwsHttpParameters.NOT_OBJECT_LOCK;
import static cc.iotkit.s3mock.util.AwsHttpParameters.NOT_UPLOADS;
import static cc.iotkit.s3mock.util.AwsHttpParameters.NOT_VERSIONS;
import static cc.iotkit.s3mock.util.AwsHttpParameters.OBJECT_LOCK;
import static cc.iotkit.s3mock.util.AwsHttpParameters.START_AFTER;
import static cc.iotkit.s3mock.util.AwsHttpParameters.VERSIONS;
import static cc.iotkit.s3mock.util.AwsHttpParameters.VERSION_ID_MARKER;
import static org.springframework.http.MediaType.APPLICATION_XML_VALUE;
import cn.dev33.satoken.annotation.SaIgnore;
import cc.iotkit.s3mock.dto.BucketLifecycleConfiguration;
import cc.iotkit.s3mock.dto.ListAllMyBucketsResult;
import cc.iotkit.s3mock.dto.ListBucketResult;
import cc.iotkit.s3mock.dto.ListBucketResultV2;
import cc.iotkit.s3mock.dto.ListVersionsResult;
import cc.iotkit.s3mock.dto.LocationConstraint;
import cc.iotkit.s3mock.dto.ObjectLockConfiguration;
import cc.iotkit.s3mock.service.BucketService;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import software.amazon.awssdk.regions.Region;
/**
* Handles requests related to buckets.
*/
@SaIgnore
@CrossOrigin(originPatterns = "*", exposedHeaders = "*")
@Controller
@RequestMapping("${s3mock.contextPath:}")
public class BucketController {
private final BucketService bucketService;
private final Region region;
public BucketController(BucketService bucketService, Region region) {
this.bucketService = bucketService;
this.region = region;
}
//================================================================================================
// /
//================================================================================================
/**
* List all existing buckets.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListBuckets.html">API Reference</a>
*
* @return List of all Buckets
*/
@GetMapping(
value = "/",
produces = APPLICATION_XML_VALUE
)
public ResponseEntity<ListAllMyBucketsResult> listBuckets() {
var listAllMyBucketsResult = bucketService.listBuckets();
return ResponseEntity.ok(listAllMyBucketsResult);
}
//================================================================================================
// /{bucketName:.+}
//================================================================================================
/**
* Create a bucket if the name matches a simplified version of the bucket naming rules.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html">API Reference Bucket Naming</a>
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_CreateBucket.html">API Reference</a>
*
* @param bucketName name of the bucket that should be created.
*
* @return 200 OK if creation was successful.
*/
@PutMapping(
value = {
//AWS SDK V2 pattern
"/{bucketName:.+}",
//AWS SDK V1 pattern
"/{bucketName:.+}/"
},
params = {
NOT_OBJECT_LOCK,
NOT_LIFECYCLE
}
)
public ResponseEntity<Void> createBucket(@PathVariable final String bucketName,
@RequestHeader(value = X_AMZ_BUCKET_OBJECT_LOCK_ENABLED,
required = false, defaultValue = "false") boolean objectLockEnabled) {
bucketService.verifyBucketNameIsAllowed(bucketName);
bucketService.verifyBucketDoesNotExist(bucketName);
bucketService.createBucket(bucketName, objectLockEnabled);
return ResponseEntity.ok().build();
}
/**
* Check if a bucket exists.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadBucket.html">API Reference</a>
*
* @param bucketName name of the Bucket.
*
* @return 200 if it exists; 404 if not found.
*/
@RequestMapping(
value = {
//AWS SDK V2 pattern
"/{bucketName:.+}",
//AWS SDK V1 pattern
"/{bucketName:.+}/"
},
method = RequestMethod.HEAD
)
public ResponseEntity<Void> headBucket(@PathVariable final String bucketName) {
bucketService.verifyBucketExists(bucketName);
//return bucket region
//return bucket location
return ResponseEntity.ok().build();
}
/**
* Delete a bucket.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteBucket.html">API Reference</a>
*
* @param bucketName name of the Bucket.
*
* @return 204 if Bucket was deleted; 404 if not found
*/
@DeleteMapping(
value = {
//AWS SDK V2 pattern
"/{bucketName:.+}",
//AWS SDK V1 pattern
"/{bucketName:.+}/"
},
params = {
NOT_LIFECYCLE
}
)
public ResponseEntity<Void> deleteBucket(@PathVariable String bucketName) {
bucketService.verifyBucketExists(bucketName);
bucketService.verifyBucketIsEmpty(bucketName);
bucketService.deleteBucket(bucketName);
return ResponseEntity.noContent().build();
}
/**
* Get ObjectLockConfiguration of a bucket.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObjectLockConfiguration.html">API Reference</a>
*
* @param bucketName name of the Bucket.
*
* @return 200, ObjectLockConfiguration
*/
@GetMapping(
value = {
//AWS SDK V2 pattern
"/{bucketName:.+}",
//AWS SDK V1 pattern
"/{bucketName:.+}/"
},
params = {
OBJECT_LOCK,
NOT_LIST_TYPE
},
produces = APPLICATION_XML_VALUE
)
public ResponseEntity<ObjectLockConfiguration> getObjectLockConfiguration(
@PathVariable String bucketName) {
bucketService.verifyBucketExists(bucketName);
var configuration = bucketService.getObjectLockConfiguration(bucketName);
return ResponseEntity.ok(configuration);
}
/**
* Put ObjectLockConfiguration of a bucket.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObjectLockConfiguration.html">API Reference</a>
*
* @param bucketName name of the Bucket.
*
* @return 200, ObjectLockConfiguration
*/
@PutMapping(
value = {
//AWS SDK V2 pattern
"/{bucketName:.+}",
//AWS SDK V1 pattern
"/{bucketName:.+}/"
},
params = {
OBJECT_LOCK
},
consumes = APPLICATION_XML_VALUE
)
public ResponseEntity<Void> putObjectLockConfiguration(
@PathVariable String bucketName,
@RequestBody ObjectLockConfiguration configuration) {
bucketService.verifyBucketExists(bucketName);
bucketService.setObjectLockConfiguration(bucketName, configuration);
return ResponseEntity.ok().build();
}
/**
* Get BucketLifecycleConfiguration of a bucket.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketLifecycleConfiguration.html">API Reference</a>
*
* @param bucketName name of the Bucket.
*
* @return 200, ObjectLockConfiguration
*/
@GetMapping(
value = {
//AWS SDK V2 pattern
"/{bucketName:.+}",
//AWS SDK V1 pattern
"/{bucketName:.+}/"
},
params = {
LIFECYCLE,
NOT_LIST_TYPE
},
produces = APPLICATION_XML_VALUE
)
public ResponseEntity<BucketLifecycleConfiguration> getBucketLifecycleConfiguration(
@PathVariable String bucketName) {
bucketService.verifyBucketExists(bucketName);
var configuration = bucketService.getBucketLifecycleConfiguration(bucketName);
return ResponseEntity.ok(configuration);
}
/**
* Put BucketLifecycleConfiguration of a bucket.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketLifecycleConfiguration.html">API Reference</a>
*
* @param bucketName name of the Bucket.
*
* @return 200, ObjectLockConfiguration
*/
@PutMapping(
value = {
//AWS SDK V2 pattern
"/{bucketName:.+}",
//AWS SDK V1 pattern
"/{bucketName:.+}/"
},
params = {
LIFECYCLE
},
consumes = APPLICATION_XML_VALUE
)
public ResponseEntity<Void> putBucketLifecycleConfiguration(
@PathVariable String bucketName,
@RequestBody BucketLifecycleConfiguration configuration) {
bucketService.verifyBucketExists(bucketName);
bucketService.setBucketLifecycleConfiguration(bucketName, configuration);
return ResponseEntity.ok().build();
}
/**
* Delete BucketLifecycleConfiguration of a bucket.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteBucketLifecycle.html">API Reference</a>
*
* @param bucketName name of the Bucket.
*
* @return 200, ObjectLockConfiguration
*/
@DeleteMapping(
value = {
//AWS SDK V2 pattern
"/{bucketName:.+}",
//AWS SDK V1 pattern
"/{bucketName:.+}/"
},
params = {
LIFECYCLE
}
)
public ResponseEntity<Void> deleteBucketLifecycleConfiguration(
@PathVariable String bucketName) {
bucketService.verifyBucketExists(bucketName);
bucketService.deleteBucketLifecycleConfiguration(bucketName);
return ResponseEntity.noContent().build();
}
/**
* Get location of a bucket.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketLocation.html">API Reference</a>
*
* @param bucketName name of the Bucket.
*
* @return 200, LocationConstraint
*/
@GetMapping(
value = "/{bucketName:.+}",
params = {
LOCATION
}
)
public ResponseEntity<LocationConstraint> getBucketLocation(
@PathVariable String bucketName) {
bucketService.verifyBucketExists(bucketName);
return ResponseEntity.ok(new LocationConstraint(region));
}
/**
* Retrieve list of objects of a bucket.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjects.html">API Reference</a>
*
* @param bucketName {@link String} set bucket name
* @param prefix {@link String} find object names they start with prefix
* @param encodingType whether to use URL encoding (encodingtype="url") or not
*
* @return {@link ListBucketResult} a list of objects in Bucket
* @deprecated Long since replaced by ListObjectsV2, {@see #listObjectsInsideBucketV2}
*/
@GetMapping(
value = {
//AWS SDK V2 pattern
"/{bucketName:.+}",
//AWS SDK V1 pattern
"/{bucketName:.+}/"
},
params = {
NOT_UPLOADS,
NOT_OBJECT_LOCK,
NOT_LIST_TYPE,
NOT_LIFECYCLE,
NOT_LOCATION,
NOT_VERSIONS
},
produces = APPLICATION_XML_VALUE
)
@Deprecated(since = "2.12.2", forRemoval = true)
public ResponseEntity<ListBucketResult> listObjects(
@PathVariable String bucketName,
@RequestParam(required = false) String prefix,
@RequestParam(required = false) String delimiter,
@RequestParam(required = false) String marker,
@RequestParam(name = ENCODING_TYPE, required = false) String encodingType,
@RequestParam(name = MAX_KEYS, defaultValue = "1000", required = false) Integer maxKeys) {
bucketService.verifyBucketExists(bucketName);
bucketService.verifyMaxKeys(maxKeys);
bucketService.verifyEncodingType(encodingType);
var listBucketResult = bucketService.listObjectsV1(bucketName, prefix, delimiter,
marker, encodingType, maxKeys);
return ResponseEntity.ok(listBucketResult);
}
/**
* Retrieve list of objects of a bucket.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectsV2.html">API Reference</a>
*
* @param bucketName {@link String} set bucket name
* @param prefix {@link String} find object names they start with prefix
* @param startAfter {@link String} return key names after a specific object key in your key
* space
* @param maxKeys {@link Integer} set maximum number of keys to be returned
* @param continuationToken {@link String} pagination token returned by previous request
*
* @return {@link ListBucketResultV2} a list of objects in Bucket
*/
@GetMapping(
value = {
//AWS SDK V2 pattern
"/{bucketName:.+}",
//AWS SDK V1 pattern
"/{bucketName:.+}/"
},
params = {
LIST_TYPE_V2
},
produces = APPLICATION_XML_VALUE
)
public ResponseEntity<ListBucketResultV2> listObjectsV2(
@PathVariable String bucketName,
@RequestParam(required = false) String prefix,
@RequestParam(required = false) String delimiter,
@RequestParam(name = ENCODING_TYPE, required = false) String encodingType,
@RequestParam(name = START_AFTER, required = false) String startAfter,
@RequestParam(name = MAX_KEYS, defaultValue = "1000", required = false) Integer maxKeys,
@RequestParam(name = CONTINUATION_TOKEN, required = false) String continuationToken) {
bucketService.verifyBucketExists(bucketName);
bucketService.verifyMaxKeys(maxKeys);
bucketService.verifyEncodingType(encodingType);
var listBucketResultV2 =
bucketService.listObjectsV2(bucketName, prefix, delimiter, encodingType, startAfter,
maxKeys, continuationToken);
return ResponseEntity.ok(listBucketResultV2);
}
/**
* Retrieve list of versions of an object of a bucket.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectVersions.html">API Reference</a>
*
* @param bucketName {@link String} set bucket name
* @param prefix {@link String} find object names they start with prefix
* @param startAfter {@link String} return key names after a specific object key in your key
* space
* @param maxKeys {@link Integer} set maximum number of keys to be returned
* @param continuationToken {@link String} pagination token returned by previous request
*
* @return {@link ListVersionsResult} a list of objects in Bucket
*/
@GetMapping(
value = {
//AWS SDK V2 pattern
"/{bucketName:.+}",
//AWS SDK V1 pattern
"/{bucketName:.+}/"
},
params = {
VERSIONS
},
produces = APPLICATION_XML_VALUE
)
public ResponseEntity<ListVersionsResult> listObjectVersions(
@PathVariable String bucketName,
@RequestParam(required = false) String prefix,
@RequestParam(required = false) String delimiter,
@RequestParam(name = KEY_MARKER, required = false) String keyMarker,
@RequestParam(name = VERSION_ID_MARKER, required = false) String versionIdMarker,
@RequestParam(name = ENCODING_TYPE, required = false) String encodingType,
@RequestParam(name = START_AFTER, required = false) String startAfter,
@RequestParam(name = MAX_KEYS, defaultValue = "1000", required = false) Integer maxKeys,
@RequestParam(name = CONTINUATION_TOKEN, required = false) String continuationToken) {
bucketService.verifyBucketExists(bucketName);
bucketService.verifyMaxKeys(maxKeys);
bucketService.verifyEncodingType(encodingType);
var listVersionsResult =
bucketService.listVersions(bucketName, prefix, delimiter, encodingType, startAfter,
maxKeys, continuationToken, keyMarker, versionIdMarker);
return ResponseEntity.ok(listVersionsResult);
}
}

View File

@ -0,0 +1,38 @@
/*
* Copyright 2017-2023 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 cc.iotkit.s3mock;
import cn.dev33.satoken.annotation.SaIgnore;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* Spring Boot 2.2+ does not include the default favicon.ico anymore.
* This is needed to check if the S3 Mock is up (at least in our examples and some use-cases)
*/
@SaIgnore
@Controller
@RequestMapping
class FaviconController {
@GetMapping("favicon.ico")
@ResponseBody
void favicon() {
// Method is intentionally empty.
}
}

View File

@ -0,0 +1,35 @@
/*
* Copyright 2017-2023 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 cc.iotkit.s3mock;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.HttpRange;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
public class HttpRangeHeaderConverter implements Converter<String, HttpRange> {
@Override
@Nullable
public HttpRange convert(@NonNull String source) {
var httpRanges = HttpRange.parseRanges(source);
if (!httpRanges.isEmpty()) {
return httpRanges.get(0);
}
return null;
}
}

View File

@ -0,0 +1,106 @@
/*
* Copyright 2017-2023 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 cc.iotkit.s3mock;
import static cc.iotkit.s3mock.util.AwsHttpHeaders.X_AMZ_SERVER_SIDE_ENCRYPTION;
import static cc.iotkit.s3mock.util.AwsHttpHeaders.X_AMZ_SERVER_SIDE_ENCRYPTION_AWS_KMS_KEY_ID;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.springframework.http.HttpHeaders.CONTENT_TYPE;
import static org.springframework.http.HttpStatus.BAD_REQUEST;
import static org.springframework.http.MediaType.APPLICATION_XML_VALUE;
import cc.iotkit.s3mock.dto.ErrorResponse;
import cc.iotkit.s3mock.store.KmsKeyStore;
import java.io.IOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
import org.springframework.lang.NonNull;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* A Filter that validates KMS keys of incoming Requests. If Keys can not be found in Keystore the
* Request will be denied immediately.
*/
class KmsValidationFilter extends OncePerRequestFilter {
private static final Logger LOG = LoggerFactory.getLogger(KmsValidationFilter.class);
private static final String AWS_KMS = "aws:kms";
private final KmsKeyStore keystore;
private final MappingJackson2XmlHttpMessageConverter messageConverter;
/**
* Constructs a new {@link KmsValidationFilter}.
*
* @param keystore Keystore for validation of KMS Keys
*/
KmsValidationFilter(KmsKeyStore keystore,
MappingJackson2XmlHttpMessageConverter messageConverter) {
this.keystore = keystore;
this.messageConverter = messageConverter;
}
@Override
protected void doFilterInternal(@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain) throws ServletException, IOException {
try {
LOG.debug("Checking KMS key, if present.");
var encryptionTypeHeader = request.getHeader(X_AMZ_SERVER_SIDE_ENCRYPTION);
var encryptionKeyId = request.getHeader(X_AMZ_SERVER_SIDE_ENCRYPTION_AWS_KMS_KEY_ID);
if (AWS_KMS.equals(encryptionTypeHeader)
&& !isBlank(encryptionKeyId)
&& !keystore.validateKeyId(encryptionKeyId)) {
LOG.info("Received invalid KMS key ID {}. Sending error response.", encryptionKeyId);
request.getInputStream().close();
response.setStatus(BAD_REQUEST.value());
response.setHeader(CONTENT_TYPE, APPLICATION_XML_VALUE);
var errorResponse = new ErrorResponse(
"KMS.NotFoundException",
"Key ID " + encryptionKeyId + " does not exist!",
null,
null
);
messageConverter.getObjectMapper().writeValue(response.getOutputStream(), errorResponse);
response.flushBuffer();
} else if (AWS_KMS.equals(encryptionTypeHeader)
&& !isBlank(encryptionKeyId)
&& keystore.validateKeyId(encryptionKeyId)) {
LOG.info("Received valid KMS key ID {}.", encryptionKeyId);
filterChain.doFilter(request, response);
} else {
filterChain.doFilter(request, response);
}
} finally {
LOG.debug("Finished checking KMS key.");
}
}
}

View File

@ -0,0 +1,375 @@
/*
* 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 cc.iotkit.s3mock;
import static cc.iotkit.s3mock.util.AwsHttpHeaders.NOT_X_AMZ_COPY_SOURCE;
import static cc.iotkit.s3mock.util.AwsHttpHeaders.NOT_X_AMZ_COPY_SOURCE_RANGE;
import static cc.iotkit.s3mock.util.AwsHttpHeaders.X_AMZ_CONTENT_SHA256;
import static cc.iotkit.s3mock.util.AwsHttpHeaders.X_AMZ_COPY_SOURCE;
import static cc.iotkit.s3mock.util.AwsHttpHeaders.X_AMZ_COPY_SOURCE_IF_MATCH;
import static cc.iotkit.s3mock.util.AwsHttpHeaders.X_AMZ_COPY_SOURCE_IF_NONE_MATCH;
import static cc.iotkit.s3mock.util.AwsHttpHeaders.X_AMZ_COPY_SOURCE_RANGE;
import static cc.iotkit.s3mock.util.AwsHttpHeaders.X_AMZ_STORAGE_CLASS;
import static cc.iotkit.s3mock.util.AwsHttpParameters.NOT_LIFECYCLE;
import static cc.iotkit.s3mock.util.AwsHttpParameters.PART_NUMBER;
import static cc.iotkit.s3mock.util.AwsHttpParameters.UPLOADS;
import static cc.iotkit.s3mock.util.AwsHttpParameters.UPLOAD_ID;
import static cc.iotkit.s3mock.util.HeaderUtil.checksumAlgorithmFrom;
import static cc.iotkit.s3mock.util.HeaderUtil.checksumFrom;
import static cc.iotkit.s3mock.util.HeaderUtil.encryptionHeadersFrom;
import static cc.iotkit.s3mock.util.HeaderUtil.isV4ChunkedWithSigningEnabled;
import static cc.iotkit.s3mock.util.HeaderUtil.storeHeadersFrom;
import static cc.iotkit.s3mock.util.HeaderUtil.userMetadataFrom;
import static org.springframework.http.HttpHeaders.CONTENT_TYPE;
import static org.springframework.http.MediaType.APPLICATION_XML_VALUE;
import cc.iotkit.s3mock.dto.*;
import cc.iotkit.s3mock.dto.CompleteMultipartUpload;
import cc.iotkit.s3mock.dto.CompleteMultipartUploadResult;
import cc.iotkit.s3mock.dto.CopyPartResult;
import cc.iotkit.s3mock.dto.CopySource;
import cc.iotkit.s3mock.dto.InitiateMultipartUploadResult;
import cc.iotkit.s3mock.dto.ListMultipartUploadsResult;
import cc.iotkit.s3mock.dto.ListPartsResult;
import cc.iotkit.s3mock.dto.ObjectKey;
import cc.iotkit.s3mock.dto.StorageClass;
import cc.iotkit.s3mock.service.BucketService;
import cc.iotkit.s3mock.service.MultipartService;
import cc.iotkit.s3mock.service.ObjectService;
import java.io.InputStream;
import java.util.List;
import java.util.UUID;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRange;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import software.amazon.awssdk.utils.http.SdkHttpUtils;
import javax.servlet.http.HttpServletRequest;
/**
* Handles requests related to parts.
*/
@CrossOrigin(originPatterns = "*", exposedHeaders = "*")
@Controller
@RequestMapping("${s3mock.contextPath:}")
public class MultipartController {
private final BucketService bucketService;
private final ObjectService objectService;
private final MultipartService multipartService;
public MultipartController(BucketService bucketService, ObjectService objectService,
MultipartService multipartService) {
this.bucketService = bucketService;
this.objectService = objectService;
this.multipartService = multipartService;
}
//================================================================================================
// /{bucketName:.+}
//================================================================================================
/**
* Lists all in-progress multipart uploads.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListMultipartUploads.html">API Reference</a>
*
* <p>Not yet supported request parameters: delimiter, encoding-type, max-uploads, key-marker,
* upload-id-marker.</p>
*
* @param bucketName the Bucket in which to store the file in.
*
* @return the {@link ListMultipartUploadsResult}
*/
@GetMapping(
value = {
//AWS SDK V2 pattern
"/{bucketName:.+}",
//AWS SDK V1 pattern
"/{bucketName:.+}/"
},
params = {
UPLOADS
},
produces = APPLICATION_XML_VALUE
)
public ResponseEntity<ListMultipartUploadsResult> listMultipartUploads(
@PathVariable String bucketName,
@RequestParam(required = false) String prefix) {
bucketService.verifyBucketExists(bucketName);
return ResponseEntity.ok(multipartService.listMultipartUploads(bucketName, prefix));
}
//================================================================================================
// /{bucketName:.+}/{key}
//================================================================================================
/**
* Aborts a multipart upload for a given uploadId.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_AbortMultipartUpload.html">API Reference</a>
*
* @param bucketName the Bucket in which to store the file in.
* @param uploadId id of the upload. Has to match all other part's uploads.
*/
@DeleteMapping(
value = "/{bucketName:.+}/{key:.*}",
params = {
UPLOAD_ID,
NOT_LIFECYCLE
},
produces = APPLICATION_XML_VALUE
)
public ResponseEntity<Void> abortMultipartUpload(@PathVariable String bucketName,
@PathVariable ObjectKey key,
@RequestParam String uploadId) {
bucketService.verifyBucketExists(bucketName);
multipartService.verifyMultipartUploadExists(uploadId);
multipartService.abortMultipartUpload(bucketName, key.getKey(), uploadId);
return ResponseEntity.noContent().build();
}
/**
* Lists all parts a file multipart upload.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListParts.html">API Reference</a>
*
* @param bucketName the Bucket in which to store the file in.
* @param uploadId id of the upload. Has to match all other part's uploads.
*
* @return the {@link ListPartsResult}
*/
@GetMapping(
value = "/{bucketName:.+}/{key:.*}",
params = {
UPLOAD_ID
},
produces = APPLICATION_XML_VALUE
)
public ResponseEntity<ListPartsResult> listParts(@PathVariable String bucketName,
@PathVariable ObjectKey key,
@RequestParam String uploadId) {
bucketService.verifyBucketExists(bucketName);
multipartService.verifyMultipartUploadExists(uploadId);
return ResponseEntity
.ok(multipartService.getMultipartUploadParts(bucketName, key.getKey(), uploadId));
}
/**
* Adds an object to a bucket accepting encryption headers.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_UploadPart.html">API Reference</a>
*
* @param bucketName the Bucket in which to store the file in.
* @param uploadId id of the upload. Has to match all other part's uploads.
* @param partNumber number of the part to upload.
*
* @return the etag of the uploaded part.
*
*/
@PutMapping(
value = "/{bucketName:.+}/{key:.*}",
params = {
UPLOAD_ID,
PART_NUMBER
},
headers = {
NOT_X_AMZ_COPY_SOURCE,
NOT_X_AMZ_COPY_SOURCE_RANGE
}
)
public ResponseEntity<Void> uploadPart(@PathVariable String bucketName,
@PathVariable ObjectKey key,
@RequestParam String uploadId,
@RequestParam String partNumber,
@RequestHeader(value = X_AMZ_CONTENT_SHA256, required = false) String sha256Header,
@RequestHeader HttpHeaders httpHeaders,
InputStream inputStream) {
bucketService.verifyBucketExists(bucketName);
multipartService.verifyMultipartUploadExists(uploadId);
multipartService.verifyPartNumberLimits(partNumber);
var checksum = checksumFrom(httpHeaders);
var checksumAlgorithm = checksumAlgorithmFrom(httpHeaders);
//persist checksum per part
var etag = multipartService.putPart(bucketName,
key.getKey(),
uploadId,
partNumber,
inputStream,
isV4ChunkedWithSigningEnabled(sha256Header),
encryptionHeadersFrom(httpHeaders));
//return checksum headers
//return encryption headers
return ResponseEntity.ok().eTag("\"" + etag + "\"").build();
}
/**
* Uploads a part by copying data from an existing object as data source.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_UploadPartCopy.html">API Reference</a>
*
* @param copySource References the Objects to be copied.
* @param copyRange Defines the byte range for this part. Optional.
* @param uploadId id of the upload. Has to match all other part's uploads.
* @param partNumber number of the part to upload.
*
* @return The etag of the uploaded part.
*
*/
@PutMapping(
value = "/{bucketName:.+}/{key:.*}",
headers = {
X_AMZ_COPY_SOURCE,
},
params = {
UPLOAD_ID,
PART_NUMBER
},
produces = APPLICATION_XML_VALUE)
public ResponseEntity<CopyPartResult> uploadPartCopy(
@PathVariable String bucketName,
@PathVariable ObjectKey key,
@RequestHeader(value = X_AMZ_COPY_SOURCE) CopySource copySource,
@RequestHeader(value = X_AMZ_COPY_SOURCE_RANGE, required = false) HttpRange copyRange,
@RequestHeader(value = X_AMZ_COPY_SOURCE_IF_MATCH, required = false) List<String> match,
@RequestHeader(value = X_AMZ_COPY_SOURCE_IF_NONE_MATCH,
required = false) List<String> noneMatch,
@RequestParam String uploadId,
@RequestParam String partNumber,
@RequestHeader HttpHeaders httpHeaders) {
//TODO: needs modified-since handling, see API
bucketService.verifyBucketExists(bucketName);
multipartService.verifyPartNumberLimits(partNumber);
var s3ObjectMetadata = objectService.verifyObjectExists(copySource.getBucket(), copySource.getKey());
objectService.verifyObjectMatchingForCopy(match, noneMatch, s3ObjectMetadata);
var result = multipartService.copyPart(copySource.getBucket(),
copySource.getKey(),
copyRange,
partNumber,
bucketName,
key.getKey(),
uploadId,
encryptionHeadersFrom(httpHeaders)
);
//return encryption headers
//return source version id
return ResponseEntity.ok(result);
}
/**
* Initiates a multipart upload accepting encryption headers.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_CreateMultipartUpload.html">API Reference</a>
*
* @param bucketName the Bucket in which to store the file in.
*
* @return the {@link InitiateMultipartUploadResult}.
*/
@PostMapping(
value = "/{bucketName:.+}/{key:.*}",
params = {
UPLOADS
},
produces = APPLICATION_XML_VALUE)
public ResponseEntity<InitiateMultipartUploadResult> createMultipartUpload(
@PathVariable String bucketName,
@PathVariable ObjectKey key,
@RequestHeader(value = CONTENT_TYPE, required = false) String contentType,
@RequestHeader(value = X_AMZ_STORAGE_CLASS, required = false, defaultValue = "STANDARD")
StorageClass storageClass,
@RequestHeader HttpHeaders httpHeaders) {
bucketService.verifyBucketExists(bucketName);
var checksum = checksumFrom(httpHeaders);
var checksumAlgorithm = checksumAlgorithmFrom(httpHeaders);
var uploadId = UUID.randomUUID().toString();
var result =
multipartService.prepareMultipartUpload(bucketName,
key.getKey(),
contentType,
storeHeadersFrom(httpHeaders),
uploadId,
Owner.DEFAULT_OWNER,
Owner.DEFAULT_OWNER,
userMetadataFrom(httpHeaders),
encryptionHeadersFrom(httpHeaders),
storageClass,
checksum,
checksumAlgorithm);
//return encryption headers
//return checksum algorithm headers
return ResponseEntity.ok(result);
}
/**
* Adds an object to a bucket.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_CompleteMultipartUpload.html">API Reference</a>
*
* @param bucketName the Bucket in which to store the file in.
* @param uploadId id of the upload. Has to match all other part's uploads.
*
* @return {@link CompleteMultipartUploadResult}
*/
@PostMapping(
value = "/{bucketName:.+}/{key:.*}",
params = {
UPLOAD_ID
},
produces = APPLICATION_XML_VALUE)
public ResponseEntity<CompleteMultipartUploadResult> completeMultipartUpload(
@PathVariable String bucketName,
@PathVariable ObjectKey key,
@RequestParam String uploadId,
@RequestBody CompleteMultipartUpload upload,
HttpServletRequest request,
@RequestHeader HttpHeaders httpHeaders) {
bucketService.verifyBucketExists(bucketName);
multipartService.verifyMultipartUploadExists(uploadId);
multipartService.verifyMultipartParts(bucketName, key.getKey(), uploadId, upload.getParts());
var objectName = key.getKey();
var locationWithEncodedKey = request
.getRequestURL()
.toString()
.replace(objectName, SdkHttpUtils.urlEncode(objectName));
var result = multipartService.completeMultipartUpload(bucketName,
key.getKey(),
uploadId,
upload.getParts(),
encryptionHeadersFrom(httpHeaders),
locationWithEncodedKey);
//return checksum and encryption headers.
//return version id
return ResponseEntity.ok(result);
}
}

View File

@ -0,0 +1,38 @@
/*
* 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 cc.iotkit.s3mock;
import cc.iotkit.s3mock.util.AwsHttpHeaders;
import org.springframework.core.convert.converter.Converter;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import software.amazon.awssdk.services.s3.model.ObjectCannedACL;
/**
* Converts values of the {@link AwsHttpHeaders#X_AMZ_ACL} which is sent by the Amazon client.
* Example: x-amz-acl: private
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html">API Reference</a>
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html#canned-acl">API Reference</a>
*/
class ObjectCannedAclHeaderConverter implements Converter<String, ObjectCannedACL> {
@Override
@Nullable
public ObjectCannedACL convert(@NonNull String source) {
return ObjectCannedACL.fromValue(source);
}
}

View File

@ -0,0 +1,745 @@
/*
* 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 cc.iotkit.s3mock;
import static cc.iotkit.s3mock.util.AwsHttpHeaders.CONTENT_MD5;
import static cc.iotkit.s3mock.util.AwsHttpHeaders.MetadataDirective.METADATA_DIRECTIVE_COPY;
import static cc.iotkit.s3mock.util.AwsHttpHeaders.NOT_X_AMZ_COPY_SOURCE;
import static cc.iotkit.s3mock.util.AwsHttpHeaders.RANGE;
import static cc.iotkit.s3mock.util.AwsHttpHeaders.X_AMZ_ACL;
import static cc.iotkit.s3mock.util.AwsHttpHeaders.X_AMZ_CONTENT_SHA256;
import static cc.iotkit.s3mock.util.AwsHttpHeaders.X_AMZ_COPY_SOURCE;
import static cc.iotkit.s3mock.util.AwsHttpHeaders.X_AMZ_COPY_SOURCE_IF_MATCH;
import static cc.iotkit.s3mock.util.AwsHttpHeaders.X_AMZ_COPY_SOURCE_IF_NONE_MATCH;
import static cc.iotkit.s3mock.util.AwsHttpHeaders.X_AMZ_DELETE_MARKER;
import static cc.iotkit.s3mock.util.AwsHttpHeaders.X_AMZ_METADATA_DIRECTIVE;
import static cc.iotkit.s3mock.util.AwsHttpHeaders.X_AMZ_OBJECT_ATTRIBUTES;
import static cc.iotkit.s3mock.util.AwsHttpHeaders.X_AMZ_STORAGE_CLASS;
import static cc.iotkit.s3mock.util.AwsHttpHeaders.X_AMZ_TAGGING;
import static cc.iotkit.s3mock.util.AwsHttpParameters.ACL;
import static cc.iotkit.s3mock.util.AwsHttpParameters.ATTRIBUTES;
import static cc.iotkit.s3mock.util.AwsHttpParameters.DELETE;
import static cc.iotkit.s3mock.util.AwsHttpParameters.LEGAL_HOLD;
import static cc.iotkit.s3mock.util.AwsHttpParameters.NOT_ACL;
import static cc.iotkit.s3mock.util.AwsHttpParameters.NOT_ATTRIBUTES;
import static cc.iotkit.s3mock.util.AwsHttpParameters.NOT_LEGAL_HOLD;
import static cc.iotkit.s3mock.util.AwsHttpParameters.NOT_LIFECYCLE;
import static cc.iotkit.s3mock.util.AwsHttpParameters.NOT_RETENTION;
import static cc.iotkit.s3mock.util.AwsHttpParameters.NOT_TAGGING;
import static cc.iotkit.s3mock.util.AwsHttpParameters.NOT_UPLOADS;
import static cc.iotkit.s3mock.util.AwsHttpParameters.NOT_UPLOAD_ID;
import static cc.iotkit.s3mock.util.AwsHttpParameters.RETENTION;
import static cc.iotkit.s3mock.util.AwsHttpParameters.TAGGING;
import static cc.iotkit.s3mock.util.HeaderUtil.checksumAlgorithmFrom;
import static cc.iotkit.s3mock.util.HeaderUtil.checksumFrom;
import static cc.iotkit.s3mock.util.HeaderUtil.checksumHeaderFrom;
import static cc.iotkit.s3mock.util.HeaderUtil.encryptionHeadersFrom;
import static cc.iotkit.s3mock.util.HeaderUtil.isV4ChunkedWithSigningEnabled;
import static cc.iotkit.s3mock.util.HeaderUtil.mediaTypeFrom;
import static cc.iotkit.s3mock.util.HeaderUtil.overrideHeadersFrom;
import static cc.iotkit.s3mock.util.HeaderUtil.storeHeadersFrom;
import static cc.iotkit.s3mock.util.HeaderUtil.userMetadataFrom;
import static cc.iotkit.s3mock.util.HeaderUtil.userMetadataHeadersFrom;
import static org.springframework.http.HttpHeaders.CONTENT_TYPE;
import static org.springframework.http.HttpHeaders.IF_MATCH;
import static org.springframework.http.HttpHeaders.IF_NONE_MATCH;
import static org.springframework.http.HttpStatus.NOT_FOUND;
import static org.springframework.http.HttpStatus.PARTIAL_CONTENT;
import static org.springframework.http.HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE;
import static org.springframework.http.MediaType.APPLICATION_XML_VALUE;
import cc.iotkit.s3mock.service.ObjectService;
import cn.dev33.satoken.annotation.SaIgnore;
import cc.iotkit.s3mock.dto.AccessControlPolicy;
import cc.iotkit.s3mock.dto.CopyObjectResult;
import cc.iotkit.s3mock.dto.CopySource;
import cc.iotkit.s3mock.dto.Delete;
import cc.iotkit.s3mock.dto.DeleteResult;
import cc.iotkit.s3mock.dto.GetObjectAttributesOutput;
import cc.iotkit.s3mock.dto.LegalHold;
import cc.iotkit.s3mock.dto.ObjectAttributes;
import cc.iotkit.s3mock.dto.ObjectKey;
import cc.iotkit.s3mock.dto.Owner;
import cc.iotkit.s3mock.dto.Retention;
import cc.iotkit.s3mock.dto.StorageClass;
import cc.iotkit.s3mock.dto.Tag;
import cc.iotkit.s3mock.dto.TagSet;
import cc.iotkit.s3mock.dto.Tagging;
import cc.iotkit.s3mock.service.BucketService;
import cc.iotkit.s3mock.store.S3ObjectMetadata;
import cc.iotkit.s3mock.util.AwsHttpHeaders.MetadataDirective;
import cc.iotkit.s3mock.util.CannedAclUtil;
import cc.iotkit.s3mock.util.XmlUtil;
import jakarta.xml.bind.JAXBException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import javax.xml.stream.XMLStreamException;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.input.BoundedInputStream;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRange;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import software.amazon.awssdk.services.s3.model.ObjectCannedACL;
/**
* Handles requests related to objects.
*/
@SaIgnore
@CrossOrigin(originPatterns = "*", exposedHeaders = "*")
@Controller
@RequestMapping("${s3mock.contextPath:}")
public class ObjectController {
private static final String RANGES_BYTES = "bytes";
private final BucketService bucketService;
private final ObjectService objectService;
public ObjectController(BucketService bucketService, ObjectService objectService) {
this.bucketService = bucketService;
this.objectService = objectService;
}
//================================================================================================
// /{bucketName:.+}
//================================================================================================
/**
* This operation removes multiple objects.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObjects.html">API Reference</a>
*
* @param bucketName name of bucket containing the object.
* @param body The delete request.
* @return The {@link DeleteResult}
*/
@PostMapping(
value = {
//AWS SDK V2 pattern
"/{bucketName:.+}",
//AWS SDK V1 pattern
"/{bucketName:.+}/"
},
params = {
DELETE
},
produces = APPLICATION_XML_VALUE
)
public ResponseEntity<DeleteResult> deleteObjects(
@PathVariable String bucketName,
@RequestBody Delete body) {
bucketService.verifyBucketExists(bucketName);
//return version id
return ResponseEntity.ok(objectService.deleteObjects(bucketName, body));
}
//================================================================================================
// /{bucketName:.+}/{key}
//================================================================================================
/**
* Retrieves metadata from an object without returning the object itself.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadObject.html">API Reference</a>
*
* @param bucketName name of the bucket to look in
* @return 200 with object metadata headers, 404 if not found.
*/
@RequestMapping(
value = "/{bucketName:.+}/{key:.*}",
method = RequestMethod.HEAD
)
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) {
//TODO: needs modified-since handling, see API
bucketService.verifyBucketExists(bucketName);
var s3ObjectMetadata = objectService.verifyObjectExists(bucketName, key.getKey());
//return version id
if (s3ObjectMetadata != null) {
objectService.verifyObjectMatching(match, noneMatch, s3ObjectMetadata);
return ResponseEntity.ok()
.eTag(s3ObjectMetadata.getEtag())
.header(HttpHeaders.ACCEPT_RANGES, RANGES_BYTES)
.headers(headers -> headers.setAll(s3ObjectMetadata.getStoreHeaders()))
.headers(headers -> headers.setAll(userMetadataHeadersFrom(s3ObjectMetadata)))
.headers(headers -> headers.setAll(s3ObjectMetadata.getEncryptionHeaders()))
.headers(h -> h.setAll(checksumHeaderFrom(s3ObjectMetadata)))
.header(X_AMZ_STORAGE_CLASS, s3ObjectMetadata.getStorageClass().toString())
.lastModified(s3ObjectMetadata.getLastModified())
.contentLength(Long.parseLong(s3ObjectMetadata.getSize()))
.contentType(mediaTypeFrom(s3ObjectMetadata.getContentType()))
.build();
} else {
return ResponseEntity.status(NOT_FOUND).build();
}
}
/**
* The DELETE operation removes an object.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObject.html">API Reference</a>
*
* @param bucketName name of bucket containing the object.
* @return ResponseEntity with Status Code 204 if object was successfully deleted.
*/
@DeleteMapping(
value = "/{bucketName:.+}/{key:.*}",
params = {
NOT_LIFECYCLE
}
)
public ResponseEntity<Void> deleteObject(@PathVariable String bucketName,
@PathVariable ObjectKey key) {
bucketService.verifyBucketExists(bucketName);
var deleted = objectService.deleteObject(bucketName, key.getKey());
//return version id
return ResponseEntity.noContent()
.header(X_AMZ_DELETE_MARKER, String.valueOf(deleted))
.build();
}
/**
* Returns the File identified by bucketName and fileName.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObject.html">API Reference</a>
*
* @param bucketName The Bucket's name
* @param range byte range
*/
@GetMapping(
value = "/{bucketName:.+}/{key:.*}",
params = {
NOT_UPLOADS,
NOT_UPLOAD_ID,
NOT_TAGGING,
NOT_LEGAL_HOLD,
NOT_RETENTION,
NOT_ACL,
NOT_ATTRIBUTES
}
)
public ResponseEntity<StreamingResponseBody> getObject(@PathVariable String bucketName,
@PathVariable ObjectKey key,
@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) {
//TODO: needs modified-since handling, see API
bucketService.verifyBucketExists(bucketName);
var s3ObjectMetadata = objectService.verifyObjectExists(bucketName, key.getKey());
objectService.verifyObjectMatching(match, noneMatch, s3ObjectMetadata);
if (range != null) {
return getObjectWithRange(range, s3ObjectMetadata);
}
//return version id
return ResponseEntity
.ok()
.eTag(s3ObjectMetadata.getEtag())
.header(HttpHeaders.ACCEPT_RANGES, RANGES_BYTES)
.headers(headers -> headers.setAll(s3ObjectMetadata.getStoreHeaders()))
.headers(headers -> headers.setAll(userMetadataHeadersFrom(s3ObjectMetadata)))
.headers(headers -> headers.setAll(s3ObjectMetadata.getEncryptionHeaders()))
.headers(h -> h.setAll(checksumHeaderFrom(s3ObjectMetadata)))
.header(X_AMZ_STORAGE_CLASS, s3ObjectMetadata.getStorageClass().toString())
.lastModified(s3ObjectMetadata.getLastModified())
.contentLength(Long.parseLong(s3ObjectMetadata.getSize()))
.contentType(mediaTypeFrom(s3ObjectMetadata.getContentType()))
.headers(headers -> headers.setAll(overrideHeadersFrom(queryParams)))
.body(outputStream -> Files.copy(s3ObjectMetadata.getDataPath(), outputStream));
}
/**
* Adds an ACL to an object.
* This method accepts a String instead of the POJO. We need to use JAX-B annotations
* instead of Jackson annotations because AWS decided to use xsi:type annotations in the XML
* representation, which are not supported by Jackson.
* It doesn't seem to be possible to use bot JAX-B and Jackson for (de-)serialization in parallel.
* :-(
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObjectAcl.html">API Reference</a>
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html">API Reference</a>
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html#canned-acl">API Reference</a>
*
* @param bucketName the Bucket in which to store the file in.
* @return {@link ResponseEntity} with Status Code and empty ETag.
*/
@PutMapping(
value = "/{bucketName:.+}/{key:.*}",
params = {
ACL,
},
consumes = APPLICATION_XML_VALUE
)
public ResponseEntity<Void> putObjectAcl(@PathVariable final String bucketName,
@PathVariable ObjectKey key,
@RequestHeader(value = X_AMZ_ACL, required = false) ObjectCannedACL cannedAcl,
@RequestBody(required = false) String body) throws XMLStreamException, JAXBException {
bucketService.verifyBucketExists(bucketName);
objectService.verifyObjectExists(bucketName, key.getKey());
AccessControlPolicy policy;
if (body != null) {
policy = XmlUtil.deserializeJaxb(body);
} else if (cannedAcl != null) {
policy = CannedAclUtil.policyForCannedAcl(cannedAcl);
} else {
return ResponseEntity.badRequest().build();
}
objectService.setAcl(bucketName, key.getKey(), policy);
return ResponseEntity
.ok()
.build();
}
/**
* Gets ACL of an object.
* This method returns a String instead of the POJO. We need to use JAX-B annotations
* instead of Jackson annotations because AWS decided to use xsi:type annotations in the XML
* representation, which are not supported by Jackson.
* It doesn't seem to be possible to use bot JAX-B and Jackson for (de-)serialization in parallel.
* :-(
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObjectAcl.html">API Reference</a>
*
* @param bucketName the Bucket in which to store the file in.
* @return {@link ResponseEntity} with Status Code and empty ETag.
*/
@GetMapping(
value = "/{bucketName:.+}/{key:.*}",
params = {
ACL,
},
produces = APPLICATION_XML_VALUE
)
public ResponseEntity<String> getObjectAcl(@PathVariable final String bucketName,
@PathVariable ObjectKey key) throws JAXBException {
bucketService.verifyBucketExists(bucketName);
objectService.verifyObjectExists(bucketName, key.getKey());
var acl = objectService.getAcl(bucketName, key.getKey());
return ResponseEntity.ok(XmlUtil.serializeJaxb(acl));
}
/**
* Returns the tags identified by bucketName and fileName.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObjectTagging.html">API Reference</a>
*
* @param bucketName The Bucket's name
*/
@GetMapping(
value = "/{bucketName:.+}/{key:.*}",
params = {
TAGGING
},
produces = {
APPLICATION_XML_VALUE,
APPLICATION_XML_VALUE + ";charset=UTF-8"
}
)
public ResponseEntity<Tagging> getObjectTagging(@PathVariable String bucketName,
@PathVariable ObjectKey key) {
bucketService.verifyBucketExists(bucketName);
var s3ObjectMetadata = objectService.verifyObjectExists(bucketName, key.getKey());
//return version id
return ResponseEntity
.ok()
.eTag(s3ObjectMetadata.getEtag())
.lastModified(s3ObjectMetadata.getLastModified())
.body(new Tagging(new TagSet(s3ObjectMetadata.getTags())));
}
/**
* Sets tags for a file identified by bucketName and fileName.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObjectTagging.html">API Reference</a>
*
* @param bucketName The Bucket's name
* @param body Tagging object
*/
@PutMapping(
value = "/{bucketName:.+}/{key:.*}",
params = {
TAGGING
},
consumes = APPLICATION_XML_VALUE
)
public ResponseEntity<Void> putObjectTagging(@PathVariable String bucketName,
@PathVariable ObjectKey key,
@RequestBody Tagging body) {
bucketService.verifyBucketExists(bucketName);
var s3ObjectMetadata = objectService.verifyObjectExists(bucketName, key.getKey());
objectService.setObjectTags(bucketName, key.getKey(), body.getTagSet().getTags());
return ResponseEntity
.ok()
.eTag(s3ObjectMetadata.getEtag())
.lastModified(s3ObjectMetadata.getLastModified())
.build();
}
/**
* Returns the legal hold for an object.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObjectLegalHold.html">API Reference</a>
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lock.html">API Reference</a>
*
* @param bucketName The Bucket's name
*/
@GetMapping(
value = "/{bucketName:.+}/{key:.*}",
params = {
LEGAL_HOLD
},
produces = APPLICATION_XML_VALUE
)
public ResponseEntity<LegalHold> getLegalHold(@PathVariable String bucketName,
@PathVariable ObjectKey key) {
bucketService.verifyBucketExists(bucketName);
bucketService.verifyBucketObjectLockEnabled(bucketName);
var s3ObjectMetadata = objectService.verifyObjectLockConfiguration(bucketName, key.getKey());
return ResponseEntity
.ok()
.body(s3ObjectMetadata.getLegalHold());
}
/**
* Sets legal hold for an object.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObjectLegalHold.html">API Reference</a>
*
* @param bucketName The Bucket's name
* @param body legal hold
*/
@PutMapping(
value = "/{bucketName:.+}/{key:.*}",
params = {
LEGAL_HOLD
},
consumes = APPLICATION_XML_VALUE
)
public ResponseEntity<Void> putLegalHold(@PathVariable String bucketName,
@PathVariable ObjectKey key,
@RequestBody LegalHold body) {
bucketService.verifyBucketExists(bucketName);
bucketService.verifyBucketObjectLockEnabled(bucketName);
objectService.verifyObjectExists(bucketName, key.getKey());
objectService.setLegalHold(bucketName, key.getKey(), body);
return ResponseEntity
.ok()
.build();
}
/**
* Returns the retention for an object.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObjectRetention.html">API Reference</a>
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lock.html">API Reference</a>
*
* @param bucketName The Bucket's name
*/
@GetMapping(
value = "/{bucketName:.+}/{key:.*}",
params = {
RETENTION
},
produces = APPLICATION_XML_VALUE
)
public ResponseEntity<Retention> getObjectRetention(@PathVariable String bucketName,
@PathVariable ObjectKey key) {
bucketService.verifyBucketExists(bucketName);
bucketService.verifyBucketObjectLockEnabled(bucketName);
var s3ObjectMetadata = objectService.verifyObjectLockConfiguration(bucketName, key.getKey());
return ResponseEntity
.ok()
.body(s3ObjectMetadata.getRetention());
}
/**
* Sets retention for an object.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObjectRetention.html">API Reference</a>
*
* @param bucketName The Bucket's name
* @param body retention
*/
@PutMapping(
value = "/{bucketName:.+}/{key:.*}",
params = {
RETENTION
},
consumes = APPLICATION_XML_VALUE
)
public ResponseEntity<Void> putObjectRetention(@PathVariable String bucketName,
@PathVariable ObjectKey key,
@RequestBody Retention body) {
bucketService.verifyBucketExists(bucketName);
bucketService.verifyBucketObjectLockEnabled(bucketName);
objectService.verifyObjectExists(bucketName, key.getKey());
objectService.verifyRetention(body);
objectService.setRetention(bucketName, key.getKey(), body);
return ResponseEntity
.ok()
.build();
}
/**
* Returns the attributes for an object.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObjectAttributes.html">API Reference</a>
*
* @param bucketName The Bucket's name
*/
@GetMapping(
value = "/{bucketName:[a-z0-9.-]+}/{key:.*}",
params = {
ATTRIBUTES
},
produces = APPLICATION_XML_VALUE
)
public ResponseEntity<GetObjectAttributesOutput> getObjectAttributes(
@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 = X_AMZ_OBJECT_ATTRIBUTES) List<String> objectAttributes) {
//TODO: needs modified-since handling, see API
bucketService.verifyBucketExists(bucketName);
//this is for either an object request, or a parts request.
S3ObjectMetadata s3ObjectMetadata = objectService.verifyObjectExists(bucketName, key.getKey());
objectService.verifyObjectMatching(match, noneMatch, s3ObjectMetadata);
GetObjectAttributesOutput response = new GetObjectAttributesOutput(
ObjectService.getChecksum(s3ObjectMetadata),
objectAttributes.contains(ObjectAttributes.ETAG.toString())
? s3ObjectMetadata.getEtag()
: null,
null, //parts not supported right now
objectAttributes.contains(ObjectAttributes.OBJECT_SIZE.toString())
? Long.parseLong(s3ObjectMetadata.getSize())
: null,
objectAttributes.contains(ObjectAttributes.STORAGE_CLASS.toString())
? s3ObjectMetadata.getStorageClass()
: null
);
//return version id
return ResponseEntity
.ok()
.lastModified(s3ObjectMetadata.getLastModified())
.body(response);
}
/**
* Adds an object to a bucket.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObject.html">API Reference</a>
*
* @param bucketName the Bucket in which to store the file in.
* @return {@link ResponseEntity} with Status Code and empty ETag.
*/
@PutMapping(
value = "/{bucketName:.+}/{key:.*}",
params = {
NOT_UPLOAD_ID,
NOT_TAGGING,
NOT_LEGAL_HOLD,
NOT_RETENTION,
NOT_ACL
},
headers = {
NOT_X_AMZ_COPY_SOURCE
}
)
public ResponseEntity<Void> putObject(@PathVariable String bucketName,
@PathVariable ObjectKey key,
@RequestHeader(name = X_AMZ_TAGGING, required = false) List<Tag> tags,
@RequestHeader(value = CONTENT_TYPE, required = false) String contentType,
@RequestHeader(value = CONTENT_MD5, required = false) String contentMd5,
@RequestHeader(value = X_AMZ_CONTENT_SHA256, required = false) String sha256Header,
@RequestHeader(value = X_AMZ_STORAGE_CLASS, required = false, defaultValue = "STANDARD")
StorageClass storageClass,
@RequestHeader HttpHeaders httpHeaders,
InputStream inputStream) {
bucketService.verifyBucketExists(bucketName);
var stream = objectService.verifyMd5(inputStream, contentMd5, sha256Header);
//TODO: need to extract owner from headers
var owner = Owner.DEFAULT_OWNER;
var s3ObjectMetadata =
objectService.putS3Object(bucketName,
key.getKey(),
mediaTypeFrom(contentType).toString(),
storeHeadersFrom(httpHeaders),
stream,
isV4ChunkedWithSigningEnabled(sha256Header),
userMetadataFrom(httpHeaders),
encryptionHeadersFrom(httpHeaders),
tags,
checksumAlgorithmFrom(httpHeaders),
checksumFrom(httpHeaders),
owner,
storageClass);
//return version id
return ResponseEntity
.ok()
.headers(h -> h.setAll(checksumHeaderFrom(s3ObjectMetadata)))
.headers(h -> h.setAll(s3ObjectMetadata.getEncryptionHeaders()))
.lastModified(s3ObjectMetadata.getLastModified())
.eTag(s3ObjectMetadata.getEtag())
.build();
}
/**
* Copies an object to another bucket.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_CopyObject.html">API Reference</a>
*
* @param bucketName name of the destination bucket
* @param copySource path to source object
* @return {@link CopyObjectResult}
*/
@PutMapping(
value = "/{bucketName:.+}/{key:.*}",
headers = {
X_AMZ_COPY_SOURCE
},
params = {
NOT_UPLOAD_ID,
NOT_TAGGING,
NOT_LEGAL_HOLD,
NOT_RETENTION,
NOT_ACL
},
produces = APPLICATION_XML_VALUE
)
public ResponseEntity<CopyObjectResult> copyObject(@PathVariable String bucketName,
@PathVariable ObjectKey key,
@RequestHeader(value = X_AMZ_COPY_SOURCE) CopySource copySource,
@RequestHeader(value = X_AMZ_METADATA_DIRECTIVE,
defaultValue = METADATA_DIRECTIVE_COPY) MetadataDirective metadataDirective,
@RequestHeader(value = X_AMZ_COPY_SOURCE_IF_MATCH, required = false) List<String> match,
@RequestHeader(value = X_AMZ_COPY_SOURCE_IF_NONE_MATCH,
required = false) List<String> noneMatch,
@RequestHeader HttpHeaders httpHeaders) {
//TODO: needs modified-since handling, see API
bucketService.verifyBucketExists(bucketName);
var s3ObjectMetadata = objectService.verifyObjectExists(copySource.getBucket(), copySource.getKey());
objectService.verifyObjectMatchingForCopy(match, noneMatch, s3ObjectMetadata);
Map<String, String> metadata = Collections.emptyMap();
if (MetadataDirective.REPLACE == metadataDirective) {
metadata = userMetadataFrom(httpHeaders);
}
//TODO: this is potentially illegal on S3. S3 throws a 400:
// "This copy request is illegal because it is trying to copy an object to itself without
// changing the object's metadata, storage class, website redirect location or encryption
// attributes."
var copyObjectResult = objectService.copyS3Object(copySource.getBucket(),
copySource.getKey(),
bucketName,
key.getKey(),
encryptionHeadersFrom(httpHeaders),
metadata);
//return version id / copy source version id
//return expiration
if (copyObjectResult == null) {
return ResponseEntity
.notFound()
.headers(headers -> headers.setAll(s3ObjectMetadata.getEncryptionHeaders()))
.build();
}
return ResponseEntity
.ok()
.headers(headers -> headers.setAll(s3ObjectMetadata.getEncryptionHeaders()))
.body(copyObjectResult);
}
/**
* supports range different range ends. e.g. if content has 100 bytes, the range request could be:
* bytes=10-100, 10--1 and 10-200
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObject.html">API Reference</a>
*
* @param range {@link String}
* @param s3ObjectMetadata {@link S3ObjectMetadata}
*/
private ResponseEntity<StreamingResponseBody> getObjectWithRange(HttpRange range,
S3ObjectMetadata s3ObjectMetadata) {
var fileSize = s3ObjectMetadata.getDataPath().toFile().length();
var bytesToRead = Math.min(fileSize - 1, range.getRangeEnd(fileSize))
- range.getRangeStart(fileSize) + 1;
if (bytesToRead < 0 || fileSize < range.getRangeStart(fileSize)) {
return ResponseEntity.status(REQUESTED_RANGE_NOT_SATISFIABLE.value()).build();
}
return ResponseEntity
.status(PARTIAL_CONTENT.value())
.headers(headers -> headers.setAll(userMetadataHeadersFrom(s3ObjectMetadata)))
.headers(headers -> headers.setAll(s3ObjectMetadata.getStoreHeaders()))
.headers(headers -> headers.setAll(s3ObjectMetadata.getEncryptionHeaders()))
.header(HttpHeaders.ACCEPT_RANGES, RANGES_BYTES)
.header(HttpHeaders.CONTENT_RANGE,
String.format("bytes %s-%s/%s",
range.getRangeStart(fileSize), bytesToRead + range.getRangeStart(fileSize) - 1,
s3ObjectMetadata.getSize()))
.eTag(s3ObjectMetadata.getEtag())
.contentType(mediaTypeFrom(s3ObjectMetadata.getContentType()))
.lastModified(s3ObjectMetadata.getLastModified())
.contentLength(bytesToRead)
.body(outputStream ->
extractBytesToOutputStream(range, s3ObjectMetadata, outputStream, fileSize, bytesToRead)
);
}
private static void extractBytesToOutputStream(HttpRange range, S3ObjectMetadata s3ObjectMetadata,
OutputStream outputStream, long fileSize, long bytesToRead) throws IOException {
try (var fis = Files.newInputStream(s3ObjectMetadata.getDataPath())) {
var skip = fis.skip(range.getRangeStart(fileSize));
if (skip == range.getRangeStart(fileSize)) {
IOUtils.copy(new BoundedInputStream(fis, bytesToRead), outputStream);
} else {
throw new IllegalStateException("Could not skip exact byte range");
}
}
}
}

View File

@ -0,0 +1,132 @@
/*
* Copyright 2017-2023 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 cc.iotkit.s3mock;
import static org.springframework.http.HttpStatus.BAD_REQUEST;
import static org.springframework.http.HttpStatus.CONFLICT;
import static org.springframework.http.HttpStatus.NOT_FOUND;
import cc.iotkit.s3mock.dto.ErrorResponse;
import org.springframework.http.HttpStatus;
/**
* {@link RuntimeException} to communicate general S3 errors.
* These are handled by {@link S3MockConfiguration.S3MockExceptionHandler},
* mapped to {@link ErrorResponse} and serialized.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html">API Reference</a>
*/
public class S3Exception extends RuntimeException {
private static final String INVALID_REQUEST = "InvalidRequest";
public static final S3Exception INVALID_PART_NUMBER =
new S3Exception(BAD_REQUEST.value(), "InvalidArgument",
"Part number must be an integer between 1 and 10000, inclusive");
public static final S3Exception INVALID_PART = new S3Exception(BAD_REQUEST.value(), "InvalidPart",
"One or more of the specified parts could not be found. The part might not have been "
+ "uploaded, or the specified entity tagSet might not have matched the part's entity"
+ " tagSet.");
public static final S3Exception INVALID_PART_ORDER =
new S3Exception(BAD_REQUEST.value(), "InvalidPartOrder",
"The list of parts was not in ascending order. The parts list must be specified in "
+ "order by part number.");
public static final S3Exception NO_SUCH_UPLOAD_MULTIPART =
new S3Exception(NOT_FOUND.value(), "NoSuchUpload",
"The specified multipart upload does not exist. The upload ID might be invalid, or the "
+ "multipart upload might have been aborted or completed.");
public static final S3Exception ENTITY_TOO_SMALL =
new S3Exception(BAD_REQUEST.value(), "EntityTooSmall",
"Your proposed upload is smaller than the minimum allowed object size. "
+ "Each part must be at least 5 MB in size, except the last part.");
public static final S3Exception NO_SUCH_BUCKET =
new S3Exception(NOT_FOUND.value(), "NoSuchBucket",
"The specified bucket does not exist.");
public static final S3Exception NO_SUCH_LIFECYCLE_CONFIGURATION =
new S3Exception(NOT_FOUND.value(), "NoSuchLifecycleConfiguration",
"The lifecycle configuration does not exist.");
public static final S3Exception NO_SUCH_KEY =
new S3Exception(NOT_FOUND.value(), "NoSuchKey", "The specified key does not exist.");
public static final S3Exception NOT_MODIFIED =
new S3Exception(HttpStatus.NOT_MODIFIED.value(), "NotModified", "Not Modified");
public static final S3Exception PRECONDITION_FAILED =
new S3Exception(HttpStatus.PRECONDITION_FAILED.value(),
"PreconditionFailed", "At least one of the pre-conditions you specified did not hold");
public static final S3Exception BUCKET_NOT_EMPTY =
new S3Exception(CONFLICT.value(), "BucketNotEmpty",
"The bucket you tried to delete is not empty.");
public static final S3Exception INVALID_BUCKET_NAME =
new S3Exception(BAD_REQUEST.value(), "InvalidBucketName",
"The specified bucket is not valid.");
public static final S3Exception BUCKET_ALREADY_EXISTS =
new S3Exception(CONFLICT.value(), "BucketAlreadyExists",
"The requested bucket name is not available. "
+ "The bucket namespace is shared by all users of the system. "
+ "Please select a different name and try again.");
public static final S3Exception BUCKET_ALREADY_OWNED_BY_YOU =
new S3Exception(CONFLICT.value(), "BucketAlreadyOwnedByYou",
"Your previous request to create the named bucket succeeded and you already own it.");
public static final S3Exception NOT_FOUND_BUCKET_OBJECT_LOCK =
new S3Exception(BAD_REQUEST.value(), INVALID_REQUEST,
"Bucket is missing Object Lock Configuration");
public static final S3Exception NOT_FOUND_OBJECT_LOCK =
new S3Exception(NOT_FOUND.value(), "NotFound",
"The specified object does not have a ObjectLock configuration");
public static final S3Exception INVALID_REQUEST_RETAINDATE =
new S3Exception(BAD_REQUEST.value(), INVALID_REQUEST,
"The retain until date must be in the future!");
public static final S3Exception INVALID_REQUEST_MAXKEYS =
new S3Exception(BAD_REQUEST.value(), INVALID_REQUEST,
"maxKeys should be non-negative");
public static final S3Exception INVALID_REQUEST_ENCODINGTYPE =
new S3Exception(BAD_REQUEST.value(), INVALID_REQUEST,
"encodingtype can only be none or 'url'");
public static final S3Exception BAD_REQUEST_MD5 =
new S3Exception(BAD_REQUEST.value(), "BadRequest",
"Content-MD5 does not match object md5");
public static final S3Exception BAD_REQUEST_CONTENT =
new S3Exception(BAD_REQUEST.value(), "UnexpectedContent",
"This request contains unsupported content.");
private final int status;
private final String code;
private final String message;
/**
* Creates a new S3Exception to be mapped as an {@link ErrorResponse}.
*
* @param status The Error Status.
* @param code The Error Code.
* @param message The Error Message.
*/
public S3Exception(final int status, final String code, final String message) {
super(message);
this.status = status;
this.code = code;
this.message = message;
}
public int getStatus() {
return status;
}
public String getCode() {
return code;
}
@Override
public String getMessage() {
return message;
}
}

View File

@ -0,0 +1,42 @@
/*
* Copyright 2017-2023 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 cc.iotkit.s3mock;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
/**
* File Store Application that mocks Amazon S3.
*/
@SpringBootApplication
@ComponentScan(excludeFilters = {
/*
* TypeFilter to exclude classes with annotations that inherit {@link Component} to be
* instantiated automatically by Spring so that we can still manually instantiate them through
* a @Configuration class.
*/
@ComponentScan.Filter(type = FilterType.REGEX, pattern = ".*s3mock.*Controller")
})
public class S3MockApplication {
public static void main(final String[] args) {
SpringApplication.run(S3MockApplication.class, args);
}
}

View File

@ -0,0 +1,223 @@
/*
* 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 cc.iotkit.s3mock;
import cc.iotkit.s3mock.dto.ErrorResponse;
import cc.iotkit.s3mock.service.BucketService;
import cc.iotkit.s3mock.service.MultipartService;
import cc.iotkit.s3mock.service.ObjectService;
import cc.iotkit.s3mock.store.KmsKeyStore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.filter.OrderedFormContentFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
import org.springframework.lang.NonNull;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.filter.CommonsRequestLoggingFilter;
import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import javax.servlet.Filter;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;
@Configuration
@EnableConfigurationProperties(S3MockProperties.class)
public class S3MockConfiguration implements WebMvcConfigurer {
@Bean
Filter kmsFilter(final KmsKeyStore kmsKeyStore,
MappingJackson2XmlHttpMessageConverter messageConverter) {
return new KmsValidationFilter(kmsKeyStore, messageConverter);
}
@Override
public void configureContentNegotiation(final ContentNegotiationConfigurer configurer) {
configurer
.defaultContentType(MediaType.APPLICATION_JSON, MediaType.APPLICATION_FORM_URLENCODED, MediaType.APPLICATION_XML);
configurer.mediaType("json", MediaType.APPLICATION_JSON);
configurer.mediaType("xml", MediaType.TEXT_XML);
}
@Bean
@Profile("debug")
public CommonsRequestLoggingFilter logFilter() {
var filter = new CommonsRequestLoggingFilter();
filter.setIncludeQueryString(true);
filter.setIncludePayload(true);
filter.setMaxPayloadLength(10000);
filter.setIncludeHeaders(true);
filter.setAfterMessagePrefix("REQUEST DATA : ");
return filter;
}
/**
* Creates an HttpMessageConverter for XML.
*
* @return The configured {@link MappingJackson2XmlHttpMessageConverter}.
*/
@Bean
MappingJackson2XmlHttpMessageConverter messageConverter() {
var mediaTypes = new ArrayList<MediaType>();
mediaTypes.add(MediaType.APPLICATION_XML);
mediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
mediaTypes.add(MediaType.APPLICATION_OCTET_STREAM);
var xmlConverter = new MappingJackson2XmlHttpMessageConverter();
xmlConverter.setSupportedMediaTypes(mediaTypes);
return xmlConverter;
}
@Bean
OrderedFormContentFilter httpPutFormContentFilter() {
return new OrderedFormContentFilter() {
@Override
protected boolean shouldNotFilter(@NonNull HttpServletRequest request) {
return true;
}
};
}
@Bean
FaviconController faviconController() {
return new FaviconController();
}
@Bean
ObjectController fileStoreController(ObjectService objectService, BucketService bucketService) {
return new ObjectController(bucketService, objectService);
}
@Bean
BucketController bucketController(BucketService bucketService, S3MockProperties properties) {
return new BucketController(bucketService, properties.getRegion());
}
@Bean
MultipartController multipartController(BucketService bucketService,
ObjectService objectService, MultipartService multipartService) {
return new MultipartController(bucketService, objectService, multipartService);
}
@Bean
S3MockExceptionHandler s3MockExceptionHandler() {
return new S3MockExceptionHandler();
}
@Bean
IllegalStateExceptionHandler illegalStateExceptionHandler() {
return new IllegalStateExceptionHandler();
}
@Bean
ObjectCannedAclHeaderConverter objectCannedAclHeaderConverter() {
return new ObjectCannedAclHeaderConverter();
}
@Bean
TaggingHeaderConverter taggingHeaderConverter() {
return new TaggingHeaderConverter();
}
@Bean
HttpRangeHeaderConverter httpRangeHeaderConverter() {
return new HttpRangeHeaderConverter();
}
/**
* {@link ResponseEntityExceptionHandler} dealing with {@link S3Exception}s; Serializes them to
* response output as suitable ErrorResponses.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html">API Reference</a>
*/
@ControllerAdvice(basePackages = "cc.iotkit.s3mock")
static class S3MockExceptionHandler extends ResponseEntityExceptionHandler {
private static final Logger LOG = LoggerFactory.getLogger(S3MockExceptionHandler.class);
/**
* Handles the given {@link S3Exception}.
*
* @param s3Exception {@link S3Exception} to be handled.
* @return A {@link ResponseEntity} representing the handled {@link S3Exception}.
*/
@ExceptionHandler(S3Exception.class)
public ResponseEntity<ErrorResponse> handleS3Exception(final S3Exception s3Exception) {
LOG.debug("Responding with status {}: {}", s3Exception.getStatus(), s3Exception.getMessage(),
s3Exception);
var errorResponse = new ErrorResponse(
s3Exception.getCode(),
s3Exception.getMessage(),
null,
null
);
var headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_XML);
return ResponseEntity.status(s3Exception.getStatus()).headers(headers).body(errorResponse);
}
}
/**
* {@link ResponseEntityExceptionHandler} dealing with {@link IllegalStateException}s.
* Serializes them to response output as a 500 Internal Server Error {@link ErrorResponse}.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html">API Reference</a>
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_Error.html">API Reference</a>
*/
@ControllerAdvice(basePackages = "cc.iotkit.s3mock")
static class IllegalStateExceptionHandler extends ResponseEntityExceptionHandler {
private static final Logger LOG = LoggerFactory.getLogger(IllegalStateExceptionHandler.class);
/**
* Handles the given {@link IllegalStateException}.
*
* @param exception {@link IllegalStateException} to be handled.
* @return A {@link ResponseEntity} representing the handled {@link IllegalStateException}.
*/
@ExceptionHandler(IllegalStateException.class)
public ResponseEntity<ErrorResponse> handleS3Exception(IllegalStateException exception) {
LOG.debug("Responding with status {}: {}", INTERNAL_SERVER_ERROR, exception.getMessage(),
exception);
var errorResponse = new ErrorResponse(
"InternalError",
"We encountered an internal error. Please try again.",
null,
null
);
var headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_XML);
return ResponseEntity.internalServerError().headers(headers).body(errorResponse);
}
}
}

View File

@ -0,0 +1,40 @@
/*
* Copyright 2017-2022 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 cc.iotkit.s3mock;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import software.amazon.awssdk.regions.Region;
@ConfigurationProperties("s3mock")
@Data
public class S3MockProperties {
// Property name for passing the HTTPS port to use. Defaults to
// {@value S3MockApplication#DEFAULT_HTTPS_PORT}. If set to
// {@value S3MockApplication#RANDOM_PORT}, a random port will be chosen.
int httpPort;
// Property name for passing the global context path to use.
// Defaults to "".
// For example if set to `s3-mock` all endpoints will be available at
// `http://host:port/s3-mock` instead of `http://host:port/`
String contextPath="";
// Region is S3Mock is supposed to mock.
// Must be an official AWS region string like "us-east-1"
Region region;
}

View File

@ -0,0 +1,45 @@
/*
* Copyright 2017-2023 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 cc.iotkit.s3mock;
import cc.iotkit.s3mock.dto.Tag;
import cc.iotkit.s3mock.util.AwsHttpHeaders;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.convert.converter.Converter;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
/**
* Converts values of the {@link AwsHttpHeaders#X_AMZ_TAGGING} which is sent by the Amazon client.
* Example: x-amz-tagging: tag1=value1&tag2=value2
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-tagging.html">API Reference</a>
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObject.html">API Reference</a>
*/
class TaggingHeaderConverter implements Converter<String, List<Tag>> {
@Override
@Nullable
public List<Tag> convert(@NonNull String source) {
var tags = new ArrayList<Tag>();
String[] tagPairs = StringUtils.split(source, '&');
for (String tag : tagPairs) {
tags.add(new Tag(tag));
}
return tags.isEmpty() ? null : tags;
}
}

View File

@ -0,0 +1,32 @@
/*
* Copyright 2017-2022 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 cc.iotkit.s3mock.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonRootName;
import lombok.Data;
/**
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_AbortIncompleteMultipartUpload.html">API Reference</a>.
*/
@Data
@JsonRootName("ListPartsResult")
public class AbortIncompleteMultipartUpload {
@JsonProperty("DaysAfterInitiation")
private Integer daysAfterInitiation;
}

View File

@ -0,0 +1,85 @@
/*
* Copyright 2017-2023 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 cc.iotkit.s3mock.dto;
import jakarta.xml.bind.annotation.XmlAccessType;
import jakarta.xml.bind.annotation.XmlAccessorType;
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlElementWrapper;
import jakarta.xml.bind.annotation.XmlRootElement;
import java.util.List;
import java.util.Objects;
/**
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_AccessControlPolicy.html">API Reference</a>.
* This POJO uses JAX-B annotations instead of Jackson annotations because AWS decided to use
* xsi:type annotations in the XML representation, which are not supported by Jackson.
* JAX-B currently does not support Java records, see https://github.com/jakartaee/jaxb-api/issues/183
*/
@XmlRootElement(name = "AccessControlPolicy")
@XmlAccessorType(XmlAccessType.FIELD)
public class AccessControlPolicy {
@XmlElement(name = "Owner")
private Owner owner;
@XmlElement(name = "Grant")
@XmlElementWrapper(name = "AccessControlList")
private List<Grant> accessControlList;
public AccessControlPolicy() {
// Jackson needs the default constructor for deserialization.
}
public AccessControlPolicy(Owner owner, List<Grant> accessControlList) {
this.owner = owner;
this.accessControlList = accessControlList;
}
public Owner getOwner() {
return owner;
}
public void setOwner(Owner owner) {
this.owner = owner;
}
public List<Grant> getAccessControlList() {
return accessControlList;
}
public void setAccessControlList(List<Grant> accessControlList) {
this.accessControlList = accessControlList;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
AccessControlPolicy policy = (AccessControlPolicy) o;
return Objects.equals(owner, policy.owner) && Objects.equals(
accessControlList, policy.accessControlList);
}
@Override
public int hashCode() {
return Objects.hash(owner, accessControlList);
}
}

View File

@ -0,0 +1,52 @@
/*
* Copyright 2017-2022 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 cc.iotkit.s3mock.dto;
import cc.iotkit.s3mock.store.BucketMetadata;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonRootName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.nio.file.Path;
/**
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_Bucket.html">API Reference</a>.
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonRootName("Bucket")
public class Bucket {
@JsonIgnore
private Path path;
@JsonProperty("Name")
private String name;
@JsonProperty("CreationDate")
private String creationDate;
public static Bucket from(BucketMetadata bucketMetadata) {
if (bucketMetadata == null) {
return null;
}
return new Bucket(bucketMetadata.getPath(),
bucketMetadata.getName(),
bucketMetadata.getCreationDate());
}
}

View File

@ -0,0 +1,39 @@
/*
* Copyright 2017-2023 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 cc.iotkit.s3mock.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonRootName;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import lombok.Data;
import java.util.List;
/**
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_BucketLifecycleConfiguration.html">API Reference</a>.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_LifecycleConfiguration.html">API Reference</a>.
*/
@Data
@JsonRootName("LifecycleConfiguration")
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class BucketLifecycleConfiguration {
@JsonProperty("Rule")
@JacksonXmlElementWrapper(useWrapping = false)
private List<LifecycleRule> rules;
}

View File

@ -0,0 +1,39 @@
/*
* Copyright 2017-2022 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 cc.iotkit.s3mock.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonRootName;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.List;
/**
* DTO representing a list of buckets.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_Bucket.html">API Reference</a>
*/
@Data
@AllArgsConstructor
@JsonRootName("Buckets")
public class Buckets {
@JsonProperty("Bucket")
@JacksonXmlElementWrapper(useWrapping = false)
private List<Bucket> buckets;
}

View File

@ -0,0 +1,42 @@
/*
* Copyright 2017-2023 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 cc.iotkit.s3mock.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonRootName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_Checksum.html">API Reference</a>.
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonRootName("Checksum")
public class Checksum {
@JsonProperty("ChecksumCRC32")
private String checksumCRC32;
@JsonProperty("ChecksumCRC32C")
private String checksumCRC32C;
@JsonProperty("ChecksumSHA1")
private String checksumSHA1;
@JsonProperty("ChecksumSHA256")
private String checksumSHA256;
}

View File

@ -0,0 +1,56 @@
/*
* Copyright 2017-2023 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 cc.iotkit.s3mock.dto;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
public enum ChecksumAlgorithm {
CRC32("CRC32"),
CRC32C("CRC32C"),
SHA1("SHA1"),
SHA256("SHA256");
private final String value;
@JsonCreator
ChecksumAlgorithm(String value) {
this.value = value;
}
public static ChecksumAlgorithm fromString(String value) {
value = value.toUpperCase();
switch (value) {
case "SHA256":
return SHA256;
case "SHA1":
return SHA1;
case "CRC32":
return CRC32;
case "CRC32C":
return CRC32C;
default:
return null;
}
}
@Override
@JsonValue
public String toString() {
return String.valueOf(this.value);
}
}

View File

@ -0,0 +1,40 @@
/*
* Copyright 2017-2022 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 cc.iotkit.s3mock.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonRootName;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import lombok.Data;
import java.util.List;
/**
* Request to complete multipart upload.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_CompleteMultipartUpload.html">API Reference</a>
*/
@Data
@JsonRootName("CompleteMultipartUpload")
public class CompleteMultipartUpload {
@JsonProperty("Part")
@JacksonXmlElementWrapper(useWrapping = false)
private List<CompletedPart> parts;
public void addPart(CompletedPart part) {
this.parts.add(part);
}
}

View File

@ -0,0 +1,46 @@
/*
* Copyright 2017-2022 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 cc.iotkit.s3mock.dto;
import static cc.iotkit.s3mock.util.EtagUtil.normalizeEtag;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonRootName;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* Result to be returned when completing a multipart request.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_CompleteMultipartUpload.html">API Reference</a>
*/
@Data
@AllArgsConstructor
@JsonRootName("CompleteMultipartUploadResult")
public class CompleteMultipartUploadResult {
@JsonProperty("Location")
private String location;
@JsonProperty("Bucket")
private String bucket;
@JsonProperty("Key")
private String key;
@JsonProperty("ETag")
private String etag;
public CompleteMultipartUploadResult() {
etag = normalizeEtag(etag);
}
}

View File

@ -0,0 +1,49 @@
/*
* Copyright 2017-2023 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 cc.iotkit.s3mock.dto;
import static cc.iotkit.s3mock.util.EtagUtil.normalizeEtag;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonRootName;
import lombok.Data;
/**
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_CompletedPart.html">API Reference</a>.
*/
@Data
@JsonRootName("CompletedPart")
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class CompletedPart {
@JsonProperty("PartNumber")
private Integer partNumber;
@JsonProperty("ETag")
private String etag;
@JsonProperty("ChecksumCRC32")
private String checksumCRC32;
@JsonProperty("ChecksumCRC32C")
private String checksumCRC32C;
@JsonProperty("ChecksumSHA1")
private String checksumSHA1;
@JsonProperty("ChecksumSHA256")
private String checksumSHA256;
public CompletedPart() {
etag = normalizeEtag(etag);
}
}

View File

@ -0,0 +1,41 @@
/*
* Copyright 2017-2022 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 cc.iotkit.s3mock.dto;
import static cc.iotkit.s3mock.util.EtagUtil.normalizeEtag;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonRootName;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_CopyObjectResult.html">API Reference</a>.
*/
@Data
@AllArgsConstructor
@JsonRootName("CopyObjectResult")
public class CopyObjectResult {
@JsonProperty("LastModified")
private String lastModified;
@JsonProperty("ETag")
private String etag;
public CopyObjectResult() {
etag = normalizeEtag(etag);
}
}

View File

@ -0,0 +1,49 @@
/*
* Copyright 2017-2022 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 cc.iotkit.s3mock.dto;
import static cc.iotkit.s3mock.util.EtagUtil.normalizeEtag;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonRootName;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.Date;
/**
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_CopyPartResult.html">API Reference</a>.
*/
@Data
@AllArgsConstructor
@JsonRootName("CopyPartResult")
public class CopyPartResult {
@JsonProperty("LastModified")
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone = "UTC")
private Date lastModified;
@JsonProperty("ETag")
private String etag;
public CopyPartResult() {
etag = normalizeEtag(etag);
}
public static CopyPartResult from(final Date date, final String etag) {
return new CopyPartResult(date, etag);
}
}

View File

@ -0,0 +1,71 @@
/*
* Copyright 2017-2022 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 cc.iotkit.s3mock.dto;
import static java.util.Objects.requireNonNull;
import lombok.Data;
import software.amazon.awssdk.utils.http.SdkHttpUtils;
/**
* Represents a S3 Object referenced by Bucket and Key.
*/
@Data
public class CopySource {
private String bucket;
private String key;
public static final String DELIMITER = "/";
/**
* Creates a {@link CopySource} expecting the given String represents the source as {@code
* /{bucket}/{key}}.
*
* @param copySource The object references.
* @throws IllegalArgumentException If {@code copySource} could not be parsed.
* @throws NullPointerException If {@code copySource} is null.
*/
public CopySource(String copySource) {
//inefficient duplicate parsing of incoming String, call to default constructor must be the
//first statement...
this.bucket = extractBucketAndKeyArray(SdkHttpUtils.urlDecode(copySource))[0];
this.key = extractBucketAndKeyArray(SdkHttpUtils.urlDecode(copySource))[1];
}
/**
* we need to decode here because Spring does not do the decoding for RequestHeaders as it does
* for path parameters.
*/
private static String[] extractBucketAndKeyArray(final String copySource) {
requireNonNull(copySource, "copySource == null");
final String source = normalizeCopySource(copySource);
final String[] bucketAndKey = source.split(DELIMITER, 2);
if (bucketAndKey.length != 2) {
throw new IllegalArgumentException(
"Expected a copySource as '/{bucket}/{key}' but got: " + copySource);
}
return bucketAndKey;
}
private static String normalizeCopySource(final String copySource) {
if (copySource.startsWith("/")) {
return copySource.substring(1);
}
return copySource;
}
}

View File

@ -0,0 +1,37 @@
/*
* Copyright 2017-2022 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 cc.iotkit.s3mock.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_DefaultRetention.html">API Reference</a>.
*/
@Data
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class DefaultRetention {
//TODO: setting days & years not allowed!
@JsonProperty("Days")
private Integer days;
@JsonProperty("Years")
private Integer years;
@JsonProperty("Mode")
private Mode mode;
}

View File

@ -0,0 +1,39 @@
/*
* Copyright 2017-2022 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 cc.iotkit.s3mock.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonRootName;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import lombok.Data;
import java.util.List;
/**
* Request to initiate a batch delete request.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObjects.html">API Reference</a>
*/
@Data
@JsonRootName("Delete")
public class Delete {
@JsonProperty("Quiet")
private boolean quiet;
@JsonProperty("Object")
@JacksonXmlElementWrapper(useWrapping = false)
private List<S3ObjectIdentifier> objectsToDelete;
}

View File

@ -0,0 +1,40 @@
/*
* Copyright 2017-2023 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 cc.iotkit.s3mock.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteMarkerEntry.html">API Reference</a>.
*/
@Data
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class DeleteMarkerEntry {
@JsonProperty("IsLatest")
private Boolean isLatest;
@JsonProperty("Key")
private String key;
@JsonProperty("LastModified")
private String lastModified;
@JsonProperty("Owner")
private Owner owner;
@JsonProperty("VersionId")
private String versionId;
}

View File

@ -0,0 +1,52 @@
/*
* Copyright 2017-2022 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 cc.iotkit.s3mock.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonRootName;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* Result to be returned after batch delete request.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObjects.html">API Reference</a>
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonRootName("DeleteResult")
public class DeleteResult {
@JsonProperty("Deleted")
@JacksonXmlElementWrapper(useWrapping = false)
private List<DeletedS3Object> deletedObjects;
@JsonProperty("Error")
@JacksonXmlElementWrapper(useWrapping = false)
private List<Error> errors;
public void addDeletedObject(DeletedS3Object deletedObject) {
deletedObjects.add(deletedObject);
}
public void addError(Error error) {
errors.add(error);
}
}

View File

@ -0,0 +1,50 @@
/*
* Copyright 2017-2022 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 cc.iotkit.s3mock.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeletedObject.html">API Reference</a>.
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class DeletedS3Object{
@JsonProperty("Key")
private String key;
@JsonProperty("VersionId")
private String versionId;
@JsonProperty("DeleteMarker")
private Boolean deleteMarker;
@JsonProperty("DeleteMarkerVersionId")
private String deleteMarkerVersionId;
public static DeletedS3Object from(S3ObjectIdentifier s3ObjectIdentifier) {
return new DeletedS3Object(
s3ObjectIdentifier.getKey(),
s3ObjectIdentifier.getVersionId(),
null,
null
);
}
}

View File

@ -0,0 +1,39 @@
/*
* Copyright 2017-2022 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 cc.iotkit.s3mock.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_Error.html">API Reference</a>.
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Error {
@JsonProperty("Code")
private String code;
@JsonProperty("Key")
private String key;
@JsonProperty("Message")
private String message;
@JsonProperty("VersionId")
private String versionId;
}

View File

@ -0,0 +1,43 @@
/*
* Copyright 2017-2023 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 cc.iotkit.s3mock.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonRootName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* A DTO which can be used as a response body if an error occurred.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html">API Reference</a>
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonRootName("Error")
public class ErrorResponse {
@JsonProperty("Code")
String code;
@JsonProperty("Message")
String message;
@JsonProperty("Resource")
String resource;
@JsonProperty("RequestId")
String requestId;
}

View File

@ -0,0 +1,59 @@
/*
* 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 cc.iotkit.s3mock.dto;
import static cc.iotkit.s3mock.util.EtagUtil.normalizeEtag;
import cc.iotkit.s3mock.store.S3ObjectMetadata;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonRootName;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.List;
@Data
@AllArgsConstructor
@JsonRootName("GetObjectAttributesOutput")
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class GetObjectAttributesOutput {
@JsonProperty("Checksum")
private Checksum checksum;
@JsonProperty("ETag")
private String etag;
@JsonProperty("ObjectParts")
@JacksonXmlElementWrapper(useWrapping = false)
private List<GetObjectAttributesParts> objectParts;
@JsonProperty("ObjectSize")
private Long objectSize;
@JsonProperty("StorageClass")
private StorageClass storageClass;
public GetObjectAttributesOutput() {
etag = normalizeEtag(etag);
}
GetObjectAttributesOutput from(S3ObjectMetadata metadata) {
return new GetObjectAttributesOutput(null,
metadata.getEtag(),
null,
Long.valueOf(metadata.getSize()),
metadata.getStorageClass());
}
}

View File

@ -0,0 +1,48 @@
/*
* Copyright 2017-2023 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 cc.iotkit.s3mock.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonRootName;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import lombok.Data;
import java.util.List;
/**
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObjectAttributesParts.html">API Reference</a>.
*/
@Data
@JsonRootName("GetObjectAttributesParts")
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class GetObjectAttributesParts {
@JsonProperty("MaxParts")
private int maxParts;
@JsonProperty("IsTruncated")
private boolean isTruncated;
@JsonProperty("NextPartNumberMarker")
private int nextPartNumberMarker;
@JsonProperty("PartNumberMarker")
private int partNumberMarker;
@JsonProperty("TotalPartsCount")
private int totalPartsCount;
@JsonProperty("Parts")
@JacksonXmlElementWrapper(useWrapping = false)
private List<ObjectPart> parts;
}

View File

@ -0,0 +1,102 @@
/*
* Copyright 2017-2022 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 cc.iotkit.s3mock.dto;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import jakarta.xml.bind.annotation.XmlAccessType;
import jakarta.xml.bind.annotation.XmlAccessorType;
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlRootElement;
import java.util.Objects;
/**
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_Grant.html">API Reference</a>.
*/
@XmlRootElement(name = "Grant")
@XmlAccessorType(XmlAccessType.FIELD)
public class Grant {
@XmlElement(name = "Grantee")
private Grantee grantee;
@XmlElement(name = "Permission")
private Permission permission;
public Grant() {
// Jackson needs the default constructor for deserialization.
}
public Grant(Grantee grantee, Permission permission) {
this.grantee = grantee;
this.permission = permission;
}
public Grantee getGrantee() {
return grantee;
}
public void setGrantee(Grantee grantee) {
this.grantee = grantee;
}
public Permission getPermission() {
return permission;
}
public void setPermission(Permission permission) {
this.permission = permission;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Grant grant = (Grant) o;
return Objects.equals(grantee, grant.grantee) && permission == grant.permission;
}
@Override
public int hashCode() {
return Objects.hash(grantee, permission);
}
public enum Permission {
FULL_CONTROL("FULL_CONTROL"),
WRITE("WRITE"),
WRITE_ACP("WRITE_ACP"),
READ("READ"),
READ_ACP("READ_ACP");
private final String value;
@JsonCreator
Permission(String value) {
this.value = value;
}
@Override
@JsonValue
public String toString() {
return value;
}
}
}

View File

@ -0,0 +1,124 @@
/*
* 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 cc.iotkit.s3mock.dto;
import jakarta.xml.bind.annotation.XmlAccessType;
import jakarta.xml.bind.annotation.XmlAccessorType;
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlRootElement;
import jakarta.xml.bind.annotation.XmlType;
import java.net.URI;
import java.util.Objects;
/**
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_Grantee.html">API Reference</a>.
*/
@XmlRootElement(name = "Grantee")
@XmlAccessorType(XmlAccessType.FIELD)
public abstract class Grantee extends Owner {
@XmlElement(name = "EmailAddress")
private String emailAddress;
@XmlElement(name = "URI")
private URI uri;
protected Grantee() {
// Jackson needs the default constructor for deserialization.
}
protected Grantee(String id, String displayName, String emailAddress, URI uri) {
super(id, displayName);
this.emailAddress = emailAddress;
this.uri = uri;
}
public static Grantee from(Owner owner) {
return new CanonicalUser(owner.getId(), owner.getDisplayName(), null, null);
}
public String getEmailAddress() {
return emailAddress;
}
public void setEmailAddress(String emailAddress) {
this.emailAddress = emailAddress;
}
public URI getUri() {
return uri;
}
public void setUri(URI uri) {
this.uri = uri;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
if (!super.equals(o)) {
return false;
}
Grantee grantee = (Grantee) o;
return Objects.equals(emailAddress, grantee.emailAddress) && Objects.equals(
uri, grantee.uri);
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), emailAddress, uri);
}
@XmlType(name = "CanonicalUser")
public static class CanonicalUser extends Grantee {
public CanonicalUser() {
}
public CanonicalUser(String id, String displayName, String emailAddress, URI uri) {
super(id, displayName, emailAddress, uri);
}
}
@XmlType(name = "Group")
public static class Group extends Grantee {
public Group() {
}
public Group(String id, String displayName, String emailAddress, URI uri) {
super(id, displayName, emailAddress, uri);
}
public static final URI AUTHENTICATED_USERS_URI = URI.create("http://acs.amazonaws.com/groups/global/AuthenticatedUsers");
public static final URI ALL_USERS_URI = URI.create("http://acs.amazonaws.com/groups/global/AllUsers");
public static final URI LOG_DELIVERY_URI = URI.create("http://acs.amazonaws.com/groups/s3/LogDelivery");
}
@XmlType(name = "AmazonCustomerByEmail")
public static class AmazonCustomerByEmail extends Grantee {
public AmazonCustomerByEmail() {
}
public AmazonCustomerByEmail(String id, String displayName, String emailAddress,
URI uri) {
super(id, displayName, emailAddress, uri);
}
}
}

View File

@ -0,0 +1,41 @@
/*
* Copyright 2017-2022 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 cc.iotkit.s3mock.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonRootName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Result to be returned after multipart upload initiation.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_CreateMultipartUpload.html">API Reference</a>
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonRootName("InitiateMultipartUploadResult")
public class InitiateMultipartUploadResult {
@JsonProperty("Bucket")
private String bucketName;
@JsonProperty("Key")
private String fileName;
@JsonProperty("UploadId")
private String uploadId;
}

View File

@ -0,0 +1,32 @@
/*
* Copyright 2017-2023 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 cc.iotkit.s3mock.dto;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import java.io.IOException;
import java.time.Instant;
import software.amazon.awssdk.utils.DateUtils;
public class InstantDeserializer extends JsonDeserializer<Instant> {
@Override
public Instant deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
var deserialized = p.readValueAs(String.class);
return DateUtils.parseIso8601Date(deserialized);
}
}

View File

@ -0,0 +1,33 @@
/*
* Copyright 2017-2022 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 cc.iotkit.s3mock.dto;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
import java.time.Instant;
import software.amazon.awssdk.utils.DateUtils;
public class InstantSerializer extends JsonSerializer<Instant> {
@Override
public void serialize(Instant value, JsonGenerator gen, SerializerProvider serializers)
throws IOException {
gen.writeString(DateUtils.formatIso8601Date(value));
}
}

View File

@ -0,0 +1,55 @@
/*
* Copyright 2017-2022 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 cc.iotkit.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 lombok.Data;
/**
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObjectLegalHold.html#API_PutObjectLegalHold_RequestSyntax">API Reference</a>.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lock.html">API Reference</a>.
*/
@Data
@JsonRootName("LegalHold")
public class LegalHold {
@JsonProperty("Status")
private Status status;
/**
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObjectLegalHold.html#API_PutObjectLegalHold_RequestSyntax">API Reference</a>.
*/
public enum Status {
ON("ON"),
OFF("OFF");
private final String value;
@JsonCreator
Status(String value) {
this.value = value;
}
@Override
@JsonValue
public String toString() {
return value;
}
}
}

View File

@ -0,0 +1,46 @@
/*
* Copyright 2017-2022 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 cc.iotkit.s3mock.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonRootName;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import lombok.Data;
import java.time.Instant;
/**
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_LifecycleExpiration.html">API Reference</a>.
*/
@Data
@JsonRootName("LifecycleExpiration")
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class LifecycleExpiration {
@JsonProperty("Date")
@JsonSerialize(using = InstantSerializer.class)
@JsonDeserialize(using = InstantDeserializer.class)
private Instant date;
@JsonProperty("Days")
private Integer days;
@JsonProperty("ExpiredObjectDeleteMarker")
private Boolean expiredObjectDeleteMarker;
}

View File

@ -0,0 +1,76 @@
/*
* Copyright 2017-2022 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 cc.iotkit.s3mock.dto;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonInclude;
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.JacksonXmlElementWrapper;
import lombok.Data;
import java.util.List;
/**
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_LifecycleRule.html">API Reference</a>.
*/
@Data
@JsonRootName("LifecycleRule")
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class LifecycleRule {
@JsonProperty("AbortIncompleteMultipartUpload")
private AbortIncompleteMultipartUpload abortIncompleteMultipartUpload;
@JsonProperty("Expiration")
private LifecycleExpiration expiration;
@JsonProperty("Filter")
private LifecycleRuleFilter filter;
@JsonProperty("ID")
private String id;
@JsonProperty("NoncurrentVersionExpiration")
@JacksonXmlElementWrapper(useWrapping = false)
private NoncurrentVersionExpiration noncurrentVersionExpiration;
@JsonProperty("NoncurrentVersionTransition")
@JacksonXmlElementWrapper(useWrapping = false)
private List<NoncurrentVersionTransition> noncurrentVersionTransitions;
@JsonProperty("Status")
private Status status;
@JsonProperty("Transition")
@JacksonXmlElementWrapper(useWrapping = false)
private List<Transition> transitions;
/**
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_LifecycleRule.html">API Reference</a>.
*/
public enum Status {
ENABLED("Enabled"),
DISABLED("Disabled");
private final String value;
@JsonCreator
Status(String value) {
this.value = value;
}
@Override
@JsonValue
public String toString() {
return value;
}
}
}

View File

@ -0,0 +1,44 @@
/*
* Copyright 2017-2022 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 cc.iotkit.s3mock.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonRootName;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import lombok.Data;
import java.util.List;
/**
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_LifecycleRuleAndOperator.html">API Reference</a>.
*/
@Data
@JsonRootName("LifecycleRuleAndOperator")
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class LifecycleRuleAndOperator {
@JsonProperty("ObjectSizeGreaterThan")
private Long objectSizeGreaterThan;
@JsonProperty("ObjectSizeLessThan")
private Long objectSizeLessThan;
@JsonProperty("Prefix")
private String prefix;
@JsonProperty("Tags")
@JacksonXmlElementWrapper(useWrapping = false)
private List<Tag> tags;
}

View File

@ -0,0 +1,46 @@
/*
* Copyright 2017-2022 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 cc.iotkit.s3mock.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonRootName;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import lombok.Data;
import java.util.List;
/**
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_LifecycleRuleFilter.html">API Reference</a>.
*/
@Data
@JsonRootName("LifecycleRuleFilter")
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class LifecycleRuleFilter {
@JsonProperty("ObjectSizeGreaterThan")
private Long objectSizeGreaterThan;
@JsonProperty("ObjectSizeLessThan")
private Long objectSizeLessThan;
@JsonProperty("Prefix")
private String prefix;
@JsonProperty("Tags")
@JacksonXmlElementWrapper(useWrapping = false)
private List<Tag> tags;
@JsonProperty("And")
private LifecycleRuleAndOperator and;
}

View File

@ -0,0 +1,39 @@
/*
* Copyright 2017-2022 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 cc.iotkit.s3mock.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonRootName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Represents a result of listing all Buckets.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListBuckets.html">API Reference</a>
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonRootName("ListAllMyBucketsResult")
public class ListAllMyBucketsResult {
@JsonProperty("Owner")
private Owner owner;
@JsonProperty("Buckets")
private Buckets buckets;
}

View File

@ -0,0 +1,60 @@
/*
* Copyright 2017-2022 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 cc.iotkit.s3mock.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonRootName;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* Represents a result of listing objects that reside in a Bucket.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjects.html">API Reference</a>
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonRootName("ListBucketResult")
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class ListBucketResult {
@JsonProperty("Name")
private String name;
@JsonProperty("Prefix")
private String prefix;
@JsonProperty("Marker")
private String marker;
@JsonProperty("MaxKeys")
private int maxKeys;
@JsonProperty("IsTruncated")
private boolean isTruncated;
@JsonProperty("EncodingType")
private String encodingType;
@JsonProperty("NextMarker")
private String nextMarker;
@JsonProperty("Contents")
@JacksonXmlElementWrapper(useWrapping = false)
private List<S3Object> contents;
@JsonProperty("CommonPrefixes")
@JacksonXmlElementWrapper(useWrapping = false)
private List<Prefix> commonPrefixes;
}

View File

@ -0,0 +1,64 @@
/*
* Copyright 2017-2022 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 cc.iotkit.s3mock.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonRootName;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* Represents a result of listing objects that reside in a Bucket.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectsV2.html">API Reference</a>
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonRootName("ListBucketResult")
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class ListBucketResultV2 {
@JsonProperty("Name")
private String name;
@JsonProperty("Prefix")
private String prefix;
@JsonProperty("MaxKeys")
private int maxKeys;
@JsonProperty("IsTruncated")
private boolean isTruncated;
@JsonProperty("Contents")
@JacksonXmlElementWrapper(useWrapping = false)
private List<S3Object> contents;
@JsonProperty("CommonPrefixes")
@JacksonXmlElementWrapper(useWrapping = false)
private List<Prefix> commonPrefixes;
@JsonProperty("ContinuationToken")
private String continuationToken;
@JsonProperty("KeyCount")
private String keyCount;
@JsonProperty("NextContinuationToken")
private String nextContinuationToken;
@JsonProperty("StartAfter")
private String startAfter;
@JsonProperty("EncodingType")
private String encodingType;
}

View File

@ -0,0 +1,60 @@
/*
* Copyright 2017-2022 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 cc.iotkit.s3mock.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonRootName;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.List;
/**
* List Multipart Uploads result.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListMultipartUploads.html">API Reference</a>
*/
@Data
@AllArgsConstructor
@JsonRootName("ListMultipartUploadsResult")
public class ListMultipartUploadsResult {
@JsonProperty("Bucket")
private String bucket;
@JsonProperty("KeyMarker")
private String keyMarker;
@JsonProperty("Delimiter")
private String delimiter;
@JsonProperty("Prefix")
private String prefix;
@JsonProperty("UploadIdMarker")
private String uploadIdMarker;
@JsonProperty("MaxUploads")
private int maxUploads;
@JsonProperty("IsTruncated")
private boolean isTruncated;
@JsonProperty("NextKeyMarker")
private String nextKeyMarker;
@JsonProperty("NextUploadIdMarker")
private String nextUploadIdMarker;
@JsonProperty("Upload")
@JacksonXmlElementWrapper(useWrapping = false)
private List<MultipartUpload> multipartUploads;
@JsonProperty("CommonPrefixes")
@JacksonXmlElementWrapper(useWrapping = false)
private List<Prefix> commonPrefixes;
}

View File

@ -0,0 +1,74 @@
/*
* Copyright 2017-2023 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 cc.iotkit.s3mock.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonRootName;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.List;
/**
* List-Parts result with some hard-coded values as this is sufficient for now.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListParts.html">API Reference</a>
*/
@Data
@AllArgsConstructor
@JsonRootName("ListPartsResult")
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class ListPartsResult {
@JsonProperty("Bucket")
private String bucket;
@JsonProperty("Key")
private String key;
@JsonProperty("UploadId")
private String uploadId;
@JsonProperty("PartNumberMarker")
private String partNumberMarker;
@JsonProperty("NextPartNumberMarker")
private String nextPartNumberMarker;
@JsonProperty("IsTruncated")
private boolean truncated;
@JsonProperty("StorageClass")
private StorageClass storageClass;
@JsonProperty("Part")
@JacksonXmlElementWrapper(useWrapping = false)
private List<Part> parts;
@JsonProperty("Owner")
private Owner owner;
@JsonProperty("Initiator")
private Owner initiator;
@JsonProperty("ChecksumAlgorithm")
private ChecksumAlgorithm checksumAlgorithm;
public ListPartsResult() {
this.partNumberMarker = this.partNumberMarker == null ? "0" : this.partNumberMarker;
this.nextPartNumberMarker = this.nextPartNumberMarker == null ? "1" : this.nextPartNumberMarker;
this.storageClass = this.storageClass == null ? StorageClass.STANDARD : this.storageClass;
}
public ListPartsResult(String bucketName,
String key,
String uploadId,
List<Part> parts) {
this(bucketName, key, uploadId, null, null, false, null, parts,
null, null, null);
}
}

View File

@ -0,0 +1,70 @@
/*
* Copyright 2017-2023 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 cc.iotkit.s3mock.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonRootName;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* Represents a result of listing object versions that reside in a Bucket.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectVersions.html">API Reference</a>
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonRootName("ListBucketResult")
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class ListVersionsResult {
@JsonProperty("Name")
private String name;
@JsonProperty("Prefix")
private String prefix;
@JsonProperty("MaxKeys")
private int maxKeys;
@JsonProperty("IsTruncated")
private boolean isTruncated;
@JsonProperty("CommonPrefixes")
@JacksonXmlElementWrapper(useWrapping = false)
private List<Prefix> commonPrefixes;
@JsonProperty("Delimiter")
private String delimiter;
@JsonProperty("EncodingType")
private String encodingType;
@JsonProperty("KeyMarker")
private String keyMarker;
@JsonProperty("VersionIdMarker")
private String versionIdMarker;
@JsonProperty("NextKeyMarker")
private String nextKeyMarker;
@JsonProperty("NextVersionIdMarker")
private String nextVersionIdMarker;
@JsonProperty("Version")
@JacksonXmlElementWrapper(useWrapping = false)
private List<ObjectVersion> objectVersions;
@JsonProperty("DeleteMarker")
@JacksonXmlElementWrapper(useWrapping = false)
private List<DeleteMarkerEntry> deleteMarkers;
}

View File

@ -0,0 +1,40 @@
/*
* Copyright 2017-2023 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 cc.iotkit.s3mock.dto;
import com.fasterxml.jackson.annotation.JsonRootName;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import software.amazon.awssdk.regions.Region;
/**
* Get Bucket location result.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketLocation.html">API Reference</a>
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonRootName("LocationConstraint")
public class LocationConstraint {
@JsonSerialize(using = RegionSerializer.class)
@JacksonXmlText
private Region region;
}

View File

@ -0,0 +1,42 @@
/*
* Copyright 2017-2022 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 cc.iotkit.s3mock.dto;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
/**
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_DefaultRetention.html">API Reference</a>.
*/
public enum Mode {
GOVERNANCE("GOVERNANCE"),
COMPLIANCE("COMPLIANCE");
private final String value;
@JsonCreator
Mode(String value) {
this.value = value;
}
@Override
@JsonValue
public String toString() {
return value;
}
}

View File

@ -0,0 +1,47 @@
/*
* Copyright 2017-2022 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 cc.iotkit.s3mock.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.Date;
/**
* Container for elements related to a particular multipart upload.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_MultipartUpload.html">API Reference</a>
*/
@Data
@AllArgsConstructor
public class MultipartUpload {
@JsonProperty("Key")
private String key;
@JsonProperty("UploadId")
private String uploadId;
@JsonProperty("Owner")
private Owner owner;
@JsonProperty("Initiator")
private Owner initiator;
@JsonProperty("StorageClass")
private StorageClass storageClass;
@JsonProperty("Initiated")
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone = "UTC")
private Date initiated;
}

View File

@ -0,0 +1,35 @@
/*
* Copyright 2017-2022 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 cc.iotkit.s3mock.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonRootName;
import lombok.Data;
/**
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_NoncurrentVersionExpiration.html">API Reference</a>.
*/
@Data
@JsonRootName("NoncurrentVersionExpiration")
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class NoncurrentVersionExpiration {
@JsonProperty("NewerNoncurrentVersions")
private Integer newerNoncurrentVersions;
@JsonProperty("NoncurrentDays")
private Integer noncurrentDays;
}

View File

@ -0,0 +1,39 @@
/*
* Copyright 2017-2022 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 cc.iotkit.s3mock.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonRootName;
import lombok.Data;
/**
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_NoncurrentVersionTransition.html">API Reference</a>.
*/
@Data
@JsonRootName("NoncurrentVersionTransition")
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class NoncurrentVersionTransition {
@JsonProperty("NewerNoncurrentVersions")
private Integer newerNoncurrentVersions;
@JsonProperty("NoncurrentDays")
private Integer noncurrentDays;
@JsonProperty("StorageClass")
private StorageClass storageClass;
}

View File

@ -0,0 +1,40 @@
/*
* Copyright 2017-2022 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 cc.iotkit.s3mock.dto;
/**
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObjectAttributes.html">API Reference</a>.
* See x-amz-object-attributes
*/
public enum ObjectAttributes {
ETAG("ETag"),
CHECKSUM("Checksum"),
OBJECT_PARTS("ObjectParts"),
STORAGE_CLASS("StorageClass"),
OBJECT_SIZE("ObjectSize");
private final String value;
ObjectAttributes(String value) {
this.value = value;
}
@Override
public String toString() {
return value;
}
}

View File

@ -0,0 +1,46 @@
/*
* Copyright 2017-2022 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 cc.iotkit.s3mock.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import static java.util.Objects.requireNonNull;
/**
* Key request value object.
* Removes the trailing slash extracted from paths by Spring.
* Used in conjunction with the PathVariable extracted by using "{key}" in the path pattern.
* See {@link org.springframework.web.util.pattern.PathPattern}
* Example path pattern: "/{bucketName:.+}/{key}"
* Example incoming path: "/my-bucket/prefix/before/my/key"
* By declaring "{key}", Spring extracts the absolute path "/prefix/before/my/key", but in S3, all
* keys within a bucket are relative to the bucket, in this example "prefix/before/my/key".
*/
@Data
@AllArgsConstructor
public class ObjectKey {
private String key;
public ObjectKey() {
requireNonNull(key);
if (key.startsWith("/")) {
key = key.substring(1);
}
}
}

View File

@ -0,0 +1,40 @@
/*
* Copyright 2017-2022 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 cc.iotkit.s3mock.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonRootName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_ObjectLockConfiguration.html">API Reference</a>.
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonRootName("ObjectLockConfiguration")
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class ObjectLockConfiguration {
@JsonProperty("ObjectLockEnabled")
private ObjectLockEnabled objectLockEnabled;
@JsonProperty("Rule")
private ObjectLockRule objectLockRule;
}

View File

@ -0,0 +1,40 @@
/*
* Copyright 2017-2022 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 cc.iotkit.s3mock.dto;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
/**
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_ObjectLockConfiguration.html">API Reference</a>.
*/
public enum ObjectLockEnabled {
ENABLED("Enabled");
private final String value;
@JsonCreator
ObjectLockEnabled(String value) {
this.value = value;
}
@Override
@JsonValue
public String toString() {
return value;
}
}

View File

@ -0,0 +1,32 @@
/*
* Copyright 2017-2022 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 cc.iotkit.s3mock.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_ObjectLockRule.html">API Reference</a>.
*/
@Data
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class ObjectLockRule {
@JsonProperty("DefaultRetention")
private DefaultRetention defaultRetention;
}

View File

@ -0,0 +1,43 @@
/*
* Copyright 2017-2023 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 cc.iotkit.s3mock.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonRootName;
import lombok.Data;
/**
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_ObjectPart.html">API Reference</a>.
*/
@Data
@JsonRootName("ObjectPart")
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class ObjectPart {
@JsonProperty("ChecksumCRC32")
private String checksumCRC32;
@JsonProperty("ChecksumCRC32C")
private String checksumCRC32C;
@JsonProperty("ChecksumSHA1")
private String checksumSHA1;
@JsonProperty("ChecksumSHA256")
private String checksumSHA256;
@JsonProperty("Size")
private Long size;
@JsonProperty("PartNumber")
private Integer partNumber;
}

View File

@ -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 cc.iotkit.s3mock.dto;
import static cc.iotkit.s3mock.util.EtagUtil.normalizeEtag;
import cc.iotkit.s3mock.store.S3ObjectMetadata;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_ObjectVersion.html">API Reference</a>.
*/
@Data
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class ObjectVersion {
@JsonProperty("Key")
private String key;
@JsonProperty("LastModified")
private String lastModified;
@JsonProperty("ETag")
private String etag;
@JsonProperty("Size")
private String size;
@JsonProperty("StorageClass")
private StorageClass storageClass;
@JsonProperty("Owner")
private Owner owner;
@JsonProperty("ChecksumAlgorithm")
private ChecksumAlgorithm checksumAlgorithm;
@JsonProperty("IsLatest")
private Boolean isLatest;
@JsonProperty("VersionId")
private String versionId;
public ObjectVersion() {
etag = normalizeEtag(etag);
}
public static ObjectVersion from(S3ObjectMetadata s3ObjectMetadata) {
return new ObjectVersion(s3ObjectMetadata.getKey(),
s3ObjectMetadata.getModificationDate(),
s3ObjectMetadata.getEtag(),
s3ObjectMetadata.getSize(),
s3ObjectMetadata.getStorageClass(),
s3ObjectMetadata.getOwner(),
s3ObjectMetadata.getChecksumAlgorithm(),
true,
"staticVersion");
}
public static ObjectVersion from(S3Object s3Object) {
return new ObjectVersion(s3Object.getKey(),
s3Object.getLastModified(),
s3Object.getEtag(),
s3Object.getSize(),
s3Object.getStorageClass(),
s3Object.getOwner(),
s3Object.getChecksumAlgorithm(),
true,
"staticVersion");
}
}

View File

@ -0,0 +1,93 @@
/*
* 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 cc.iotkit.s3mock.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.xml.bind.annotation.XmlAccessType;
import jakarta.xml.bind.annotation.XmlAccessorType;
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlRootElement;
import java.util.Objects;
/**
* Owner of a Bucket.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_Owner.html">API Reference</a>
*/
@XmlRootElement(name = "Owner")
@XmlAccessorType(XmlAccessType.FIELD)
public class Owner {
/**
* Default owner in S3Mock until support for ownership is implemented.
*/
public static final Owner DEFAULT_OWNER =
new Owner("79a59df900b949e55d96a1e698fbacedfd6e09d98eacf8f8d5218e7cd47ef2be",
"s3-mock-file-store");
public static final Owner DEFAULT_OWNER_BUCKET =
new Owner("79a59df900b949e55d96a1e698fbacedfd6e09d98eacf8f8d5218e7cd47ef2df",
"s3-mock-file-store-bucket");
@XmlElement(name = "ID")
@JsonProperty("ID")
private String id;
@XmlElement(name = "DisplayName")
@JsonProperty("DisplayName")
private String displayName;
public Owner() {
// Jackson needs the default constructor for deserialization.
}
public Owner(String id, String displayName) {
this.id = id;
this.displayName = displayName;
}
public String getDisplayName() {
return displayName;
}
public String getId() {
return id;
}
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
public void setId(String id) {
this.id = id;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Owner owner = (Owner) o;
return Objects.equals(id, owner.id) && Objects.equals(displayName,
owner.displayName);
}
@Override
public int hashCode() {
return Objects.hash(id, displayName);
}
}

View File

@ -0,0 +1,47 @@
/*
* Copyright 2017-2022 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 cc.iotkit.s3mock.dto;
import static cc.iotkit.s3mock.util.EtagUtil.normalizeEtag;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.Date;
/**
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_Part.html">API Reference</a>.
*/
@Data
@AllArgsConstructor
public class Part {
@JsonProperty("PartNumber")
private Integer partNumber;
@JsonProperty("ETag")
private String etag;
@JsonProperty("LastModified")
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone = "UTC")
private Date lastModified;
@JsonProperty("Size")
private Long size;
public Part() {
etag = normalizeEtag(etag);
}
}

View File

@ -0,0 +1,31 @@
/*
* Copyright 2017-2022 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 cc.iotkit.s3mock.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Prefix {
@JsonProperty("Prefix")
private String prefix;
}

View File

@ -0,0 +1,40 @@
/*
* Copyright 2017-2023 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 cc.iotkit.s3mock.dto;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
import software.amazon.awssdk.regions.Region;
/**
* Serialize AWS Region objects.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketLocation.html#API_GetBucketLocation_ResponseSyntax">API Reference</a>
*/
public class RegionSerializer extends JsonSerializer<Region> {
@Override
public void serialize(Region value, JsonGenerator gen, SerializerProvider serializers)
throws IOException {
var regionString = value.id();
//API doc says to return "null" for the us-east-1 region.
if ("us-east-1".equals(regionString)) {
gen.writeString("null");
}
gen.writeString(regionString);
}
}

View File

@ -0,0 +1,44 @@
/*
* Copyright 2017-2022 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 cc.iotkit.s3mock.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonRootName;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import lombok.Data;
import java.time.Instant;
/**
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_control_S3Retention.html">API Reference</a>.
* For unknown reasons, the timestamps in the Retention are serialized in Nanoseconds instead of
* Milliseconds, like everywhere else.
*/
@Data
@JsonRootName("Retention")
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class Retention {
@JsonProperty("Mode")
private Mode mode;
@JsonProperty("RetainUntilDate")
@JsonSerialize(using = InstantSerializer.class)
@JsonDeserialize(using = InstantDeserializer.class)
private Instant retainUntilDate;
}

View File

@ -0,0 +1,63 @@
/*
* 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 cc.iotkit.s3mock.dto;
import static cc.iotkit.s3mock.util.EtagUtil.normalizeEtag;
import cc.iotkit.s3mock.store.S3ObjectMetadata;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* Class representing an Object on S3.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_Object.html">API Reference</a>
*/
@Data
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class S3Object{
@JsonProperty("Key")
String key;
@JsonProperty("LastModified")
String lastModified;
@JsonProperty("ETag")
String etag;
@JsonProperty("Size")
String size;
@JsonProperty("StorageClass")
StorageClass storageClass;
@JsonProperty("Owner")
Owner owner;
@JsonProperty("ChecksumAlgorithm")
ChecksumAlgorithm checksumAlgorithm;
public S3Object() {
etag = normalizeEtag(etag);
}
public static S3Object from(S3ObjectMetadata s3ObjectMetadata) {
return new S3Object(s3ObjectMetadata.getKey(),
s3ObjectMetadata.getModificationDate(),
s3ObjectMetadata.getEtag(),
s3ObjectMetadata.getSize(),
s3ObjectMetadata.getStorageClass(),
s3ObjectMetadata.getOwner(),
s3ObjectMetadata.getChecksumAlgorithm());
}
}

View File

@ -0,0 +1,33 @@
/*
* Copyright 2017-2022 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 cc.iotkit.s3mock.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* Object identifier used in many APIs.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_ObjectIdentifier.html">API Reference</a>
*/
@Data
public class S3ObjectIdentifier {
@JsonProperty("Key")
private String key;
@JsonProperty("VersionId")
private String versionId;
}

View File

@ -0,0 +1,49 @@
/*
* Copyright 2017-2022 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 cc.iotkit.s3mock.dto;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
/**
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/storage-class-intro.html">API Reference</a>.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_Object.html">API Reference</a>.
*/
public enum StorageClass {
STANDARD("STANDARD"),
REDUCED_REDUNDANCY("REDUCED_REDUNDANCY"),
GLACIER("GLACIER"),
STANDARD_IA("STANDARD_IA"),
ONEZONE_IA("ONEZONE_IA"),
INTELLIGENT_TIERING("INTELLIGENT_TIERING"),
DEEP_ARCHIVE("DEEP_ARCHIVE"),
OUTPOSTS("OUTPOSTS"),
GLACIER_IR("GLACIER_IR");
private final String value;
@JsonCreator
StorageClass(String value) {
this.value = value;
}
@Override
@JsonValue
public String toString() {
return value;
}
}

View File

@ -0,0 +1,44 @@
/*
* Copyright 2017-2022 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 cc.iotkit.s3mock.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonRootName;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import lombok.Data;
/**
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_Tag.html">API Reference</a>.
*/
@Data
@JsonRootName("Tag")
@JacksonXmlRootElement(localName = "Tag")
public class Tag {
@JsonProperty("Key")
private String key;
@JsonProperty("Value")
private String value;
/**
* Constructor for Spring's automatic header conversion.
*/
public Tag(final String keyValuePair) {
this.key = keyValuePair.split("=")[0];
this.value = keyValuePair.split("=")[1];
}
}

View File

@ -0,0 +1,35 @@
/*
* Copyright 2017-2022 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 cc.iotkit.s3mock.dto;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TagSet {
@JacksonXmlProperty(localName = "Tag")
@JacksonXmlElementWrapper(useWrapping = false)
private List<Tag> tags;
}

View File

@ -0,0 +1,38 @@
/*
* Copyright 2017-2022 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 cc.iotkit.s3mock.dto;
import com.fasterxml.jackson.annotation.JsonRootName;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Result to be returned for GetObjectTagging.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-tagging.html">API Reference</a>
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_Tagging.html">API Reference</a>
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonRootName("Tagging")
public class Tagging {
@JacksonXmlProperty(localName = "TagSet")
private TagSet tagSet;
}

View File

@ -0,0 +1,45 @@
/*
* Copyright 2017-2022 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 cc.iotkit.s3mock.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonRootName;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import lombok.Data;
import java.time.Instant;
/**
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_Transition.html">API Reference</a>.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/lifecycle-transition-general-considerations.html">API Reference</a>.
*/
@Data
@JsonRootName("Transition")
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class Transition {
@JsonProperty("Date")
@JsonSerialize(using = InstantSerializer.class)
@JsonDeserialize(using = InstantDeserializer.class)
private Instant date;
@JsonProperty("Days")
private Integer days;
@JsonProperty("StorageClass")
private StorageClass storageClass;
}

View File

@ -0,0 +1,27 @@
/*
* Copyright 2017-2022 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.
*/
/**
* This set the namespace for all JAX-B serialization / deserialization POJOs in this package.
*/
@XmlSchema(namespace = "http://s3.amazonaws.com/doc/2006-03-01/",
xmlns = { @XmlNs(prefix = "", namespaceURI = "http://s3.amazonaws.com/doc/2006-03-01/") },
elementFormDefault = XmlNsForm.QUALIFIED)
package cc.iotkit.s3mock.dto;
import jakarta.xml.bind.annotation.XmlNs;
import jakarta.xml.bind.annotation.XmlNsForm;
import jakarta.xml.bind.annotation.XmlSchema;

View File

@ -0,0 +1,412 @@
/*
* Copyright 2017-2023 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 cc.iotkit.s3mock.service;
import static org.apache.commons.lang3.StringUtils.isEmpty;
import static org.apache.commons.lang3.StringUtils.isNotEmpty;
import static software.amazon.awssdk.utils.http.SdkHttpUtils.urlEncodeIgnoreSlashes;
import cc.iotkit.s3mock.S3Exception;
import cc.iotkit.s3mock.dto.*;
import cc.iotkit.s3mock.dto.Bucket;
import cc.iotkit.s3mock.dto.BucketLifecycleConfiguration;
import cc.iotkit.s3mock.dto.Buckets;
import cc.iotkit.s3mock.dto.ListAllMyBucketsResult;
import cc.iotkit.s3mock.dto.ListBucketResult;
import cc.iotkit.s3mock.dto.ListBucketResultV2;
import cc.iotkit.s3mock.dto.ListVersionsResult;
import cc.iotkit.s3mock.dto.ObjectLockConfiguration;
import cc.iotkit.s3mock.dto.ObjectVersion;
import cc.iotkit.s3mock.dto.Prefix;
import cc.iotkit.s3mock.dto.S3Object;
import cc.iotkit.s3mock.store.BucketStore;
import cc.iotkit.s3mock.store.ObjectStore;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
import software.amazon.awssdk.utils.http.SdkHttpUtils;
import javax.annotation.PostConstruct;
public class BucketService {
private final Map<String, String> listObjectsPagingStateCache = new ConcurrentHashMap<>();
private final BucketStore bucketStore;
private final ObjectStore objectStore;
public BucketService(BucketStore bucketStore, ObjectStore objectStore) {
this.bucketStore = bucketStore;
this.objectStore = objectStore;
}
@PostConstruct
public void init() {
// createBucket("bucket1", false);
}
public boolean isBucketEmpty(String bucketName) {
return bucketStore.isBucketEmpty(bucketName);
}
public boolean doesBucketExist(String bucketName) {
return bucketStore.doesBucketExist(bucketName);
}
public ListAllMyBucketsResult listBuckets() {
var buckets = bucketStore
.listBuckets()
.stream()
.filter(Objects::nonNull)
.map(Bucket::from)
.collect(Collectors.toList());
return new ListAllMyBucketsResult(Owner.DEFAULT_OWNER, new Buckets(buckets));
}
/**
* Retrieves a Bucket identified by its name.
*
* @param bucketName of the Bucket to be retrieved
* @return the Bucket or null if not found
*/
public Bucket getBucket(String bucketName) {
return Bucket.from(bucketStore.getBucketMetadata(bucketName));
}
/**
* Creates a Bucket identified by its name.
*
* @param bucketName of the Bucket to be created
* @return the Bucket
*/
public Bucket createBucket(String bucketName, boolean objectLockEnabled) {
return Bucket.from(bucketStore.createBucket(bucketName, objectLockEnabled));
}
public boolean deleteBucket(String bucketName) {
return bucketStore.deleteBucket(bucketName);
}
public void setObjectLockConfiguration(String bucketName, ObjectLockConfiguration configuration) {
var bucketMetadata = bucketStore.getBucketMetadata(bucketName);
bucketStore.storeObjectLockConfiguration(bucketMetadata, configuration);
}
public ObjectLockConfiguration getObjectLockConfiguration(String bucketName) {
var bucketMetadata = bucketStore.getBucketMetadata(bucketName);
var objectLockConfiguration = bucketMetadata.getObjectLockConfiguration();
if (objectLockConfiguration != null) {
return objectLockConfiguration;
} else {
throw S3Exception.NOT_FOUND_BUCKET_OBJECT_LOCK;
}
}
public void setBucketLifecycleConfiguration(String bucketName,
BucketLifecycleConfiguration configuration) {
var bucketMetadata = bucketStore.getBucketMetadata(bucketName);
bucketStore.storeBucketLifecycleConfiguration(bucketMetadata, configuration);
}
public void deleteBucketLifecycleConfiguration(String bucketName) {
setBucketLifecycleConfiguration(bucketName, null);
}
public BucketLifecycleConfiguration getBucketLifecycleConfiguration(String bucketName) {
var bucketMetadata = bucketStore.getBucketMetadata(bucketName);
var configuration = bucketMetadata.getBucketLifecycleConfiguration();
if (configuration != null) {
return configuration;
} else {
throw S3Exception.NO_SUCH_LIFECYCLE_CONFIGURATION;
}
}
/**
* Retrieves S3Objects from a bucket.
*
* @param bucketName the Bucket in which to list the file(s) in.
* @param prefix {@link String} object file name starts with
* @return S3Objects found in bucket for the given prefix
*/
public List<S3Object> getS3Objects(String bucketName, String prefix) {
var bucketMetadata = bucketStore.getBucketMetadata(bucketName);
var uuids = bucketStore.lookupKeysInBucket(prefix, bucketName);
return uuids
.stream()
.map(uuid -> objectStore.getS3ObjectMetadata(bucketMetadata, uuid))
.filter(Objects::nonNull)
.map(S3Object::from)
// List Objects results are expected to be sorted by key
.sorted(Comparator.comparing(S3Object::getKey))
.collect(Collectors.toList());
}
public ListVersionsResult listVersions(String bucketName,
String prefix,
String delimiter,
String encodingType,
String startAfter,
Integer maxKeys,
String continuationToken,
String keyMarker,
String versionIdMarker) {
//first implementation with dummy versions, just list objects for now.
ListBucketResultV2 result = listObjectsV2(bucketName, prefix, delimiter, encodingType,
startAfter, maxKeys, continuationToken);
var versions = result.getContents().stream().map(ObjectVersion::from).collect(Collectors.toList());
return new ListVersionsResult(result.getName(),
result.getPrefix(),
result.getMaxKeys(),
result.isTruncated(),
result.getCommonPrefixes(),
delimiter,
result.getEncodingType(),
result.getContinuationToken(),
null,
result.getNextContinuationToken(),
null,
versions,
null);
}
public ListBucketResultV2 listObjectsV2(String bucketName,
String prefix,
String delimiter,
String encodingType,
String startAfter,
Integer maxKeys,
String continuationToken) {
var contents = getS3Objects(bucketName, prefix);
var nextContinuationToken = (String) null;
var isTruncated = false;
/*
Start-after is valid only in first request.
If the response is truncated,
you can specify this parameter along with the continuation-token parameter,
and then Amazon S3 ignores this parameter.
*/
if (continuationToken != null) {
var continueAfter = listObjectsPagingStateCache.get(continuationToken);
contents = filterObjectsBy(contents, continueAfter);
listObjectsPagingStateCache.remove(continuationToken);
} else {
contents = filterObjectsBy(contents, startAfter);
}
var commonPrefixes = collapseCommonPrefixes(prefix, delimiter, contents);
contents = filterObjectsBy(contents, commonPrefixes);
if (contents.size() > maxKeys) {
isTruncated = true;
nextContinuationToken = UUID.randomUUID().toString();
contents = contents.subList(0, maxKeys);
listObjectsPagingStateCache.put(nextContinuationToken,
contents.get(maxKeys - 1).getKey());
}
var returnPrefix = prefix;
var returnStartAfter = startAfter;
var returnCommonPrefixes = commonPrefixes;
if (Objects.equals("url", encodingType)) {
contents = apply(contents, object -> new S3Object(urlEncodeIgnoreSlashes(object.getKey()),
object.getLastModified(),
object.getEtag(),
object.getSize(),
object.getStorageClass(),
object.getOwner(),
object.getChecksumAlgorithm()));
returnPrefix = urlEncodeIgnoreSlashes(prefix);
returnStartAfter = urlEncodeIgnoreSlashes(startAfter);
returnCommonPrefixes = apply(commonPrefixes, SdkHttpUtils::urlEncodeIgnoreSlashes);
}
return new ListBucketResultV2(bucketName, returnPrefix, maxKeys,
isTruncated, contents, returnCommonPrefixes.stream().map(Prefix::new).collect(Collectors.toList()),
continuationToken, String.valueOf(contents.size()),
nextContinuationToken, returnStartAfter, encodingType);
}
@Deprecated(since = "2.12.2", forRemoval = true)
public ListBucketResult listObjectsV1(String bucketName, String prefix, String delimiter,
String marker, String encodingType, Integer maxKeys) {
verifyMaxKeys(maxKeys);
verifyEncodingType(encodingType);
var contents = getS3Objects(bucketName, prefix);
contents = filterObjectsBy(contents, marker);
var isTruncated = false;
var nextMarker = (String) null;
var commonPrefixes = collapseCommonPrefixes(prefix, delimiter, contents);
contents = filterObjectsBy(contents, commonPrefixes);
if (maxKeys < contents.size()) {
contents = contents.subList(0, maxKeys);
isTruncated = true;
if (maxKeys > 0) {
nextMarker = contents.get(maxKeys - 1).getKey();
}
}
var returnPrefix = prefix;
var returnCommonPrefixes = commonPrefixes;
if (Objects.equals("url", encodingType)) {
contents = apply(contents, object -> new S3Object(urlEncodeIgnoreSlashes(object.getKey()),
object.getLastModified(),
object.getEtag(),
object.getSize(),
object.getStorageClass(),
object.getOwner(),
object.getChecksumAlgorithm()));
returnPrefix = urlEncodeIgnoreSlashes(prefix);
returnCommonPrefixes = apply(commonPrefixes, SdkHttpUtils::urlEncodeIgnoreSlashes);
}
return new ListBucketResult(bucketName, returnPrefix, marker, maxKeys, isTruncated,
encodingType, nextMarker, contents,
returnCommonPrefixes.stream().map(Prefix::new).collect(Collectors.toList()));
}
public void verifyBucketExists(String bucketName) {
if (!bucketStore.doesBucketExist(bucketName)) {
throw S3Exception.NO_SUCH_BUCKET;
}
}
public void verifyBucketObjectLockEnabled(String bucketName) {
if (!bucketStore.isObjectLockEnabled(bucketName)) {
throw S3Exception.NOT_FOUND_BUCKET_OBJECT_LOCK;
}
}
/**
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html">API Reference Bucket Naming</a>.
*/
public void verifyBucketNameIsAllowed(String bucketName) {
if (!bucketName.matches("[a-z0-9.-]+")) {
throw S3Exception.INVALID_BUCKET_NAME;
}
}
public void verifyBucketIsEmpty(String bucketName) {
if (!bucketStore.isBucketEmpty(bucketName)) {
throw S3Exception.BUCKET_NOT_EMPTY;
}
}
public void verifyBucketDoesNotExist(String bucketName) {
if (bucketStore.doesBucketExist(bucketName)) {
//currently, all buckets have the same owner in S3Mock. If the bucket exists, it's owned by
//the owner that tries to create the bucket owns the existing bucket too.
throw S3Exception.BUCKET_ALREADY_OWNED_BY_YOU;
}
}
public void verifyMaxKeys(Integer maxKeys) {
if (maxKeys < 0) {
throw S3Exception.INVALID_REQUEST_MAXKEYS;
}
}
public void verifyEncodingType(String encodingType) {
if (isNotEmpty(encodingType) && !"url".equals(encodingType)) {
throw S3Exception.INVALID_REQUEST_ENCODINGTYPE;
}
}
/**
* Collapse all bucket elements with keys starting with some prefix up to the given delimiter into
* one prefix entry. Collapsed elements are removed from the contents list.
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectsV2.html">API Reference</a>
*
* @param queryPrefix the key prefix as specified in the list request
* @param delimiter the delimiter used to separate a prefix from the rest of the object name
* @param s3Objects the list of objects to use for collapsing the prefixes
*/
static List<String> collapseCommonPrefixes(String queryPrefix, String delimiter,
List<S3Object> s3Objects) {
var commonPrefixes = new ArrayList<String>();
if (isEmpty(delimiter)) {
return commonPrefixes;
}
var normalizedQueryPrefix = queryPrefix == null ? "" : queryPrefix;
for (var c : s3Objects) {
var key = c.getKey();
if (key.startsWith(normalizedQueryPrefix)) {
int delimiterIndex = key.indexOf(delimiter, normalizedQueryPrefix.length());
if (delimiterIndex > 0) {
var commonPrefix = key.substring(0, delimiterIndex + delimiter.length());
if (!commonPrefixes.contains(commonPrefix)) {
commonPrefixes.add(commonPrefix);
}
}
}
}
return commonPrefixes;
}
private static <T> List<T> apply(List<T> contents, UnaryOperator<T> extractor) {
return contents
.stream()
.map(extractor)
.collect(Collectors.toList());
}
static List<S3Object> filterObjectsBy(List<S3Object> s3Objects,
String startAfter) {
if (isNotEmpty(startAfter)) {
return s3Objects
.stream()
.filter(p -> p.getKey().compareTo(startAfter) > 0)
.collect(Collectors.toList());
} else {
return s3Objects;
}
}
static List<S3Object> filterObjectsBy(List<S3Object> s3Objects,
List<String> commonPrefixes) {
if (commonPrefixes != null && !commonPrefixes.isEmpty()) {
return s3Objects
.stream()
.filter(c -> commonPrefixes
.stream()
.noneMatch(p -> c.getKey().startsWith(p))
)
.collect(Collectors.toList());
} else {
return s3Objects;
}
}
}

View File

@ -0,0 +1,329 @@
/*
* 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 cc.iotkit.s3mock.service;
import cc.iotkit.s3mock.S3Exception;
import cc.iotkit.s3mock.dto.ChecksumAlgorithm;
import cc.iotkit.s3mock.dto.CompleteMultipartUploadResult;
import cc.iotkit.s3mock.dto.CompletedPart;
import cc.iotkit.s3mock.dto.CopyPartResult;
import cc.iotkit.s3mock.dto.InitiateMultipartUploadResult;
import cc.iotkit.s3mock.dto.ListMultipartUploadsResult;
import cc.iotkit.s3mock.dto.ListPartsResult;
import cc.iotkit.s3mock.dto.Owner;
import cc.iotkit.s3mock.dto.Part;
import cc.iotkit.s3mock.dto.StorageClass;
import cc.iotkit.s3mock.store.BucketStore;
import cc.iotkit.s3mock.store.MultipartStore;
import java.io.InputStream;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpRange;
public class MultipartService {
private static final Logger LOG = LoggerFactory.getLogger(MultipartService.class);
static final Long MINIMUM_PART_SIZE = 5L * 1024L * 1024L;
private final BucketStore bucketStore;
private final MultipartStore multipartStore;
public MultipartService(BucketStore bucketStore, MultipartStore multipartStore) {
this.bucketStore = bucketStore;
this.multipartStore = multipartStore;
}
/**
* Uploads a part of a multipart upload.
*
* @param bucketName in which to upload
* @param key of the object to upload
* @param uploadId id of the upload
* @param partNumber number of the part to store
* @param inputStream file data to be stored
* @param useV4ChunkedWithSigningFormat If {@code true}, V4-style signing is enabled.
* @return the md5 digest of this part
*/
public String putPart(String bucketName,
String key,
String uploadId,
String partNumber,
InputStream inputStream,
boolean useV4ChunkedWithSigningFormat,
Map<String, String> encryptionHeaders) {
var bucketMetadata = bucketStore.getBucketMetadata(bucketName);
var uuid = bucketMetadata.getID(key);
if (uuid == null) {
return null;
}
return multipartStore.putPart(bucketMetadata, uuid, uploadId, partNumber, inputStream,
useV4ChunkedWithSigningFormat, encryptionHeaders);
}
/**
* Copies the range, define by from/to, from the S3 Object, identified by the given key to given
* destination into the given bucket.
*
* @param bucketName The source Bucket.
* @param key Identifies the S3 Object.
* @param copyRange Byte range to copy. Optional.
* @param partNumber The part to copy.
* @param destinationBucket The Bucket the target object (will) reside in.
* @param destinationKey The target object key.
* @param uploadId id of the upload.
* @return etag of the uploaded file.
*/
public CopyPartResult copyPart(String bucketName,
String key,
HttpRange copyRange,
String partNumber,
String destinationBucket,
String destinationKey,
String uploadId,
Map<String, String> encryptionHeaders) {
var sourceBucketMetadata = bucketStore.getBucketMetadata(bucketName);
var destinationBucketMetadata = bucketStore.getBucketMetadata(destinationBucket);
var sourceId = sourceBucketMetadata.getID(key);
if (sourceId == null) {
return null;
}
// source must be copied to destination
var destinationId = bucketStore.addToBucket(destinationKey, destinationBucket);
try {
var partEtag =
multipartStore.copyPart(sourceBucketMetadata, sourceId, copyRange, partNumber,
destinationBucketMetadata, destinationId, uploadId, encryptionHeaders);
return CopyPartResult.from(new Date(), "\"" + partEtag + "\"");
} catch (Exception e) {
//something went wrong with writing the destination file, clean up ID from BucketStore.
bucketStore.removeFromBucket(destinationKey, destinationBucket);
throw new IllegalStateException(String.format(
"Could not copy part. sourceBucket=%s, destinationBucket=%s, key=%s, sourceId=%s, "
+ "destinationId=%s, uploadId=%s", sourceBucketMetadata, destinationBucketMetadata,
key, sourceId, destinationId, uploadId
), e);
}
}
/**
* Get all multipart upload parts.
*
* @param bucketName name of the bucket
* @param key object key
* @param uploadId upload identifier
* @return List of Parts
*/
public ListPartsResult getMultipartUploadParts(String bucketName, String key, String uploadId) {
var bucketMetadata = bucketStore.getBucketMetadata(bucketName);
var id = bucketMetadata.getID(key);
if (id == null) {
return null;
}
var parts = multipartStore.getMultipartUploadParts(bucketMetadata, id, uploadId);
return new ListPartsResult(bucketName, key, uploadId, parts);
}
/**
* Aborts the upload.
*
* @param bucketName to which was uploaded
* @param key which was uploaded
* @param uploadId of the upload
*/
public void abortMultipartUpload(String bucketName, String key, String uploadId) {
var bucketMetadata = bucketStore.getBucketMetadata(bucketName);
var id = bucketMetadata.getID(key);
try {
multipartStore.abortMultipartUpload(bucketMetadata, id, uploadId);
} finally {
bucketStore.removeFromBucket(key, bucketName);
}
}
/**
* Completes a Multipart Upload for the given ID.
*
* @param bucketName in which to upload.
* @param key of the file to upload.
* @param uploadId id of the upload.
* @param parts to concatenate.
* @param location the location link to embed in result
* @return etag of the uploaded file.
*/
public CompleteMultipartUploadResult completeMultipartUpload(String bucketName, String key,
String uploadId, List<CompletedPart> parts, Map<String, String> encryptionHeaders,
String location) {
var bucketMetadata = bucketStore.getBucketMetadata(bucketName);
var id = bucketMetadata.getID(key);
if (id == null) {
return null;
}
var etag = multipartStore
.completeMultipartUpload(bucketMetadata, key, id, uploadId, parts, encryptionHeaders);
return new CompleteMultipartUploadResult(location, bucketName, key, etag);
}
/**
* Prepares everything to store an object uploaded as multipart upload.
*
* @param bucketName Bucket to upload object in
* @param key object to upload
* @param contentType the content type
* @param storeHeaders various headers to store
* @param uploadId id of the upload
* @param owner owner of the upload
* @param initiator initiator of the upload
* @param userMetadata custom metadata
* @return upload result
*/
public InitiateMultipartUploadResult prepareMultipartUpload(String bucketName,
String key,
String contentType,
Map<String, String> storeHeaders,
String uploadId,
Owner owner,
Owner initiator,
Map<String, String> userMetadata,
Map<String, String> encryptionHeaders,
StorageClass storageClass,
String checksum,
ChecksumAlgorithm checksumAlgorithm) {
var bucketMetadata = bucketStore.getBucketMetadata(bucketName);
var id = bucketStore.addToBucket(key, bucketName);
try {
multipartStore.prepareMultipartUpload(bucketMetadata,
key,
id,
contentType,
storeHeaders,
uploadId,
owner,
initiator,
userMetadata,
encryptionHeaders,
storageClass,
checksum,
checksumAlgorithm);
return new InitiateMultipartUploadResult(bucketName, key, uploadId);
} catch (Exception e) {
//something went wrong with writing the destination file, clean up ID from BucketStore.
bucketStore.removeFromBucket(key, bucketName);
throw new IllegalStateException(String.format(
"Could prepare Multipart Upload. bucket=%s, key=%s, id=%s, uploadId=%s",
bucketMetadata, key, id, uploadId
), e);
}
}
/**
* Lists all not-yet completed parts of multipart uploads in a bucket.
*
* @param bucketName the bucket to use as a filter
* @param prefix the prefix use as a filter
* @return the list of not-yet completed multipart uploads.
*/
public ListMultipartUploadsResult listMultipartUploads(String bucketName, String prefix) {
var multipartUploads = multipartStore.listMultipartUploads(bucketName, prefix);
// the result contains all uploads, use some common value as default
var maxUploads = Math.max(1000, multipartUploads.size());
// delimiter / prefix search not supported
return new ListMultipartUploadsResult(bucketName, null, null, prefix, null,
maxUploads, false, null, null, multipartUploads,
Collections.emptyList());
}
public void verifyPartNumberLimits(String partNumberString) {
int partNumber;
try {
partNumber = Integer.parseInt(partNumberString);
if (partNumber < 1 || partNumber > 10000) {
LOG.error("Multipart part number invalid. partNumber={}", partNumberString);
throw S3Exception.INVALID_PART_NUMBER;
}
} catch (NumberFormatException nfe) {
LOG.error("Multipart part number invalid. partNumber={}", partNumberString, nfe);
throw S3Exception.INVALID_PART_NUMBER;
}
}
public void verifyMultipartParts(String bucketName, String key,
String uploadId, List<CompletedPart> requestedParts) throws S3Exception {
var bucketMetadata = bucketStore.getBucketMetadata(bucketName);
var id = bucketMetadata.getID(key);
if (id == null) {
//TODO: is this the correct error?
throw S3Exception.INVALID_PART;
}
verifyMultipartParts(bucketName, id, uploadId);
var uploadedParts = multipartStore.getMultipartUploadParts(bucketMetadata, id, uploadId);
var uploadedPartsMap =
uploadedParts
.stream()
.collect(Collectors.toMap(Part::getPartNumber, Part::getEtag));
var prevPartNumber = 0;
for (var part : requestedParts) {
if (!uploadedPartsMap.containsKey(part.getPartNumber())
|| !uploadedPartsMap.get(part.getPartNumber()).equals(part.getEtag())) {
LOG.error("Multipart part not valid. bucket={}, id={}, uploadId={}, partNumber={}",
bucketMetadata, id, uploadId, part.getPartNumber());
throw S3Exception.INVALID_PART;
}
if (part.getPartNumber() < prevPartNumber) {
LOG.error("Multipart parts order invalid. bucket={}, id={}, uploadId={}, partNumber={}",
bucketMetadata, id, uploadId, part.getPartNumber());
throw S3Exception.INVALID_PART_ORDER;
}
prevPartNumber = part.getPartNumber();
}
}
public void verifyMultipartParts(String bucketName, UUID id, String uploadId) throws S3Exception {
verifyMultipartUploadExists(uploadId);
var bucketMetadata = bucketStore.getBucketMetadata(bucketName);
var uploadedParts = multipartStore.getMultipartUploadParts(bucketMetadata, id, uploadId);
if (!uploadedParts.isEmpty()) {
for (int i = 0; i < uploadedParts.size() - 1; i++) {
var part = uploadedParts.get(i);
if (part.getSize() < MINIMUM_PART_SIZE) {
LOG.error("Multipart part size too small. bucket={}, id={}, uploadId={}, size={}",
bucketMetadata, id, uploadId, part.getSize());
throw S3Exception.ENTITY_TOO_SMALL;
}
}
}
}
public void verifyMultipartUploadExists(String uploadId) throws S3Exception {
try {
multipartStore.getMultipartUpload(uploadId);
} catch (IllegalArgumentException e) {
throw S3Exception.NO_SUCH_UPLOAD_MULTIPART;
}
}
}

View File

@ -0,0 +1,351 @@
/*
* 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 cc.iotkit.s3mock.service;
import static cc.iotkit.s3mock.util.HeaderUtil.isV4ChunkedWithSigningEnabled;
import cc.iotkit.s3mock.S3Exception;
import cc.iotkit.s3mock.dto.Error;
import cc.iotkit.s3mock.dto.AccessControlPolicy;
import cc.iotkit.s3mock.dto.Checksum;
import cc.iotkit.s3mock.dto.ChecksumAlgorithm;
import cc.iotkit.s3mock.dto.CopyObjectResult;
import cc.iotkit.s3mock.dto.Delete;
import cc.iotkit.s3mock.dto.DeleteResult;
import cc.iotkit.s3mock.dto.DeletedS3Object;
import cc.iotkit.s3mock.dto.LegalHold;
import cc.iotkit.s3mock.dto.Owner;
import cc.iotkit.s3mock.dto.Retention;
import cc.iotkit.s3mock.dto.StorageClass;
import cc.iotkit.s3mock.dto.Tag;
import cc.iotkit.s3mock.store.BucketStore;
import cc.iotkit.s3mock.store.ObjectStore;
import cc.iotkit.s3mock.store.S3ObjectMetadata;
import cc.iotkit.s3mock.util.AwsChunkedDecodingInputStream;
import cc.iotkit.s3mock.util.DigestUtil;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ObjectService {
static final String WILDCARD_ETAG = "\"*\"";
private static final Logger LOG = LoggerFactory.getLogger(ObjectService.class);
private final BucketStore bucketStore;
private final ObjectStore objectStore;
public ObjectService(BucketStore bucketStore, ObjectStore objectStore) {
this.bucketStore = bucketStore;
this.objectStore = objectStore;
}
/**
* Copies an object to another bucket and encrypted object.
*
* @param sourceBucketName bucket to copy from.
* @param sourceKey object key to copy.
* @param destinationBucketName destination bucket.
* @param destinationKey destination object key.
* @param userMetadata User metadata to store for destination object
*
* @return an {@link CopyObjectResult} or null if source couldn't be found.
*/
public CopyObjectResult copyS3Object(String sourceBucketName,
String sourceKey,
String destinationBucketName,
String destinationKey,
Map<String, String> encryptionHeaders,
Map<String, String> userMetadata) {
var sourceBucketMetadata = bucketStore.getBucketMetadata(sourceBucketName);
var destinationBucketMetadata = bucketStore.getBucketMetadata(destinationBucketName);
var sourceId = sourceBucketMetadata.getID(sourceKey);
if (sourceId == null) {
return null;
}
// source and destination is the same, pretend we copied - S3 does the same.
if (sourceKey.equals(destinationKey) && sourceBucketName.equals(destinationBucketName)) {
return objectStore.pretendToCopyS3Object(sourceBucketMetadata, sourceId, userMetadata);
}
// source must be copied to destination
var destinationId = bucketStore.addToBucket(destinationKey, destinationBucketName);
try {
return objectStore.copyS3Object(sourceBucketMetadata, sourceId,
destinationBucketMetadata, destinationId, destinationKey,
encryptionHeaders, userMetadata);
} catch (Exception e) {
//something went wrong with writing the destination file, clean up ID from BucketStore.
bucketStore.removeFromBucket(destinationKey, destinationBucketName);
throw e;
}
}
/**
* Stores an object inside a Bucket.
*
* @param bucketName Bucket to store the object in.
* @param key object key to be stored.
* @param contentType The files Content Type.
* @param storeHeaders various headers to store
* @param dataStream The File as InputStream.
* @param useV4ChunkedWithSigningFormat If {@code true}, V4-style signing is enabled.
* @param userMetadata User metadata to store for this object, will be available for the
* object with the key prefixed with "x-amz-meta-".
*
* @return {@link S3ObjectMetadata}.
*/
public S3ObjectMetadata putS3Object(String bucketName,
String key,
String contentType,
Map<String, String> storeHeaders,
InputStream dataStream,
boolean useV4ChunkedWithSigningFormat,
Map<String, String> userMetadata,
Map<String, String> encryptionHeaders,
List<Tag> tags,
ChecksumAlgorithm checksumAlgorithm,
String checksum,
Owner owner,
StorageClass storageClass) {
var bucketMetadata = bucketStore.getBucketMetadata(bucketName);
var id = bucketMetadata.getID(key);
if (id == null) {
id = bucketStore.addToBucket(key, bucketName);
}
return objectStore.storeS3ObjectMetadata(bucketMetadata, id, key, contentType, storeHeaders,
dataStream, useV4ChunkedWithSigningFormat, userMetadata, encryptionHeaders, null, tags,
checksumAlgorithm, checksum, owner, storageClass);
}
public DeleteResult deleteObjects(String bucketName, Delete delete) {
var response = new DeleteResult(new ArrayList<>(), new ArrayList<>());
for (var object : delete.getObjectsToDelete()) {
try {
// ignore result of delete object.
deleteObject(bucketName, object.getKey());
// add deleted object even if it does not exist S3 does the same.
response.addDeletedObject(DeletedS3Object.from(object));
} catch (IllegalStateException e) {
response.addError(
new Error("InternalError",
object.getKey(),
"We encountered an internal error. Please try again.",
object.getVersionId()));
LOG.error("Object could not be deleted!", e);
}
}
return response;
}
/**
* Removes an object key from a bucket.
*
* @param bucketName bucket containing the object.
* @param key object to be deleted.
*
* @return true if deletion succeeded.
*/
public boolean deleteObject(String bucketName, String key) {
var bucketMetadata = bucketStore.getBucketMetadata(bucketName);
var id = bucketMetadata.getID(key);
if (id == null) {
return false;
}
if (objectStore.deleteObject(bucketMetadata, id)) {
return bucketStore.removeFromBucket(key, bucketName);
} else {
return false;
}
}
/**
* Sets tags for a given object.
*
* @param bucketName Bucket the object is stored in.
* @param key object key to store tags for.
* @param tags List of tagSet objects.
*/
public void setObjectTags(String bucketName, String key, List<Tag> tags) {
var bucketMetadata = bucketStore.getBucketMetadata(bucketName);
var uuid = bucketMetadata.getID(key);
objectStore.storeObjectTags(bucketMetadata, uuid, tags);
}
/**
* Sets LegalHold for a given object.
*
* @param bucketName Bucket the object is stored in.
* @param key object key to store tags for.
* @param legalHold the legal hold.
*/
public void setLegalHold(String bucketName, String key, LegalHold legalHold) {
var bucketMetadata = bucketStore.getBucketMetadata(bucketName);
var uuid = bucketMetadata.getID(key);
objectStore.storeLegalHold(bucketMetadata, uuid, legalHold);
}
/**
* Sets AccessControlPolicy for a given object.
*
* @param bucketName Bucket the object is stored in.
* @param key object key to store tags for.
* @param policy the ACL.
*/
public void setAcl(String bucketName, String key, AccessControlPolicy policy) {
var bucketMetadata = bucketStore.getBucketMetadata(bucketName);
var uuid = bucketMetadata.getID(key);
objectStore.storeAcl(bucketMetadata, uuid, policy);
}
/**
* Retrieves AccessControlPolicy for a given object.
*
* @param bucketName Bucket the object is stored in.
* @param key object key to store tags for.
*/
public AccessControlPolicy getAcl(String bucketName, String key) {
var bucketMetadata = bucketStore.getBucketMetadata(bucketName);
var uuid = bucketMetadata.getID(key);
return objectStore.readAcl(bucketMetadata, uuid);
}
/**
* Sets Retention for a given object.
*
* @param bucketName Bucket the object is stored in.
* @param key object key to store tags for.
* @param retention the retention.
*/
public void setRetention(String bucketName, String key, Retention retention) {
var bucketMetadata = bucketStore.getBucketMetadata(bucketName);
var uuid = bucketMetadata.getID(key);
objectStore.storeRetention(bucketMetadata, uuid, retention);
}
public void verifyRetention(Retention retention) {
var retainUntilDate = retention.getRetainUntilDate();
if (Instant.now().isAfter(retainUntilDate)) {
throw S3Exception.INVALID_REQUEST_RETAINDATE;
}
}
public InputStream verifyMd5(InputStream inputStream, String contentMd5,
String sha256Header) {
try {
var tempFile = Files.createTempFile("md5Check", "");
inputStream.transferTo(Files.newOutputStream(tempFile));
try (var stream = isV4ChunkedWithSigningEnabled(sha256Header)
? new AwsChunkedDecodingInputStream(Files.newInputStream(tempFile))
: Files.newInputStream(tempFile)) {
verifyMd5(stream, contentMd5);
return Files.newInputStream(tempFile);
}
} catch (IOException e) {
throw S3Exception.BAD_REQUEST_CONTENT;
}
}
public void verifyMd5(InputStream inputStream, String contentMd5) {
if (contentMd5 != null) {
var md5 = DigestUtil.base64Digest(inputStream);
if (!md5.equals(contentMd5)) {
LOG.error("Content-MD5 {} does not match object md5 {}", contentMd5, md5);
throw S3Exception.BAD_REQUEST_MD5;
}
}
}
/**
* FOr copy use-cases, we need to return PRECONDITION_FAILED only.
*/
public void verifyObjectMatchingForCopy(List<String> match, List<String> noneMatch,
S3ObjectMetadata s3ObjectMetadata) {
try {
verifyObjectMatching(match, noneMatch, s3ObjectMetadata);
} catch (S3Exception e) {
if (S3Exception.NOT_MODIFIED.equals(e)) {
throw S3Exception.PRECONDITION_FAILED;
} else {
throw e;
}
}
}
public void verifyObjectMatching(List<String> match, List<String> noneMatch,
S3ObjectMetadata s3ObjectMetadata) {
if (s3ObjectMetadata != null) {
var etag = s3ObjectMetadata.getEtag();
if (match != null) {
if (match.contains(WILDCARD_ETAG)) {
//request cares only that the object exists
return;
} else if (!match.contains(etag)) {
throw S3Exception.PRECONDITION_FAILED;
}
}
if (noneMatch != null && (noneMatch.contains(WILDCARD_ETAG) || noneMatch.contains(etag))) {
//request cares only that the object DOES NOT exist.
throw S3Exception.NOT_MODIFIED;
}
}
}
public S3ObjectMetadata verifyObjectExists(String bucketName, String key) {
var bucketMetadata = bucketStore.getBucketMetadata(bucketName);
var uuid = bucketMetadata.getID(key);
if (uuid == null) {
throw S3Exception.NO_SUCH_KEY;
}
var s3ObjectMetadata = objectStore.getS3ObjectMetadata(bucketMetadata, uuid);
if (s3ObjectMetadata == null) {
throw S3Exception.NO_SUCH_KEY;
}
return s3ObjectMetadata;
}
public S3ObjectMetadata verifyObjectLockConfiguration(String bucketName, String key) {
var s3ObjectMetadata = verifyObjectExists(bucketName, key);
var noLegalHold = s3ObjectMetadata.getLegalHold() == null;
var noRetention = s3ObjectMetadata.getRetention() == null;
if (noLegalHold && noRetention) {
throw S3Exception.NOT_FOUND_OBJECT_LOCK;
}
return s3ObjectMetadata;
}
public static Checksum getChecksum(S3ObjectMetadata s3ObjectMetadata) {
ChecksumAlgorithm checksumAlgorithm = s3ObjectMetadata.getChecksumAlgorithm();
if (checksumAlgorithm != null) {
return new Checksum(
checksumAlgorithm == ChecksumAlgorithm.CRC32 ? s3ObjectMetadata.getChecksum() : null,
checksumAlgorithm == ChecksumAlgorithm.CRC32C ? s3ObjectMetadata.getChecksum() : null,
checksumAlgorithm == ChecksumAlgorithm.SHA1 ? s3ObjectMetadata.getChecksum() : null,
checksumAlgorithm == ChecksumAlgorithm.SHA256 ? s3ObjectMetadata.getChecksum() : null
);
}
return null;
}
}

View File

@ -0,0 +1,42 @@
/*
* Copyright 2017-2022 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 cc.iotkit.s3mock.service;
import cc.iotkit.s3mock.store.BucketStore;
import cc.iotkit.s3mock.store.MultipartStore;
import cc.iotkit.s3mock.store.ObjectStore;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ServiceConfiguration {
@Bean
BucketService bucketService(BucketStore bucketStore, ObjectStore objectStore) {
return new BucketService(bucketStore, objectStore);
}
@Bean
ObjectService objectService(BucketStore bucketStore, ObjectStore objectStore) {
return new ObjectService(bucketStore, objectStore);
}
@Bean
MultipartService multipartService(BucketStore bucketStore, MultipartStore multipartStore) {
return new MultipartService(bucketStore, multipartStore);
}
}

Some files were not shown because too many files have changed in this diff Show More