diff --git a/docs/src/main/asciidoc/s3.adoc b/docs/src/main/asciidoc/s3.adoc index ce8614a6c..ceec7e874 100644 --- a/docs/src/main/asciidoc/s3.adoc +++ b/docs/src/main/asciidoc/s3.adoc @@ -148,6 +148,45 @@ If `DiskBufferingS3OutputStream` behavior does not fit your needs, you can imple Possible alternative implementations can use multi-part upload (for example with https://github.com/CI-CMG/aws-s3-outputstream[aws-s3-outputstream library)] or https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/transfer/s3/S3TransferManager.html[S3TransferManager]. +=== Using S3Template + +Spring Cloud AWS provides a higher abstraction on the top of `S3Client` providing methods for the most common use cases when working with S3. + +On the top of self-explanatory methods for creating and deleting buckets, `S3Template` provides a simple methods for uploading and downloading files: + +[source,java] +---- +@Autowired +private S3Template s3Template; + +InputStream is = ... +// uploading file without metadata +s3Template.upload(BUCKET, "file.txt", is); + +// uploading file with metadata +s3Template.upload(BUCKET, "file.txt", is, ObjectMetadata.builder().contentType("text/plain").build()); +---- + +`S3Template` also allows storing & retrieving Java objects. + +[source,java] +---- +Person p = new Person("John", "Doe"); +s3Template.store(BUCKET, "person.json", p); + +Person loadedPerson = s3Template.read(BUCKET, "person.json", Person.class); +---- + +By default, if Jackson is on the classpath, `S3Template` uses `ObjectMapper` based `Jackson2JsonS3ObjectConverter` to convert from S3 object to Java object and vice versa. +This behavior can be overwritten by providing custom bean of type `S3ObjectConverter`. + +=== Determining S3 Objects Content Type + +All S3 objects stored in S3 through `S3Template`, `S3Resource` or `S3OutputStream` automatically get set a `contentType` property on the S3 object metadata, based on the S3 object key (file name). + +By default, `PropertiesS3ObjectContentTypeResolver` - a component supporting over 800 file extensions is responsible for content type resolution. +If this content type resolution does not meet your needs, you can provide a custom bean of type `S3ObjectContentTypeResolver` which will be automatically used in all components responsible for uploading files. + === Configuration The Spring Boot Starter for S3 provides the following configuration options: diff --git a/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/s3/S3AutoConfiguration.java b/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/s3/S3AutoConfiguration.java index 684cca996..7184541f0 100644 --- a/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/s3/S3AutoConfiguration.java +++ b/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/s3/S3AutoConfiguration.java @@ -15,13 +15,22 @@ */ package io.awspring.cloud.autoconfigure.s3; +import com.fasterxml.jackson.databind.ObjectMapper; import io.awspring.cloud.autoconfigure.core.AwsClientBuilderConfigurer; import io.awspring.cloud.autoconfigure.core.AwsProperties; import io.awspring.cloud.s3.DiskBufferingS3OutputStreamProvider; +import io.awspring.cloud.s3.Jackson2JsonS3ObjectConverter; +import io.awspring.cloud.s3.PropertiesS3ObjectContentTypeResolver; +import io.awspring.cloud.s3.S3ObjectContentTypeResolver; +import io.awspring.cloud.s3.S3ObjectConverter; +import io.awspring.cloud.s3.S3Operations; import io.awspring.cloud.s3.S3OutputStreamProvider; import io.awspring.cloud.s3.S3ProtocolResolver; +import io.awspring.cloud.s3.S3Template; import io.awspring.cloud.s3.crossregion.CrossRegionS3Client; +import java.util.Optional; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; @@ -64,8 +73,18 @@ S3ClientBuilder s3ClientBuilder(AwsClientBuilderConfigurer awsClientBuilderConfi @Bean @ConditionalOnMissingBean - S3OutputStreamProvider s3OutputStreamProvider(S3Client s3Client) { - return new DiskBufferingS3OutputStreamProvider(s3Client); + S3OutputStreamProvider s3OutputStreamProvider(S3Client s3Client, + Optional contentTypeResolver) { + return new DiskBufferingS3OutputStreamProvider(s3Client, + contentTypeResolver.orElseGet(PropertiesS3ObjectContentTypeResolver::new)); + } + + @Bean + @ConditionalOnMissingBean(S3Operations.class) + @ConditionalOnBean(S3ObjectConverter.class) + S3Template s3Template(S3Client s3Client, S3OutputStreamProvider s3OutputStreamProvider, + S3ObjectConverter s3ObjectConverter) { + return new S3Template(s3Client, s3OutputStreamProvider, s3ObjectConverter); } private S3Configuration s3ServiceConfiguration() { @@ -105,4 +124,15 @@ S3Client s3Client(S3ClientBuilder s3ClientBuilder) { } + @Configuration + @ConditionalOnClass(ObjectMapper.class) + static class Jackson2JsonS3ObjectConverterConfiguration { + + @ConditionalOnMissingBean + @Bean + S3ObjectConverter s3ObjectConverter(Optional objectMapper) { + return new Jackson2JsonS3ObjectConverter(objectMapper.orElseGet(ObjectMapper::new)); + } + } + } diff --git a/spring-cloud-aws-autoconfigure/src/test/java/io/awspring/cloud/autoconfigure/s3/S3AutoConfigurationTests.java b/spring-cloud-aws-autoconfigure/src/test/java/io/awspring/cloud/autoconfigure/s3/S3AutoConfigurationTests.java index 32bb550e2..361d56ab9 100644 --- a/spring-cloud-aws-autoconfigure/src/test/java/io/awspring/cloud/autoconfigure/s3/S3AutoConfigurationTests.java +++ b/spring-cloud-aws-autoconfigure/src/test/java/io/awspring/cloud/autoconfigure/s3/S3AutoConfigurationTests.java @@ -18,17 +18,21 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; +import com.fasterxml.jackson.databind.ObjectMapper; import io.awspring.cloud.autoconfigure.ConfiguredAwsClient; import io.awspring.cloud.autoconfigure.core.AwsAutoConfiguration; import io.awspring.cloud.autoconfigure.core.CredentialsProviderAutoConfiguration; import io.awspring.cloud.autoconfigure.core.RegionProviderAutoConfiguration; import io.awspring.cloud.s3.DiskBufferingS3OutputStreamProvider; import io.awspring.cloud.s3.ObjectMetadata; +import io.awspring.cloud.s3.S3ObjectConverter; import io.awspring.cloud.s3.S3OutputStream; import io.awspring.cloud.s3.S3OutputStreamProvider; +import io.awspring.cloud.s3.S3Template; import io.awspring.cloud.s3.crossregion.CrossRegionS3Client; import java.io.IOException; import java.net.URI; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.FilteredClassLoader; @@ -36,6 +40,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.lang.Nullable; +import org.springframework.test.util.ReflectionTestUtils; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.S3ClientBuilder; @@ -82,68 +87,141 @@ void autoconfigurationIsNotTriggeredWhenS3ModuleIsNotOnClasspath() { }); } - @Test - void byDefaultCreatesCrossRegionS3Client() { - this.contextRunner - .run(context -> assertThat(context).getBean(S3Client.class).isInstanceOf(CrossRegionS3Client.class)); - } + @Nested + class S3ClientTests { + @Test + void byDefaultCreatesCrossRegionS3Client() { + contextRunner.run( + context -> assertThat(context).getBean(S3Client.class).isInstanceOf(CrossRegionS3Client.class)); + } - @Test - void s3ClientCanBeOverwritten() { - this.contextRunner.withUserConfiguration(CustomS3ClientConfiguration.class).run(context -> { - assertThat(context).hasSingleBean(S3Client.class); - assertThat(context).getBean(S3Client.class).isNotInstanceOf(CrossRegionS3Client.class); - }); - } + @Test + void s3ClientCanBeOverwritten() { + contextRunner.withUserConfiguration(CustomS3ClientConfiguration.class).run(context -> { + assertThat(context).hasSingleBean(S3Client.class); + assertThat(context).getBean(S3Client.class).isNotInstanceOf(CrossRegionS3Client.class); + }); + } - @Test - void byDefaultCreatesDiskBufferingS3OutputStreamProvider() { - this.contextRunner.run(context -> assertThat(context).hasSingleBean(DiskBufferingS3OutputStreamProvider.class)); + @Test + void createsStandardClientWhenCrossRegionModuleIsNotInClasspath() { + contextRunner.withClassLoader(new FilteredClassLoader(CrossRegionS3Client.class)).run(context -> { + assertThat(context).doesNotHaveBean(CrossRegionS3Client.class); + assertThat(context).hasSingleBean(S3Client.class); + }); + } } - @Test - void customS3OutputStreamProviderCanBeConfigured() { - this.contextRunner.withUserConfiguration(CustomS3OutputStreamProviderConfiguration.class) - .run(context -> assertThat(context).hasSingleBean(CustomS3OutputStreamProvider.class)); + @Nested + class OutputStreamProviderTests { + @Test + void byDefaultCreatesDiskBufferingS3OutputStreamProvider() { + contextRunner.run(context -> assertThat(context).hasSingleBean(DiskBufferingS3OutputStreamProvider.class)); + } + + @Test + void customS3OutputStreamProviderCanBeConfigured() { + contextRunner.withUserConfiguration(CustomS3OutputStreamProviderConfiguration.class) + .run(context -> assertThat(context).hasSingleBean(CustomS3OutputStreamProvider.class)); + } } - @Test - void createsStandardClientWhenCrossRegionModuleIsNotInClasspath() { - this.contextRunner.withClassLoader(new FilteredClassLoader(CrossRegionS3Client.class)).run(context -> { - assertThat(context).doesNotHaveBean(CrossRegionS3Client.class); - assertThat(context).hasSingleBean(S3Client.class); - }); + @Nested + class EndpointConfigurationTests { + @Test + void withCustomEndpoint() { + contextRunner.withPropertyValues("spring.cloud.aws.s3.endpoint:http://localhost:8090").run(context -> { + S3ClientBuilder builder = context.getBean(S3ClientBuilder.class); + ConfiguredAwsClient client = new ConfiguredAwsClient(builder.build()); + assertThat(client.getEndpoint()).isEqualTo(URI.create("http://localhost:8090")); + assertThat(client.isEndpointOverridden()).isTrue(); + }); + } + + @Test + void withCustomGlobalEndpoint() { + contextRunner.withPropertyValues("spring.cloud.aws.endpoint:http://localhost:8090").run(context -> { + S3ClientBuilder builder = context.getBean(S3ClientBuilder.class); + ConfiguredAwsClient client = new ConfiguredAwsClient(builder.build()); + assertThat(client.getEndpoint()).isEqualTo(URI.create("http://localhost:8090")); + assertThat(client.isEndpointOverridden()).isTrue(); + }); + } + + @Test + void withCustomGlobalEndpointAndS3Endpoint() { + contextRunner.withPropertyValues("spring.cloud.aws.endpoint:http://localhost:8090", + "spring.cloud.aws.s3.endpoint:http://localhost:9999").run(context -> { + S3ClientBuilder builder = context.getBean(S3ClientBuilder.class); + ConfiguredAwsClient client = new ConfiguredAwsClient(builder.build()); + assertThat(client.getEndpoint()).isEqualTo(URI.create("http://localhost:9999")); + assertThat(client.isEndpointOverridden()).isTrue(); + }); + } } - @Test - void withCustomEndpoint() { - this.contextRunner.withPropertyValues("spring.cloud.aws.s3.endpoint:http://localhost:8090").run(context -> { - S3ClientBuilder builder = context.getBean(S3ClientBuilder.class); - ConfiguredAwsClient client = new ConfiguredAwsClient(builder.build()); - assertThat(client.getEndpoint()).isEqualTo(URI.create("http://localhost:8090")); - assertThat(client.isEndpointOverridden()).isTrue(); - }); + @Nested + class S3TemplateAutoConfigurationTests { + + @Test + void withJacksonOnClasspathAutoconfiguresObjectConverter() { + contextRunner.run(context -> { + assertThat(context).hasSingleBean(S3ObjectConverter.class); + assertThat(context).hasSingleBean(S3Template.class); + }); + } + + @Test + void withoutJacksonOnClasspathDoesNotConfigureObjectConverter() { + contextRunner.withClassLoader(new FilteredClassLoader(ObjectMapper.class)).run(context -> { + assertThat(context).doesNotHaveBean(S3ObjectConverter.class); + assertThat(context).doesNotHaveBean(S3Template.class); + }); + } + + @Test + void usesCustomObjectMapperBean() { + contextRunner.withUserConfiguration(CustomJacksonConfiguration.class).run(context -> { + S3ObjectConverter bean = context.getBean(S3ObjectConverter.class); + ObjectMapper objectMapper = (ObjectMapper) ReflectionTestUtils.getField(bean, "objectMapper"); + assertThat(objectMapper).isEqualTo(context.getBean("customObjectMapper")); + }); + } + + @Test + void usesCustomS3ObjectConverter() { + contextRunner + .withUserConfiguration(CustomJacksonConfiguration.class, CustomS3ObjectConverterConfiguration.class) + .run(context -> { + S3ObjectConverter s3ObjectConverter = context.getBean(S3ObjectConverter.class); + S3ObjectConverter customS3ObjectConverter = (S3ObjectConverter) context + .getBean("customS3ObjectConverter"); + assertThat(s3ObjectConverter).isEqualTo(customS3ObjectConverter); + + S3Template s3Template = context.getBean(S3Template.class); + + S3ObjectConverter converter = (S3ObjectConverter) ReflectionTestUtils.getField(s3Template, + "s3ObjectConverter"); + assertThat(converter).isEqualTo(customS3ObjectConverter); + }); + } } - @Test - void withCustomGlobalEndpoint() { - this.contextRunner.withPropertyValues("spring.cloud.aws.endpoint:http://localhost:8090").run(context -> { - S3ClientBuilder builder = context.getBean(S3ClientBuilder.class); - ConfiguredAwsClient client = new ConfiguredAwsClient(builder.build()); - assertThat(client.getEndpoint()).isEqualTo(URI.create("http://localhost:8090")); - assertThat(client.isEndpointOverridden()).isTrue(); - }); + @Configuration(proxyBeanMethods = false) + static class CustomJacksonConfiguration { + @Bean + ObjectMapper customObjectMapper() { + return new ObjectMapper(); + } } - @Test - void withCustomGlobalEndpointAndS3Endpoint() { - this.contextRunner.withPropertyValues("spring.cloud.aws.endpoint:http://localhost:8090", - "spring.cloud.aws.s3.endpoint:http://localhost:9999").run(context -> { - S3ClientBuilder builder = context.getBean(S3ClientBuilder.class); - ConfiguredAwsClient client = new ConfiguredAwsClient(builder.build()); - assertThat(client.getEndpoint()).isEqualTo(URI.create("http://localhost:9999")); - assertThat(client.isEndpointOverridden()).isTrue(); - }); + @Configuration(proxyBeanMethods = false) + static class CustomS3ObjectConverterConfiguration { + + @Bean + S3ObjectConverter customS3ObjectConverter() { + return mock(S3ObjectConverter.class); + } } @Configuration(proxyBeanMethods = false) @@ -172,7 +250,6 @@ static class CustomS3OutputStreamProvider implements S3OutputStreamProvider { public S3OutputStream create(String bucket, String key, @Nullable ObjectMetadata metadata) throws IOException { return null; } - } } diff --git a/spring-cloud-aws-s3-parent/spring-cloud-aws-s3-cross-region-client/src/main/java/io/awspring/cloud/s3/crossregion/CrossRegionS3Client.java b/spring-cloud-aws-s3-parent/spring-cloud-aws-s3-cross-region-client/src/main/java/io/awspring/cloud/s3/crossregion/CrossRegionS3Client.java index c0d604a42..bbda6fdb8 100644 --- a/spring-cloud-aws-s3-parent/spring-cloud-aws-s3-cross-region-client/src/main/java/io/awspring/cloud/s3/crossregion/CrossRegionS3Client.java +++ b/spring-cloud-aws-s3-parent/spring-cloud-aws-s3-cross-region-client/src/main/java/io/awspring/cloud/s3/crossregion/CrossRegionS3Client.java @@ -38,595 +38,806 @@ public class CrossRegionS3Client implements S3Client { - private static final Logger LOGGER = LoggerFactory.getLogger("io.awspring.cloud.s3.CrossRegionS3Client"); - - private static final int DEFAULT_BUCKET_CACHE_SIZE = 20; - - private final Map clientCache = new ConcurrentHashMap<>(Region.regions().size()); - - private final S3Client defaultS3Client; - - private final ConcurrentLruCache bucketCache; - - public CrossRegionS3Client(S3ClientBuilder clientBuilder) { - this(DEFAULT_BUCKET_CACHE_SIZE, clientBuilder); - } - - public CrossRegionS3Client(int bucketCacheSize, S3ClientBuilder clientBuilder) { - this.defaultS3Client = clientBuilder.build(); - this.bucketCache = new ConcurrentLruCache<>(bucketCacheSize, bucket -> { - Region region = resolveBucketRegion(bucket); - return clientCache.computeIfAbsent(region, r -> { - LOGGER.debug("Creating new S3 client for region: {}", r); - return clientBuilder.region(r).build(); - }); - }); - } - - @Override - public ListBucketsResponse listBuckets(ListBucketsRequest request) throws AwsServiceException, SdkClientException { - return defaultS3Client.listBuckets(request); - } - - @Override - public WriteGetObjectResponseResponse writeGetObjectResponse(WriteGetObjectResponseRequest request, RequestBody requestBody) throws AwsServiceException, SdkClientException { - return defaultS3Client.writeGetObjectResponse(request, requestBody); - } - - // visible for testing - Map getClientCache() { - return clientCache; - } - - private Result executeInBucketRegion(String bucket, Function fn) { - try { - if (bucketCache.contains(bucket)) { - return fn.apply(bucketCache.get(bucket)); - } else { - return fn.apply(defaultS3Client); - } - } catch (S3Exception e) { - if (LOGGER.isTraceEnabled()) { - LOGGER.trace("Exception when requesting S3: {}", e.awsErrorDetails().errorCode(), e); - } else { - LOGGER.debug("Exception when requesting S3 for bucket: {}: [{}] {}", bucket, e.awsErrorDetails().errorCode(), e.awsErrorDetails().errorMessage()); - } - // "PermanentRedirect" means that the bucket is in different region than the - // defaultS3Client is configured for - if ("PermanentRedirect".equals(e.awsErrorDetails().errorCode())) { - return fn.apply(bucketCache.get(bucket)); - } else { - throw e; - } - } - } - - private Region resolveBucketRegion(String bucket) { - LOGGER.debug("Resolving region for bucket {}", bucket); - String bucketLocation = defaultS3Client.getBucketLocation(GetBucketLocationRequest.builder().bucket(bucket).build()).locationConstraintAsString(); - Region region = StringUtils.hasLength(bucketLocation) ? Region.of(bucketLocation) : Region.US_EAST_1; - LOGGER.debug("Region for bucket {} is {}", bucket, region); - return region; - } - - @Override - public String serviceName() { - return S3Client.SERVICE_NAME; - } - - @Override - public void close() { - this.clientCache.values().forEach(SdkAutoCloseable::close); - } - - @Override - public software.amazon.awssdk.services.s3.model.AbortMultipartUploadResponse abortMultipartUpload(software.amazon.awssdk.services.s3.model.AbortMultipartUploadRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.abortMultipartUpload(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.CompleteMultipartUploadResponse completeMultipartUpload(software.amazon.awssdk.services.s3.model.CompleteMultipartUploadRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.completeMultipartUpload(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.CopyObjectResponse copyObject(software.amazon.awssdk.services.s3.model.CopyObjectRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.copyObject(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.CreateBucketResponse createBucket(software.amazon.awssdk.services.s3.model.CreateBucketRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.createBucket(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.CreateMultipartUploadResponse createMultipartUpload(software.amazon.awssdk.services.s3.model.CreateMultipartUploadRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.createMultipartUpload(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.DeleteBucketResponse deleteBucket(software.amazon.awssdk.services.s3.model.DeleteBucketRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.deleteBucket(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.DeleteBucketAnalyticsConfigurationResponse deleteBucketAnalyticsConfiguration(software.amazon.awssdk.services.s3.model.DeleteBucketAnalyticsConfigurationRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.deleteBucketAnalyticsConfiguration(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.DeleteBucketCorsResponse deleteBucketCors(software.amazon.awssdk.services.s3.model.DeleteBucketCorsRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.deleteBucketCors(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.DeleteBucketEncryptionResponse deleteBucketEncryption(software.amazon.awssdk.services.s3.model.DeleteBucketEncryptionRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.deleteBucketEncryption(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.DeleteBucketIntelligentTieringConfigurationResponse deleteBucketIntelligentTieringConfiguration(software.amazon.awssdk.services.s3.model.DeleteBucketIntelligentTieringConfigurationRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.deleteBucketIntelligentTieringConfiguration(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.DeleteBucketInventoryConfigurationResponse deleteBucketInventoryConfiguration(software.amazon.awssdk.services.s3.model.DeleteBucketInventoryConfigurationRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.deleteBucketInventoryConfiguration(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.DeleteBucketLifecycleResponse deleteBucketLifecycle(software.amazon.awssdk.services.s3.model.DeleteBucketLifecycleRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.deleteBucketLifecycle(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.DeleteBucketMetricsConfigurationResponse deleteBucketMetricsConfiguration(software.amazon.awssdk.services.s3.model.DeleteBucketMetricsConfigurationRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.deleteBucketMetricsConfiguration(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.DeleteBucketOwnershipControlsResponse deleteBucketOwnershipControls(software.amazon.awssdk.services.s3.model.DeleteBucketOwnershipControlsRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.deleteBucketOwnershipControls(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.DeleteBucketPolicyResponse deleteBucketPolicy(software.amazon.awssdk.services.s3.model.DeleteBucketPolicyRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.deleteBucketPolicy(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.DeleteBucketReplicationResponse deleteBucketReplication(software.amazon.awssdk.services.s3.model.DeleteBucketReplicationRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.deleteBucketReplication(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.DeleteBucketTaggingResponse deleteBucketTagging(software.amazon.awssdk.services.s3.model.DeleteBucketTaggingRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.deleteBucketTagging(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.DeleteBucketWebsiteResponse deleteBucketWebsite(software.amazon.awssdk.services.s3.model.DeleteBucketWebsiteRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.deleteBucketWebsite(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.DeleteObjectResponse deleteObject(software.amazon.awssdk.services.s3.model.DeleteObjectRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.deleteObject(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.DeleteObjectTaggingResponse deleteObjectTagging(software.amazon.awssdk.services.s3.model.DeleteObjectTaggingRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.deleteObjectTagging(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.DeleteObjectsResponse deleteObjects(software.amazon.awssdk.services.s3.model.DeleteObjectsRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.deleteObjects(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.DeletePublicAccessBlockResponse deletePublicAccessBlock(software.amazon.awssdk.services.s3.model.DeletePublicAccessBlockRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.deletePublicAccessBlock(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.GetBucketAccelerateConfigurationResponse getBucketAccelerateConfiguration(software.amazon.awssdk.services.s3.model.GetBucketAccelerateConfigurationRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getBucketAccelerateConfiguration(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.GetBucketAclResponse getBucketAcl(software.amazon.awssdk.services.s3.model.GetBucketAclRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getBucketAcl(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.GetBucketAnalyticsConfigurationResponse getBucketAnalyticsConfiguration(software.amazon.awssdk.services.s3.model.GetBucketAnalyticsConfigurationRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getBucketAnalyticsConfiguration(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.GetBucketCorsResponse getBucketCors(software.amazon.awssdk.services.s3.model.GetBucketCorsRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getBucketCors(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.GetBucketEncryptionResponse getBucketEncryption(software.amazon.awssdk.services.s3.model.GetBucketEncryptionRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getBucketEncryption(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.GetBucketIntelligentTieringConfigurationResponse getBucketIntelligentTieringConfiguration(software.amazon.awssdk.services.s3.model.GetBucketIntelligentTieringConfigurationRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getBucketIntelligentTieringConfiguration(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.GetBucketInventoryConfigurationResponse getBucketInventoryConfiguration(software.amazon.awssdk.services.s3.model.GetBucketInventoryConfigurationRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getBucketInventoryConfiguration(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.GetBucketLifecycleConfigurationResponse getBucketLifecycleConfiguration(software.amazon.awssdk.services.s3.model.GetBucketLifecycleConfigurationRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getBucketLifecycleConfiguration(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.GetBucketLocationResponse getBucketLocation(software.amazon.awssdk.services.s3.model.GetBucketLocationRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getBucketLocation(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.GetBucketLoggingResponse getBucketLogging(software.amazon.awssdk.services.s3.model.GetBucketLoggingRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getBucketLogging(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.GetBucketMetricsConfigurationResponse getBucketMetricsConfiguration(software.amazon.awssdk.services.s3.model.GetBucketMetricsConfigurationRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getBucketMetricsConfiguration(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.GetBucketNotificationConfigurationResponse getBucketNotificationConfiguration(software.amazon.awssdk.services.s3.model.GetBucketNotificationConfigurationRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getBucketNotificationConfiguration(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.GetBucketOwnershipControlsResponse getBucketOwnershipControls(software.amazon.awssdk.services.s3.model.GetBucketOwnershipControlsRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getBucketOwnershipControls(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.GetBucketPolicyResponse getBucketPolicy(software.amazon.awssdk.services.s3.model.GetBucketPolicyRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getBucketPolicy(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.GetBucketPolicyStatusResponse getBucketPolicyStatus(software.amazon.awssdk.services.s3.model.GetBucketPolicyStatusRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getBucketPolicyStatus(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.GetBucketReplicationResponse getBucketReplication(software.amazon.awssdk.services.s3.model.GetBucketReplicationRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getBucketReplication(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.GetBucketRequestPaymentResponse getBucketRequestPayment(software.amazon.awssdk.services.s3.model.GetBucketRequestPaymentRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getBucketRequestPayment(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.GetBucketTaggingResponse getBucketTagging(software.amazon.awssdk.services.s3.model.GetBucketTaggingRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getBucketTagging(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.GetBucketVersioningResponse getBucketVersioning(software.amazon.awssdk.services.s3.model.GetBucketVersioningRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getBucketVersioning(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.GetBucketWebsiteResponse getBucketWebsite(software.amazon.awssdk.services.s3.model.GetBucketWebsiteRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getBucketWebsite(p0)); - } - - @Override - public software.amazon.awssdk.core.ResponseInputStream getObject(software.amazon.awssdk.services.s3.model.GetObjectRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getObject(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.GetObjectResponse getObject(software.amazon.awssdk.services.s3.model.GetObjectRequest p0, java.nio.file.Path p1) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getObject(p0, p1)); - } - - @Override - public ReturnT getObject(software.amazon.awssdk.services.s3.model.GetObjectRequest p0, software.amazon.awssdk.core.sync.ResponseTransformer p1) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getObject(p0, p1)); - } - - @Override - public software.amazon.awssdk.services.s3.model.GetObjectAclResponse getObjectAcl(software.amazon.awssdk.services.s3.model.GetObjectAclRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getObjectAcl(p0)); - } - - @Override - public software.amazon.awssdk.core.ResponseBytes getObjectAsBytes(software.amazon.awssdk.services.s3.model.GetObjectRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getObjectAsBytes(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.GetObjectAttributesResponse getObjectAttributes(software.amazon.awssdk.services.s3.model.GetObjectAttributesRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getObjectAttributes(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.GetObjectLegalHoldResponse getObjectLegalHold(software.amazon.awssdk.services.s3.model.GetObjectLegalHoldRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getObjectLegalHold(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.GetObjectLockConfigurationResponse getObjectLockConfiguration(software.amazon.awssdk.services.s3.model.GetObjectLockConfigurationRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getObjectLockConfiguration(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.GetObjectRetentionResponse getObjectRetention(software.amazon.awssdk.services.s3.model.GetObjectRetentionRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getObjectRetention(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.GetObjectTaggingResponse getObjectTagging(software.amazon.awssdk.services.s3.model.GetObjectTaggingRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getObjectTagging(p0)); - } - - @Override - public software.amazon.awssdk.core.ResponseInputStream getObjectTorrent(software.amazon.awssdk.services.s3.model.GetObjectTorrentRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getObjectTorrent(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.GetObjectTorrentResponse getObjectTorrent(software.amazon.awssdk.services.s3.model.GetObjectTorrentRequest p0, java.nio.file.Path p1) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getObjectTorrent(p0, p1)); - } - - @Override - public ReturnT getObjectTorrent(software.amazon.awssdk.services.s3.model.GetObjectTorrentRequest p0, software.amazon.awssdk.core.sync.ResponseTransformer p1) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getObjectTorrent(p0, p1)); - } - - @Override - public software.amazon.awssdk.core.ResponseBytes getObjectTorrentAsBytes(software.amazon.awssdk.services.s3.model.GetObjectTorrentRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getObjectTorrentAsBytes(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.GetPublicAccessBlockResponse getPublicAccessBlock(software.amazon.awssdk.services.s3.model.GetPublicAccessBlockRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getPublicAccessBlock(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.HeadBucketResponse headBucket(software.amazon.awssdk.services.s3.model.HeadBucketRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.headBucket(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.HeadObjectResponse headObject(software.amazon.awssdk.services.s3.model.HeadObjectRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.headObject(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.ListBucketAnalyticsConfigurationsResponse listBucketAnalyticsConfigurations(software.amazon.awssdk.services.s3.model.ListBucketAnalyticsConfigurationsRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.listBucketAnalyticsConfigurations(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.ListBucketIntelligentTieringConfigurationsResponse listBucketIntelligentTieringConfigurations(software.amazon.awssdk.services.s3.model.ListBucketIntelligentTieringConfigurationsRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.listBucketIntelligentTieringConfigurations(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.ListBucketInventoryConfigurationsResponse listBucketInventoryConfigurations(software.amazon.awssdk.services.s3.model.ListBucketInventoryConfigurationsRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.listBucketInventoryConfigurations(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.ListBucketMetricsConfigurationsResponse listBucketMetricsConfigurations(software.amazon.awssdk.services.s3.model.ListBucketMetricsConfigurationsRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.listBucketMetricsConfigurations(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.ListMultipartUploadsResponse listMultipartUploads(software.amazon.awssdk.services.s3.model.ListMultipartUploadsRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.listMultipartUploads(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.paginators.ListMultipartUploadsIterable listMultipartUploadsPaginator(software.amazon.awssdk.services.s3.model.ListMultipartUploadsRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.listMultipartUploadsPaginator(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.ListObjectVersionsResponse listObjectVersions(software.amazon.awssdk.services.s3.model.ListObjectVersionsRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.listObjectVersions(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.paginators.ListObjectVersionsIterable listObjectVersionsPaginator(software.amazon.awssdk.services.s3.model.ListObjectVersionsRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.listObjectVersionsPaginator(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.ListObjectsResponse listObjects(software.amazon.awssdk.services.s3.model.ListObjectsRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.listObjects(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.ListObjectsV2Response listObjectsV2(software.amazon.awssdk.services.s3.model.ListObjectsV2Request p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.listObjectsV2(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.paginators.ListObjectsV2Iterable listObjectsV2Paginator(software.amazon.awssdk.services.s3.model.ListObjectsV2Request p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.listObjectsV2Paginator(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.ListPartsResponse listParts(software.amazon.awssdk.services.s3.model.ListPartsRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.listParts(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.paginators.ListPartsIterable listPartsPaginator(software.amazon.awssdk.services.s3.model.ListPartsRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.listPartsPaginator(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.PutBucketAccelerateConfigurationResponse putBucketAccelerateConfiguration(software.amazon.awssdk.services.s3.model.PutBucketAccelerateConfigurationRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.putBucketAccelerateConfiguration(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.PutBucketAclResponse putBucketAcl(software.amazon.awssdk.services.s3.model.PutBucketAclRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.putBucketAcl(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.PutBucketAnalyticsConfigurationResponse putBucketAnalyticsConfiguration(software.amazon.awssdk.services.s3.model.PutBucketAnalyticsConfigurationRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.putBucketAnalyticsConfiguration(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.PutBucketCorsResponse putBucketCors(software.amazon.awssdk.services.s3.model.PutBucketCorsRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.putBucketCors(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.PutBucketEncryptionResponse putBucketEncryption(software.amazon.awssdk.services.s3.model.PutBucketEncryptionRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.putBucketEncryption(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.PutBucketIntelligentTieringConfigurationResponse putBucketIntelligentTieringConfiguration(software.amazon.awssdk.services.s3.model.PutBucketIntelligentTieringConfigurationRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.putBucketIntelligentTieringConfiguration(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.PutBucketInventoryConfigurationResponse putBucketInventoryConfiguration(software.amazon.awssdk.services.s3.model.PutBucketInventoryConfigurationRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.putBucketInventoryConfiguration(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.PutBucketLifecycleConfigurationResponse putBucketLifecycleConfiguration(software.amazon.awssdk.services.s3.model.PutBucketLifecycleConfigurationRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.putBucketLifecycleConfiguration(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.PutBucketLoggingResponse putBucketLogging(software.amazon.awssdk.services.s3.model.PutBucketLoggingRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.putBucketLogging(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.PutBucketMetricsConfigurationResponse putBucketMetricsConfiguration(software.amazon.awssdk.services.s3.model.PutBucketMetricsConfigurationRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.putBucketMetricsConfiguration(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.PutBucketNotificationConfigurationResponse putBucketNotificationConfiguration(software.amazon.awssdk.services.s3.model.PutBucketNotificationConfigurationRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.putBucketNotificationConfiguration(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.PutBucketOwnershipControlsResponse putBucketOwnershipControls(software.amazon.awssdk.services.s3.model.PutBucketOwnershipControlsRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.putBucketOwnershipControls(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.PutBucketPolicyResponse putBucketPolicy(software.amazon.awssdk.services.s3.model.PutBucketPolicyRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.putBucketPolicy(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.PutBucketReplicationResponse putBucketReplication(software.amazon.awssdk.services.s3.model.PutBucketReplicationRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.putBucketReplication(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.PutBucketRequestPaymentResponse putBucketRequestPayment(software.amazon.awssdk.services.s3.model.PutBucketRequestPaymentRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.putBucketRequestPayment(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.PutBucketTaggingResponse putBucketTagging(software.amazon.awssdk.services.s3.model.PutBucketTaggingRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.putBucketTagging(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.PutBucketVersioningResponse putBucketVersioning(software.amazon.awssdk.services.s3.model.PutBucketVersioningRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.putBucketVersioning(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.PutBucketWebsiteResponse putBucketWebsite(software.amazon.awssdk.services.s3.model.PutBucketWebsiteRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.putBucketWebsite(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.PutObjectResponse putObject(software.amazon.awssdk.services.s3.model.PutObjectRequest p0, java.nio.file.Path p1) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.putObject(p0, p1)); - } - - @Override - public software.amazon.awssdk.services.s3.model.PutObjectResponse putObject(software.amazon.awssdk.services.s3.model.PutObjectRequest p0, software.amazon.awssdk.core.sync.RequestBody p1) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.putObject(p0, p1)); - } - - @Override - public software.amazon.awssdk.services.s3.model.PutObjectAclResponse putObjectAcl(software.amazon.awssdk.services.s3.model.PutObjectAclRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.putObjectAcl(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.PutObjectLegalHoldResponse putObjectLegalHold(software.amazon.awssdk.services.s3.model.PutObjectLegalHoldRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.putObjectLegalHold(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.PutObjectLockConfigurationResponse putObjectLockConfiguration(software.amazon.awssdk.services.s3.model.PutObjectLockConfigurationRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.putObjectLockConfiguration(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.PutObjectRetentionResponse putObjectRetention(software.amazon.awssdk.services.s3.model.PutObjectRetentionRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.putObjectRetention(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.PutObjectTaggingResponse putObjectTagging(software.amazon.awssdk.services.s3.model.PutObjectTaggingRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.putObjectTagging(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.PutPublicAccessBlockResponse putPublicAccessBlock(software.amazon.awssdk.services.s3.model.PutPublicAccessBlockRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.putPublicAccessBlock(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.RestoreObjectResponse restoreObject(software.amazon.awssdk.services.s3.model.RestoreObjectRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.restoreObject(p0)); - } - - @Override - public software.amazon.awssdk.services.s3.model.UploadPartResponse uploadPart(software.amazon.awssdk.services.s3.model.UploadPartRequest p0, java.nio.file.Path p1) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.uploadPart(p0, p1)); - } - - @Override - public software.amazon.awssdk.services.s3.model.UploadPartResponse uploadPart(software.amazon.awssdk.services.s3.model.UploadPartRequest p0, software.amazon.awssdk.core.sync.RequestBody p1) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.uploadPart(p0, p1)); - } - - @Override - public software.amazon.awssdk.services.s3.model.UploadPartCopyResponse uploadPartCopy(software.amazon.awssdk.services.s3.model.UploadPartCopyRequest p0) throws AwsServiceException, SdkClientException { - return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.uploadPartCopy(p0)); - } + private static final Logger LOGGER = LoggerFactory.getLogger("io.awspring.cloud.s3.CrossRegionS3Client"); + + private static final int DEFAULT_BUCKET_CACHE_SIZE = 20; + + private final Map clientCache = new ConcurrentHashMap<>(Region.regions().size()); + + private final S3Client defaultS3Client; + + private final ConcurrentLruCache bucketCache; + + public CrossRegionS3Client(S3ClientBuilder clientBuilder) { + this(DEFAULT_BUCKET_CACHE_SIZE, clientBuilder); + } + + public CrossRegionS3Client(int bucketCacheSize, S3ClientBuilder clientBuilder) { + this.defaultS3Client = clientBuilder.build(); + this.bucketCache = new ConcurrentLruCache<>(bucketCacheSize, bucket -> { + Region region = resolveBucketRegion(bucket); + return clientCache.computeIfAbsent(region, r -> { + LOGGER.debug("Creating new S3 client for region: {}", r); + return clientBuilder.region(r).build(); + }); + }); + } + + @Override + public ListBucketsResponse listBuckets(ListBucketsRequest request) throws AwsServiceException, SdkClientException { + return defaultS3Client.listBuckets(request); + } + + @Override + public WriteGetObjectResponseResponse writeGetObjectResponse(WriteGetObjectResponseRequest request, + RequestBody requestBody) throws AwsServiceException, SdkClientException { + return defaultS3Client.writeGetObjectResponse(request, requestBody); + } + + // visible for testing + Map getClientCache() { + return clientCache; + } + + private Result executeInBucketRegion(String bucket, Function fn) { + try { + if (bucketCache.contains(bucket)) { + return fn.apply(bucketCache.get(bucket)); + } + else { + return fn.apply(defaultS3Client); + } + } + catch (S3Exception e) { + if (LOGGER.isTraceEnabled()) { + LOGGER.trace("Exception when requesting S3: {}", e.awsErrorDetails().errorCode(), e); + } + else { + LOGGER.debug("Exception when requesting S3 for bucket: {}: [{}] {}", bucket, + e.awsErrorDetails().errorCode(), e.awsErrorDetails().errorMessage()); + } + // "PermanentRedirect" means that the bucket is in different region than the + // defaultS3Client is configured for + if ("PermanentRedirect".equals(e.awsErrorDetails().errorCode())) { + return fn.apply(bucketCache.get(bucket)); + } + else { + throw e; + } + } + } + + private Region resolveBucketRegion(String bucket) { + LOGGER.debug("Resolving region for bucket {}", bucket); + String bucketLocation = defaultS3Client + .getBucketLocation(GetBucketLocationRequest.builder().bucket(bucket).build()) + .locationConstraintAsString(); + Region region = StringUtils.hasLength(bucketLocation) ? Region.of(bucketLocation) : Region.US_EAST_1; + LOGGER.debug("Region for bucket {} is {}", bucket, region); + return region; + } + + @Override + public String serviceName() { + return S3Client.SERVICE_NAME; + } + + @Override + public void close() { + this.clientCache.values().forEach(SdkAutoCloseable::close); + } + + @Override + public software.amazon.awssdk.services.s3.model.AbortMultipartUploadResponse abortMultipartUpload( + software.amazon.awssdk.services.s3.model.AbortMultipartUploadRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.abortMultipartUpload(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.CompleteMultipartUploadResponse completeMultipartUpload( + software.amazon.awssdk.services.s3.model.CompleteMultipartUploadRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.completeMultipartUpload(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.CopyObjectResponse copyObject( + software.amazon.awssdk.services.s3.model.CopyObjectRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.copyObject(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.CreateBucketResponse createBucket( + software.amazon.awssdk.services.s3.model.CreateBucketRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.createBucket(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.CreateMultipartUploadResponse createMultipartUpload( + software.amazon.awssdk.services.s3.model.CreateMultipartUploadRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.createMultipartUpload(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.DeleteBucketResponse deleteBucket( + software.amazon.awssdk.services.s3.model.DeleteBucketRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.deleteBucket(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.DeleteBucketAnalyticsConfigurationResponse deleteBucketAnalyticsConfiguration( + software.amazon.awssdk.services.s3.model.DeleteBucketAnalyticsConfigurationRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.deleteBucketAnalyticsConfiguration(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.DeleteBucketCorsResponse deleteBucketCors( + software.amazon.awssdk.services.s3.model.DeleteBucketCorsRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.deleteBucketCors(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.DeleteBucketEncryptionResponse deleteBucketEncryption( + software.amazon.awssdk.services.s3.model.DeleteBucketEncryptionRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.deleteBucketEncryption(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.DeleteBucketIntelligentTieringConfigurationResponse deleteBucketIntelligentTieringConfiguration( + software.amazon.awssdk.services.s3.model.DeleteBucketIntelligentTieringConfigurationRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.deleteBucketIntelligentTieringConfiguration(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.DeleteBucketInventoryConfigurationResponse deleteBucketInventoryConfiguration( + software.amazon.awssdk.services.s3.model.DeleteBucketInventoryConfigurationRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.deleteBucketInventoryConfiguration(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.DeleteBucketLifecycleResponse deleteBucketLifecycle( + software.amazon.awssdk.services.s3.model.DeleteBucketLifecycleRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.deleteBucketLifecycle(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.DeleteBucketMetricsConfigurationResponse deleteBucketMetricsConfiguration( + software.amazon.awssdk.services.s3.model.DeleteBucketMetricsConfigurationRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.deleteBucketMetricsConfiguration(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.DeleteBucketOwnershipControlsResponse deleteBucketOwnershipControls( + software.amazon.awssdk.services.s3.model.DeleteBucketOwnershipControlsRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.deleteBucketOwnershipControls(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.DeleteBucketPolicyResponse deleteBucketPolicy( + software.amazon.awssdk.services.s3.model.DeleteBucketPolicyRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.deleteBucketPolicy(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.DeleteBucketReplicationResponse deleteBucketReplication( + software.amazon.awssdk.services.s3.model.DeleteBucketReplicationRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.deleteBucketReplication(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.DeleteBucketTaggingResponse deleteBucketTagging( + software.amazon.awssdk.services.s3.model.DeleteBucketTaggingRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.deleteBucketTagging(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.DeleteBucketWebsiteResponse deleteBucketWebsite( + software.amazon.awssdk.services.s3.model.DeleteBucketWebsiteRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.deleteBucketWebsite(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.DeleteObjectResponse deleteObject( + software.amazon.awssdk.services.s3.model.DeleteObjectRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.deleteObject(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.DeleteObjectTaggingResponse deleteObjectTagging( + software.amazon.awssdk.services.s3.model.DeleteObjectTaggingRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.deleteObjectTagging(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.DeleteObjectsResponse deleteObjects( + software.amazon.awssdk.services.s3.model.DeleteObjectsRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.deleteObjects(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.DeletePublicAccessBlockResponse deletePublicAccessBlock( + software.amazon.awssdk.services.s3.model.DeletePublicAccessBlockRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.deletePublicAccessBlock(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.GetBucketAccelerateConfigurationResponse getBucketAccelerateConfiguration( + software.amazon.awssdk.services.s3.model.GetBucketAccelerateConfigurationRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getBucketAccelerateConfiguration(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.GetBucketAclResponse getBucketAcl( + software.amazon.awssdk.services.s3.model.GetBucketAclRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getBucketAcl(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.GetBucketAnalyticsConfigurationResponse getBucketAnalyticsConfiguration( + software.amazon.awssdk.services.s3.model.GetBucketAnalyticsConfigurationRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getBucketAnalyticsConfiguration(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.GetBucketCorsResponse getBucketCors( + software.amazon.awssdk.services.s3.model.GetBucketCorsRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getBucketCors(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.GetBucketEncryptionResponse getBucketEncryption( + software.amazon.awssdk.services.s3.model.GetBucketEncryptionRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getBucketEncryption(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.GetBucketIntelligentTieringConfigurationResponse getBucketIntelligentTieringConfiguration( + software.amazon.awssdk.services.s3.model.GetBucketIntelligentTieringConfigurationRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getBucketIntelligentTieringConfiguration(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.GetBucketInventoryConfigurationResponse getBucketInventoryConfiguration( + software.amazon.awssdk.services.s3.model.GetBucketInventoryConfigurationRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getBucketInventoryConfiguration(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.GetBucketLifecycleConfigurationResponse getBucketLifecycleConfiguration( + software.amazon.awssdk.services.s3.model.GetBucketLifecycleConfigurationRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getBucketLifecycleConfiguration(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.GetBucketLocationResponse getBucketLocation( + software.amazon.awssdk.services.s3.model.GetBucketLocationRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getBucketLocation(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.GetBucketLoggingResponse getBucketLogging( + software.amazon.awssdk.services.s3.model.GetBucketLoggingRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getBucketLogging(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.GetBucketMetricsConfigurationResponse getBucketMetricsConfiguration( + software.amazon.awssdk.services.s3.model.GetBucketMetricsConfigurationRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getBucketMetricsConfiguration(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.GetBucketNotificationConfigurationResponse getBucketNotificationConfiguration( + software.amazon.awssdk.services.s3.model.GetBucketNotificationConfigurationRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getBucketNotificationConfiguration(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.GetBucketOwnershipControlsResponse getBucketOwnershipControls( + software.amazon.awssdk.services.s3.model.GetBucketOwnershipControlsRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getBucketOwnershipControls(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.GetBucketPolicyResponse getBucketPolicy( + software.amazon.awssdk.services.s3.model.GetBucketPolicyRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getBucketPolicy(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.GetBucketPolicyStatusResponse getBucketPolicyStatus( + software.amazon.awssdk.services.s3.model.GetBucketPolicyStatusRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getBucketPolicyStatus(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.GetBucketReplicationResponse getBucketReplication( + software.amazon.awssdk.services.s3.model.GetBucketReplicationRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getBucketReplication(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.GetBucketRequestPaymentResponse getBucketRequestPayment( + software.amazon.awssdk.services.s3.model.GetBucketRequestPaymentRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getBucketRequestPayment(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.GetBucketTaggingResponse getBucketTagging( + software.amazon.awssdk.services.s3.model.GetBucketTaggingRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getBucketTagging(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.GetBucketVersioningResponse getBucketVersioning( + software.amazon.awssdk.services.s3.model.GetBucketVersioningRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getBucketVersioning(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.GetBucketWebsiteResponse getBucketWebsite( + software.amazon.awssdk.services.s3.model.GetBucketWebsiteRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getBucketWebsite(p0)); + } + + @Override + public software.amazon.awssdk.core.ResponseInputStream getObject( + software.amazon.awssdk.services.s3.model.GetObjectRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getObject(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.GetObjectResponse getObject( + software.amazon.awssdk.services.s3.model.GetObjectRequest p0, java.nio.file.Path p1) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getObject(p0, p1)); + } + + @Override + public ReturnT getObject(software.amazon.awssdk.services.s3.model.GetObjectRequest p0, + software.amazon.awssdk.core.sync.ResponseTransformer p1) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getObject(p0, p1)); + } + + @Override + public software.amazon.awssdk.services.s3.model.GetObjectAclResponse getObjectAcl( + software.amazon.awssdk.services.s3.model.GetObjectAclRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getObjectAcl(p0)); + } + + @Override + public software.amazon.awssdk.core.ResponseBytes getObjectAsBytes( + software.amazon.awssdk.services.s3.model.GetObjectRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getObjectAsBytes(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.GetObjectAttributesResponse getObjectAttributes( + software.amazon.awssdk.services.s3.model.GetObjectAttributesRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getObjectAttributes(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.GetObjectLegalHoldResponse getObjectLegalHold( + software.amazon.awssdk.services.s3.model.GetObjectLegalHoldRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getObjectLegalHold(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.GetObjectLockConfigurationResponse getObjectLockConfiguration( + software.amazon.awssdk.services.s3.model.GetObjectLockConfigurationRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getObjectLockConfiguration(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.GetObjectRetentionResponse getObjectRetention( + software.amazon.awssdk.services.s3.model.GetObjectRetentionRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getObjectRetention(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.GetObjectTaggingResponse getObjectTagging( + software.amazon.awssdk.services.s3.model.GetObjectTaggingRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getObjectTagging(p0)); + } + + @Override + public software.amazon.awssdk.core.ResponseInputStream getObjectTorrent( + software.amazon.awssdk.services.s3.model.GetObjectTorrentRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getObjectTorrent(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.GetObjectTorrentResponse getObjectTorrent( + software.amazon.awssdk.services.s3.model.GetObjectTorrentRequest p0, java.nio.file.Path p1) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getObjectTorrent(p0, p1)); + } + + @Override + public ReturnT getObjectTorrent(software.amazon.awssdk.services.s3.model.GetObjectTorrentRequest p0, + software.amazon.awssdk.core.sync.ResponseTransformer p1) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getObjectTorrent(p0, p1)); + } + + @Override + public software.amazon.awssdk.core.ResponseBytes getObjectTorrentAsBytes( + software.amazon.awssdk.services.s3.model.GetObjectTorrentRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getObjectTorrentAsBytes(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.GetPublicAccessBlockResponse getPublicAccessBlock( + software.amazon.awssdk.services.s3.model.GetPublicAccessBlockRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.getPublicAccessBlock(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.HeadBucketResponse headBucket( + software.amazon.awssdk.services.s3.model.HeadBucketRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.headBucket(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.HeadObjectResponse headObject( + software.amazon.awssdk.services.s3.model.HeadObjectRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.headObject(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.ListBucketAnalyticsConfigurationsResponse listBucketAnalyticsConfigurations( + software.amazon.awssdk.services.s3.model.ListBucketAnalyticsConfigurationsRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.listBucketAnalyticsConfigurations(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.ListBucketIntelligentTieringConfigurationsResponse listBucketIntelligentTieringConfigurations( + software.amazon.awssdk.services.s3.model.ListBucketIntelligentTieringConfigurationsRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.listBucketIntelligentTieringConfigurations(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.ListBucketInventoryConfigurationsResponse listBucketInventoryConfigurations( + software.amazon.awssdk.services.s3.model.ListBucketInventoryConfigurationsRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.listBucketInventoryConfigurations(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.ListBucketMetricsConfigurationsResponse listBucketMetricsConfigurations( + software.amazon.awssdk.services.s3.model.ListBucketMetricsConfigurationsRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.listBucketMetricsConfigurations(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.ListMultipartUploadsResponse listMultipartUploads( + software.amazon.awssdk.services.s3.model.ListMultipartUploadsRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.listMultipartUploads(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.paginators.ListMultipartUploadsIterable listMultipartUploadsPaginator( + software.amazon.awssdk.services.s3.model.ListMultipartUploadsRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.listMultipartUploadsPaginator(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.ListObjectVersionsResponse listObjectVersions( + software.amazon.awssdk.services.s3.model.ListObjectVersionsRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.listObjectVersions(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.paginators.ListObjectVersionsIterable listObjectVersionsPaginator( + software.amazon.awssdk.services.s3.model.ListObjectVersionsRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.listObjectVersionsPaginator(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.ListObjectsResponse listObjects( + software.amazon.awssdk.services.s3.model.ListObjectsRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.listObjects(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.ListObjectsV2Response listObjectsV2( + software.amazon.awssdk.services.s3.model.ListObjectsV2Request p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.listObjectsV2(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.paginators.ListObjectsV2Iterable listObjectsV2Paginator( + software.amazon.awssdk.services.s3.model.ListObjectsV2Request p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.listObjectsV2Paginator(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.ListPartsResponse listParts( + software.amazon.awssdk.services.s3.model.ListPartsRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.listParts(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.paginators.ListPartsIterable listPartsPaginator( + software.amazon.awssdk.services.s3.model.ListPartsRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.listPartsPaginator(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.PutBucketAccelerateConfigurationResponse putBucketAccelerateConfiguration( + software.amazon.awssdk.services.s3.model.PutBucketAccelerateConfigurationRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.putBucketAccelerateConfiguration(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.PutBucketAclResponse putBucketAcl( + software.amazon.awssdk.services.s3.model.PutBucketAclRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.putBucketAcl(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.PutBucketAnalyticsConfigurationResponse putBucketAnalyticsConfiguration( + software.amazon.awssdk.services.s3.model.PutBucketAnalyticsConfigurationRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.putBucketAnalyticsConfiguration(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.PutBucketCorsResponse putBucketCors( + software.amazon.awssdk.services.s3.model.PutBucketCorsRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.putBucketCors(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.PutBucketEncryptionResponse putBucketEncryption( + software.amazon.awssdk.services.s3.model.PutBucketEncryptionRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.putBucketEncryption(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.PutBucketIntelligentTieringConfigurationResponse putBucketIntelligentTieringConfiguration( + software.amazon.awssdk.services.s3.model.PutBucketIntelligentTieringConfigurationRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.putBucketIntelligentTieringConfiguration(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.PutBucketInventoryConfigurationResponse putBucketInventoryConfiguration( + software.amazon.awssdk.services.s3.model.PutBucketInventoryConfigurationRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.putBucketInventoryConfiguration(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.PutBucketLifecycleConfigurationResponse putBucketLifecycleConfiguration( + software.amazon.awssdk.services.s3.model.PutBucketLifecycleConfigurationRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.putBucketLifecycleConfiguration(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.PutBucketLoggingResponse putBucketLogging( + software.amazon.awssdk.services.s3.model.PutBucketLoggingRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.putBucketLogging(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.PutBucketMetricsConfigurationResponse putBucketMetricsConfiguration( + software.amazon.awssdk.services.s3.model.PutBucketMetricsConfigurationRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.putBucketMetricsConfiguration(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.PutBucketNotificationConfigurationResponse putBucketNotificationConfiguration( + software.amazon.awssdk.services.s3.model.PutBucketNotificationConfigurationRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.putBucketNotificationConfiguration(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.PutBucketOwnershipControlsResponse putBucketOwnershipControls( + software.amazon.awssdk.services.s3.model.PutBucketOwnershipControlsRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.putBucketOwnershipControls(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.PutBucketPolicyResponse putBucketPolicy( + software.amazon.awssdk.services.s3.model.PutBucketPolicyRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.putBucketPolicy(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.PutBucketReplicationResponse putBucketReplication( + software.amazon.awssdk.services.s3.model.PutBucketReplicationRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.putBucketReplication(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.PutBucketRequestPaymentResponse putBucketRequestPayment( + software.amazon.awssdk.services.s3.model.PutBucketRequestPaymentRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.putBucketRequestPayment(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.PutBucketTaggingResponse putBucketTagging( + software.amazon.awssdk.services.s3.model.PutBucketTaggingRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.putBucketTagging(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.PutBucketVersioningResponse putBucketVersioning( + software.amazon.awssdk.services.s3.model.PutBucketVersioningRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.putBucketVersioning(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.PutBucketWebsiteResponse putBucketWebsite( + software.amazon.awssdk.services.s3.model.PutBucketWebsiteRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.putBucketWebsite(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.PutObjectResponse putObject( + software.amazon.awssdk.services.s3.model.PutObjectRequest p0, java.nio.file.Path p1) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.putObject(p0, p1)); + } + + @Override + public software.amazon.awssdk.services.s3.model.PutObjectResponse putObject( + software.amazon.awssdk.services.s3.model.PutObjectRequest p0, + software.amazon.awssdk.core.sync.RequestBody p1) throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.putObject(p0, p1)); + } + + @Override + public software.amazon.awssdk.services.s3.model.PutObjectAclResponse putObjectAcl( + software.amazon.awssdk.services.s3.model.PutObjectAclRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.putObjectAcl(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.PutObjectLegalHoldResponse putObjectLegalHold( + software.amazon.awssdk.services.s3.model.PutObjectLegalHoldRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.putObjectLegalHold(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.PutObjectLockConfigurationResponse putObjectLockConfiguration( + software.amazon.awssdk.services.s3.model.PutObjectLockConfigurationRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.putObjectLockConfiguration(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.PutObjectRetentionResponse putObjectRetention( + software.amazon.awssdk.services.s3.model.PutObjectRetentionRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.putObjectRetention(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.PutObjectTaggingResponse putObjectTagging( + software.amazon.awssdk.services.s3.model.PutObjectTaggingRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.putObjectTagging(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.PutPublicAccessBlockResponse putPublicAccessBlock( + software.amazon.awssdk.services.s3.model.PutPublicAccessBlockRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.putPublicAccessBlock(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.RestoreObjectResponse restoreObject( + software.amazon.awssdk.services.s3.model.RestoreObjectRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.restoreObject(p0)); + } + + @Override + public software.amazon.awssdk.services.s3.model.UploadPartResponse uploadPart( + software.amazon.awssdk.services.s3.model.UploadPartRequest p0, java.nio.file.Path p1) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.uploadPart(p0, p1)); + } + + @Override + public software.amazon.awssdk.services.s3.model.UploadPartResponse uploadPart( + software.amazon.awssdk.services.s3.model.UploadPartRequest p0, + software.amazon.awssdk.core.sync.RequestBody p1) throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.uploadPart(p0, p1)); + } + + @Override + public software.amazon.awssdk.services.s3.model.UploadPartCopyResponse uploadPartCopy( + software.amazon.awssdk.services.s3.model.UploadPartCopyRequest p0) + throws AwsServiceException, SdkClientException { + return executeInBucketRegion(p0.bucket(), s3Client -> s3Client.uploadPartCopy(p0)); + } } - diff --git a/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/pom.xml b/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/pom.xml index 64a9e0b2f..f37a06532 100644 --- a/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/pom.xml +++ b/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/pom.xml @@ -26,6 +26,10 @@ software.amazon.awssdk s3 + + com.fasterxml.jackson.core + jackson-databind + org.testcontainers diff --git a/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/DiskBufferingS3OutputStream.java b/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/DiskBufferingS3OutputStream.java index 7080e3348..56f8fb101 100644 --- a/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/DiskBufferingS3OutputStream.java +++ b/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/DiskBufferingS3OutputStream.java @@ -27,7 +27,6 @@ import java.util.UUID; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import software.amazon.awssdk.core.sync.RequestBody; @@ -48,15 +47,7 @@ class DiskBufferingS3OutputStream extends S3OutputStream { private static final Logger LOG = LoggerFactory.getLogger(DiskBufferingS3OutputStream.class); - /** - * Bucket name of the S3 bucket. - */ - private final String bucket; - - /** - * Key of the file when it is uploaded to S3. - */ - private final String key; + private final Location location; /** * The local file that will be uploaded when the stream is closed. @@ -84,13 +75,21 @@ class DiskBufferingS3OutputStream extends S3OutputStream { */ private boolean closed; - DiskBufferingS3OutputStream(@NonNull String bucket, @NonNull String key, @NonNull S3Client client, - @Nullable ObjectMetadata objectMetadata) throws IOException { - Assert.notNull(bucket, "Bucket name must not be null."); - this.bucket = bucket; - this.key = key; + @Nullable + private final S3ObjectContentTypeResolver contentTypeResolver; + + DiskBufferingS3OutputStream(Location location, S3Client s3Client, @Nullable ObjectMetadata objectMetadata) + throws IOException { + this(location, s3Client, objectMetadata, null); + } + + DiskBufferingS3OutputStream(Location location, S3Client client, @Nullable ObjectMetadata objectMetadata, + @Nullable S3ObjectContentTypeResolver contentTypeResolver) throws IOException { + Assert.notNull(location, "Location must not be null."); + this.location = location; this.s3Client = client; this.objectMetadata = objectMetadata; + this.contentTypeResolver = contentTypeResolver; this.file = File.createTempFile("DiskBufferingS3OutputStream", UUID.randomUUID().toString()); try { hash = MessageDigest.getInstance("MD5"); @@ -132,8 +131,8 @@ public void close() throws IOException { localOutputStream.close(); closed = true; try { - PutObjectRequest.Builder builder = PutObjectRequest.builder().bucket(bucket).key(key) - .contentLength(file.length()); + PutObjectRequest.Builder builder = PutObjectRequest.builder().bucket(location.getBucket()) + .key(location.getObject()).contentLength(file.length()); if (objectMetadata != null) { objectMetadata.apply(builder); } @@ -141,11 +140,17 @@ public void close() throws IOException { String contentMD5 = new String(Base64.getEncoder().encode(hash.digest())); builder = builder.contentMD5(contentMD5); } + if (contentTypeResolver != null && (objectMetadata == null || objectMetadata.getContentType() == null)) { + String contentType = contentTypeResolver.resolveContentType(location.getObject()); + if (contentType != null) { + builder.contentType(contentType); + } + } s3Client.putObject(builder.build(), RequestBody.fromFile(file)); file.delete(); } catch (Exception se) { - LOG.error("Failed to upload " + key + ". Temporary file @ " + file.getPath()); + LOG.error(String.format("Failed to upload %s. Temporary file @%s", location.getObject(), file.getPath())); throw new UploadFailedException(file.getPath(), se); } } diff --git a/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/DiskBufferingS3OutputStreamProvider.java b/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/DiskBufferingS3OutputStreamProvider.java index 9422052a4..03890f1bb 100644 --- a/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/DiskBufferingS3OutputStreamProvider.java +++ b/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/DiskBufferingS3OutputStreamProvider.java @@ -28,14 +28,19 @@ public class DiskBufferingS3OutputStreamProvider implements S3OutputStreamProvider { private final S3Client s3Client; + @Nullable + private final S3ObjectContentTypeResolver contentTypeResolver; - public DiskBufferingS3OutputStreamProvider(S3Client s3Client) { + public DiskBufferingS3OutputStreamProvider(S3Client s3Client, + @Nullable S3ObjectContentTypeResolver contentTypeResolver) { this.s3Client = s3Client; + this.contentTypeResolver = contentTypeResolver; } @Override public S3OutputStream create(String bucket, String key, @Nullable ObjectMetadata metadata) throws IOException { - return new DiskBufferingS3OutputStream(bucket, key, s3Client, metadata); + return new DiskBufferingS3OutputStream(new Location(bucket, key, null), s3Client, metadata, + contentTypeResolver); } } diff --git a/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/Jackson2JsonS3ObjectConverter.java b/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/Jackson2JsonS3ObjectConverter.java new file mode 100644 index 000000000..e211d425b --- /dev/null +++ b/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/Jackson2JsonS3ObjectConverter.java @@ -0,0 +1,60 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * 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 + * + * https://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 io.awspring.cloud.s3; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.io.InputStream; +import software.amazon.awssdk.core.sync.RequestBody; + +/** + * Jackson based implementation of {@link S3ObjectConverter}. Serializes/deserializes objects to/from JSON. + * + * @author Maciej Walkowiak + */ +public class Jackson2JsonS3ObjectConverter implements S3ObjectConverter { + private final ObjectMapper objectMapper; + + public Jackson2JsonS3ObjectConverter(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + @Override + public RequestBody write(T object) { + try { + return RequestBody.fromBytes(objectMapper.writeValueAsBytes(object)); + } + catch (JsonProcessingException e) { + throw new S3Exception("Failed to serialize object to JSON", e); + } + } + + @Override + public T read(InputStream is, Class clazz) { + try { + return objectMapper.readValue(is, clazz); + } + catch (IOException e) { + throw new S3Exception("Failed to deserialize object from JSON", e); + } + } + + @Override + public String contentType() { + return "application/json"; + } +} diff --git a/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/Location.java b/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/Location.java index 38bf7799a..5db81f65f 100644 --- a/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/Location.java +++ b/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/Location.java @@ -48,6 +48,15 @@ static Location of(String location) { return new Location(location); } + /** + * Creates location. + * @param bucket - the bucket name + * @param object - the object key + */ + Location(String bucket, String object) { + this(bucket, object, null); + } + /** * Creates location. * @param bucket - the bucket name diff --git a/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/ObjectMetadata.java b/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/ObjectMetadata.java index 7c4a20b04..75d54a9cb 100644 --- a/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/ObjectMetadata.java +++ b/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/ObjectMetadata.java @@ -252,6 +252,146 @@ void apply(PutObjectRequest.Builder builder) { } } + @Nullable + public String getAcl() { + return acl; + } + + @Nullable + public String getCacheControl() { + return cacheControl; + } + + @Nullable + public String getContentDisposition() { + return contentDisposition; + } + + @Nullable + public String getContentEncoding() { + return contentEncoding; + } + + @Nullable + public String getContentLanguage() { + return contentLanguage; + } + + @Nullable + public String getContentType() { + return contentType; + } + + @Nullable + public Instant getExpires() { + return expires; + } + + @Nullable + public String getGrantFullControl() { + return grantFullControl; + } + + @Nullable + public String getGrantRead() { + return grantRead; + } + + @Nullable + public String getGrantReadACP() { + return grantReadACP; + } + + @Nullable + public String getGrantWriteACP() { + return grantWriteACP; + } + + @Nullable + public Map getMetadata() { + return metadata; + } + + @Nullable + public String getServerSideEncryption() { + return serverSideEncryption; + } + + @Nullable + public String getStorageClass() { + return storageClass; + } + + @Nullable + public String getWebsiteRedirectLocation() { + return websiteRedirectLocation; + } + + @Nullable + public String getSseCustomerAlgorithm() { + return sseCustomerAlgorithm; + } + + @Nullable + public String getSseCustomerKey() { + return sseCustomerKey; + } + + @Nullable + public String getSseCustomerKeyMD5() { + return sseCustomerKeyMD5; + } + + @Nullable + public String getSsekmsKeyId() { + return ssekmsKeyId; + } + + @Nullable + public String getSsekmsEncryptionContext() { + return ssekmsEncryptionContext; + } + + @Nullable + public Boolean getBucketKeyEnabled() { + return bucketKeyEnabled; + } + + @Nullable + public String getRequestPayer() { + return requestPayer; + } + + @Nullable + public String getTagging() { + return tagging; + } + + @Nullable + public String getObjectLockMode() { + return objectLockMode; + } + + @Nullable + public Instant getObjectLockRetainUntilDate() { + return objectLockRetainUntilDate; + } + + @Nullable + public String getObjectLockLegalHoldStatus() { + return objectLockLegalHoldStatus; + } + + @Nullable + public String getExpectedBucketOwner() { + return expectedBucketOwner; + } + + @Nullable + public String getChecksumAlgorithm() { + return checksumAlgorithm; + } + public static class Builder { private final Map metadata = new HashMap<>(); diff --git a/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/PropertiesS3ObjectContentTypeResolver.java b/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/PropertiesS3ObjectContentTypeResolver.java new file mode 100644 index 000000000..0fe455320 --- /dev/null +++ b/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/PropertiesS3ObjectContentTypeResolver.java @@ -0,0 +1,77 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * 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 + * + * https://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 io.awspring.cloud.s3; + +import java.io.IOException; +import java.util.Properties; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.support.PropertiesLoaderUtils; +import org.springframework.lang.Nullable; + +/** + * Resolves content type for S3 object from a properties file. + * + * If properties file is not given by constructor, loads default list of known extension to content type relations from + * a classpath. + * + * @author Maciej Walkowiak + * @since 3.0 + */ +public class PropertiesS3ObjectContentTypeResolver implements S3ObjectContentTypeResolver { + private static final String PROPERTIES_FILE_LOCATION = "/io/awspring/cloud/s3/S3ObjectContentTypeResolver.properties"; + + private final Properties properties; + + public PropertiesS3ObjectContentTypeResolver() { + this(loadProperties()); + } + + private static Properties loadProperties() { + try { + return PropertiesLoaderUtils.loadProperties(new ClassPathResource(PROPERTIES_FILE_LOCATION)); + } + catch (IOException e) { + throw new S3Exception( + "Error when loading properties from " + PROPERTIES_FILE_LOCATION + " for content type resolution", + e); + } + } + + public PropertiesS3ObjectContentTypeResolver(Properties properties) { + this.properties = properties; + } + + @Override + public String resolveContentType(String fileName) { + String extension = resolveExtension(fileName); + if (extension != null) { + return properties.getProperty(extension); + } + else { + return null; + } + } + + @Nullable + public String resolveExtension(String filename) { + if (filename.contains(".")) { + return filename.substring(filename.lastIndexOf(".") + 1); + } + else { + return null; + } + } +} diff --git a/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/S3Exception.java b/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/S3Exception.java new file mode 100644 index 000000000..95309ab6e --- /dev/null +++ b/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/S3Exception.java @@ -0,0 +1,30 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * 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 + * + * https://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 io.awspring.cloud.s3; + +import org.springframework.lang.Nullable; + +/** + * Exception thrown when S3 operation fails. + * + * @author Maciej Walkowiak + */ +public class S3Exception extends RuntimeException { + + public S3Exception(String message, @Nullable Throwable cause) { + super(message, cause); + } +} diff --git a/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/S3ObjectContentTypeResolver.java b/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/S3ObjectContentTypeResolver.java new file mode 100644 index 000000000..651b17634 --- /dev/null +++ b/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/S3ObjectContentTypeResolver.java @@ -0,0 +1,35 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * 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 + * + * https://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 io.awspring.cloud.s3; + +import org.springframework.lang.Nullable; + +/** + * Resolves content type of S3 objects. + * + * @author Maciej Walkowiak + * @since 3.0 + */ +public interface S3ObjectContentTypeResolver { + /** + * Resolves content type from a file name. + * + * @param fileName - the file name + * @return content type or null if not resolved + */ + @Nullable + String resolveContentType(String fileName); +} diff --git a/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/S3ObjectConverter.java b/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/S3ObjectConverter.java new file mode 100644 index 000000000..e2986e0bb --- /dev/null +++ b/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/S3ObjectConverter.java @@ -0,0 +1,51 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * 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 + * + * https://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 io.awspring.cloud.s3; + +import java.io.InputStream; +import software.amazon.awssdk.core.sync.RequestBody; + +/** + * Converter used to serialize Java objects into S3 objects. + * + * @author Maciej Walkowiak + */ +public interface S3ObjectConverter { + /** + * Converts object into a {@link RequestBody}. + * + * @param object - the object to serialize + * @param - type of the object + * @return the request body + */ + RequestBody write(T object); + + /** + * Reads S3 object from the input stream into a Java object. + * @param is - the input stream + * @param clazz - the class of the object + * @param - the the type of the object + * @return deserialized object + */ + T read(InputStream is, Class clazz); + + /** + * Supported content type. + * + * @return the content type + */ + String contentType(); +} diff --git a/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/S3Operations.java b/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/S3Operations.java new file mode 100644 index 000000000..64e950181 --- /dev/null +++ b/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/S3Operations.java @@ -0,0 +1,104 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * 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 + * + * https://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 io.awspring.cloud.s3; + +import java.io.InputStream; +import org.springframework.core.io.Resource; +import org.springframework.lang.Nullable; +import software.amazon.awssdk.services.s3.model.CreateBucketResponse; + +public interface S3Operations { + + /** + * Creates a bucket in S3. + * + * @param bucketName - the bucket name + * @return created bucket location {@link CreateBucketResponse#location()} + */ + String createBucket(String bucketName); + + /** + * Deletes a S3 bucket. + * + * @param bucketName - the bucket name + */ + void deleteBucket(String bucketName); + + /** + * Deletes an object from S3 bucket. + * + * @param bucketName - the bucket name + * @param key - the object key + */ + void deleteObject(String bucketName, String key); + + /** + * Deletes an object from S3 bucket. + * + * @param s3Url - the S3 url s3://bucket/key + */ + void deleteObject(String s3Url); + + /** + * Stores a Java object in a S3 bucket. Uses {@link S3ObjectConverter} for serialization. + * + * @param bucketName - the bucket name + * @param key - the object key + * @param object - the Java object to serialize and store + */ + void store(String bucketName, String key, Object object); + + /** + * Reads a Java object from a S3 bucket. Uses {@link S3ObjectConverter} for deserialization. + * + * @param bucketName - the bucket name + * @param key - the object key + * @param clazz - the class of the read object + * @param - the type of the read object + * @return an object + */ + T read(String bucketName, String key, Class clazz); + + /** + * Uploads data from an input stream to a S3 bucket. + * + * @param bucketName - the bucket name + * @param key - the object key + * @param inputStream - the input stream + * @param objectMetadata - the object metadata + */ + void upload(String bucketName, String key, InputStream inputStream, @Nullable ObjectMetadata objectMetadata); + + /** + * Uploads data from an input stream to a S3 bucket. + * + * @param bucketName - the bucket name + * @param key - the object key + * @param inputStream - the input stream + */ + default void upload(String bucketName, String key, InputStream inputStream) { + upload(bucketName, key, inputStream, null); + } + + /** + * Downloads object from S3. + * + * @param bucketName - the bucket name + * @param key - the object key + * @return downloaded object represented as {@link Resource} + */ + Resource download(String bucketName, String key); +} diff --git a/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/S3OutputStreamProvider.java b/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/S3OutputStreamProvider.java index 8e6a5f37e..0ca81bfc5 100644 --- a/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/S3OutputStreamProvider.java +++ b/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/S3OutputStreamProvider.java @@ -29,6 +29,7 @@ public interface S3OutputStreamProvider { /** * Creates an {@link OutputStream} that writes data to S3. + * * @param bucket - the bucket name * @param key - the object key * @param metadata - object metadata, can be {@code null} @@ -37,4 +38,16 @@ public interface S3OutputStreamProvider { */ S3OutputStream create(String bucket, String key, @Nullable ObjectMetadata metadata) throws IOException; + /** + * Creates an {@link OutputStream} that writes data to S3. + * + * @param location - the bucket location + * @param metadata - object metadata, can be {@code null} + * @return the S3 output stream + * @throws IOException - when IO operation fails + */ + default S3OutputStream create(Location location, @Nullable ObjectMetadata metadata) throws IOException { + return create(location.getBucket(), location.getObject(), metadata); + } + } diff --git a/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/S3Resource.java b/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/S3Resource.java index 037ca6964..007e59e90 100644 --- a/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/S3Resource.java +++ b/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/S3Resource.java @@ -91,6 +91,13 @@ public InputStream getInputStream() throws IOException { .versionId(location.getVersion())); } + public String contentType() { + if (headMetadata == null) { + fetchMetadata(); + } + return headMetadata.contentType; + } + public void setObjectMetadata(@Nullable ObjectMetadata objectMetadata) { this.objectMetadata = objectMetadata; } @@ -145,9 +152,12 @@ private static class HeadMetadata { private final Instant lastModified; + private final String contentType; + HeadMetadata(HeadObjectResponse headObjectResponse) { this.contentLength = headObjectResponse.contentLength(); this.lastModified = headObjectResponse.lastModified(); + this.contentType = headObjectResponse.contentType(); } } diff --git a/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/S3Template.java b/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/S3Template.java new file mode 100644 index 000000000..494b86771 --- /dev/null +++ b/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/S3Template.java @@ -0,0 +1,106 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * 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 + * + * https://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 io.awspring.cloud.s3; + +import java.io.InputStream; +import java.io.OutputStream; +import org.springframework.core.io.Resource; +import org.springframework.lang.Nullable; +import org.springframework.util.StreamUtils; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; + +/** + * Higher level abstraction over {@link S3Client} providing methods for the most common use cases. + * + * @author Maciej Walkowiak + */ +public class S3Template implements S3Operations { + + private final S3Client s3Client; + + private final S3OutputStreamProvider s3OutputStreamProvider; + + private final S3ObjectConverter s3ObjectConverter; + + public S3Template(S3Client s3Client, S3OutputStreamProvider s3OutputStreamProvider, + S3ObjectConverter s3ObjectConverter) { + this.s3Client = s3Client; + this.s3OutputStreamProvider = s3OutputStreamProvider; + this.s3ObjectConverter = s3ObjectConverter; + } + + @Override + public String createBucket(String bucketName) { + return s3Client.createBucket(request -> request.bucket(bucketName)).location(); + } + + @Override + public void deleteBucket(String bucketName) { + s3Client.deleteBucket(request -> request.bucket(bucketName)); + } + + @Override + public void deleteObject(String bucketName, String key) { + s3Client.deleteObject(request -> request.bucket(bucketName).key(key)); + } + + @Override + public void deleteObject(String s3Url) { + Location location = Location.of(s3Url); + this.deleteObject(location.getBucket(), location.getObject()); + } + + @Override + public void store(String bucketName, String key, Object object) { + PutObjectRequest.Builder requestBuilder = PutObjectRequest.builder().bucket(bucketName).key(key) + .contentType(s3ObjectConverter.contentType()); + s3Client.putObject(requestBuilder.build(), s3ObjectConverter.write(object)); + } + + @Override + public T read(String bucketName, String key, Class clazz) { + try (InputStream is = s3Client.getObject(r -> r.bucket(bucketName).key(key))) { + return s3ObjectConverter.read(is, clazz); + } + catch (Exception e) { + throw new S3Exception( + String.format("Failed to read object with a key '%s' from bucket '%s'", key, bucketName), e); + } + } + + @Override + public void upload(String bucketName, String key, InputStream inputStream, + @Nullable ObjectMetadata objectMetadata) { + S3Resource s3Resource = new S3Resource(bucketName, key, s3Client, s3OutputStreamProvider); + if (objectMetadata != null) { + s3Resource.setObjectMetadata(objectMetadata); + } + try (OutputStream os = s3Resource.getOutputStream()) { + StreamUtils.copy(inputStream, os); + } + catch (Exception e) { + throw new S3Exception( + String.format("Failed to upload object with a key '%s' to bucket '%s'", key, bucketName), e); + } + } + + @Override + public Resource download(String bucketName, String key) { + return new S3Resource(bucketName, key, s3Client, s3OutputStreamProvider); + } + +} diff --git a/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/UploadFailedException.java b/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/UploadFailedException.java index bfa78c344..e171f6f94 100644 --- a/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/UploadFailedException.java +++ b/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/UploadFailedException.java @@ -22,7 +22,7 @@ * * @author Maciej Walkowiak */ -public class UploadFailedException extends RuntimeException { +public class UploadFailedException extends S3Exception { /** * A path to temporary location containing a file that has not been uploaded to S3. @@ -31,7 +31,7 @@ public class UploadFailedException extends RuntimeException { private final String path; public UploadFailedException(@Nullable String path, @Nullable Exception se) { - super(se); + super("Upload failed. File is stored in a temporary folder in the filesystem " + path, se); this.path = path; } diff --git a/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/main/resources/io/awspring/cloud/s3/S3ObjectContentTypeResolver.properties b/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/main/resources/io/awspring/cloud/s3/S3ObjectContentTypeResolver.properties new file mode 100644 index 000000000..55bb1641b --- /dev/null +++ b/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/main/resources/io/awspring/cloud/s3/S3ObjectContentTypeResolver.properties @@ -0,0 +1,817 @@ +123=application/vnd.lotus-1-2-3 +3dml=text/vnd.in3d.3dml +3g2=video/3gpp2 +3gp=video/3gpp +a=application/octet-stream +aab=application/x-authorware-bin +aac=audio/x-aac +aam=application/x-authorware-map +aas=application/x-authorware-seg +abw=application/x-abiword +acc=application/vnd.americandynamics.acc +ace=application/x-ace-compressed +acu=application/vnd.acucobol +acutc=application/vnd.acucorp +adp=audio/adpcm +aep=application/vnd.audiograph +afm=application/x-font-type1 +afp=application/vnd.ibm.modcap +ai=application/postscript +aif=audio/x-aiff +aifc=audio/x-aiff +aiff=audio/x-aiff +air=application/vnd.adobe.air-application-installer-package+zip +ami=application/vnd.amiga.ami +apk=application/vnd.android.package-archive +application=application/x-ms-application +apr=application/vnd.lotus-approach +asc=application/pgp-signature +asf=video/x-ms-asf +asm=text/x-asm +aso=application/vnd.accpac.simply.aso +asx=video/x-ms-asf +atc=application/vnd.acucorp +atom=application/atom+xml +atomcat=application/atomcat+xml +atomsvc=application/atomsvc+xml +atx=application/vnd.antix.game-component +au=audio/basic +avi=video/x-msvideo +aw=application/applixware +azf=application/vnd.airzip.filesecure.azf +azs=application/vnd.airzip.filesecure.azs +azw=application/vnd.amazon.ebook +bat=application/x-msdownload +bcpio=application/x-bcpio +bdf=application/x-font-bdf +bdm=application/vnd.syncml.dm+wbxml +bh2=application/vnd.fujitsu.oasysprs +bin=application/octet-stream +bmi=application/vnd.bmi +bmp=image/bmp +book=application/vnd.framemaker +box=application/vnd.previewsystems.box +boz=application/x-bzip2 +bpk=application/octet-stream +btif=image/prs.btif +bz=application/x-bzip +bz2=application/x-bzip2 +c=text/x-c +c4d=application/vnd.clonk.c4group +c4f=application/vnd.clonk.c4group +c4g=application/vnd.clonk.c4group +c4p=application/vnd.clonk.c4group +c4u=application/vnd.clonk.c4group +cab=application/vnd.ms-cab-compressed +car=application/vnd.curl.car +cat=application/vnd.ms-pki.seccat +cc=text/x-c +cct=application/x-director +ccxml=application/ccxml+xml +cdbcmsg=application/vnd.contact.cmsg +cdf=application/x-netcdf +cdkey=application/vnd.mediastation.cdkey +cdx=chemical/x-cdx +cdxml=application/vnd.chemdraw+xml +cdy=application/vnd.cinderella +cer=application/pkix-cert +cgm=image/cgm +chat=application/x-chat +chm=application/vnd.ms-htmlhelp +chrt=application/vnd.kde.kchart +cif=chemical/x-cif +cii=application/vnd.anser-web-certificate-issue-initiation +cil=application/vnd.ms-artgalry +cla=application/vnd.claymore +class=application/java-vm +clkk=application/vnd.crick.clicker.keyboard +clkp=application/vnd.crick.clicker.palette +clkt=application/vnd.crick.clicker.template +clkw=application/vnd.crick.clicker.wordbank +clkx=application/vnd.crick.clicker +clp=application/x-msclip +cmc=application/vnd.cosmocaller +cmdf=chemical/x-cmdf +cml=chemical/x-cml +cmp=application/vnd.yellowriver-custom-menu +cmx=image/x-cmx +cod=application/vnd.rim.cod +com=application/x-msdownload +conf=text/plain +cpio=application/x-cpio +cpp=text/x-c +cpt=application/mac-compactpro +crd=application/x-mscardfile +crl=application/pkix-crl +crt=application/x-x509-ca-cert +csh=application/x-csh +csml=chemical/x-csml +csp=application/vnd.commonspace +css=text/css +cst=application/x-director +csv=text/csv +cu=application/cu-seeme +curl=text/vnd.curl +cww=application/prs.cww +cxt=application/x-director +cxx=text/x-c +daf=application/vnd.mobius.daf +dataless=application/vnd.fdsn.seed +davmount=application/davmount+xml +dcr=application/x-director +dcurl=text/vnd.curl.dcurl +dd2=application/vnd.oma.dd2+xml +ddd=application/vnd.fujixerox.ddd +deb=application/x-debian-package +def=text/plain +deploy=application/octet-stream +der=application/x-x509-ca-cert +dfac=application/vnd.dreamfactory +dic=text/x-c +diff=text/plain +dir=application/x-director +dis=application/vnd.mobius.dis +dist=application/octet-stream +distz=application/octet-stream +djv=image/vnd.djvu +djvu=image/vnd.djvu +dll=application/x-msdownload +dmg=application/octet-stream +dms=application/octet-stream +dna=application/vnd.dna +doc=application/msword +docm=application/vnd.ms-word.document.macroenabled.12 +docx=application/vnd.openxmlformats-officedocument.wordprocessingml.document +dot=application/msword +dotm=application/vnd.ms-word.template.macroenabled.12 +dotx=application/vnd.openxmlformats-officedocument.wordprocessingml.template +dp=application/vnd.osgi.dp +dpg=application/vnd.dpgraph +dsc=text/prs.lines.tag +dtb=application/x-dtbook+xml +dtd=application/xml-dtd +dts=audio/vnd.dts +dtshd=audio/vnd.dts.hd +dump=application/octet-stream +dvi=application/x-dvi +dwf=model/vnd.dwf +dwg=image/vnd.dwg +dxf=image/vnd.dxf +dxp=application/vnd.spotfire.dxp +dxr=application/x-director +ecelp4800=audio/vnd.nuera.ecelp4800 +ecelp7470=audio/vnd.nuera.ecelp7470 +ecelp9600=audio/vnd.nuera.ecelp9600 +ecma=application/ecmascript +edm=application/vnd.novadigm.edm +edx=application/vnd.novadigm.edx +efif=application/vnd.picsel +ei6=application/vnd.pg.osasli +elc=application/octet-stream +eml=message/rfc822 +emma=application/emma+xml +eol=audio/vnd.digital-winds +eot=application/vnd.ms-fontobject +eps=application/postscript +epub=application/epub+zip +es3=application/vnd.eszigno3+xml +esf=application/vnd.epson.esf +et3=application/vnd.eszigno3+xml +etx=text/x-setext +exe=application/x-msdownload +ext=application/vnd.novadigm.ext +ez=application/andrew-inset +ez2=application/vnd.ezpix-album +ez3=application/vnd.ezpix-package +f=text/x-fortran +f4v=video/x-f4v +f77=text/x-fortran +f90=text/x-fortran +fbs=image/vnd.fastbidsheet +fdf=application/vnd.fdf +fe_launch=application/vnd.denovo.fcselayout-link +fg5=application/vnd.fujitsu.oasysgp +fgd=application/x-director +fh=image/x-freehand +fh4=image/x-freehand +fh5=image/x-freehand +fh7=image/x-freehand +fhc=image/x-freehand +fig=application/x-xfig +fli=video/x-fli +flo=application/vnd.micrografx.flo +flv=video/x-flv +flw=application/vnd.kde.kivio +flx=text/vnd.fmi.flexstor +fly=text/vnd.fly +fm=application/vnd.framemaker +fnc=application/vnd.frogans.fnc +for=text/x-fortran +fpx=image/vnd.fpx +frame=application/vnd.framemaker +fsc=application/vnd.fsc.weblaunch +fst=image/vnd.fst +ftc=application/vnd.fluxtime.clip +fti=application/vnd.anser-web-funds-transfer-initiation +fvt=video/vnd.fvt +fzs=application/vnd.fuzzysheet +g3=image/g3fax +gac=application/vnd.groove-account +gdl=model/vnd.gdl +geo=application/vnd.dynageo +gex=application/vnd.geometry-explorer +ggb=application/vnd.geogebra.file +ggt=application/vnd.geogebra.tool +ghf=application/vnd.groove-help +gif=image/gif +gim=application/vnd.groove-identity-message +gmx=application/vnd.gmx +gnumeric=application/x-gnumeric +gph=application/vnd.flographit +gqf=application/vnd.grafeq +gqs=application/vnd.grafeq +gram=application/srgs +gre=application/vnd.geometry-explorer +grv=application/vnd.groove-injector +grxml=application/srgs+xml +gsf=application/x-font-ghostscript +gtar=application/x-gtar +gtm=application/vnd.groove-tool-message +gtw=model/vnd.gtw +gv=text/vnd.graphviz +gz=application/x-gzip +h=text/x-c +h261=video/h261 +h263=video/h263 +h264=video/h264 +hbci=application/vnd.hbci +hdf=application/x-hdf +hh=text/x-c +hlp=application/winhlp +hpgl=application/vnd.hp-hpgl +hpid=application/vnd.hp-hpid +hps=application/vnd.hp-hps +hqx=application/mac-binhex40 +htke=application/vnd.kenameaapp +htm=text/html +html=text/html +hvd=application/vnd.yamaha.hv-dic +hvp=application/vnd.yamaha.hv-voice +hvs=application/vnd.yamaha.hv-script +icc=application/vnd.iccprofile +ice=x-conference/x-cooltalk +icm=application/vnd.iccprofile +ico=image/x-icon +ics=text/calendar +ief=image/ief +ifb=text/calendar +ifm=application/vnd.shana.informed.formdata +iges=model/iges +igl=application/vnd.igloader +igs=model/iges +igx=application/vnd.micrografx.igx +iif=application/vnd.shana.informed.interchange +imp=application/vnd.accpac.simply.imp +ims=application/vnd.ms-ims +in=text/plain +ipk=application/vnd.shana.informed.package +irm=application/vnd.ibm.rights-management +irp=application/vnd.irepository.package+xml +iso=application/octet-stream +itp=application/vnd.shana.informed.formtemplate +ivp=application/vnd.immervision-ivp +ivu=application/vnd.immervision-ivu +jad=text/vnd.sun.j2me.app-descriptor +jam=application/vnd.jam +jar=application/java-archive +java=text/x-java-source +jisp=application/vnd.jisp +jlt=application/vnd.hp-jlyt +jnlp=application/x-java-jnlp-file +joda=application/vnd.joost.joda-archive +jpe=image/jpeg +jpeg=image/jpeg +jpg=image/jpeg +jpgm=video/jpm +jpgv=video/jpeg +jpm=video/jpm +js=application/javascript +json=application/json +kar=audio/midi +karbon=application/vnd.kde.karbon +kfo=application/vnd.kde.kformula +kia=application/vnd.kidspiration +kil=application/x-killustrator +kml=application/vnd.google-earth.kml+xml +kmz=application/vnd.google-earth.kmz +kne=application/vnd.kinar +knp=application/vnd.kinar +kon=application/vnd.kde.kontour +kpr=application/vnd.kde.kpresenter +kpt=application/vnd.kde.kpresenter +ksh=text/plain +ksp=application/vnd.kde.kspread +ktr=application/vnd.kahootz +ktz=application/vnd.kahootz +kwd=application/vnd.kde.kword +kwt=application/vnd.kde.kword +latex=application/x-latex +lbd=application/vnd.llamagraphics.life-balance.desktop +lbe=application/vnd.llamagraphics.life-balance.exchange+xml +les=application/vnd.hhe.lesson-player +lha=application/octet-stream +link66=application/vnd.route66.link66+xml +list=text/plain +list3820=application/vnd.ibm.modcap +listafp=application/vnd.ibm.modcap +log=text/plain +lostxml=application/lost+xml +lrf=application/octet-stream +lrm=application/vnd.ms-lrm +ltf=application/vnd.frogans.ltf +lvp=audio/vnd.lucent.voice +lwp=application/vnd.lotus-wordpro +lzh=application/octet-stream +m13=application/x-msmediaview +m14=application/x-msmediaview +m1v=video/mpeg +m2a=audio/mpeg +m2v=video/mpeg +m3a=audio/mpeg +m3u=audio/x-mpegurl +m4u=video/vnd.mpegurl +m4v=video/x-m4v +ma=application/mathematica +mag=application/vnd.ecowin.chart +maker=application/vnd.framemaker +man=text/troff +mathml=application/mathml+xml +mb=application/mathematica +mbk=application/vnd.mobius.mbk +mbox=application/mbox +mc1=application/vnd.medcalcdata +mcd=application/vnd.mcd +mcurl=text/vnd.curl.mcurl +mdb=application/x-msaccess +mdi=image/vnd.ms-modi +me=text/troff +mesh=model/mesh +mfm=application/vnd.mfmp +mgz=application/vnd.proteus.magazine +mht=message/rfc822 +mhtml=message/rfc822 +mid=audio/midi +midi=audio/midi +mif=application/vnd.mif +mime=message/rfc822 +mj2=video/mj2 +mjp2=video/mj2 +mlp=application/vnd.dolby.mlp +mmd=application/vnd.chipnuts.karaoke-mmd +mmf=application/vnd.smaf +mmr=image/vnd.fujixerox.edmics-mmr +mny=application/x-msmoney +mobi=application/x-mobipocket-ebook +mov=video/quicktime +movie=video/x-sgi-movie +mp2=audio/mpeg +mp2a=audio/mpeg +mp3=audio/mpeg +mp4=video/mp4 +mp4a=audio/mp4 +mp4s=application/mp4 +mp4v=video/mp4 +mpa=video/mpeg +mpc=application/vnd.mophun.certificate +mpe=video/mpeg +mpeg=video/mpeg +mpg=video/mpeg +mpg4=video/mp4 +mpga=audio/mpeg +mpkg=application/vnd.apple.installer+xml +mpm=application/vnd.blueice.multipass +mpn=application/vnd.mophun.application +mpp=application/vnd.ms-project +mpt=application/vnd.ms-project +mpy=application/vnd.ibm.minipay +mqy=application/vnd.mobius.mqy +mrc=application/marc +ms=text/troff +mscml=application/mediaservercontrol+xml +mseed=application/vnd.fdsn.mseed +mseq=application/vnd.mseq +msf=application/vnd.epson.msf +msh=model/mesh +msi=application/x-msdownload +msl=application/vnd.mobius.msl +msty=application/vnd.muvee.style +mts=model/vnd.mts +mus=application/vnd.musician +musicxml=application/vnd.recordare.musicxml+xml +mvb=application/x-msmediaview +mwf=application/vnd.mfer +mxf=application/mxf +mxl=application/vnd.recordare.musicxml +mxml=application/xv+xml +mxs=application/vnd.triscape.mxs +mxu=video/vnd.mpegurl +n-gage=application/vnd.nokia.n-gage.symbian.install +nb=application/mathematica +nc=application/x-netcdf +ncx=application/x-dtbncx+xml +ngdat=application/vnd.nokia.n-gage.data +nlu=application/vnd.neurolanguage.nlu +nml=application/vnd.enliven +nnd=application/vnd.noblenet-directory +nns=application/vnd.noblenet-sealer +nnw=application/vnd.noblenet-web +npx=image/vnd.net-fpx +nsf=application/vnd.lotus-notes +nws=message/rfc822 +o=application/octet-stream +oa2=application/vnd.fujitsu.oasys2 +oa3=application/vnd.fujitsu.oasys3 +oas=application/vnd.fujitsu.oasys +obd=application/x-msbinder +obj=application/octet-stream +oda=application/oda +odb=application/vnd.oasis.opendocument.database +odc=application/vnd.oasis.opendocument.chart +odf=application/vnd.oasis.opendocument.formula +odft=application/vnd.oasis.opendocument.formula-template +odg=application/vnd.oasis.opendocument.graphics +odi=application/vnd.oasis.opendocument.image +odp=application/vnd.oasis.opendocument.presentation +ods=application/vnd.oasis.opendocument.spreadsheet +odt=application/vnd.oasis.opendocument.text +oga=audio/ogg +ogg=audio/ogg +ogv=video/ogg +ogx=application/ogg +onepkg=application/onenote +onetmp=application/onenote +onetoc=application/onenote +onetoc2=application/onenote +opf=application/oebps-package+xml +oprc=application/vnd.palm +org=application/vnd.lotus-organizer +osf=application/vnd.yamaha.openscoreformat +osfpvg=application/vnd.yamaha.openscoreformat.osfpvg+xml +otc=application/vnd.oasis.opendocument.chart-template +otf=application/x-font-otf +otg=application/vnd.oasis.opendocument.graphics-template +oth=application/vnd.oasis.opendocument.text-web +oti=application/vnd.oasis.opendocument.image-template +otm=application/vnd.oasis.opendocument.text-master +otp=application/vnd.oasis.opendocument.presentation-template +ots=application/vnd.oasis.opendocument.spreadsheet-template +ott=application/vnd.oasis.opendocument.text-template +oxt=application/vnd.openofficeorg.extension +p=text/x-pascal +p10=application/pkcs10 +p12=application/x-pkcs12 +p7b=application/x-pkcs7-certificates +p7c=application/pkcs7-mime +p7m=application/pkcs7-mime +p7r=application/x-pkcs7-certreqresp +p7s=application/pkcs7-signature +pas=text/x-pascal +pbd=application/vnd.powerbuilder6 +pbm=image/x-portable-bitmap +pcf=application/x-font-pcf +pcl=application/vnd.hp-pcl +pclxl=application/vnd.hp-pclxl +pct=image/x-pict +pcurl=application/vnd.curl.pcurl +pcx=image/x-pcx +pdb=application/vnd.palm +pdf=application/pdf +pfa=application/x-font-type1 +pfb=application/x-font-type1 +pfm=application/x-font-type1 +pfr=application/font-tdpfr +pfx=application/x-pkcs12 +pgm=image/x-portable-graymap +pgn=application/x-chess-pgn +pgp=application/pgp-encrypted +pic=image/x-pict +pkg=application/octet-stream +pki=application/pkixcmp +pkipath=application/pkix-pkipath +pl=text/plain +plb=application/vnd.3gpp.pic-bw-large +plc=application/vnd.mobius.plc +plf=application/vnd.pocketlearn +pls=application/pls+xml +pml=application/vnd.ctc-posml +png=image/png +pnm=image/x-portable-anymap +portpkg=application/vnd.macports.portpkg +pot=application/vnd.ms-powerpoint +potm=application/vnd.ms-powerpoint.template.macroenabled.12 +potx=application/vnd.openxmlformats-officedocument.presentationml.template +ppa=application/vnd.ms-powerpoint +ppam=application/vnd.ms-powerpoint.addin.macroenabled.12 +ppd=application/vnd.cups-ppd +ppm=image/x-portable-pixmap +pps=application/vnd.ms-powerpoint +ppsm=application/vnd.ms-powerpoint.slideshow.macroenabled.12 +ppsx=application/vnd.openxmlformats-officedocument.presentationml.slideshow +ppt=application/vnd.ms-powerpoint +pptm=application/vnd.ms-powerpoint.presentation.macroenabled.12 +pptx=application/vnd.openxmlformats-officedocument.presentationml.presentation +pqa=application/vnd.palm +prc=application/x-mobipocket-ebook +pre=application/vnd.lotus-freelance +prf=application/pics-rules +ps=application/postscript +psb=application/vnd.3gpp.pic-bw-small +psd=image/vnd.adobe.photoshop +psf=application/x-font-linux-psf +ptid=application/vnd.pvi.ptid1 +pub=application/x-mspublisher +pvb=application/vnd.3gpp.pic-bw-var +pwn=application/vnd.3m.post-it-notes +pwz=application/vnd.ms-powerpoint +py=text/x-python +pya=audio/vnd.ms-playready.media.pya +pyc=application/x-python-code +pyo=application/x-python-code +pyv=video/vnd.ms-playready.media.pyv +qam=application/vnd.epson.quickanime +qbo=application/vnd.intu.qbo +qfx=application/vnd.intu.qfx +qps=application/vnd.publishare-delta-tree +qt=video/quicktime +qwd=application/vnd.quark.quarkxpress +qwt=application/vnd.quark.quarkxpress +qxb=application/vnd.quark.quarkxpress +qxd=application/vnd.quark.quarkxpress +qxl=application/vnd.quark.quarkxpress +qxt=application/vnd.quark.quarkxpress +ra=audio/x-pn-realaudio +ram=audio/x-pn-realaudio +rar=application/x-rar-compressed +ras=image/x-cmu-raster +rcprofile=application/vnd.ipunplugged.rcprofile +rdf=application/rdf+xml +rdz=application/vnd.data-vision.rdz +rep=application/vnd.businessobjects +res=application/x-dtbresource+xml +rgb=image/x-rgb +rif=application/reginfo+xml +rl=application/resource-lists+xml +rlc=image/vnd.fujixerox.edmics-rlc +rld=application/resource-lists-diff+xml +rm=application/vnd.rn-realmedia +rmi=audio/midi +rmp=audio/x-pn-realaudio-plugin +rms=application/vnd.jcp.javame.midlet-rms +rnc=application/relax-ng-compact-syntax +roff=text/troff +rpm=application/x-rpm +rpss=application/vnd.nokia.radio-presets +rpst=application/vnd.nokia.radio-preset +rq=application/sparql-query +rs=application/rls-services+xml +rsd=application/rsd+xml +rss=application/rss+xml +rtf=application/rtf +rtx=text/richtext +s=text/x-asm +saf=application/vnd.yamaha.smaf-audio +sbml=application/sbml+xml +sc=application/vnd.ibm.secure-container +scd=application/x-msschedule +scm=application/vnd.lotus-screencam +scq=application/scvp-cv-request +scs=application/scvp-cv-response +scurl=text/vnd.curl.scurl +sda=application/vnd.stardivision.draw +sdc=application/vnd.stardivision.calc +sdd=application/vnd.stardivision.impress +sdkd=application/vnd.solent.sdkm+xml +sdkm=application/vnd.solent.sdkm+xml +sdp=application/sdp +sdw=application/vnd.stardivision.writer +see=application/vnd.seemail +seed=application/vnd.fdsn.seed +sema=application/vnd.sema +semd=application/vnd.semd +semf=application/vnd.semf +ser=application/java-serialized-object +setpay=application/set-payment-initiation +setreg=application/set-registration-initiation +sfd-hdstx=application/vnd.hydrostatix.sof-data +sfs=application/vnd.spotfire.sfs +sgl=application/vnd.stardivision.writer-global +sgm=text/sgml +sgml=text/sgml +sh=application/x-sh +shar=application/x-shar +shf=application/shf+xml +si=text/vnd.wap.si +sic=application/vnd.wap.sic +sig=application/pgp-signature +silo=model/mesh +sis=application/vnd.symbian.install +sisx=application/vnd.symbian.install +sit=application/x-stuffit +sitx=application/x-stuffitx +skd=application/vnd.koan +skm=application/vnd.koan +skp=application/vnd.koan +skt=application/vnd.koan +sl=text/vnd.wap.sl +slc=application/vnd.wap.slc +sldm=application/vnd.ms-powerpoint.slide.macroenabled.12 +sldx=application/vnd.openxmlformats-officedocument.presentationml.slide +slt=application/vnd.epson.salt +smf=application/vnd.stardivision.math +smi=application/smil+xml +smil=application/smil+xml +snd=audio/basic +snf=application/x-font-snf +so=application/octet-stream +spc=application/x-pkcs7-certificates +spf=application/vnd.yamaha.smaf-phrase +spl=application/x-futuresplash +spot=text/vnd.in3d.spot +spp=application/scvp-vp-response +spq=application/scvp-vp-request +spx=audio/ogg +src=application/x-wais-source +srx=application/sparql-results+xml +sse=application/vnd.kodak-descriptor +ssf=application/vnd.epson.ssf +ssml=application/ssml+xml +stc=application/vnd.sun.xml.calc.template +std=application/vnd.sun.xml.draw.template +stf=application/vnd.wt.stf +sti=application/vnd.sun.xml.impress.template +stk=application/hyperstudio +stl=application/vnd.ms-pki.stl +str=application/vnd.pg.format +stw=application/vnd.sun.xml.writer.template +sus=application/vnd.sus-calendar +susp=application/vnd.sus-calendar +sv4cpio=application/x-sv4cpio +sv4crc=application/x-sv4crc +svd=application/vnd.svd +svg=image/svg+xml +svgz=image/svg+xml +swa=application/x-director +swf=application/x-shockwave-flash +swi=application/vnd.arastra.swi +sxc=application/vnd.sun.xml.calc +sxd=application/vnd.sun.xml.draw +sxg=application/vnd.sun.xml.writer.global +sxi=application/vnd.sun.xml.impress +sxm=application/vnd.sun.xml.math +sxw=application/vnd.sun.xml.writer +t=text/troff +tao=application/vnd.tao.intent-module-archive +tar=application/x-tar +tcap=application/vnd.3gpp2.tcap +tcl=application/x-tcl +teacher=application/vnd.smart.teacher +tex=application/x-tex +texi=application/x-texinfo +texinfo=application/x-texinfo +text=text/plain +tfm=application/x-tex-tfm +tgz=application/x-gzip +tif=image/tiff +tiff=image/tiff +tmo=application/vnd.tmobile-livetv +torrent=application/x-bittorrent +tpl=application/vnd.groove-tool-template +tpt=application/vnd.trid.tpt +tr=text/troff +tra=application/vnd.trueapp +trm=application/x-msterminal +tsv=text/tab-separated-values +ttc=application/x-font-ttf +ttf=application/x-font-ttf +twd=application/vnd.simtech-mindmapper +twds=application/vnd.simtech-mindmapper +txd=application/vnd.genomatix.tuxedo +txf=application/vnd.mobius.txf +txt=text/plain +u32=application/x-authorware-bin +udeb=application/x-debian-package +ufd=application/vnd.ufdl +ufdl=application/vnd.ufdl +umj=application/vnd.umajin +unityweb=application/vnd.unity +uoml=application/vnd.uoml+xml +uri=text/uri-list +uris=text/uri-list +urls=text/uri-list +ustar=application/x-ustar +utz=application/vnd.uiq.theme +uu=text/x-uuencode +vcd=application/x-cdlink +vcf=text/x-vcard +vcg=application/vnd.groove-vcard +vcs=text/x-vcalendar +vcx=application/vnd.vcx +vis=application/vnd.visionary +viv=video/vnd.vivo +vor=application/vnd.stardivision.writer +vox=application/x-authorware-bin +vrml=model/vrml +vsd=application/vnd.visio +vsf=application/vnd.vsf +vss=application/vnd.visio +vst=application/vnd.visio +vsw=application/vnd.visio +vtu=model/vnd.vtu +vxml=application/voicexml+xml +w3d=application/x-director +wad=application/x-doom +wav=audio/x-wav +wax=audio/x-ms-wax +wbmp=image/vnd.wap.wbmp +wbs=application/vnd.criticaltools.wbs+xml +wbxml=application/vnd.wap.wbxml +wcm=application/vnd.ms-works +wdb=application/vnd.ms-works +wiz=application/msword +wks=application/vnd.ms-works +wm=video/x-ms-wm +wma=audio/x-ms-wma +wmd=application/x-ms-wmd +wmf=application/x-msmetafile +wml=text/vnd.wap.wml +wmlc=application/vnd.wap.wmlc +wmls=text/vnd.wap.wmlscript +wmlsc=application/vnd.wap.wmlscriptc +wmv=video/x-ms-wmv +wmx=video/x-ms-wmx +wmz=application/x-ms-wmz +wpd=application/vnd.wordperfect +wpl=application/vnd.ms-wpl +wps=application/vnd.ms-works +wqd=application/vnd.wqd +wri=application/x-mswrite +wrl=model/vrml +wsdl=application/wsdl+xml +wspolicy=application/wspolicy+xml +wtb=application/vnd.webturbo +wvx=video/x-ms-wvx +x32=application/x-authorware-bin +x3d=application/vnd.hzn-3d-crossword +xap=application/x-silverlight-app +xar=application/vnd.xara +xbap=application/x-ms-xbap +xbd=application/vnd.fujixerox.docuworks.binder +xbm=image/x-xbitmap +xdm=application/vnd.syncml.dm+xml +xdp=application/vnd.adobe.xdp+xml +xdw=application/vnd.fujixerox.docuworks +xenc=application/xenc+xml +xer=application/patch-ops-error+xml +xfdf=application/vnd.adobe.xfdf +xfdl=application/vnd.xfdl +xht=application/xhtml+xml +xhtml=application/xhtml+xml +xhvml=application/xv+xml +xif=image/vnd.xiff +xla=application/vnd.ms-excel +xlam=application/vnd.ms-excel.addin.macroenabled.12 +xlb=application/vnd.ms-excel +xlc=application/vnd.ms-excel +xlm=application/vnd.ms-excel +xls=application/vnd.ms-excel +xlsb=application/vnd.ms-excel.sheet.binary.macroenabled.12 +xlsm=application/vnd.ms-excel.sheet.macroenabled.12 +xlsx=application/vnd.openxmlformats-officedocument.spreadsheetml.sheet +xlt=application/vnd.ms-excel +xltm=application/vnd.ms-excel.template.macroenabled.12 +xltx=application/vnd.openxmlformats-officedocument.spreadsheetml.template +xlw=application/vnd.ms-excel +xml=application/xml +xo=application/vnd.olpc-sugar +xop=application/xop+xml +xpdl=application/xml +xpi=application/x-xpinstall +xpm=image/x-xpixmap +xpr=application/vnd.is-xpr +xps=application/vnd.ms-xpsdocument +xpw=application/vnd.intercon.formnet +xpx=application/vnd.intercon.formnet +xsl=application/xml +xslt=application/xslt+xml +xsm=application/vnd.syncml+xml +xspf=application/xspf+xml +xul=application/vnd.mozilla.xul+xml +xvm=application/xv+xml +xvml=application/xv+xml +xwd=image/x-xwindowdump +xyz=chemical/x-xyz +zaz=application/vnd.zzazz.deck+xml +zip=application/zip +zir=application/vnd.zul +zirz=application/vnd.zul +zmm=application/vnd.handheld-entertainment+xml diff --git a/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/test/java/io/awspring/cloud/s3/DiskBufferingS3OutputStreamTests.java b/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/test/java/io/awspring/cloud/s3/DiskBufferingS3OutputStreamTests.java index fb41060aa..704a622af 100644 --- a/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/test/java/io/awspring/cloud/s3/DiskBufferingS3OutputStreamTests.java +++ b/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/test/java/io/awspring/cloud/s3/DiskBufferingS3OutputStreamTests.java @@ -42,8 +42,8 @@ class DiskBufferingS3OutputStreamTests { void setsMd5hash() throws IOException { S3Client s3Client = mock(S3Client.class); - try (DiskBufferingS3OutputStream diskBufferingS3OutputStream = new DiskBufferingS3OutputStream("bucket", "key", - s3Client, null)) { + try (DiskBufferingS3OutputStream diskBufferingS3OutputStream = new DiskBufferingS3OutputStream( + new Location("bucket", "key"), s3Client, null)) { diskBufferingS3OutputStream.write("hello".getBytes(StandardCharsets.UTF_8)); } @@ -59,8 +59,8 @@ void throwsExceptionWhenUploadFails() throws IOException { when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class))).thenThrow(S3Exception.class); try { - try (DiskBufferingS3OutputStream diskBufferingS3OutputStream = new DiskBufferingS3OutputStream("bucket", - "key", s3Client, null)) { + try (DiskBufferingS3OutputStream diskBufferingS3OutputStream = new DiskBufferingS3OutputStream( + new Location("bucket", "key"), s3Client, null)) { diskBufferingS3OutputStream.write("hello".getBytes(StandardCharsets.UTF_8)); } fail("UploadFailedException should be thrown"); diff --git a/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/test/java/io/awspring/cloud/s3/PropertiesS3ObjectContentTypeResolverTests.java b/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/test/java/io/awspring/cloud/s3/PropertiesS3ObjectContentTypeResolverTests.java new file mode 100644 index 000000000..053607f8d --- /dev/null +++ b/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/test/java/io/awspring/cloud/s3/PropertiesS3ObjectContentTypeResolverTests.java @@ -0,0 +1,41 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * 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 + * + * https://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 io.awspring.cloud.s3; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link PropertiesS3ObjectContentTypeResolver}. + * + * @author Maciej Walkowiak + */ +class PropertiesS3ObjectContentTypeResolverTests { + private final PropertiesS3ObjectContentTypeResolver resolver = new PropertiesS3ObjectContentTypeResolver(); + + @Test + void resolvesTypeFromKnownProperties() { + String contentType = resolver.resolveContentType("object.txt"); + assertThat(contentType).isEqualTo("text/plain"); + } + + @Test + void returnsNullForUnknownExtension() { + String contentType = resolver.resolveContentType("object.xxx"); + assertThat(contentType).isNull(); + } +} diff --git a/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/test/java/io/awspring/cloud/s3/S3ResourceTests.java b/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/test/java/io/awspring/cloud/s3/S3ResourceIntegrationTests.java similarity index 81% rename from spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/test/java/io/awspring/cloud/s3/S3ResourceTests.java rename to spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/test/java/io/awspring/cloud/s3/S3ResourceIntegrationTests.java index 65ee1a1dc..d2b8f901a 100644 --- a/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/test/java/io/awspring/cloud/s3/S3ResourceTests.java +++ b/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/test/java/io/awspring/cloud/s3/S3ResourceIntegrationTests.java @@ -51,7 +51,7 @@ * @author Maciej Walkowiak */ @Testcontainers -class S3ResourceTests { +class S3ResourceIntegrationTests { @Container static LocalStackContainer localstack = new LocalStackContainer( @@ -62,9 +62,7 @@ class S3ResourceTests { @BeforeAll static void beforeAll() { // region and credentials are irrelevant for test, but must be added to make - // test - // work on - // environments without AWS cli configured + // test work on environments without AWS cli configured AWSCredentials localstackCredentials = localstack.getDefaultCredentialsProvider().getCredentials(); StaticCredentialsProvider credentialsProvider = StaticCredentialsProvider.create(AwsBasicCredentials .create(localstackCredentials.getAWSAccessKeyId(), localstackCredentials.getAWSSecretKey())); @@ -106,6 +104,15 @@ void objectHasContentLength() throws IOException { assertThat(resource.contentLength()).isEqualTo(contents.length()); } + @Test + void objectHasContentType() { + String contents = "{\"foo\":\"bar\"}"; + client.putObject(PutObjectRequest.builder().bucket("first-bucket").key("test-file.json") + .contentType("application/json").build(), RequestBody.fromString(contents)); + S3Resource resource = s3Resource("s3://first-bucket/test-file.json"); + assertThat(resource.contentType()).isEqualTo("application/json"); + } + @Test void contentLengthThrowsWhenResourceDoesNotExist() { S3Resource resource = s3Resource("s3://first-bucket/non-existing-file.txt"); @@ -131,8 +138,7 @@ void returnsEncodedResourceUrlAndUri() throws IOException, URISyntaxException { void resourceIsWritableWithDiskBuffering() throws IOException { client.putObject(PutObjectRequest.builder().bucket("first-bucket").key("test-file.txt").build(), RequestBody.fromString("test-file-content")); - S3Resource resource = s3Resource("s3://first-bucket/test-file.txt", - new DiskBufferingS3OutputStreamProvider(client)); + S3Resource resource = s3Resource("s3://first-bucket/test-file.txt", s3OutputStreamProvider()); try (OutputStream outputStream = resource.getOutputStream()) { outputStream.write("overwritten with buffering".getBytes(StandardCharsets.UTF_8)); @@ -140,10 +146,13 @@ void resourceIsWritableWithDiskBuffering() throws IOException { assertThat(retrieveContent(resource)).isEqualTo("overwritten with buffering"); } + private DiskBufferingS3OutputStreamProvider s3OutputStreamProvider() { + return new DiskBufferingS3OutputStreamProvider(client, new PropertiesS3ObjectContentTypeResolver()); + } + @Test void objectMetadataCanBeSetOnWriting() throws IOException { - S3Resource resource = s3Resource("s3://first-bucket/new-file.txt", - new DiskBufferingS3OutputStreamProvider(client)); + S3Resource resource = s3Resource("s3://first-bucket/new-file.txt", s3OutputStreamProvider()); ObjectMetadata objectMetadata = ObjectMetadata.builder().storageClass(StorageClass.ONEZONE_IA.name()) .metadata("key", "value").contentLanguage("en").build(); @@ -159,9 +168,21 @@ void objectMetadataCanBeSetOnWriting() throws IOException { assertThat(result.metadata()).containsEntry("key", "value"); } + @Test + void contentTypeCanBeResolved() throws IOException { + S3Resource resource = s3Resource("s3://first-bucket/new-file.txt", s3OutputStreamProvider()); + + try (OutputStream outputStream = resource.getOutputStream()) { + outputStream.write("content".getBytes(StandardCharsets.UTF_8)); + } + GetObjectResponse result = client + .getObject(request -> request.bucket("first-bucket").key("new-file.txt").build()).response(); + assertThat(result.contentType()).isEqualTo("text/plain"); + } + @NotNull private S3Resource s3Resource(String location) { - return new S3Resource(location, client, new DiskBufferingS3OutputStreamProvider(client)); + return new S3Resource(location, client, s3OutputStreamProvider()); } @NotNull @@ -175,4 +196,21 @@ private String retrieveContent(S3Resource resource) throws IOException { .collect(Collectors.joining("\n")); } + static class Person { + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return "Person{" + "name='" + name + '\'' + '}'; + } + } + } diff --git a/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/test/java/io/awspring/cloud/s3/S3TemplateIntegrationTests.java b/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/test/java/io/awspring/cloud/s3/S3TemplateIntegrationTests.java new file mode 100644 index 000000000..1d1874e38 --- /dev/null +++ b/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/test/java/io/awspring/cloud/s3/S3TemplateIntegrationTests.java @@ -0,0 +1,223 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * 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 + * + * https://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 io.awspring.cloud.s3; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatNoException; + +import com.amazonaws.auth.AWSCredentials; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.util.StreamUtils; +import org.testcontainers.containers.localstack.LocalStackContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.core.ResponseInputStream; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.GetObjectResponse; +import software.amazon.awssdk.services.s3.model.ListBucketsResponse; +import software.amazon.awssdk.services.s3.model.NoSuchKeyException; + +/** + * Integration tests for {@link S3Template}. + * + * @author Maciej Walkowiak + */ +@Testcontainers +class S3TemplateIntegrationTests { + + private static final String BUCKET_NAME = "test-bucket"; + + @Container + static LocalStackContainer localstack = new LocalStackContainer( + DockerImageName.parse("localstack/localstack:0.14.0")).withServices(LocalStackContainer.Service.S3) + .withReuse(true); + + private static S3Client client; + + private S3Template s3Template; + + @BeforeAll + static void beforeAll() { + // region and credentials are irrelevant for test, but must be added to make + // test work on environments without AWS cli configured + AWSCredentials localstackCredentials = localstack.getDefaultCredentialsProvider().getCredentials(); + StaticCredentialsProvider credentialsProvider = StaticCredentialsProvider.create(AwsBasicCredentials + .create(localstackCredentials.getAWSAccessKeyId(), localstackCredentials.getAWSSecretKey())); + client = S3Client.builder().region(Region.of(localstack.getRegion())).credentialsProvider(credentialsProvider) + .endpointOverride(localstack.getEndpointOverride(LocalStackContainer.Service.S3)).build(); + } + + @BeforeEach + void init() { + this.s3Template = new S3Template(client, + new DiskBufferingS3OutputStreamProvider(client, new PropertiesS3ObjectContentTypeResolver()), + new Jackson2JsonS3ObjectConverter(new ObjectMapper())); + + client.createBucket(r -> r.bucket("test-bucket")); + } + + @AfterEach + void destroyBuckets() { + client.listBuckets().buckets().forEach(b -> { + client.listObjects(r -> r.bucket(b.name())).contents() + .forEach(s3Object -> client.deleteObject(r -> r.bucket(b.name()).key(s3Object.key()))); + client.deleteBucket(r -> r.bucket(b.name())); + }); + } + + @Test + void createsBucket() { + String location = s3Template.createBucket("test-bucket"); + + assertThat(location).isNotNull(); + assertThat(client.listBuckets()).satisfies(r -> this.bucketExists(r, "test-bucket")); + } + + @Test + void deletesBucket() { + client.createBucket(r -> r.bucket("test-bucket")); + assertThat(client.listBuckets()).satisfies(r -> this.bucketExists(r, "test-bucket")); + + s3Template.deleteBucket("test-bucket"); + + assertThat(client.listBuckets()).satisfies(r -> this.bucketDoesNotExist(r, "test-bucket")); + } + + @Test + void deletesObject() { + client.createBucket(r -> r.bucket("test-bucket")); + client.putObject(r -> r.bucket("test-bucket").key("key.txt"), RequestBody.fromString("foo")); + assertThatNoException().isThrownBy(() -> client.headObject(r -> r.bucket("test-bucket").key("key.txt"))); + + s3Template.deleteObject("test-bucket", "key.txt"); + + assertThatExceptionOfType(NoSuchKeyException.class) + .isThrownBy(() -> client.headObject(r -> r.bucket("test-bucket").key("key.txt"))); + } + + @Test + void deletesObjectByS3Url() { + client.putObject(r -> r.bucket("test-bucket").key("key.txt"), RequestBody.fromString("foo")); + assertThatNoException().isThrownBy(() -> client.headObject(r -> r.bucket("test-bucket").key("key.txt"))); + + s3Template.deleteObject("s3://test-bucket/key.txt"); + + assertThatExceptionOfType(NoSuchKeyException.class) + .isThrownBy(() -> client.headObject(r -> r.bucket("test-bucket").key("key.txt"))); + } + + @Test + void storesObject() throws IOException { + s3Template.store("test-bucket", "person.json", new Person("John", "Doe")); + + ResponseInputStream response = client + .getObject(r -> r.bucket("test-bucket").key("person.json")); + String result = StreamUtils.copyToString(response, StandardCharsets.UTF_8); + + assertThat(result).isEqualTo("{\"firstName\":\"John\",\"lastName\":\"Doe\"}"); + assertThat(response.response().contentType()).isEqualTo("application/json"); + } + + @Test + void readsObject() { + client.putObject(r -> r.bucket("test-bucket").key("person.json"), + RequestBody.fromString("{\"firstName\":\"John\",\"lastName\":\"Doe\"}")); + + Person person = s3Template.read("test-bucket", "person.json", Person.class); + + assertThat(person.firstName).isEqualTo("John"); + assertThat(person.lastName).isEqualTo("Doe"); + } + + @Test + void uploadsFile() throws IOException { + try (InputStream is = new ByteArrayInputStream("hello".getBytes(StandardCharsets.UTF_8))) { + s3Template.upload("test-bucket", "file.txt", is, + ObjectMetadata.builder().contentType("text/plain").build()); + } + + ResponseInputStream response = client + .getObject(r -> r.bucket("test-bucket").key("file.txt")); + String result = StreamUtils.copyToString(response, StandardCharsets.UTF_8); + assertThat(result).isEqualTo("hello"); + assertThat(response.response().contentType()).isEqualTo("text/plain"); + } + + @Test + void downloadsFile() throws IOException { + client.putObject(r -> r.bucket("test-bucket").key("file.txt"), RequestBody.fromString("hello")); + S3Resource resource = (S3Resource) s3Template.download("test-bucket", "file.txt"); + assertThat(resource.contentLength()).isEqualTo(5); + assertThat(resource.getDescription()).isNotNull(); + + try (InputStream is = resource.getInputStream()) { + String result = StreamUtils.copyToString(is, StandardCharsets.UTF_8); + assertThat(result).isEqualTo("hello"); + } + } + + private void bucketDoesNotExist(ListBucketsResponse r, String bucketName) { + assertThat(r.buckets().stream().filter(b -> b.name().equals(bucketName)).findAny()).isEmpty(); + } + + private void bucketExists(ListBucketsResponse r, String bucketName) { + assertThat(r.buckets().stream().filter(b -> b.name().equals(bucketName)).findAny()).isPresent(); + } + + static class Person { + private String firstName; + private String lastName; + + public Person() { + } + + public Person(String firstName, String lastName) { + this.firstName = firstName; + this.lastName = lastName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + } + +} diff --git a/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/test/java/io/awspring/cloud/s3/S3TemplateTests.java b/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/test/java/io/awspring/cloud/s3/S3TemplateTests.java new file mode 100644 index 000000000..ec338760f --- /dev/null +++ b/spring-cloud-aws-s3-parent/spring-cloud-aws-s3/src/test/java/io/awspring/cloud/s3/S3TemplateTests.java @@ -0,0 +1,75 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * 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 + * + * https://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 io.awspring.cloud.s3; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.function.Consumer; +import org.junit.jupiter.api.Test; +import org.mockito.Answers; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; + +/** + * Unit tests for {@link S3Template}. Tests edge cases not tested by {@link S3TemplateIntegrationTests}. + * + * @author Maciej Walkowiak + */ +class S3TemplateTests { + + private final S3Client client = mock(S3Client.class); + private final S3OutputStreamProvider s3OutputStreamProvider = mock(S3OutputStreamProvider.class, + Answers.RETURNS_DEEP_STUBS); + private final S3ObjectConverter s3ObjectConverter = mock(S3ObjectConverter.class); + + private final S3Template s3Template = new S3Template(client, s3OutputStreamProvider, s3ObjectConverter); + + @Test + void throwsExceptionWhenUploadFails() throws IOException { + when(s3OutputStreamProvider.create(any(), any(), any())).thenThrow(IOException.class); + + assertThatExceptionOfType(S3Exception.class).isThrownBy(() -> { + try (InputStream is = new ByteArrayInputStream("content".getBytes(StandardCharsets.UTF_8))) { + s3Template.upload("bucket-name", "key-name", is); + } + }).satisfies(ex -> { + assertThat(ex.getMessage()) + .isEqualTo("Failed to upload object with a key 'key-name' to bucket 'bucket-name'"); + }); + } + + @Test + void throwsExceptionWhenReadFails() { + when(client.getObject((Consumer) any())).thenThrow(RuntimeException.class); + + assertThatExceptionOfType(S3Exception.class).isThrownBy(() -> { + s3Template.read("bucket-name", "key-name", String.class); + }).satisfies(ex -> { + assertThat(ex.getMessage()) + .isEqualTo("Failed to read object with a key 'key-name' from bucket 'bucket-name'"); + }); + } + +} diff --git a/spring-cloud-aws-samples/spring-cloud-aws-s3-sample/app/pom.xml b/spring-cloud-aws-samples/spring-cloud-aws-s3-sample/app/pom.xml index e338f04c0..f1207c666 100644 --- a/spring-cloud-aws-samples/spring-cloud-aws-s3-sample/app/pom.xml +++ b/spring-cloud-aws-samples/spring-cloud-aws-s3-sample/app/pom.xml @@ -15,7 +15,7 @@ org.springframework.boot - spring-boot-starter + spring-boot-starter-web diff --git a/spring-cloud-aws-samples/spring-cloud-aws-s3-sample/app/src/main/java/io/awspring/cloud/samples/s3/FileController.java b/spring-cloud-aws-samples/spring-cloud-aws-s3-sample/app/src/main/java/io/awspring/cloud/samples/s3/FileController.java new file mode 100644 index 000000000..eb44615a2 --- /dev/null +++ b/spring-cloud-aws-samples/spring-cloud-aws-s3-sample/app/src/main/java/io/awspring/cloud/samples/s3/FileController.java @@ -0,0 +1,61 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * 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 + * + * https://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 io.awspring.cloud.samples.s3; + +import io.awspring.cloud.s3.ObjectMetadata; +import io.awspring.cloud.s3.S3Operations; +import java.io.IOException; +import java.io.InputStream; +import org.springframework.core.io.Resource; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +/** + * An example on how to create file upload to S3. + */ +@RestController +@RequestMapping("/file") +public class FileController { + private static final String BUCKET = "spring-cloud-aws-sample-bucket1"; + private final S3Operations s3Operations; + + public FileController(S3Operations s3Operations) { + this.s3Operations = s3Operations; + } + + @PostMapping + ResponseEntity upload(MultipartFile multipartFile) throws IOException { + if (!MediaType.IMAGE_PNG.toString().equals(multipartFile.getContentType())) { + return ResponseEntity.badRequest().body("only png images are allowed"); + } + try (InputStream is = multipartFile.getInputStream()) { + s3Operations.upload(BUCKET, multipartFile.getOriginalFilename(), is, + ObjectMetadata.builder().contentType(multipartFile.getContentType()).build()); + } + return ResponseEntity.accepted().build(); + } + + @GetMapping + ResponseEntity download(@RequestParam String key) { + return ResponseEntity.ok().contentType(MediaType.IMAGE_PNG).body(s3Operations.download(BUCKET, key)); + } +} diff --git a/spring-cloud-aws-samples/spring-cloud-aws-s3-sample/app/src/main/java/io/awspring/cloud/samples/s3/ObjectStorageController.java b/spring-cloud-aws-samples/spring-cloud-aws-s3-sample/app/src/main/java/io/awspring/cloud/samples/s3/ObjectStorageController.java new file mode 100644 index 000000000..ecf4ae157 --- /dev/null +++ b/spring-cloud-aws-samples/spring-cloud-aws-s3-sample/app/src/main/java/io/awspring/cloud/samples/s3/ObjectStorageController.java @@ -0,0 +1,87 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * 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 + * + * https://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 io.awspring.cloud.samples.s3; + +import io.awspring.cloud.s3.S3Template; +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.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * Stores regular Java objects as S3 files. + */ +@RestController +@RequestMapping("/object") +public class ObjectStorageController { + private static final String BUCKET = "spring-cloud-aws-sample-bucket1"; + + private final S3Template s3Template; + + public ObjectStorageController(S3Template s3Template) { + this.s3Template = s3Template; + } + + @PostMapping + void store(@RequestBody Person p) { + s3Template.store(BUCKET, p.id + ".json", p); + } + + @GetMapping("/{id}") + Person read(@PathVariable Long id) { + return s3Template.read(BUCKET, id + ".json", Person.class); + } + + static class Person { + private Long id; + private String firstName; + private String lastName; + + public Person() { + } + + public Person(Long id, String firstName, String lastName) { + this.firstName = firstName; + this.lastName = lastName; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + } +}