Skip to content

Commit

Permalink
Auto-configure S3 TransferManager (#361)
Browse files Browse the repository at this point in the history
- autoconfigures S3 Transfer Manager when transfer manager is added to the classpath
- autoconfigures Transfer Manager based `S3OutputStreamProvider`

Fixes #300 

Co-authored-by: Maciej Walkowiak <walkowiak.maciej@yahoo.com>
  • Loading branch information
krimsz and maciejwalkowiak authored May 4, 2022
1 parent baabdaa commit 263211e
Show file tree
Hide file tree
Showing 22 changed files with 851 additions and 171 deletions.
19 changes: 18 additions & 1 deletion docs/src/main/asciidoc/s3.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,22 @@ public class S3ClientSample {
}
}
----
=== Using S3TransferManager

[CAUTION]
====
S3 Transfer Manager provided by AWS is in developer preview phase and should not be used in production.
====

AWS launched https://aws.amazon.com/blogs/developer/introducing-amazon-s3-transfer-manager-in-the-aws-sdk-for-java-2-x/[a high level file transfer utility], called Transfer Manager. The starter automatically configures and registers an `software.amazon.awssdk.transfer.s3.S3TransferManager` bean if it finds the following is added to the project:

[source,xml]
----
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3-transfer-manager</artifactId>
</dependency>
----

=== Using Cross-Region S3 client

Expand Down Expand Up @@ -143,10 +159,11 @@ try (OutputStream outputStream = s3Resource.getOutputStream()) {
// e.getPath contains a file location in temporary folder
}
----
If you are using the `S3TransferManager`, the default implementation will switch to `io.awspring.cloud.s3.TransferManagerS3OutputStream`. This OutputStream also uses a temporary file to write it on disk before uploading it to S3, but it may be faster as it uses a multi-part upload under the hood.

If `DiskBufferingS3OutputStream` behavior does not fit your needs, you can implement custom `S3OutputStream` and provide a bean of type `io.awspring.cloud.s3.S3OutputStreamProvider` that is responsible for creating stream from `S3Resource`.

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].
Possible alternative implementations can use multi-part upload (for example with https://github.com/CI-CMG/aws-s3-outputstream[aws-s3-outputstream library]).

=== Using S3Template

Expand Down
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
<spring-cloud-commons.version>3.1.0</spring-cloud-commons.version>
<testcontainers.version>1.17.1</testcontainers.version>
<spotless.version>2.22.0</spotless.version>
<awssdk.version>2.17.172</awssdk.version>
</properties>

<modules>
Expand Down
5 changes: 5 additions & 0 deletions spring-cloud-aws-autoconfigure/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@
<artifactId>spring-cloud-aws-s3</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3-transfer-manager</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.awspring.cloud</groupId>
<artifactId>spring-cloud-aws-s3-cross-region-client</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@
package io.awspring.cloud.autoconfigure.core;

import io.awspring.cloud.autoconfigure.AwsClientProperties;
import io.awspring.cloud.autoconfigure.s3.properties.S3Properties;
import io.awspring.cloud.core.SpringCloudClientConfiguration;
import java.util.Optional;
import org.springframework.util.StringUtils;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.awscore.client.builder.AwsClientBuilder;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.regions.providers.AwsRegionProvider;
import software.amazon.awssdk.transfer.s3.S3ClientConfiguration;

/**
* Provides a convenience method to apply common configuration to any {@link AwsClientBuilder}.
Expand All @@ -43,12 +45,24 @@ public class AwsClientBuilderConfigurer {
}

public AwsClientBuilder<?, ?> configure(AwsClientBuilder<?, ?> builder, AwsClientProperties clientProperties) {
Region region = StringUtils.hasLength(clientProperties.getRegion()) ? Region.of(clientProperties.getRegion())
: regionProvider.getRegion();
builder.credentialsProvider(credentialsProvider).region(region)
builder.credentialsProvider(credentialsProvider).region(resolveRegion(clientProperties))
.overrideConfiguration(SpringCloudClientConfiguration.clientOverrideConfiguration());
Optional.ofNullable(awsProperties.getEndpoint()).ifPresent(builder::endpointOverride);
Optional.ofNullable(clientProperties.getEndpoint()).ifPresent(builder::endpointOverride);
return builder;
}

public S3ClientConfiguration.Builder configure(S3ClientConfiguration.Builder builder,
S3Properties clientProperties) {
builder.credentialsProvider(credentialsProvider).region(resolveRegion(clientProperties));
// TODO: how to set client override configuration?
Optional.ofNullable(awsProperties.getEndpoint()).ifPresent(builder::endpointOverride);
Optional.ofNullable(clientProperties.getEndpoint()).ifPresent(builder::endpointOverride);
return builder;
}

private Region resolveRegion(AwsClientProperties clientProperties) {
return StringUtils.hasLength(clientProperties.getRegion()) ? Region.of(clientProperties.getRegion())
: regionProvider.getRegion();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import io.awspring.cloud.autoconfigure.core.AwsClientBuilderConfigurer;
import io.awspring.cloud.autoconfigure.core.AwsProperties;
import io.awspring.cloud.autoconfigure.s3.properties.S3Properties;
import io.awspring.cloud.s3.DiskBufferingS3OutputStreamProvider;
import io.awspring.cloud.s3.Jackson2JsonS3ObjectConverter;
import io.awspring.cloud.s3.PropertiesS3ObjectContentTypeResolver;
Expand Down Expand Up @@ -71,14 +72,6 @@ S3ClientBuilder s3ClientBuilder(AwsClientBuilderConfigurer awsClientBuilderConfi
return builder;
}

@Bean
@ConditionalOnMissingBean
S3OutputStreamProvider s3OutputStreamProvider(S3Client s3Client,
Optional<S3ObjectContentTypeResolver> contentTypeResolver) {
return new DiskBufferingS3OutputStreamProvider(s3Client,
contentTypeResolver.orElseGet(PropertiesS3ObjectContentTypeResolver::new));
}

@Bean
@ConditionalOnMissingBean(S3Operations.class)
@ConditionalOnBean(S3ObjectConverter.class)
Expand Down Expand Up @@ -135,4 +128,12 @@ S3ObjectConverter s3ObjectConverter(Optional<ObjectMapper> objectMapper) {
}
}

@Bean
@ConditionalOnMissingBean
S3OutputStreamProvider diskBufferingS3StreamProvider(S3Client s3Client,
Optional<S3ObjectContentTypeResolver> contentTypeResolver) {
return new DiskBufferingS3OutputStreamProvider(s3Client,
contentTypeResolver.orElseGet(PropertiesS3ObjectContentTypeResolver::new));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* 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.autoconfigure.s3;

import io.awspring.cloud.autoconfigure.core.AwsClientBuilderConfigurer;
import io.awspring.cloud.autoconfigure.s3.properties.S3Properties;
import io.awspring.cloud.autoconfigure.s3.properties.S3TransferManagerProperties;
import io.awspring.cloud.s3.PropertiesS3ObjectContentTypeResolver;
import io.awspring.cloud.s3.S3ObjectContentTypeResolver;
import io.awspring.cloud.s3.S3OutputStreamProvider;
import io.awspring.cloud.s3.TransferManagerS3OutputStreamProvider;
import java.util.Optional;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import software.amazon.awssdk.transfer.s3.S3ClientConfiguration;
import software.amazon.awssdk.transfer.s3.S3TransferManager;
import software.amazon.awssdk.transfer.s3.S3TransferManagerOverrideConfiguration;
import software.amazon.awssdk.transfer.s3.UploadDirectoryOverrideConfiguration;

/**
* {@link EnableAutoConfiguration} for {@link S3TransferManager}
*
* @author Anton Perez
* @since 3.0
*/
@ConditionalOnClass({ S3TransferManager.class, S3OutputStreamProvider.class })
@EnableConfigurationProperties({ S3Properties.class })
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(name = "spring.cloud.aws.s3.enabled", havingValue = "true", matchIfMissing = true)
@AutoConfigureBefore(S3AutoConfiguration.class)
public class S3TransferManagerAutoConfiguration {

private final S3Properties properties;

public S3TransferManagerAutoConfiguration(S3Properties properties) {
this.properties = properties;
}

@Bean
@ConditionalOnMissingBean
S3TransferManager s3TransferManager(AwsClientBuilderConfigurer awsClientBuilderConfigurer) {
return S3TransferManager.builder().s3ClientConfiguration(s3ClientConfiguration(awsClientBuilderConfigurer))
.transferConfiguration(extractUploadDirectoryOverrideConfiguration()).build();
}

@Bean
@ConditionalOnMissingBean
S3OutputStreamProvider transferManagerS3StreamProvider(S3TransferManager s3TransferManager,
Optional<S3ObjectContentTypeResolver> contentTypeResolver) {
return new TransferManagerS3OutputStreamProvider(s3TransferManager,
contentTypeResolver.orElseGet(PropertiesS3ObjectContentTypeResolver::new));
}

private S3ClientConfiguration s3ClientConfiguration(AwsClientBuilderConfigurer awsClientBuilderConfigurer) {
S3ClientConfiguration.Builder builder = awsClientBuilderConfigurer.configure(S3ClientConfiguration.builder(),
properties);
if (properties.getTransferManager() != null) {
S3TransferManagerProperties transferManagerProperties = properties.getTransferManager();
PropertyMapper propertyMapper = PropertyMapper.get();
propertyMapper.from(transferManagerProperties::getMaxConcurrency).whenNonNull().to(builder::maxConcurrency);
propertyMapper.from(transferManagerProperties::getTargetThroughputInGbps).whenNonNull()
.to(builder::targetThroughputInGbps);
propertyMapper.from(transferManagerProperties::getMinimumPartSizeInBytes).whenNonNull()
.to(builder::minimumPartSizeInBytes);
}
return builder.build();
}

private S3TransferManagerOverrideConfiguration extractUploadDirectoryOverrideConfiguration() {
UploadDirectoryOverrideConfiguration.Builder config = UploadDirectoryOverrideConfiguration.builder();
if (properties.getTransferManager() != null && properties.getTransferManager().getUploadDirectory() != null) {
S3TransferManagerProperties.S3UploadDirectoryProperties s3UploadDirectoryProperties = properties
.getTransferManager().getUploadDirectory();
PropertyMapper propertyMapper = PropertyMapper.get();
propertyMapper.from(s3UploadDirectoryProperties::getMaxDepth).whenNonNull().to(config::maxDepth);
propertyMapper.from(s3UploadDirectoryProperties::getRecursive).whenNonNull().to(config::recursive);
propertyMapper.from(s3UploadDirectoryProperties::getFollowSymbolicLinks).whenNonNull()
.to(config::followSymbolicLinks);
}
return S3TransferManagerOverrideConfiguration.builder().uploadDirectoryConfiguration(config.build()).build();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.awspring.cloud.autoconfigure.s3;
package io.awspring.cloud.autoconfigure.s3.properties;

import io.awspring.cloud.autoconfigure.AwsClientProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
import org.springframework.lang.Nullable;
import software.amazon.awssdk.transfer.s3.S3TransferManager;

/**
* Properties related to AWS S3.
Expand Down Expand Up @@ -75,6 +77,13 @@ public class S3Properties extends AwsClientProperties {
@Nullable
private Boolean useArnRegionEnabled;

/**
* Configuration properties for {@link S3TransferManager} integration.
*/
@Nullable
@NestedConfigurationProperty
private S3TransferManagerProperties transferManager;

@Nullable
public Boolean getAccelerateModeEnabled() {
return accelerateModeEnabled;
Expand Down Expand Up @@ -129,4 +138,12 @@ public void setUseArnRegionEnabled(@Nullable Boolean useArnRegionEnabled) {
this.useArnRegionEnabled = useArnRegionEnabled;
}

@Nullable
public S3TransferManagerProperties getTransferManager() {
return transferManager;
}

public void setTransferManager(@Nullable S3TransferManagerProperties transferManager) {
this.transferManager = transferManager;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* 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.autoconfigure.s3.properties;

import org.springframework.lang.Nullable;
import software.amazon.awssdk.transfer.s3.S3TransferManager;

/**
* Properties related to AWS S3 {@link S3TransferManager}.
*
* @author Anton Perez
* @since 3.0
*/
public class S3TransferManagerProperties {
@Nullable
private Double targetThroughputInGbps;

@Nullable
private Integer maxConcurrency;

@Nullable
private Long minimumPartSizeInBytes;

@Nullable
private S3UploadDirectoryProperties uploadDirectory;

@Nullable
public Double getTargetThroughputInGbps() {
return targetThroughputInGbps;
}

public void setTargetThroughputInGbps(@Nullable Double targetThroughputInGbps) {
this.targetThroughputInGbps = targetThroughputInGbps;
}

@Nullable
public S3UploadDirectoryProperties getUploadDirectory() {
return uploadDirectory;
}

public void setUploadDirectory(@Nullable S3UploadDirectoryProperties uploadDirectory) {
this.uploadDirectory = uploadDirectory;
}

@Nullable
public Integer getMaxConcurrency() {
return maxConcurrency;
}

public void setMaxConcurrency(@Nullable Integer maxConcurrency) {
this.maxConcurrency = maxConcurrency;
}

@Nullable
public Long getMinimumPartSizeInBytes() {
return minimumPartSizeInBytes;
}

public void setMinimumPartSizeInBytes(@Nullable Long minimumPartSizeInBytes) {
this.minimumPartSizeInBytes = minimumPartSizeInBytes;
}

public static class S3UploadDirectoryProperties {
@Nullable
private Boolean recursive;
@Nullable
private Boolean followSymbolicLinks;
@Nullable
private Integer maxDepth;

@Nullable
public Boolean getRecursive() {
return recursive;
}

public void setRecursive(@Nullable Boolean recursive) {
this.recursive = recursive;
}

@Nullable
public Boolean getFollowSymbolicLinks() {
return followSymbolicLinks;
}

public void setFollowSymbolicLinks(@Nullable Boolean followSymbolicLinks) {
this.followSymbolicLinks = followSymbolicLinks;
}

@Nullable
public Integer getMaxDepth() {
return maxDepth;
}

public void setMaxDepth(@Nullable Integer maxDepth) {
this.maxDepth = maxDepth;
}
}

}
Loading

0 comments on commit 263211e

Please sign in to comment.