Skip to content

Commit e16f9ba

Browse files
Refactor S3TransferManager to support non-file-based transfers (#2817)
* Refactor S3TransferManager to support non-file-based transfers ## Motivation We would like for S3TransferManager to support non-file-based uploads and downloads, such as reactive streams or in-memory byte buffers, but the current implementation is directly coupled to file/Path-based transfers. While the underlying S3AsyncClient already has support for AsyncRequestBodies and AsyncResponseTransformers, these constructs are not exposed through the current S3TransferManager interface. Additionally, the existing S3TransferManager interfaces have made some assumptions that make it difficult to introduce this support, such as UploadRequest/DownloadRequest having a Path attribute, and not having interfaces that allow us to distinguish between transfer types, e.g., single-object and multi-object (directory) transfers. Likewise, the current interface hierarchy also makes it difficult to distinguish between file-based and non-file-based. Therefore, this refactoring requires backwards-incompatible changes, which are permissible while S3TransferManager is still in PREVIEW. ## Modifications The diff here is very large, but this change set primarily introduces the following interface hierarchy: * TransferRequest * TransferObjectRequest * UploadFileRequest * DownloadFileRequest * UploadRequest * DownloadRequest<T> * TransferDirectoryRequest * UploadDirectoryRequest * Transfer * ObjectTransfer * FileUpload * FileDownload * Upload * Download<T> * DirectoryTransfer * DirectoryUpload * CompletedTransfer * CompletedObjectTransfer * CompletedFileUpload * CompletedFileDownload * CompletedUpload * CompletedDownload<T> * CompletedDirectoryTransfer * CompletedDirectoryUpload * FailedObjectTransfer * FailedFileUpload These interfaces allow us to more selectively choose which attributes will be shared between different data types. Additionally, we take this opportunity to make the naming convention and class-vs-interface distinction consistent across all the above data types. It is important that we distinguish between file-based-requests and non-file-based-requests so that the Collection<> of failed file transfers from a directory upload can be re-driven, if needed. Note that the above Download-oriented data types are all generic. This is because of how AsyncResponseTransformer is designed to be generic, being parameterized with the type of data that a given AsyncResponseTransformer produces. Therefore, a user must declare the type at the time of instantiating a DownloadRequest, and the type will be consistent throughout the transfer lifecycle, including the Download and CompletedDownload interfaces.
1 parent 844a9fd commit e16f9ba

File tree

72 files changed

+3125
-1011
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

72 files changed

+3125
-1011
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"category": "Amazon S3",
3+
"contributor": "",
4+
"type": "feature",
5+
"description": "[Breaking Changes] Refactor S3TransferManager (PREVIEW) to support non-file-based transfers. This release refactors the S3TransferManager interface hierarchy and client API to differentiate between file-based and non-file-based transfers, allowing arbitrary object transfers. As a result, some S3TransferManager method signatures have changed in a backwards-incompatible way. Most notably, `Upload upload(UploadRequest)` becomes `FileUpload uploadFile(UploadFileRequest)`, and likewise for download variants. Please see https://github.com/aws/aws-sdk-java-v2/pull/2817 for a full list of changes."
6+
}

services-custom/s3-transfer-manager/README.md

Lines changed: 59 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,47 +19,88 @@ First, you need to include the dependency in your project.
1919
```
2020

2121
Note that you need to replace `${awsjavasdk.version}` with the latest
22-
SDK version
22+
SDK version.
2323

2424
### Instantiate the transfer manager
25-
You can instantiate the transfer manager easily using the default settings
2625

27-
```java
26+
You can instantiate the transfer manager easily using the default settings:
2827

29-
S3TransferManager transferManager = S3TransferManager.create();
30-
28+
```java
29+
S3TransferManager tm = S3TransferManager.create();
3130
```
3231

3332
If you wish to configure settings, we recommend using the builder instead:
33+
3434
```java
35-
S3TransferManager transferManager =
35+
S3TransferManager tm =
3636
S3TransferManager.builder()
3737
.s3ClientConfiguration(b -> b.credentialsProvider(credentialProvider)
3838
.region(Region.US_WEST_2)
3939
.targetThroughputInGbps(20.0)
40-
.minimumPartSizeInBytes(10 * MB))
40+
.minimumPartSizeInBytes(8 * MB))
4141
.build();
4242
```
4343

44+
### Upload a file to S3
45+
46+
To upload a file to S3, you just need to provide the source file path and the `PutObjectRequest` that should be used for the upload:
47+
48+
```java
49+
FileUpload upload =
50+
tm.uploadFile(u -> u.source(Paths.get("myFile.txt"))
51+
.putObjectRequest(p -> p.bucket("bucket").key("key")));
52+
upload.completionFuture().join();
53+
```
54+
4455
### Download an S3 object to a file
45-
To download an object, you just need to provide the destination file path and the `GetObjectRequest` that should be used for the download.
56+
57+
To download an object, you just need to provide the destination file path and the `GetObjectRequest` that should be used for the download:
4658

4759
```java
48-
Download download =
49-
transferManager.download(b -> b.destination(path)
50-
.getObjectRequest(r -> r.bucket("bucket")
51-
.key("key")));
60+
FileDownload download =
61+
tm.downloadFile(d -> d.getObjectRequest(g -> g.bucket("bucket").key("key"))
62+
.destination(Paths.get("myFile.txt")));
5263
download.completionFuture().join();
5364
```
5465

55-
### Upload a file to S3
56-
To upload a file to S3, you just need to provide the source file path and the `PutObjectRequest` that should be used for the upload.
66+
### Upload any content to S3
5767

58-
```java
59-
Upload upload = transferManager.upload(b -> b.source(path)
60-
.putObjectRequest(r -> r.bucket("bucket")
61-
.key("key")));
68+
You may upload any arbitrary content to S3 by providing an `AsyncRequestBody`:
6269

70+
```java
71+
Upload upload =
72+
tm.upload(u -> u.requestBody(AsyncRequestBody.fromString("Hello world"))
73+
.putObjectRequest(p -> p.bucket("bucket").key("key")));
6374
upload.completionFuture().join();
75+
```
6476

77+
Refer to the static factory methods available in `AsyncRequestBody` for other content sources.
78+
79+
### Download an S3 object to a custom destination
80+
81+
You may download an object from S3 to a custom destination by providing an `AsyncResponseTransformer`:
82+
83+
*(This example buffers the entire object in memory and is not suitable for large objects.)*
84+
85+
```java
86+
Download<ResponseBytes<GetObjectResponse>> download =
87+
tm.download(d -> d.getObjectRequest(g -> g.bucket("bucket").key("key"))
88+
.responseTransformer(AsyncResponseTransformer.toBytes()));
89+
download.completionFuture().join();
90+
```
91+
92+
Refer to the static factory methods available in `AsyncResponseTransformer` for other destinations.
93+
94+
### Attach a TransferListener
95+
96+
To monitor a transfer's progress, you can include a `TransferListener` with your transfer request:
97+
98+
```java
99+
FileUpload upload =
100+
tm.uploadFile(u -> u.source(Paths.get("myFile.txt"))
101+
.putObjectRequest(p -> p.bucket("bucket").key("key"))
102+
.overrideConfiguration(o -> o.addListener(LoggingTransferListener.create())));
103+
upload.completionFuture().join();
65104
```
105+
106+
You can provide your own implementation of a `TransferListener` to implement progress-bar-type functionality.

services-custom/s3-transfer-manager/src/it/java/software/amazon/awssdk/transfer/s3/CrtExceptionTransformationIntegrationTest.java

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@
1515

1616
package software.amazon.awssdk.transfer.s3;
1717

18+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
19+
import static software.amazon.awssdk.testutils.service.S3BucketUtils.temporaryBucketName;
20+
21+
import java.io.IOException;
22+
import java.nio.file.Files;
23+
import java.nio.file.Paths;
1824
import org.junit.AfterClass;
1925
import org.junit.BeforeClass;
2026
import org.junit.Test;
@@ -25,13 +31,6 @@
2531
import software.amazon.awssdk.testutils.RandomTempFile;
2632
import software.amazon.awssdk.transfer.s3.internal.S3CrtAsyncClient;
2733

28-
import java.io.IOException;
29-
import java.nio.file.Files;
30-
import java.nio.file.Paths;
31-
32-
import static org.assertj.core.api.Assertions.assertThatThrownBy;
33-
import static software.amazon.awssdk.testutils.service.S3BucketUtils.temporaryBucketName;
34-
3534
public class CrtExceptionTransformationIntegrationTest extends S3IntegrationTestBase {
3635

3736
private static final String BUCKET = temporaryBucketName(CrtExceptionTransformationIntegrationTest.class);
@@ -87,21 +86,21 @@ public void getObjectNoSuchBucket() throws IOException {
8786
@Test
8887
public void transferManagerDownloadObjectWithNoSuchKey() throws IOException {
8988
String randomBaseDirectory = Files.createTempDirectory(getClass().getSimpleName()).toString();
90-
assertThatThrownBy(() -> transferManager.download(DownloadRequest.builder()
91-
.getObjectRequest(GetObjectRequest.builder().bucket(BUCKET).key("randomKey").build())
92-
.destination(Paths.get(randomBaseDirectory).resolve("testFile"))
93-
.build()).completionFuture().join())
89+
assertThatThrownBy(() -> transferManager.downloadFile(DownloadFileRequest.builder()
90+
.getObjectRequest(GetObjectRequest.builder().bucket(BUCKET).key("randomKey").build())
91+
.destination(Paths.get(randomBaseDirectory).resolve("testFile"))
92+
.build()).completionFuture().join())
9493
.hasCauseInstanceOf(NoSuchKeyException.class)
9594
.hasMessageContaining("software.amazon.awssdk.services.s3.model.NoSuchKeyException: The specified key does not exist");
9695
}
9796

9897
@Test
9998
public void transferManagerDownloadObjectWithNoSuchBucket() throws IOException {
10099
String randomBaseDirectory = Files.createTempDirectory(getClass().getSimpleName()).toString();
101-
assertThatThrownBy(() -> transferManager.download(DownloadRequest.builder()
102-
.getObjectRequest(GetObjectRequest.builder().bucket("nonExistingTestBucket").key(KEY).build())
103-
.destination(Paths.get(randomBaseDirectory).resolve("testFile"))
104-
.build()).completionFuture().join())
100+
assertThatThrownBy(() -> transferManager.downloadFile(DownloadFileRequest.builder()
101+
.getObjectRequest(GetObjectRequest.builder().bucket("nonExistingTestBucket").key(KEY).build())
102+
.destination(Paths.get(randomBaseDirectory).resolve("testFile"))
103+
.build()).completionFuture().join())
105104
.hasCauseInstanceOf(NoSuchBucketException.class)
106105
.hasMessageContaining("software.amazon.awssdk.services.s3.model.NoSuchBucketException: The specified bucket does not exist");
107106
}
@@ -127,10 +126,10 @@ public void putObjectNoSuchBucket() throws IOException {
127126

128127
@Test
129128
public void transferManagerUploadObjectWithNoSuchObject() throws IOException{
130-
assertThatThrownBy(() -> transferManager.upload(UploadRequest.builder()
131-
.putObjectRequest(PutObjectRequest.builder().bucket("nonExistingTestBucket").key("someKey").build())
132-
.source(testFile.toPath())
133-
.build()).completionFuture().join())
129+
assertThatThrownBy(() -> transferManager.uploadFile(UploadFileRequest.builder()
130+
.putObjectRequest(PutObjectRequest.builder().bucket("nonExistingTestBucket").key("someKey").build())
131+
.source(testFile.toPath())
132+
.build()).completionFuture().join())
134133
.hasCauseInstanceOf(NoSuchBucketException.class)
135134
.hasMessageContaining("software.amazon.awssdk.services.s3.model.NoSuchBucketException: The specified bucket does not exist");
136135
}

services-custom/s3-transfer-manager/src/it/java/software/amazon/awssdk/transfer/s3/S3TransferManagerDownloadIntegrationTest.java

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
import org.junit.AfterClass;
2525
import org.junit.BeforeClass;
2626
import org.junit.Test;
27+
import software.amazon.awssdk.core.ResponseBytes;
28+
import software.amazon.awssdk.core.async.AsyncResponseTransformer;
29+
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
2730
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
2831
import software.amazon.awssdk.testutils.RandomTempFile;
2932
import software.amazon.awssdk.transfer.s3.progress.LoggingTransferListener;
@@ -46,7 +49,7 @@ public static void setup() throws IOException {
4649
.build(), file.toPath());
4750
tm = S3TransferManager.builder()
4851
.s3ClientConfiguration(b -> b.region(DEFAULT_REGION)
49-
.credentialsProvider(CREDENTIALS_PROVIDER_CHAIN))
52+
.credentialsProvider(CREDENTIALS_PROVIDER_CHAIN))
5053
.build();
5154
}
5255

@@ -58,15 +61,30 @@ public static void cleanup() {
5861
}
5962

6063
@Test
61-
public void download_shouldWork() throws IOException {
64+
public void download_toFile() throws IOException {
6265
Path path = RandomTempFile.randomUncreatedFile().toPath();
63-
Download download = tm.download(DownloadRequest.builder()
64-
.getObjectRequest(b -> b.bucket(BUCKET).key(KEY))
65-
.destination(path)
66-
.overrideConfiguration(b -> b.addListener(LoggingTransferListener.create()))
67-
.build());
68-
CompletedDownload completedDownload = download.completionFuture().join();
66+
FileDownload download =
67+
tm.downloadFile(DownloadFileRequest.builder()
68+
.getObjectRequest(b -> b.bucket(BUCKET).key(KEY))
69+
.destination(path)
70+
.overrideConfiguration(b -> b.addListener(LoggingTransferListener.create()))
71+
.build());
72+
CompletedFileDownload completedFileDownload = download.completionFuture().join();
6973
assertThat(Md5Utils.md5AsBase64(path.toFile())).isEqualTo(Md5Utils.md5AsBase64(file));
70-
assertThat(completedDownload.response().responseMetadata().requestId()).isNotNull();
74+
assertThat(completedFileDownload.response().responseMetadata().requestId()).isNotNull();
75+
}
76+
77+
@Test
78+
public void download_toBytes() throws Exception {
79+
Download<ResponseBytes<GetObjectResponse>> download =
80+
tm.download(DownloadRequest.builder()
81+
.getObjectRequest(b -> b.bucket(BUCKET).key(KEY))
82+
.responseTransformer(AsyncResponseTransformer.toBytes())
83+
.overrideConfiguration(b -> b.addListener(LoggingTransferListener.create()))
84+
.build());
85+
CompletedDownload<ResponseBytes<GetObjectResponse>> completedDownload = download.completionFuture().join();
86+
ResponseBytes<GetObjectResponse> result = completedDownload.result();
87+
assertThat(Md5Utils.md5AsBase64(result.asByteArray())).isEqualTo(Md5Utils.md5AsBase64(file));
88+
assertThat(result.response().responseMetadata().requestId()).isNotNull();
7189
}
7290
}

services-custom/s3-transfer-manager/src/it/java/software/amazon/awssdk/transfer/s3/S3TransferManagerUploadDirectoryIntegrationTest.java

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,12 @@ public static void teardown() {
8585
@Test
8686
public void uploadDirectory_filesSentCorrectly() {
8787
String prefix = "yolo";
88-
UploadDirectoryTransfer uploadDirectory = tm.uploadDirectory(u -> u.sourceDirectory(directory)
89-
.bucket(TEST_BUCKET)
90-
.prefix(prefix)
91-
.overrideConfiguration(o -> o.recursive(true)));
92-
CompletedUploadDirectory completedUploadDirectory = uploadDirectory.completionFuture().join();
93-
assertThat(completedUploadDirectory.failedUploads()).isEmpty();
88+
DirectoryUpload uploadDirectory = tm.uploadDirectory(u -> u.sourceDirectory(directory)
89+
.bucket(TEST_BUCKET)
90+
.prefix(prefix)
91+
.overrideConfiguration(o -> o.recursive(true)));
92+
CompletedDirectoryUpload completedDirectoryUpload = uploadDirectory.completionFuture().join();
93+
assertThat(completedDirectoryUpload.failedTransfers()).isEmpty();
9494

9595
List<String> keys =
9696
s3Client.listObjectsV2Paginator(b -> b.bucket(TEST_BUCKET).prefix(prefix)).contents().stream().map(S3Object::key)
@@ -105,13 +105,13 @@ public void uploadDirectory_filesSentCorrectly() {
105105
public void uploadDirectory_withDelimiter_filesSentCorrectly() {
106106
String prefix = "hello";
107107
String delimiter = "0";
108-
UploadDirectoryTransfer uploadDirectory = tm.uploadDirectory(u -> u.sourceDirectory(directory)
109-
.bucket(TEST_BUCKET)
110-
.delimiter(delimiter)
111-
.prefix(prefix)
112-
.overrideConfiguration(o -> o.recursive(true)));
113-
CompletedUploadDirectory completedUploadDirectory = uploadDirectory.completionFuture().join();
114-
assertThat(completedUploadDirectory.failedUploads()).isEmpty();
108+
DirectoryUpload uploadDirectory = tm.uploadDirectory(u -> u.sourceDirectory(directory)
109+
.bucket(TEST_BUCKET)
110+
.delimiter(delimiter)
111+
.prefix(prefix)
112+
.overrideConfiguration(o -> o.recursive(true)));
113+
CompletedDirectoryUpload completedDirectoryUpload = uploadDirectory.completionFuture().join();
114+
assertThat(completedDirectoryUpload.failedTransfers()).isEmpty();
115115

116116
List<String> keys =
117117
s3Client.listObjectsV2Paginator(b -> b.bucket(TEST_BUCKET).prefix(prefix)).contents().stream().map(S3Object::key)

0 commit comments

Comments
 (0)