Skip to content

Commit 821a342

Browse files
authored
Added UploadDirectoryOverrideConfiguration.Builder#uploadFileRequestTransformer that allows modifying the UploadFileRequests generated during directory uploads. (#2799)
1 parent 4656690 commit 821a342

File tree

5 files changed

+174
-5
lines changed

5 files changed

+174
-5
lines changed

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.nio.file.Path;
2626
import java.nio.file.Paths;
2727
import java.util.List;
28+
import java.util.concurrent.TimeUnit;
2829
import java.util.stream.Collectors;
2930
import org.apache.commons.lang3.RandomStringUtils;
3031
import org.junit.AfterClass;
@@ -124,6 +125,26 @@ public void uploadDirectory_withDelimiter_filesSentCorrectly() {
124125
});
125126
}
126127

128+
@Test
129+
public void uploadDirectory_withRequestTransformer_usesRequestTransformer() throws Exception {
130+
String prefix = "requestTransformerTest";
131+
Path newSourceForEachUpload = Paths.get(directory.toString(), "bar.txt");
132+
133+
CompletedDirectoryUpload result =
134+
tm.uploadDirectory(r -> r.sourceDirectory(directory)
135+
.bucket(TEST_BUCKET)
136+
.prefix(prefix)
137+
.overrideConfiguration(c -> c.uploadFileRequestTransformer(f -> f.source(newSourceForEachUpload))
138+
.recursive(true)))
139+
.completionFuture()
140+
.get(10, TimeUnit.SECONDS);
141+
assertThat(result.failedTransfers()).isEmpty();
142+
143+
s3Client.listObjectsV2Paginator(b -> b.bucket(TEST_BUCKET).prefix(prefix)).contents().forEach(object -> {
144+
verifyContent(object.key(), "bar.txt" + randomString);
145+
});
146+
}
147+
127148
private static Path createLocalTestDirectory() throws IOException {
128149
Path directory = Files.createTempDirectory("test");
129150

services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryOverrideConfiguration.java

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717

1818
import java.util.Objects;
1919
import java.util.Optional;
20+
import java.util.function.Consumer;
2021
import software.amazon.awssdk.annotations.SdkPreviewApi;
2122
import software.amazon.awssdk.annotations.SdkPublicApi;
23+
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
2224
import software.amazon.awssdk.utils.ToString;
2325
import software.amazon.awssdk.utils.Validate;
2426
import software.amazon.awssdk.utils.builder.CopyableBuilder;
@@ -39,11 +41,13 @@ public final class UploadDirectoryOverrideConfiguration implements ToCopyableBui
3941
private final Boolean followSymbolicLinks;
4042
private final Integer maxDepth;
4143
private final Boolean recursive;
44+
private final Consumer<UploadFileRequest.Builder> uploadFileRequestTransformer;
4245

4346
public UploadDirectoryOverrideConfiguration(DefaultBuilder builder) {
4447
this.followSymbolicLinks = builder.followSymbolicLinks;
4548
this.maxDepth = Validate.isPositiveOrNull(builder.maxDepth, "maxDepth");
4649
this.recursive = builder.recursive;
50+
this.uploadFileRequestTransformer = builder.uploadFileRequestTransformer;
4751
}
4852

4953
/**
@@ -70,6 +74,14 @@ public Optional<Boolean> recursive() {
7074
return Optional.ofNullable(recursive);
7175
}
7276

77+
/**
78+
* @return the optional upload request transformer
79+
* @see UploadDirectoryOverrideConfiguration.Builder#uploadFileRequestTransformer(Consumer)
80+
*/
81+
public Optional<Consumer<UploadFileRequest.Builder>> uploadFileRequestTransformer() {
82+
return Optional.ofNullable(uploadFileRequestTransformer);
83+
}
84+
7385
@Override
7486
public Builder toBuilder() {
7587
return new DefaultBuilder(this);
@@ -92,6 +104,9 @@ public boolean equals(Object o) {
92104
if (!Objects.equals(maxDepth, that.maxDepth)) {
93105
return false;
94106
}
107+
if (!Objects.equals(uploadFileRequestTransformer, that.uploadFileRequestTransformer)) {
108+
return false;
109+
}
95110
return Objects.equals(recursive, that.recursive);
96111
}
97112

@@ -100,6 +115,7 @@ public int hashCode() {
100115
int result = followSymbolicLinks != null ? followSymbolicLinks.hashCode() : 0;
101116
result = 31 * result + (maxDepth != null ? maxDepth.hashCode() : 0);
102117
result = 31 * result + (recursive != null ? recursive.hashCode() : 0);
118+
result = 31 * result + (uploadFileRequestTransformer != null ? uploadFileRequestTransformer.hashCode() : 0);
103119
return result;
104120
}
105121

@@ -109,6 +125,7 @@ public String toString() {
109125
.add("followSymbolicLinks", followSymbolicLinks)
110126
.add("maxDepth", maxDepth)
111127
.add("recursive", recursive)
128+
.add("uploadFileRequestTransformer", uploadFileRequestTransformer)
112129
.build();
113130
}
114131

@@ -156,6 +173,53 @@ public interface Builder extends CopyableBuilder<Builder, UploadDirectoryOverrid
156173
*/
157174
Builder maxDepth(Integer maxDepth);
158175

176+
/**
177+
* Specify a function used to transform the {@link UploadFileRequest}s generated by this {@link UploadDirectoryRequest}.
178+
* The provided function is called once for each file that is uploaded, allowing you to modify the paths resolved by
179+
* TransferManager on a per-file basis, modify the created {@link PutObjectRequest} before it is passed to S3, or
180+
* configure a {@link TransferRequestOverrideConfiguration}.
181+
*
182+
* <p>The factory receives the {@link UploadFileRequest}s created by Transfer Manager for each file in the directory
183+
* being uploaded, and returns a (potentially modified) {@code UploadFileRequest}.
184+
*
185+
* <p>
186+
* <b>Usage Example:</b>
187+
* <pre>
188+
* {@code
189+
* // Add a LoggingTransferListener to every transfer within the upload directory request
190+
* TransferRequestOverrideConfiguration fileUploadConfiguration =
191+
* TransferRequestOverrideConfiguration.builder()
192+
* .addListener(LoggingTransferListener.create())
193+
* .build();
194+
*
195+
* UploadDirectoryOverrideConfiguration directoryUploadConfiguration =
196+
* UploadDirectoryOverrideConfiguration.builder()
197+
* .uploadFileRequestTransformer(request -> request.overrideConfiguration(fileUploadConfiguration))
198+
* .build();
199+
*
200+
* UploadDirectoryRequest request =
201+
* UploadDirectoryRequest.builder()
202+
* .sourceDirectory(Paths.get("."))
203+
* .bucket("bucket")
204+
* .prefix("prefix")
205+
* .overrideConfiguration(directoryUploadConfiguration)
206+
* .build()
207+
*
208+
* UploadDirectoryTransfer uploadDirectory = transferManager.uploadDirectory(request);
209+
*
210+
* // Wait for the transfer to complete
211+
* CompletedUploadDirectory completedUploadDirectory = uploadDirectory.completionFuture().join();
212+
*
213+
* // Print out the failed uploads
214+
* completedUploadDirectory.failedUploads().forEach(System.out::println);
215+
* }
216+
* </pre>
217+
*
218+
* @param uploadFileRequestTransformer A transformer to use for modifying the file-level upload requests before execution
219+
* @return This builder for method chaining
220+
*/
221+
Builder uploadFileRequestTransformer(Consumer<UploadFileRequest.Builder> uploadFileRequestTransformer);
222+
159223
@Override
160224
UploadDirectoryOverrideConfiguration build();
161225
}
@@ -164,11 +228,13 @@ private static final class DefaultBuilder implements Builder {
164228
private Boolean followSymbolicLinks;
165229
private Integer maxDepth;
166230
private Boolean recursive;
231+
private Consumer<UploadFileRequest.Builder> uploadFileRequestTransformer;
167232

168233
private DefaultBuilder(UploadDirectoryOverrideConfiguration configuration) {
169234
this.followSymbolicLinks = configuration.followSymbolicLinks;
170235
this.maxDepth = configuration.maxDepth;
171236
this.recursive = configuration.recursive;
237+
this.uploadFileRequestTransformer = configuration.uploadFileRequestTransformer;
172238
}
173239

174240
private DefaultBuilder() {
@@ -216,6 +282,20 @@ public Integer getMaxDepth() {
216282
return maxDepth;
217283
}
218284

285+
@Override
286+
public Builder uploadFileRequestTransformer(Consumer<UploadFileRequest.Builder> uploadFileRequestTransformer) {
287+
this.uploadFileRequestTransformer = uploadFileRequestTransformer;
288+
return this;
289+
}
290+
291+
public Consumer<UploadFileRequest.Builder> getUploadFileRequestTransformer() {
292+
return uploadFileRequestTransformer;
293+
}
294+
295+
public void setUploadFileRequestTransformer(Consumer<UploadFileRequest.Builder> uploadFileRequestTransformer) {
296+
this.uploadFileRequestTransformer = uploadFileRequestTransformer;
297+
}
298+
219299
@Override
220300
public UploadDirectoryOverrideConfiguration build() {
221301
return new UploadDirectoryOverrideConfiguration(this);

services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryRequest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,6 @@ public String toString() {
156156
}
157157

158158
public interface Builder extends CopyableBuilder<Builder, UploadDirectoryRequest> {
159-
160159
/**
161160
* Specify the source directory to upload. The source directory must exist.
162161
* Fle wildcards are not supported and treated literally. Hidden files/directories are visited.
@@ -261,6 +260,7 @@ default Builder overrideConfiguration(Consumer<UploadDirectoryOverrideConfigurat
261260
UploadDirectoryRequest build();
262261
}
263262

263+
264264
private static final class DefaultBuilder implements Builder {
265265

266266
private Path sourceDirectory;
@@ -277,6 +277,7 @@ private DefaultBuilder(UploadDirectoryRequest request) {
277277
this.bucket = request.bucket;
278278
this.prefix = request.prefix;
279279
this.configuration = request.overrideConfiguration;
280+
this.delimiter = request.delimiter;
280281
}
281282

282283
@Override

services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryHelper.java

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import software.amazon.awssdk.transfer.s3.FailedFileUpload;
4242
import software.amazon.awssdk.transfer.s3.FileUpload;
4343
import software.amazon.awssdk.transfer.s3.S3TransferManager;
44+
import software.amazon.awssdk.transfer.s3.UploadDirectoryOverrideConfiguration;
4445
import software.amazon.awssdk.transfer.s3.UploadDirectoryRequest;
4546
import software.amazon.awssdk.transfer.s3.UploadFileRequest;
4647
import software.amazon.awssdk.utils.CompletableFutureUtils;
@@ -230,9 +231,16 @@ private UploadFileRequest constructUploadRequest(UploadDirectoryRequest uploadDi
230231
.bucket(uploadDirectoryRequest.bucket())
231232
.key(key)
232233
.build();
233-
return UploadFileRequest.builder()
234-
.source(path)
235-
.putObjectRequest(putObjectRequest)
236-
.build();
234+
235+
UploadFileRequest.Builder requestBuilder = UploadFileRequest.builder()
236+
.source(path)
237+
.putObjectRequest(putObjectRequest);
238+
239+
uploadDirectoryRequest.overrideConfiguration()
240+
.flatMap(UploadDirectoryOverrideConfiguration::uploadFileRequestTransformer)
241+
.ifPresent(c -> c.accept(requestBuilder));
242+
243+
return requestBuilder.build();
237244
}
245+
238246
}

services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryHelperTest.java

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
import java.nio.file.FileSystem;
2727
import java.nio.file.Files;
2828
import java.nio.file.Path;
29+
import java.nio.file.Paths;
30+
import java.util.List;
2931
import java.util.concurrent.CancellationException;
3032
import java.util.concurrent.CompletableFuture;
3133
import java.util.concurrent.ExecutionException;
@@ -36,12 +38,16 @@
3638
import org.junit.Before;
3739
import org.junit.BeforeClass;
3840
import org.junit.Test;
41+
import org.mockito.ArgumentCaptor;
3942
import software.amazon.awssdk.core.exception.SdkClientException;
43+
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
4044
import software.amazon.awssdk.services.s3.model.PutObjectResponse;
4145
import software.amazon.awssdk.transfer.s3.CompletedDirectoryUpload;
4246
import software.amazon.awssdk.transfer.s3.CompletedFileUpload;
4347
import software.amazon.awssdk.transfer.s3.DirectoryUpload;
4448
import software.amazon.awssdk.transfer.s3.FileUpload;
49+
import software.amazon.awssdk.transfer.s3.TransferRequestOverrideConfiguration;
50+
import software.amazon.awssdk.transfer.s3.UploadDirectoryOverrideConfiguration;
4551
import software.amazon.awssdk.transfer.s3.UploadDirectoryRequest;
4652
import software.amazon.awssdk.transfer.s3.UploadFileRequest;
4753
import software.amazon.awssdk.transfer.s3.internal.progress.DefaultTransferProgress;
@@ -156,6 +162,59 @@ public void uploadDirectory_partialSuccess_shouldProvideFailedUploads() throws E
156162
assertThat(completedDirectoryUpload.failedTransfers().iterator().next().request().source().toString()).isEqualTo("test/2");
157163
}
158164

165+
@Test
166+
public void uploadDirectory_withRequestTransformer_usesRequestTransformer() throws Exception {
167+
PutObjectResponse putObjectResponse = PutObjectResponse.builder().eTag("1234").build();
168+
CompletedFileUpload completedFileUpload = CompletedFileUpload.builder().response(putObjectResponse).build();
169+
CompletableFuture<CompletedFileUpload> successfulFuture = new CompletableFuture<>();
170+
171+
FileUpload upload = newUpload(successfulFuture);
172+
successfulFuture.complete(completedFileUpload);
173+
174+
PutObjectResponse putObjectResponse2 = PutObjectResponse.builder().eTag("5678").build();
175+
CompletedFileUpload completedFileUpload2 = CompletedFileUpload.builder().response(putObjectResponse2).build();
176+
CompletableFuture<CompletedFileUpload> failedFuture = new CompletableFuture<>();
177+
FileUpload upload2 = newUpload(failedFuture);
178+
failedFuture.complete(completedFileUpload2);
179+
180+
ArgumentCaptor<UploadFileRequest> uploadRequestCaptor = ArgumentCaptor.forClass(UploadFileRequest.class);
181+
182+
when(singleUploadFunction.apply(uploadRequestCaptor.capture())).thenReturn(upload, upload2);
183+
184+
Path newSource = Paths.get("/new/path");
185+
PutObjectRequest newPutObjectRequest = PutObjectRequest.builder().build();
186+
TransferRequestOverrideConfiguration newOverrideConfig = TransferRequestOverrideConfiguration.builder()
187+
.build();
188+
189+
UploadDirectoryOverrideConfiguration uploadConfig =
190+
UploadDirectoryOverrideConfiguration.builder()
191+
.uploadFileRequestTransformer(r -> r.source(newSource)
192+
.putObjectRequest(newPutObjectRequest)
193+
.overrideConfiguration(newOverrideConfig))
194+
.build();
195+
196+
uploadDirectoryHelper.uploadDirectory(UploadDirectoryRequest.builder()
197+
.sourceDirectory(directory)
198+
.bucket("bucket")
199+
.overrideConfiguration(uploadConfig)
200+
.build())
201+
.completionFuture()
202+
.get(5, TimeUnit.SECONDS);
203+
204+
List<UploadFileRequest> uploadRequests = uploadRequestCaptor.getAllValues();
205+
assertThat(uploadRequests).hasSize(2);
206+
assertThat(uploadRequests).element(0).satisfies(r -> {
207+
assertThat(r.source()).isEqualTo(newSource);
208+
assertThat(r.putObjectRequest()).isEqualTo(newPutObjectRequest);
209+
assertThat(r.overrideConfiguration()).hasValue(newOverrideConfig);
210+
});
211+
assertThat(uploadRequests).element(1).satisfies(r -> {
212+
assertThat(r.source()).isEqualTo(newSource);
213+
assertThat(r.putObjectRequest()).isEqualTo(newPutObjectRequest);
214+
assertThat(r.overrideConfiguration()).hasValue(newOverrideConfig);
215+
});
216+
}
217+
159218
private FileUpload newUpload(CompletableFuture<CompletedFileUpload> future) {
160219
return new DefaultFileUpload(future,
161220
new DefaultTransferProgress(DefaultTransferProgressSnapshot.builder().build())

0 commit comments

Comments
 (0)