From c47d69e5f96885cb7f5efa868950a94a5ed73b5a Mon Sep 17 00:00:00 2001 From: Zoe Wang <33073555+zoewangg@users.noreply.github.com> Date: Thu, 23 Sep 2021 19:19:50 -0700 Subject: [PATCH 1/8] Support upload directory in s3 transfer manager --- services-custom/s3-transfer-manager/pom.xml | 5 + ...ManagerUploadDirectoryIntegrationTest.java | 98 ++++++ .../transfer/s3/CompletedUploadDirectory.java | 40 +++ .../awssdk/transfer/s3/FailedUpload.java | 38 +++ .../transfer/s3/S3ClientConfiguration.java | 33 +- .../awssdk/transfer/s3/S3TransferManager.java | 50 +++ .../awssdk/transfer/s3/UploadDirectory.java | 29 ++ .../s3/UploadDirectoryConfiguration.java | 183 +++++++++++ .../transfer/s3/UploadDirectoryRequest.java | 223 ++++++++++++++ .../s3/internal/DefaultCompletedUpload.java | 30 +- .../DefaultCompletedUploadDirectory.java | 116 +++++++ .../s3/internal/DefaultFailedUpload.java | 99 ++++++ .../s3/internal/DefaultS3TransferManager.java | 86 +++++- .../s3/internal/DefaultUploadDirectory.java | 35 +++ .../s3/internal/TransferConfiguration.java | 81 +++++ .../internal/TransferConfigurationOption.java | 67 +++++ .../s3/internal/UploadDirectoryManager.java | 166 ++++++++++ .../s3/S3ClientConfigurationTest.java | 3 + .../s3/UploadDirectoryConfigurationTest.java | 66 ++++ .../s3/UploadDirectoryRequestTest.java | 61 ++++ .../CompletedUploadDirectoryTest.java | 53 ++++ .../s3/internal/FailedUploadTest.java | 52 ++++ .../s3/internal/S3TransferManagerTest.java | 51 +++- ...loadDirectoryManagerParameterizedTest.java | 284 ++++++++++++++++++ .../internal/UploadDirectoryManagerTest.java | 148 +++++++++ .../amazon/awssdk/testutils/FileUtils.java | 44 +++ 26 files changed, 2122 insertions(+), 19 deletions(-) create mode 100644 services-custom/s3-transfer-manager/src/it/java/software/amazon/awssdk/transfer/s3/S3TransferManagerUploadDirectoryIntegrationTest.java create mode 100644 services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/CompletedUploadDirectory.java create mode 100644 services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/FailedUpload.java create mode 100644 services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectory.java create mode 100644 services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryConfiguration.java create mode 100644 services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryRequest.java create mode 100644 services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultCompletedUploadDirectory.java create mode 100644 services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultFailedUpload.java create mode 100644 services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultUploadDirectory.java create mode 100644 services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/TransferConfiguration.java create mode 100644 services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/TransferConfigurationOption.java create mode 100644 services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryManager.java create mode 100644 services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/UploadDirectoryConfigurationTest.java create mode 100644 services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/UploadDirectoryRequestTest.java create mode 100644 services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/CompletedUploadDirectoryTest.java create mode 100644 services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/FailedUploadTest.java create mode 100644 services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryManagerParameterizedTest.java create mode 100644 services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryManagerTest.java create mode 100644 test/test-utils/src/main/java/software/amazon/awssdk/testutils/FileUtils.java diff --git a/services-custom/s3-transfer-manager/pom.xml b/services-custom/s3-transfer-manager/pom.xml index 3201c5b85c9c..84b598f1a52c 100644 --- a/services-custom/s3-transfer-manager/pom.xml +++ b/services-custom/s3-transfer-manager/pom.xml @@ -153,6 +153,11 @@ reactive-streams-tck test + + com.google.jimfs + jimfs + test + diff --git a/services-custom/s3-transfer-manager/src/it/java/software/amazon/awssdk/transfer/s3/S3TransferManagerUploadDirectoryIntegrationTest.java b/services-custom/s3-transfer-manager/src/it/java/software/amazon/awssdk/transfer/s3/S3TransferManagerUploadDirectoryIntegrationTest.java new file mode 100644 index 000000000000..dbbddb02f00a --- /dev/null +++ b/services-custom/s3-transfer-manager/src/it/java/software/amazon/awssdk/transfer/s3/S3TransferManagerUploadDirectoryIntegrationTest.java @@ -0,0 +1,98 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.transfer.s3; + +import static org.assertj.core.api.Assertions.assertThat; +import static software.amazon.awssdk.testutils.service.S3BucketUtils.temporaryBucketName; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.stream.Collectors; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.S3Object; +import software.amazon.awssdk.testutils.FileUtils; + +public class S3TransferManagerUploadDirectoryIntegrationTest extends S3IntegrationTestBase { + private static final String TEST_BUCKET = temporaryBucketName(S3TransferManagerUploadIntegrationTest.class); + + private static S3TransferManager tm; + private static Path directory; + private static S3Client s3Client; + + @BeforeClass + public static void setUp() throws Exception { + S3IntegrationTestBase.setUp(); + createBucket(TEST_BUCKET); + + directory = createLocalTestDirectory(); + + tm = S3TransferManager.builder() + .s3ClientConfiguration(b -> b.credentialsProvider(CREDENTIALS_PROVIDER_CHAIN) + .region(DEFAULT_REGION) + .maxConcurrency(100)) + .build(); + + s3Client = S3Client.builder() + .credentialsProvider(CREDENTIALS_PROVIDER_CHAIN).region(DEFAULT_REGION) + .build(); + } + + @AfterClass + public static void teardown() { + tm.close(); + s3Client.close(); + deleteBucketAndAllContents(TEST_BUCKET); + FileUtils.cleanUpTestDirectory(directory); + S3IntegrationTestBase.cleanUp(); + } + + @Test + public void uploadDirectory_filesSentCorrectly() { + String prefix = "yolo"; + UploadDirectory uploadDirectory = tm.uploadDirectory(u -> u.sourceDirectory(directory) + .bucket(TEST_BUCKET) + .prefix(prefix) + .overrideConfiguration(o -> o.recursive(true))); + uploadDirectory.completionFuture().join(); + + List keys = + s3Client.listObjectsV2Paginator(b -> b.bucket(TEST_BUCKET).prefix(prefix)).contents().stream().map(S3Object::key) + .collect(Collectors.toList()); + + assertThat(keys).containsOnly(prefix + "/bar.txt", prefix + "/foo/1.txt", prefix + "/foo/2.txt"); + } + + private static Path createLocalTestDirectory() throws IOException { + Path directory = Files.createTempDirectory("test"); + + String directoryName = directory.toString(); + + Files.createDirectory(Paths.get(directory + "/foo")); + + Files.write(Paths.get(directoryName, "bar.txt"), "bar".getBytes(StandardCharsets.UTF_8)); + Files.write(Paths.get(directoryName, "foo/1.txt"), "1".getBytes(StandardCharsets.UTF_8)); + Files.write(Paths.get(directoryName, "foo/2.txt"), "2".getBytes(StandardCharsets.UTF_8)); + + return directory; + } +} diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/CompletedUploadDirectory.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/CompletedUploadDirectory.java new file mode 100644 index 000000000000..e1ce79afee2e --- /dev/null +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/CompletedUploadDirectory.java @@ -0,0 +1,40 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.transfer.s3; + +import java.util.List; +import software.amazon.awssdk.annotations.SdkPreviewApi; +import software.amazon.awssdk.annotations.SdkPublicApi; + +/** + * A completed download directory transfer. + */ +@SdkPublicApi +@SdkPreviewApi +public interface CompletedUploadDirectory extends CompletedTransfer { + + /** + * A list of failed single file uploads associated with one upload directory transfer + * @return A list of failed uploads + */ + List failedUploads(); + + /** + * A list of successful single file uploads associated with one upload directory transfer + * @return a list of successful uploads + */ + List successfulObjects(); +} diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/FailedUpload.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/FailedUpload.java new file mode 100644 index 000000000000..16ea0ce16007 --- /dev/null +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/FailedUpload.java @@ -0,0 +1,38 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.transfer.s3; + +import java.nio.file.Path; +import software.amazon.awssdk.annotations.SdkPreviewApi; +import software.amazon.awssdk.annotations.SdkPublicApi; + +/** + * A failed single file upload transfer. + */ +@SdkPublicApi +@SdkPreviewApi +public interface FailedUpload { + + /** + * @return the exception thrown + */ + Throwable exception(); + + /** + * @return the path to the file that the transfer manager failed to upload + */ + Path path(); +} diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/S3ClientConfiguration.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/S3ClientConfiguration.java index 3acacb7804a3..fcb54c8e980a 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/S3ClientConfiguration.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/S3ClientConfiguration.java @@ -28,7 +28,7 @@ import software.amazon.awssdk.utils.builder.ToCopyableBuilder; /** - * Optional Configurations for the underlying S3 client for which the TransferManager already provides + * Optional configuration for the underlying S3 client for which the TransferManager already provides * sensible defaults. * *

Use {@link #builder()} to create a set of options.

@@ -42,6 +42,7 @@ public final class S3ClientConfiguration implements ToCopyableBuilder asyncConfiguration() { return Optional.ofNullable(asyncConfiguration); } + /** + * @return the optional upload directory configuration specified + */ + public Optional uploadDirectoryConfiguration() { + return Optional.ofNullable(uploadDirectoryConfiguration); + } + @Override public Builder toBuilder() { return new DefaultBuilder(this); @@ -126,7 +135,10 @@ public boolean equals(Object o) { if (!Objects.equals(maxConcurrency, that.maxConcurrency)) { return false; } - return Objects.equals(asyncConfiguration, that.asyncConfiguration); + if (!Objects.equals(asyncConfiguration, that.asyncConfiguration)) { + return false; + } + return Objects.equals(uploadDirectoryConfiguration, that.uploadDirectoryConfiguration); } @Override @@ -137,6 +149,7 @@ public int hashCode() { result = 31 * result + (targetThroughputInGbps != null ? targetThroughputInGbps.hashCode() : 0); result = 31 * result + (maxConcurrency != null ? maxConcurrency.hashCode() : 0); result = 31 * result + (asyncConfiguration != null ? asyncConfiguration.hashCode() : 0); + result = 31 * result + (uploadDirectoryConfiguration != null ? uploadDirectoryConfiguration.hashCode() : 0); return result; } @@ -253,6 +266,15 @@ public interface Builder extends CopyableBuilder default Builder asyncConfiguration(Consumer configuration) { return asyncConfiguration(ClientAsyncConfiguration.builder().applyMutation(configuration).build()); } + + Builder uploadDirectoryConfiguration(UploadDirectoryConfiguration uploadDirectoryConfiguration); + + default Builder uploadDirectoryConfiguration(Consumer uploadConfigurationBuilder) { + Validate.paramNotNull(uploadConfigurationBuilder, "uploadConfigurationBuilder"); + return uploadDirectoryConfiguration(UploadDirectoryConfiguration.builder() + .applyMutation(uploadConfigurationBuilder) + .build()); + } } private static final class DefaultBuilder implements Builder { @@ -262,6 +284,7 @@ private static final class DefaultBuilder implements Builder { private Double targetThroughputInGbps; private Integer maxConcurrency; private ClientAsyncConfiguration asyncConfiguration; + private UploadDirectoryConfiguration uploadDirectoryConfiguration; private DefaultBuilder() { } @@ -311,6 +334,12 @@ public Builder asyncConfiguration(ClientAsyncConfiguration asyncConfiguration) { return this; } + @Override + public Builder uploadDirectoryConfiguration(UploadDirectoryConfiguration uploadDirectoryConfiguration) { + this.uploadDirectoryConfiguration = uploadDirectoryConfiguration; + return this; + } + @Override public S3ClientConfiguration build() { return new S3ClientConfiguration(this); diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/S3TransferManager.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/S3TransferManager.java index 6ad075aace4a..d8b061b33b18 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/S3TransferManager.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/S3TransferManager.java @@ -160,6 +160,56 @@ default Upload upload(Consumer request) { return upload(UploadRequest.builder().applyMutation(request).build()); } + /** + * Upload the given directory to the S3 bucket under the given prefix. + * + *

+ * Usage Example: + *

+     * {@code
+     * UploadDirectory uploadDirectory =
+     *       transferManager.uploadDirectory(UploadDirectoryRequest.builder()
+     *                                                             .sourceDirectory(Paths.get("."))
+     *                                                             .bucket("bucket")
+     *                                                             .prefix("prefix")
+     *                                                             .build());
+     * // Wait for the transfer to complete
+     * uploadDirectory.completionFuture().join();
+     * }
+     * 
+ * + * @param uploadDirectoryRequest the upload directory request + * @see #uploadDirectory(UploadDirectoryRequest) + */ + default UploadDirectory uploadDirectory(UploadDirectoryRequest uploadDirectoryRequest) { + throw new UnsupportedOperationException(); + } + + /** + * Upload the given directory to the S3 bucket under the given prefix. + * + *

+ * This is a convenience method that creates an instance of the {@link UploadDirectoryRequest} builder avoiding the + * need to create one manually via {@link UploadDirectoryRequest#builder()}. + * + *

+ * Usage Example: + *

+     * {@code
+     * UploadDirectory uploadDirectory =
+     *       transferManager.uploadDirectory(b -> b.sourceDirectory(Paths.get("."))
+     *                                             .bucket("key")
+     *                                             .prefix("prefix"));
+     * // Wait for the transfer to complete
+     * uploadDirectory.completionFuture().join();
+     * }
+     * 
+ * @param requestBuilder the upload directory request builder + */ + default UploadDirectory uploadDirectory(Consumer requestBuilder) { + return uploadDirectory(UploadDirectoryRequest.builder().applyMutation(requestBuilder).build()); + } + /** * Create an {@code S3TransferManager} using the default values. */ diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectory.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectory.java new file mode 100644 index 000000000000..03e245dd954c --- /dev/null +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectory.java @@ -0,0 +1,29 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.transfer.s3; + +import java.util.concurrent.CompletableFuture; +import software.amazon.awssdk.annotations.SdkPublicApi; + +/** + * An upload transfer of an directory to S3. + */ +@SdkPublicApi +public interface UploadDirectory extends Transfer { + + @Override + CompletableFuture completionFuture(); +} diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryConfiguration.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryConfiguration.java new file mode 100644 index 000000000000..e4ae7b4f2988 --- /dev/null +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryConfiguration.java @@ -0,0 +1,183 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.transfer.s3; + +import java.util.Objects; +import java.util.Optional; +import software.amazon.awssdk.annotations.SdkPreviewApi; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.utils.ToString; +import software.amazon.awssdk.utils.builder.CopyableBuilder; +import software.amazon.awssdk.utils.builder.ToCopyableBuilder; + +/** + * Configuration options for {@link S3TransferManager#uploadDirectory}. All values are optional, and not specifying them will + * use the SDK default values. + * + *

Use {@link #builder()} to create a set of options.

+ */ +@SdkPublicApi +@SdkPreviewApi +public final class UploadDirectoryConfiguration implements ToCopyableBuilder { + + private final Boolean followSymbolicLinks; + private final Integer maxDepth; + private final Boolean recursive; + + public UploadDirectoryConfiguration(DefaultBuilder builder) { + this.followSymbolicLinks = builder.followSymbolicLinks; + this.maxDepth = builder.maxDepth; + this.recursive = builder.recursive; + } + + /** + * @return whether to follow symbolic links + */ + public Optional followSymbolicLinks() { + return Optional.ofNullable(followSymbolicLinks); + } + + /** + * @return the maximum number of directory levels to traverse + */ + public Optional maxDepth() { + return Optional.ofNullable(maxDepth); + } + + /** + * @return whether to recursively upload all files under the specified directory + */ + public Optional recursive() { + return Optional.ofNullable(recursive); + } + + @Override + public Builder toBuilder() { + return new DefaultBuilder(this); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + UploadDirectoryConfiguration that = (UploadDirectoryConfiguration) o; + + if (!Objects.equals(followSymbolicLinks, that.followSymbolicLinks)) { + return false; + } + if (!Objects.equals(maxDepth, that.maxDepth)) { + return false; + } + return Objects.equals(recursive, that.recursive); + } + + @Override + public int hashCode() { + int result = followSymbolicLinks != null ? followSymbolicLinks.hashCode() : 0; + result = 31 * result + (maxDepth != null ? maxDepth.hashCode() : 0); + result = 31 * result + (recursive != null ? recursive.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return ToString.builder("UploadDirectoryConfiguration") + .add("followSymbolicLinks", followSymbolicLinks) + .add("maxDepth", maxDepth) + .add("recursive", recursive) + .build(); + } + + public static Builder builder() { + return new DefaultBuilder(); + } + + public interface Builder extends CopyableBuilder { + + /** + * Specify whether to recursively upload all files under the specified directory + * + * @param recursive whether enable recursive upload + * @return This builder for method chaining. + */ + Builder recursive(Boolean recursive); + + /** + * Specify whether to follow Follow symbolic links when traverse the directory tree. + *

+ * Default to false + * + * @param followSymbolicLinks whether to follow symbolic links + * @return This builder for method chaining. + */ + Builder followSymbolicLinks(Boolean followSymbolicLinks); + + /** + * Specify the maximum number of directory levels to traverse + * @param maxDepth the maximum number of directory levels + * @return This builder for method chaining. + */ + Builder maxDepth(Integer maxDepth); + + @Override + UploadDirectoryConfiguration build(); + } + + private static final class DefaultBuilder implements Builder { + private Boolean followSymbolicLinks; + private Integer maxDepth; + private Boolean recursive; + + private DefaultBuilder(UploadDirectoryConfiguration configuration) { + this.followSymbolicLinks = configuration.followSymbolicLinks; + this.maxDepth = configuration.maxDepth; + this.recursive = configuration.recursive; + } + + private DefaultBuilder() { + } + + @Override + public DefaultBuilder recursive(Boolean recursive) { + this.recursive = recursive; + return this; + } + + @Override + public DefaultBuilder followSymbolicLinks(Boolean followSymbolicLinks) { + this.followSymbolicLinks = followSymbolicLinks; + return this; + } + + @Override + public DefaultBuilder maxDepth(Integer maxDepth) { + this.maxDepth = maxDepth; + return this; + } + + @Override + public UploadDirectoryConfiguration build() { + return new UploadDirectoryConfiguration(this); + } + + } +} diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryRequest.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryRequest.java new file mode 100644 index 000000000000..d0ed8dca84d2 --- /dev/null +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryRequest.java @@ -0,0 +1,223 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.transfer.s3; + + +import java.nio.file.Path; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Consumer; +import software.amazon.awssdk.annotations.SdkPreviewApi; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.utils.Validate; +import software.amazon.awssdk.utils.builder.CopyableBuilder; +import software.amazon.awssdk.utils.builder.ToCopyableBuilder; + +/** + * Request object to upload a local directory to S3 using the Transfer Manager. + */ +@SdkPublicApi +@SdkPreviewApi +public final class UploadDirectoryRequest implements TransferRequest, ToCopyableBuilder { + + private final Path sourceDirectory; + private final String bucket; + private final String prefix; + private final UploadDirectoryConfiguration overrideConfiguration; + + public UploadDirectoryRequest(DefaultBuilder builder) { + this.sourceDirectory = Validate.paramNotNull(builder.sourceDirectory, "sourceDirectory"); + this.bucket = Validate.paramNotNull(builder.bucket, "bucketName"); + this.prefix = builder.prefix; + this.overrideConfiguration = builder.configuration; + } + + /** + * The source directory to upload + * + * @return the source directory + */ + public Path sourceDirectory() { + return sourceDirectory; + } + + /** + * The name of the bucket to upload objects to. + * + * @return bucket name + */ + public String bucket() { + return bucket; + } + + /** + * @return the key prefix of the virtual directory to upload to + */ + public String prefix() { + return prefix; + } + + /** + * @return the optional override configuration + */ + public Optional overrideConfiguration() { + return Optional.ofNullable(overrideConfiguration); + } + + public static Builder builder() { + return new DefaultBuilder(); + } + + @Override + public Builder toBuilder() { + return new DefaultBuilder(this); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + UploadDirectoryRequest that = (UploadDirectoryRequest) o; + + if (!sourceDirectory.equals(that.sourceDirectory)) { + return false; + } + if (!bucket.equals(that.bucket)) { + return false; + } + if (!Objects.equals(prefix, that.prefix)) { + return false; + } + return Objects.equals(overrideConfiguration, that.overrideConfiguration); + } + + @Override + public int hashCode() { + int result = sourceDirectory.hashCode(); + result = 31 * result + bucket.hashCode(); + result = 31 * result + (prefix != null ? prefix.hashCode() : 0); + result = 31 * result + (overrideConfiguration != null ? overrideConfiguration.hashCode() : 0); + return result; + } + + public interface Builder extends CopyableBuilder { + + /** + * Specify the source directory to upload + * + * @param sourceDirectory the source directory + * @return This builder for method chaining. + */ + Builder sourceDirectory(Path sourceDirectory); + + /** + * The name of the bucket to upload objects to. + * + * @param bucket the bucket name + * @return This builder for method chaining. + */ + Builder bucket(String bucket); + + /** + * Specify the key prefix of the virtual directory to upload to. + * If not provided, files will be uploaded to the root of the bucket + * + * @param prefix the key prefix + * @return This builder for method chaining. + */ + Builder prefix(String prefix); + + /** + * Add an optional request override configuration. + * + * @param configuration The override configuration. + * @return This builder for method chaining. + */ + Builder overrideConfiguration(UploadDirectoryConfiguration configuration); + + /** + * Similar to {@link #overrideConfiguration(UploadDirectoryConfiguration)}, but takes a lambda to configure a new + * {@link UploadDirectoryConfiguration.Builder}. This removes the need to call + * {@link UploadDirectoryConfiguration#builder()} and {@link UploadDirectoryConfiguration.Builder#build()}. + * + * @param uploadConfigurationBuilder the upload configuration + * @return this builder for method chaining. + * @see #overrideConfiguration(UploadDirectoryConfiguration) + */ + default Builder overrideConfiguration(Consumer uploadConfigurationBuilder) { + Validate.paramNotNull(uploadConfigurationBuilder, "uploadConfigurationBuilder"); + return overrideConfiguration(UploadDirectoryConfiguration.builder() + .applyMutation(uploadConfigurationBuilder) + .build()); + } + + @Override + UploadDirectoryRequest build(); + } + + private static final class DefaultBuilder implements Builder { + + private Path sourceDirectory; + private String bucket; + private String prefix; + private UploadDirectoryConfiguration configuration; + + private DefaultBuilder() { + } + + private DefaultBuilder(UploadDirectoryRequest request) { + this.sourceDirectory = request.sourceDirectory; + this.bucket = request.bucket; + this.prefix = request.prefix; + this.configuration = request.overrideConfiguration; + } + + @Override + public Builder sourceDirectory(Path sourceDirectory) { + this.sourceDirectory = sourceDirectory; + return this; + } + + @Override + public Builder bucket(String bucket) { + this.bucket = bucket; + return this; + } + + @Override + public Builder prefix(String prefix) { + this.prefix = prefix; + return this; + } + + @Override + public Builder overrideConfiguration(UploadDirectoryConfiguration configuration) { + this.configuration = configuration; + return this; + } + + @Override + public UploadDirectoryRequest build() { + return new UploadDirectoryRequest(this); + } + } +} diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultCompletedUpload.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultCompletedUpload.java index 3d13b87c56bf..0a7371706116 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultCompletedUpload.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultCompletedUpload.java @@ -18,13 +18,15 @@ import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.services.s3.model.PutObjectResponse; import software.amazon.awssdk.transfer.s3.CompletedUpload; +import software.amazon.awssdk.utils.ToString; +import software.amazon.awssdk.utils.Validate; @SdkInternalApi public final class DefaultCompletedUpload implements CompletedUpload { private final PutObjectResponse response; private DefaultCompletedUpload(BuilderImpl builder) { - this.response = builder.response; + this.response = Validate.paramNotNull(builder.response, "response"); } @Override @@ -32,6 +34,32 @@ public PutObjectResponse response() { return response; } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + DefaultCompletedUpload that = (DefaultCompletedUpload) o; + + return response.equals(that.response); + } + + @Override + public int hashCode() { + return response.hashCode(); + } + + @Override + public String toString() { + return ToString.builder("CompletedUpload") + .add("response", response) + .build(); + } + /** * Creates a default builder for {@link CompletedUpload}. */ diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultCompletedUploadDirectory.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultCompletedUploadDirectory.java new file mode 100644 index 000000000000..b44d9158effb --- /dev/null +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultCompletedUploadDirectory.java @@ -0,0 +1,116 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.transfer.s3.internal; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.transfer.s3.CompletedUpload; +import software.amazon.awssdk.transfer.s3.CompletedUploadDirectory; +import software.amazon.awssdk.transfer.s3.FailedUpload; +import software.amazon.awssdk.utils.ToString; + +@SdkInternalApi +public final class DefaultCompletedUploadDirectory implements CompletedUploadDirectory { + private final List completedUploads; + private final List failedUploads; + + private DefaultCompletedUploadDirectory(DefaultBuilder builder) { + this.completedUploads = Collections.unmodifiableList(new ArrayList<>(builder.completedUploads)); + this.failedUploads = Collections.unmodifiableList(builder.failedObjects); + } + + @Override + public List failedUploads() { + return failedUploads; + } + + @Override + public List successfulObjects() { + return completedUploads; + } + + /** + * Creates a default builder for {@link CompletedUpload}. + */ + public static Builder builder() { + return new DefaultBuilder(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + DefaultCompletedUploadDirectory that = (DefaultCompletedUploadDirectory) o; + + if (!completedUploads.equals(that.completedUploads)) { + return false; + } + return failedUploads.equals(that.failedUploads); + } + + @Override + public int hashCode() { + int result = completedUploads.hashCode(); + result = 31 * result + failedUploads.hashCode(); + return result; + } + + @Override + public String toString() { + return ToString.builder("CompletedUploadDirectory") + .add("failedUploads", failedUploads) + .add("successfulUploads", completedUploads) + .build(); + } + + interface Builder { + + Builder successfulUploads(List successfulUploads); + + Builder failedUploads(List failedUploads); + + CompletedUploadDirectory build(); + } + + private static class DefaultBuilder implements Builder { + private List completedUploads = Collections.emptyList(); + private List failedObjects = Collections.emptyList(); + + @Override + public Builder successfulUploads(List completedUploads) { + this.completedUploads = completedUploads; + return this; + } + + @Override + public Builder failedUploads(List failedUploads) { + this.failedObjects = failedUploads; + return this; + } + + @Override + public CompletedUploadDirectory build() { + return new DefaultCompletedUploadDirectory(this); + } + } +} diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultFailedUpload.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultFailedUpload.java new file mode 100644 index 000000000000..a8bcd28ca0e1 --- /dev/null +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultFailedUpload.java @@ -0,0 +1,99 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.transfer.s3.internal; + +import java.nio.file.Path; +import java.util.Objects; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.transfer.s3.FailedUpload; +import software.amazon.awssdk.utils.ToString; + +@SdkInternalApi +final class DefaultFailedUpload implements FailedUpload { + private final Throwable throwable; + private final Path path; + + DefaultFailedUpload(Builder builder) { + this.throwable = builder.throwable; + this.path = builder.path; + } + + public Throwable exception() { + return throwable; + } + + public Path path() { + return path; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + DefaultFailedUpload that = (DefaultFailedUpload) o; + + if (!Objects.equals(throwable, that.throwable)) { + return false; + } + return path.equals(that.path); + } + + @Override + public int hashCode() { + int result = throwable != null ? throwable.hashCode() : 0; + result = 31 * result + path.hashCode(); + return result; + } + + @Override + public String toString() { + return ToString.builder("FailedUpload") + .add("exception", throwable) + .add("path", path) + .build(); + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private Throwable throwable; + private Path path; + + private Builder() { + } + + public Builder exception(Throwable throwable) { + this.throwable = throwable; + return this; + } + + public Builder path(Path path) { + this.path = path; + return this; + } + + public DefaultFailedUpload build() { + return new DefaultFailedUpload(this); + } + } +} diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultS3TransferManager.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultS3TransferManager.java index 5e34a995808d..ac8c541f586e 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultS3TransferManager.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultS3TransferManager.java @@ -15,6 +15,8 @@ package software.amazon.awssdk.transfer.s3.internal; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.concurrent.CompletableFuture; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.annotations.SdkTestInternalApi; @@ -30,15 +32,20 @@ import software.amazon.awssdk.transfer.s3.S3ClientConfiguration; import software.amazon.awssdk.transfer.s3.S3TransferManager; import software.amazon.awssdk.transfer.s3.Upload; +import software.amazon.awssdk.transfer.s3.UploadDirectory; +import software.amazon.awssdk.transfer.s3.UploadDirectoryRequest; import software.amazon.awssdk.transfer.s3.UploadRequest; import software.amazon.awssdk.utils.CompletableFutureUtils; @SdkInternalApi public final class DefaultS3TransferManager implements S3TransferManager { private final S3CrtAsyncClient s3CrtAsyncClient; + private final TransferConfiguration transferConfiguration; + private final UploadDirectoryManager uploadDirectoryManager; public DefaultS3TransferManager(DefaultBuilder tmBuilder) { S3CrtAsyncClient.S3CrtAsyncClientBuilder clientBuilder = S3CrtAsyncClient.builder(); + TransferConfiguration.Builder transferConfigBuilder = TransferConfiguration.builder(); if (tmBuilder.s3ClientConfiguration != null) { tmBuilder.s3ClientConfiguration.credentialsProvider().ifPresent(clientBuilder::credentialsProvider); tmBuilder.s3ClientConfiguration.maxConcurrency().ifPresent(clientBuilder::maxConcurrency); @@ -46,38 +53,72 @@ public DefaultS3TransferManager(DefaultBuilder tmBuilder) { tmBuilder.s3ClientConfiguration.region().ifPresent(clientBuilder::region); tmBuilder.s3ClientConfiguration.targetThroughputInGbps().ifPresent(clientBuilder::targetThroughputInGbps); tmBuilder.s3ClientConfiguration.asyncConfiguration().ifPresent(clientBuilder::asyncConfiguration); + + tmBuilder.s3ClientConfiguration.uploadDirectoryConfiguration().ifPresent(transferConfigBuilder::configuration); } s3CrtAsyncClient = clientBuilder.build(); + transferConfiguration = transferConfigBuilder.build(); + uploadDirectoryManager = new UploadDirectoryManager(transferConfiguration, this::upload); } @SdkTestInternalApi - DefaultS3TransferManager(S3CrtAsyncClient s3CrtAsyncClient) { + DefaultS3TransferManager(S3CrtAsyncClient s3CrtAsyncClient, UploadDirectoryManager uploadDirectoryManager) { this.s3CrtAsyncClient = s3CrtAsyncClient; + this.transferConfiguration = TransferConfiguration.builder().build(); + this.uploadDirectoryManager = uploadDirectoryManager; } @Override public Upload upload(UploadRequest uploadRequest) { - PutObjectRequest putObjectRequest = uploadRequest.putObjectRequest(); - AsyncRequestBody requestBody = requestBodyFor(uploadRequest); + try { + assertNotObjectLambdaArn(uploadRequest.putObjectRequest().bucket(), "upload"); + + PutObjectRequest putObjectRequest = uploadRequest.putObjectRequest(); + AsyncRequestBody requestBody = requestBodyFor(uploadRequest); - CompletableFuture putObjFuture = s3CrtAsyncClient.putObject(putObjectRequest, requestBody); + CompletableFuture putObjFuture = s3CrtAsyncClient.putObject(putObjectRequest, requestBody); - CompletableFuture future = putObjFuture.thenApply(r -> DefaultCompletedUpload.builder() - .response(r) - .build()); - return new DefaultUpload(CompletableFutureUtils.forwardExceptionTo(future, putObjFuture)); + CompletableFuture future = putObjFuture.thenApply(r -> DefaultCompletedUpload.builder() + .response(r) + .build()); + return new DefaultUpload(CompletableFutureUtils.forwardExceptionTo(future, putObjFuture)); + } catch (Throwable throwable) { + return new DefaultUpload(CompletableFutureUtils.failedFuture(throwable)); + } } @Override - public Download download(DownloadRequest downloadRequest) { - CompletableFuture getObjectFuture = - s3CrtAsyncClient.getObject(downloadRequest.getObjectRequest(), - AsyncResponseTransformer.toFile(downloadRequest.destination())); - CompletableFuture future = - getObjectFuture.thenApply(r -> DefaultCompletedDownload.builder().response(r).build()); + public UploadDirectory uploadDirectory(UploadDirectoryRequest uploadDirectoryRequest) { + try { + Path directory = uploadDirectoryRequest.sourceDirectory(); + + if (!Files.exists(directory) || Files.isRegularFile(directory)) { + throw new IllegalArgumentException("The source directory provided either does not exist or is not a directory"); + } + assertNotObjectLambdaArn(uploadDirectoryRequest.bucket(), "downloadDirectory"); + + return uploadDirectoryManager.uploadDirectory(uploadDirectoryRequest); + } catch (Throwable throwable) { + return new DefaultUploadDirectory(CompletableFutureUtils.failedFuture(throwable)); + } + } - return new DefaultDownload(CompletableFutureUtils.forwardExceptionTo(future, getObjectFuture)); + @Override + public Download download(DownloadRequest downloadRequest) { + try { + assertNotObjectLambdaArn(downloadRequest.getObjectRequest().bucket(), "download"); + + CompletableFuture getObjectFuture = + s3CrtAsyncClient.getObject(downloadRequest.getObjectRequest(), + AsyncResponseTransformer.toFile(downloadRequest.destination())); + CompletableFuture future = + getObjectFuture.thenApply(r -> DefaultCompletedDownload.builder().response(r).build()); + + return new DefaultDownload(CompletableFutureUtils.forwardExceptionTo(future, getObjectFuture)); + } catch (Throwable throwable) { + return new DefaultDownload(CompletableFutureUtils.failedFuture(throwable)); + } } @Override @@ -89,6 +130,21 @@ public static Builder builder() { return new DefaultBuilder(); } + private static void assertNotObjectLambdaArn(String arn, String operation) { + if (isObjectLambdaArn(arn)) { + String error = String.format("%s does not support S3 Object Lambda resources", operation); + throw new IllegalArgumentException(error); + } + } + + private static boolean isObjectLambdaArn(String arn) { + if (arn == null) { + return false; + } + + return arn.startsWith("arn:") && arn.contains(":s3-object-lambda"); + } + private AsyncRequestBody requestBodyFor(UploadRequest uploadRequest) { return AsyncRequestBody.fromFile(uploadRequest.source()); } diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultUploadDirectory.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultUploadDirectory.java new file mode 100644 index 000000000000..872b0d3a093a --- /dev/null +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultUploadDirectory.java @@ -0,0 +1,35 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.transfer.s3.internal; + +import java.util.concurrent.CompletableFuture; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.transfer.s3.CompletedUploadDirectory; +import software.amazon.awssdk.transfer.s3.UploadDirectory; + +@SdkInternalApi +public class DefaultUploadDirectory implements UploadDirectory { + private final CompletableFuture completionFuture; + + public DefaultUploadDirectory(CompletableFuture completionFuture) { + this.completionFuture = completionFuture; + } + + @Override + public CompletableFuture completionFuture() { + return completionFuture; + } +} diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/TransferConfiguration.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/TransferConfiguration.java new file mode 100644 index 000000000000..2d8839d439d9 --- /dev/null +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/TransferConfiguration.java @@ -0,0 +1,81 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.transfer.s3.internal; + +import static software.amazon.awssdk.transfer.s3.internal.TransferConfigurationOption.UPLOAD_DIRECTORY_FOLLOW_SYMBOLIC_LINKS; +import static software.amazon.awssdk.transfer.s3.internal.TransferConfigurationOption.UPLOAD_DIRECTORY_MAX_DEPTH; +import static software.amazon.awssdk.transfer.s3.internal.TransferConfigurationOption.UPLOAD_DIRECTORY_RECURSIVE; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.transfer.s3.UploadDirectoryConfiguration; +import software.amazon.awssdk.transfer.s3.UploadDirectoryRequest; +import software.amazon.awssdk.utils.AttributeMap; + +/** + * Internal transfer manager related configuration + */ +@SdkInternalApi +public final class TransferConfiguration { + private final AttributeMap options; + + private TransferConfiguration(AttributeMap mergedOptions) { + this.options = mergedOptions; + } + + public AttributeMap options() { + return options; + } + + public boolean resolveUploadDirectoryRecursive(UploadDirectoryRequest request) { + return request.overrideConfiguration() + .flatMap(UploadDirectoryConfiguration::recursive) + .orElse(options().get(UPLOAD_DIRECTORY_RECURSIVE)); + } + + public boolean resolveUploadDirectoryFollowSymbolicLinks(UploadDirectoryRequest request) { + return request.overrideConfiguration() + .flatMap(UploadDirectoryConfiguration::followSymbolicLinks) + .orElse(options().get(UPLOAD_DIRECTORY_FOLLOW_SYMBOLIC_LINKS)); + } + + public int resolveUploadDirectoryMaxDepth(UploadDirectoryRequest request) { + return request.overrideConfiguration() + .flatMap(UploadDirectoryConfiguration::maxDepth) + .orElse(options().get(UPLOAD_DIRECTORY_MAX_DEPTH)); + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private final AttributeMap.Builder standardOptions = AttributeMap.builder(); + + public Builder configuration(UploadDirectoryConfiguration configuration) { + standardOptions.put(TransferConfigurationOption.UPLOAD_DIRECTORY_FOLLOW_SYMBOLIC_LINKS, + configuration.followSymbolicLinks().orElse(null)); + standardOptions.put(TransferConfigurationOption.UPLOAD_DIRECTORY_MAX_DEPTH, configuration.maxDepth().orElse(null)); + standardOptions.put(TransferConfigurationOption.UPLOAD_DIRECTORY_RECURSIVE, configuration.recursive().orElse(null)); + return this; + } + + public TransferConfiguration build() { + AttributeMap mergedOptions = standardOptions.build().merge(TransferConfigurationOption.TRANSFER_DEFAULTS); + + return new TransferConfiguration(mergedOptions); + } + } +} diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/TransferConfigurationOption.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/TransferConfigurationOption.java new file mode 100644 index 000000000000..43a86f1ba5f3 --- /dev/null +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/TransferConfigurationOption.java @@ -0,0 +1,67 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.transfer.s3.internal; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.utils.AttributeMap; + +@SdkInternalApi +public final class TransferConfigurationOption extends AttributeMap.Key { + public static final TransferConfigurationOption UPLOAD_DIRECTORY_MAX_DEPTH = + new TransferConfigurationOption<>("UploadDirectoryMaxDepth", Integer.class); + + public static final TransferConfigurationOption UPLOAD_DIRECTORY_RECURSIVE = + new TransferConfigurationOption<>("UploadDirectoryRecursive", Boolean.class); + + public static final TransferConfigurationOption UPLOAD_DIRECTORY_FOLLOW_SYMBOLIC_LINKS = + new TransferConfigurationOption<>("UploadDirectoryFileVisitOption", Boolean.class); + + + private static final int DEFAULT_UPLOAD_DIRECTORY_MAX_DEPTH = Integer.MAX_VALUE; + private static final Boolean DEFAULT_UPLOAD_DIRECTORY_RECURSIVE = Boolean.TRUE; + // TODO: revisit + private static final Boolean DEFAULT_UPLOAD_DIRECTORY_FOLLOW_SYMBOLIC_LINKS = Boolean.FALSE; + + public static final AttributeMap TRANSFER_DEFAULTS = AttributeMap + .builder() + .put(UPLOAD_DIRECTORY_MAX_DEPTH, DEFAULT_UPLOAD_DIRECTORY_MAX_DEPTH) + .put(UPLOAD_DIRECTORY_RECURSIVE, DEFAULT_UPLOAD_DIRECTORY_RECURSIVE) + .put(UPLOAD_DIRECTORY_FOLLOW_SYMBOLIC_LINKS, DEFAULT_UPLOAD_DIRECTORY_FOLLOW_SYMBOLIC_LINKS) + .build(); + + private final String name; + + private TransferConfigurationOption(String name, Class clzz) { + super(clzz); + this.name = name; + } + + /** + * Note that the name is mainly used for debugging purposes. Two option key objects with the same name do not represent + * the same option. Option keys are compared by reference when obtaining a value from an {@link AttributeMap}. + * + * @return Name of this option key. + */ + public String name() { + return name; + } + + @Override + public String toString() { + return name; + } +} + diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryManager.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryManager.java new file mode 100644 index 000000000000..7b58aac8c112 --- /dev/null +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryManager.java @@ -0,0 +1,166 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.transfer.s3.internal; + + +import java.io.IOException; +import java.nio.file.FileVisitOption; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.core.exception.SdkClientException; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.transfer.s3.CompletedUpload; +import software.amazon.awssdk.transfer.s3.CompletedUploadDirectory; +import software.amazon.awssdk.transfer.s3.FailedUpload; +import software.amazon.awssdk.transfer.s3.S3TransferManager; +import software.amazon.awssdk.transfer.s3.Upload; +import software.amazon.awssdk.transfer.s3.UploadDirectory; +import software.amazon.awssdk.transfer.s3.UploadDirectoryRequest; +import software.amazon.awssdk.transfer.s3.UploadRequest; +import software.amazon.awssdk.utils.CompletableFutureUtils; +import software.amazon.awssdk.utils.Logger; +import software.amazon.awssdk.utils.StringUtils; + +@SdkInternalApi +public class UploadDirectoryManager { + private static final Logger log = Logger.loggerFor(S3TransferManager.class); + + private final TransferConfiguration transferConfiguration; + private final Function uploadFunction; + + public UploadDirectoryManager(TransferConfiguration transferConfiguration, + Function uploadFunction) { + + this.transferConfiguration = transferConfiguration; + this.uploadFunction = uploadFunction; + } + + public UploadDirectory uploadDirectory(UploadDirectoryRequest uploadDirectoryRequest) { + CompletableFuture returnFuture = new CompletableFuture<>(); + Path directory = uploadDirectoryRequest.sourceDirectory(); + + List uploads = new ArrayList<>(); + List failedUploads = new ArrayList<>(); + List> uploadFutures; + + try (Stream entries = listFiles(directory, uploadDirectoryRequest)) { + uploadFutures = + entries.map(path -> { + CompletableFuture future = + uploadSingleFile(uploadDirectoryRequest, uploads, failedUploads, path); + // Forward cancellation of the return future to all individual futures. + CompletableFutureUtils.forwardExceptionTo(returnFuture, future); + return future; + }).collect(Collectors.toList()); + } + + CompletableFuture.allOf(uploadFutures.toArray(new CompletableFuture[0])) + .whenComplete((r, t) -> returnFuture.complete( + DefaultCompletedUploadDirectory.builder() + .failedUploads(failedUploads) + .successfulUploads(uploads) + .build())); + return new DefaultUploadDirectory(returnFuture); + } + + private CompletableFuture uploadSingleFile(UploadDirectoryRequest uploadDirectoryRequest, + List uploads, + List failedUploads, + Path path) { + int nameCount = uploadDirectoryRequest.sourceDirectory().getNameCount(); + UploadRequest uploadRequest = constructUploadRequest(uploadDirectoryRequest, nameCount, path); + CompletableFuture future = uploadFunction.apply(uploadRequest) + .completionFuture(); + future.whenComplete((r, t) -> { + if (t != null) { + failedUploads.add(DefaultFailedUpload.builder() + .exception(t) + .path(path) + .build()); + } else { + uploads.add(r); + } + }); + return future; + } + + private Stream listFiles(Path directory, UploadDirectoryRequest request) { + + try { + boolean recursive = transferConfiguration.resolveUploadDirectoryRecursive(request); + + if (!recursive) { + return Files.list(directory).filter(Files::isRegularFile); + } + + boolean followSymbolicLinks = transferConfiguration.resolveUploadDirectoryFollowSymbolicLinks(request); + + int maxDepth = transferConfiguration.resolveUploadDirectoryMaxDepth(request); + + if (followSymbolicLinks) { + return Files.walk(directory, maxDepth, FileVisitOption.FOLLOW_LINKS) + .peek(p -> log.trace(() -> "Processing path: " + p)) + .filter(Files::isRegularFile); + } + + return Files.walk(directory, maxDepth) + .peek(p -> log.trace(() -> "Processing path: " + p)) + .filter(path -> Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)); + + } catch (IOException e) { + throw SdkClientException.create("Failed to list files under the provided directory", e); + } + } + + private static String processPrefix(String prefix) { + if (StringUtils.isEmpty(prefix)) { + return ""; + } + return prefix.endsWith("/") ? prefix : prefix + "/"; + } + + private static String getRelativePathName(int directoryNameCount, Path path) { + String relativePathName = path.subpath(directoryNameCount, + path.getNameCount()).toString(); + + // Replace "\" (Windows FS) with "/" + return relativePathName.replace('\\', '/'); + } + + private static UploadRequest constructUploadRequest(UploadDirectoryRequest uploadDirectoryRequest, int directoryNameCount, + Path path) { + String prefix = processPrefix(uploadDirectoryRequest.prefix()); + String relativePathName = getRelativePathName(directoryNameCount, path); + String key = prefix + relativePathName; + + PutObjectRequest putObjectRequest = PutObjectRequest.builder() + .bucket(uploadDirectoryRequest.bucket()) + .key(key) + .build(); + return UploadRequest.builder() + .source(path) + .putObjectRequest(putObjectRequest) + .build(); + } +} diff --git a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/S3ClientConfigurationTest.java b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/S3ClientConfigurationTest.java index 9788da748654..39b87f178dd1 100644 --- a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/S3ClientConfigurationTest.java +++ b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/S3ClientConfigurationTest.java @@ -105,6 +105,7 @@ public void equalsHashCode() { .maxConcurrency(100) .targetThroughputInGbps(10.0) .region(Region.US_WEST_2) + .uploadDirectoryConfiguration(b -> b.recursive(true)) .minimumPartSizeInBytes(5 * MB) .build(); @@ -114,12 +115,14 @@ public void equalsHashCode() { .targetThroughputInGbps(10.0) .region(Region.US_WEST_2) .minimumPartSizeInBytes(5 * MB) + .uploadDirectoryConfiguration(b -> b.recursive(true)) .build(); S3ClientConfiguration configuration3 = configuration1.toBuilder() .credentialsProvider(AnonymousCredentialsProvider.create()) .maxConcurrency(50) .targetThroughputInGbps(1.0) + .uploadDirectoryConfiguration(b -> b.recursive(false)) .asyncConfiguration(c -> c.advancedOption(SdkAdvancedAsyncClientOption.FUTURE_COMPLETION_EXECUTOR, Runnable::run)) .build(); diff --git a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/UploadDirectoryConfigurationTest.java b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/UploadDirectoryConfigurationTest.java new file mode 100644 index 000000000000..378cb4bf63d4 --- /dev/null +++ b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/UploadDirectoryConfigurationTest.java @@ -0,0 +1,66 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.transfer.s3; + + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Test; + +public class UploadDirectoryConfigurationTest { + + @Test + public void getters_allPropertiesSet() { + UploadDirectoryConfiguration object = UploadDirectoryConfiguration + .builder() + .maxDepth(100) + .followSymbolicLinks(true) + .recursive(false) + .build(); + + assertThat(object.followSymbolicLinks()).contains(true); + assertThat(object.recursive()).contains(false); + assertThat(object.maxDepth()).contains(100); + } + + @Test + public void getters_allPropertiesAbsent() { + UploadDirectoryConfiguration object = UploadDirectoryConfiguration + .builder().build(); + + assertThat(object.followSymbolicLinks()).isEmpty(); + assertThat(object.recursive()).isEmpty(); + assertThat(object.maxDepth()).isEmpty(); + } + + @Test + public void equalsHashCode() { + UploadDirectoryConfiguration object1 = UploadDirectoryConfiguration + .builder() + .maxDepth(100) + .followSymbolicLinks(true) + .recursive(false) + .build(); + + UploadDirectoryConfiguration object2 = object1.toBuilder().build(); + + UploadDirectoryConfiguration object3 = object1.toBuilder().recursive(true).build(); + assertThat(object1).isEqualTo(object2); + assertThat(object1.hashCode()).isEqualTo(object2.hashCode()); + assertThat(object1).isNotEqualTo(object3); + assertThat(object1.hashCode()).isNotEqualTo(object3.hashCode()); + } +} diff --git a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/UploadDirectoryRequestTest.java b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/UploadDirectoryRequestTest.java new file mode 100644 index 000000000000..29278c2b4f53 --- /dev/null +++ b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/UploadDirectoryRequestTest.java @@ -0,0 +1,61 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.transfer.s3; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.nio.file.Paths; +import org.junit.Test; + +public class UploadDirectoryRequestTest { + + @Test + public void noSourceDirectory_throws() { + assertThatThrownBy(() -> + UploadDirectoryRequest.builder().bucket("bucket").build() + ).isInstanceOf(NullPointerException.class).hasMessageContaining("sourceDirectory"); + } + + @Test + public void noBucket_throws() { + assertThatThrownBy(() -> + UploadDirectoryRequest.builder().sourceDirectory(Paths.get(".")).build() + ).isInstanceOf(NullPointerException.class).hasMessageContaining("bucket"); + } + + @Test + public void equals_hashcode() { + UploadDirectoryRequest request1 = UploadDirectoryRequest + .builder() + .bucket("bucket") + .sourceDirectory(Paths.get(".")) + .overrideConfiguration(o -> o.recursive(true)) + .build(); + + UploadDirectoryRequest request2 = request1.toBuilder() + .build(); + + UploadDirectoryRequest request3 = request1.toBuilder() + .bucket("anotherBucket") + .build(); + + assertThat(request1).isEqualTo(request2); + assertThat(request1.hashCode()).isEqualTo(request2.hashCode()); + assertThat(request1).isNotEqualTo(request3); + assertThat(request1.hashCode()).isNotEqualTo(request3.hashCode()); + } +} diff --git a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/CompletedUploadDirectoryTest.java b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/CompletedUploadDirectoryTest.java new file mode 100644 index 000000000000..6c30689652cf --- /dev/null +++ b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/CompletedUploadDirectoryTest.java @@ -0,0 +1,53 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.transfer.s3.internal; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; +import org.junit.Test; +import software.amazon.awssdk.services.s3.model.PutObjectResponse; +import software.amazon.awssdk.transfer.s3.CompletedUpload; +import software.amazon.awssdk.transfer.s3.CompletedUploadDirectory; +import software.amazon.awssdk.transfer.s3.FailedUpload; + +public class CompletedUploadDirectoryTest { + + @Test + public void equalsHashcode() { + List failedUploads = Arrays.asList(DefaultFailedUpload.builder().path(Paths.get(".")).build()); + List completedUploads = Arrays.asList(DefaultCompletedUpload.builder().response(PutObjectResponse.builder().build()).build()); + CompletedUploadDirectory completedUploadDirectory = DefaultCompletedUploadDirectory.builder() + .failedUploads(failedUploads) + .successfulUploads(completedUploads) + .build(); + + CompletedUploadDirectory completedUploadDirectory2 = DefaultCompletedUploadDirectory.builder() + .failedUploads(failedUploads) + .successfulUploads(completedUploads) + .build(); + + CompletedUploadDirectory completedUploadDirectory3 = DefaultCompletedUploadDirectory.builder() + .build(); + + assertThat(completedUploadDirectory).isEqualTo(completedUploadDirectory2); + assertThat(completedUploadDirectory.hashCode()).isEqualTo(completedUploadDirectory2.hashCode()); + assertThat(completedUploadDirectory2).isNotEqualTo(completedUploadDirectory3); + assertThat(completedUploadDirectory2.hashCode()).isNotEqualTo(completedUploadDirectory3); + } +} diff --git a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/FailedUploadTest.java b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/FailedUploadTest.java new file mode 100644 index 000000000000..80486b8d2114 --- /dev/null +++ b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/FailedUploadTest.java @@ -0,0 +1,52 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.transfer.s3.internal; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.nio.file.Path; +import java.nio.file.Paths; +import org.junit.Test; +import software.amazon.awssdk.core.exception.SdkClientException; +import software.amazon.awssdk.transfer.s3.FailedUpload; + +public class FailedUploadTest { + + @Test + public void equalsHashcode() { + SdkClientException exception = SdkClientException.create("test"); + Path path = Paths.get("."); + FailedUpload failedUpload1 = DefaultFailedUpload.builder() + .exception(exception) + .path(path) + .build(); + + FailedUpload failedUpload2 = DefaultFailedUpload.builder() + .exception(exception) + .path(path) + .build(); + + FailedUpload failedUpload3 = DefaultFailedUpload.builder() + .exception(SdkClientException.create("blah")) + .path(Paths.get("..")) + .build(); + + assertThat(failedUpload1).isEqualTo(failedUpload2); + assertThat(failedUpload1.hashCode()).isEqualTo(failedUpload2.hashCode()); + assertThat(failedUpload1).isNotEqualTo(failedUpload3); + assertThat(failedUpload1.hashCode()).isNotEqualTo(failedUpload3); + } +} diff --git a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/S3TransferManagerTest.java b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/S3TransferManagerTest.java index b8a8e423dc24..ba4373e06a4b 100644 --- a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/S3TransferManagerTest.java +++ b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/S3TransferManagerTest.java @@ -16,22 +16,38 @@ package software.amazon.awssdk.transfer.s3.internal; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; + +import com.google.common.jimfs.Configuration; +import com.google.common.jimfs.Jimfs; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; +import java.util.List; import java.util.concurrent.CompletableFuture; import org.junit.After; +import org.junit.AfterClass; import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.mockito.ArgumentCaptor; import software.amazon.awssdk.core.async.AsyncRequestBody; import software.amazon.awssdk.core.async.AsyncResponseTransformer; +import software.amazon.awssdk.metrics.MetricCollection; import software.amazon.awssdk.transfer.s3.CompletedDownload; import software.amazon.awssdk.transfer.s3.CompletedUpload; import software.amazon.awssdk.transfer.s3.DownloadRequest; import software.amazon.awssdk.transfer.s3.S3TransferManager; +import software.amazon.awssdk.transfer.s3.UploadDirectory; +import software.amazon.awssdk.transfer.s3.UploadDirectoryRequest; import software.amazon.awssdk.transfer.s3.UploadRequest; import software.amazon.awssdk.services.s3.model.GetObjectRequest; import software.amazon.awssdk.services.s3.model.GetObjectResponse; @@ -41,11 +57,13 @@ public class S3TransferManagerTest { private S3CrtAsyncClient mockS3Crt; private S3TransferManager tm; + private UploadDirectoryManager uploadDirectoryManager; @Before public void methodSetup() { mockS3Crt = mock(S3CrtAsyncClient.class); - tm = new DefaultS3TransferManager(mockS3Crt); + uploadDirectoryManager = mock(UploadDirectoryManager.class); + tm = new DefaultS3TransferManager(mockS3Crt, uploadDirectoryManager); } @After @@ -125,5 +143,36 @@ public void download_cancel_shouldForwardCancellation() { assertThat(s3CrtFuture).isCancelled(); } + @Test + public void objectLambdaArnBucketProvided_shouldThrowException() { + String objectLambdaArn = "arn:xxx:s3-object-lambda"; + assertThatThrownBy(() -> tm.upload(b -> b.putObjectRequest(p -> p.bucket(objectLambdaArn) + .key("key")).source(Paths.get("."))) + .completionFuture().join()) + .hasMessageContaining("support S3 Object Lambda resources").hasCauseInstanceOf(IllegalArgumentException.class); + + assertThatThrownBy(() -> tm.download(b -> b.getObjectRequest(p -> p.bucket(objectLambdaArn) + .key("key")).destination(Paths.get("."))).completionFuture().join()) + .hasMessageContaining("support S3 Object Lambda resources").hasCauseInstanceOf(IllegalArgumentException.class); + + assertThatThrownBy(() -> tm.uploadDirectory(b -> b.bucket(objectLambdaArn).sourceDirectory(Paths.get("."))).completionFuture().join()) + .hasMessageContaining("support S3 Object Lambda resources").hasCauseInstanceOf(IllegalArgumentException.class); + } + @Test + public void uploadDirectory_directoryNotExist_shouldCompleteFutureExceptionally() { + assertThatThrownBy(() -> tm.uploadDirectory(u -> u.sourceDirectory(Paths.get("randomstringneverexistas234ersaf1231")) + .bucket("bucketName")).completionFuture().join()) + .hasMessageContaining("The source directory provided either").hasCauseInstanceOf(IllegalArgumentException.class); + } + + @Test + public void uploadDirectory_throwException_shouldCompleteFutureExceptionally() { + RuntimeException exception = new RuntimeException("test"); + when(uploadDirectoryManager.uploadDirectory(any(UploadDirectoryRequest.class))).thenThrow(exception); + + assertThatThrownBy(() -> tm.uploadDirectory(u -> u.sourceDirectory(Paths.get("/")) + .bucket("bucketName")).completionFuture().join()) + .hasCause(exception); + } } diff --git a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryManagerParameterizedTest.java b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryManagerParameterizedTest.java new file mode 100644 index 000000000000..b94db21b308b --- /dev/null +++ b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryManagerParameterizedTest.java @@ -0,0 +1,284 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.transfer.s3.internal; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assume.assumeTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.google.common.jimfs.Configuration; +import com.google.common.jimfs.Jimfs; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.assertj.core.util.Sets; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.mockito.ArgumentCaptor; +import software.amazon.awssdk.services.s3.model.PutObjectResponse; +import software.amazon.awssdk.testutils.FileUtils; +import software.amazon.awssdk.transfer.s3.Upload; +import software.amazon.awssdk.transfer.s3.UploadDirectory; +import software.amazon.awssdk.transfer.s3.UploadDirectoryRequest; +import software.amazon.awssdk.transfer.s3.UploadRequest; +import software.amazon.awssdk.utils.IoUtils; + +/** + * Testing {@link UploadDirectoryManager} with different file systems. + */ +@RunWith(Parameterized.class) +public class UploadDirectoryManagerParameterizedTest { + private static final Set FILE_SYSTEMS = Sets.newHashSet(Arrays.asList(Configuration.unix(), + Configuration.osX(), + Configuration.windows(), + Configuration.forCurrentPlatform())); + private Function singleUploadFunction; + private UploadDirectoryManager tm; + private Path directory; + + @Parameterized.Parameter + public Configuration configuration; + + private FileSystem jimfs; + + @Parameterized.Parameters + public static Collection fileSystems() { + return FILE_SYSTEMS; + } + + @Before + public void methodSetup() throws IOException { + singleUploadFunction = mock(Function.class); + tm = new UploadDirectoryManager(TransferConfiguration.builder().build(), singleUploadFunction); + + if (!configuration.equals(Configuration.forCurrentPlatform())) { + jimfs = Jimfs.newFileSystem(configuration); + } + + directory = createTestDirectory(); + } + + @After + public void tearDown() { + if (jimfs != null) { + IoUtils.closeQuietly(jimfs, null); + } else { + FileUtils.cleanUpTestDirectory(directory); + } + } + + @Test + public void uploadDirectory_defaultSetting_shouldRecursivelyUpload() { + ArgumentCaptor requestArgumentCaptor = ArgumentCaptor.forClass(UploadRequest.class); + + when(singleUploadFunction.apply(requestArgumentCaptor.capture())) + .thenReturn(completedUpload()); + UploadDirectory uploadDirectory = + tm.uploadDirectory(UploadDirectoryRequest.builder() + .sourceDirectory(directory) + .bucket("bucket") + .overrideConfiguration(o -> o.followSymbolicLinks(false)) + .build()); + uploadDirectory.completionFuture().join(); + + List actualRequests = requestArgumentCaptor.getAllValues(); + actualRequests.forEach(r -> assertThat(r.putObjectRequest().bucket()).isEqualTo("bucket")); + + assertThat(actualRequests.size()).isEqualTo(3); + + List keys = + actualRequests.stream().map(u -> u.putObjectRequest().key()) + .collect(Collectors.toList()); + + assertThat(keys).containsOnly("bar.txt", "foo/1.txt", "foo/2.txt"); + } + + @Test + public void uploadDirectory_recursiveFalse_shouldOnlyUploadTopLevel() { + ArgumentCaptor requestArgumentCaptor = ArgumentCaptor.forClass(UploadRequest.class); + + when(singleUploadFunction.apply(requestArgumentCaptor.capture())).thenReturn(completedUpload()); + UploadDirectory uploadDirectory = + tm.uploadDirectory(UploadDirectoryRequest.builder() + .sourceDirectory(directory) + .bucket("bucket") + .overrideConfiguration(o -> o.recursive(false)) + .build()); + uploadDirectory.completionFuture().join(); + + List actualRequests = requestArgumentCaptor.getAllValues(); + + assertThat(actualRequests.size()).isEqualTo(1); + + actualRequests.forEach(r -> assertThat(r.putObjectRequest().bucket()).isEqualTo("bucket")); + assertThat(actualRequests.get(0).putObjectRequest().key()).isEqualTo("bar.txt"); + } + + @Test + public void uploadDirectory_FollowSymlinkTrue_shouldIncludeLinkedFiles() { + // skip the test if we are using jimfs because it doesn't work well with symlink + assumeTrue(configuration.equals(Configuration.forCurrentPlatform())); + + ArgumentCaptor requestArgumentCaptor = ArgumentCaptor.forClass(UploadRequest.class); + + when(singleUploadFunction.apply(requestArgumentCaptor.capture())).thenReturn(completedUpload()); + UploadDirectory uploadDirectory = + tm.uploadDirectory(UploadDirectoryRequest.builder() + .sourceDirectory(directory) + .bucket("bucket") + .overrideConfiguration(o -> o.followSymbolicLinks(true)) + .build()); + uploadDirectory.completionFuture().join(); + + List actualRequests = requestArgumentCaptor.getAllValues(); + actualRequests.forEach(r -> assertThat(r.putObjectRequest().bucket()).isEqualTo("bucket")); + + List keys = + actualRequests.stream().map(u -> u.putObjectRequest().key()) + .collect(Collectors.toList()); + + assertThat(keys.size()).isEqualTo(4); + assertThat(keys).containsOnly("bar.txt", "foo/1.txt", "foo/2.txt", "symlink/3.txt"); + } + + @Test + public void uploadDirectory_withPrefix_keysShouldHavePrefix() { + ArgumentCaptor requestArgumentCaptor = ArgumentCaptor.forClass(UploadRequest.class); + + when(singleUploadFunction.apply(requestArgumentCaptor.capture())).thenReturn(completedUpload()); + UploadDirectory uploadDirectory = + tm.uploadDirectory(UploadDirectoryRequest.builder() + .sourceDirectory(directory) + .bucket("bucket") + .prefix("yolo") + .build()); + uploadDirectory.completionFuture().join(); + + List keys = + requestArgumentCaptor.getAllValues().stream().map(u -> u.putObjectRequest().key()) + .collect(Collectors.toList()); + + assertThat(keys.size()).isEqualTo(3); + keys.forEach(r -> assertThat(r).startsWith("yolo/")); + } + + @Test + public void uploadDirectory_maxLengthOne_shouldOnlyUploadTopLevel() { + ArgumentCaptor requestArgumentCaptor = ArgumentCaptor.forClass(UploadRequest.class); + + when(singleUploadFunction.apply(requestArgumentCaptor.capture())) + .thenReturn(completedUpload()); + UploadDirectory uploadDirectory = + tm.uploadDirectory(UploadDirectoryRequest.builder() + .sourceDirectory(directory) + .bucket("bucket") + .overrideConfiguration(o -> o.maxDepth(1)) + .build()); + uploadDirectory.completionFuture().join(); + + List actualRequests = requestArgumentCaptor.getAllValues(); + actualRequests.forEach(r -> assertThat(r.putObjectRequest().bucket()).isEqualTo("bucket")); + + assertThat(actualRequests.size()).isEqualTo(1); + + List keys = + actualRequests.stream().map(u -> u.putObjectRequest().key()) + .collect(Collectors.toList()); + + assertThat(keys).containsOnly("bar.txt"); + } + + private DefaultUpload completedUpload() { + return new DefaultUpload(CompletableFuture.completedFuture(DefaultCompletedUpload.builder() + .response(PutObjectResponse.builder().build()) + .build())); + } + + private Path createTestDirectory() throws IOException { + + if (jimfs != null) { + return createJmfsTestDirectory(); + } + + return createLocalTestDirectoryWithSymLink(); + } + + /** + * Create a test directory with the following structure + * - test1 + * - foo + * - 1.txt + * - 2.txt + * - bar.txt + * - symlink -> test2 + * - test2 + * - 3.txt + */ + private Path createLocalTestDirectoryWithSymLink() throws IOException { + Path directory = Files.createTempDirectory("test1"); + Path anotherDirectory = Files.createTempDirectory("test2"); + + String directoryName = directory.toString(); + String anotherDirectoryName = anotherDirectory.toString(); + + Files.createDirectory(Paths.get(directory + "/foo")); + + Files.write(Paths.get(directoryName, "bar.txt"), "bar".getBytes(StandardCharsets.UTF_8)); + Files.write(Paths.get(directoryName, "foo/1.txt"), "1".getBytes(StandardCharsets.UTF_8)); + Files.write(Paths.get(directoryName, "foo/2.txt"), "2".getBytes(StandardCharsets.UTF_8)); + + Files.write(Paths.get(anotherDirectoryName, "3.txt"), "3".getBytes(StandardCharsets.UTF_8)); + + Files.createSymbolicLink(Paths.get(directoryName, "symlink"), anotherDirectory); + return directory; + } + + /** + * Create a test directory with the following structure + * - test1 + * - foo + * - 1.txt + * - 2.txt + * - bar.txt + */ + private Path createJmfsTestDirectory() throws IOException { + String directoryName = "test"; + Path directory = jimfs.getPath(directoryName); + + Files.createDirectory(directory); + + Files.createDirectory(jimfs.getPath(directoryName + "/foo")); + + Files.write(jimfs.getPath(directoryName, "bar.txt"), "bar".getBytes(StandardCharsets.UTF_8)); + Files.write(jimfs.getPath(directoryName, "foo/1.txt"), "1".getBytes(StandardCharsets.UTF_8)); + Files.write(jimfs.getPath(directoryName, "foo/2.txt"), "2".getBytes(StandardCharsets.UTF_8)); + return directory; + } +} diff --git a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryManagerTest.java b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryManagerTest.java new file mode 100644 index 000000000000..0890949b2641 --- /dev/null +++ b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryManagerTest.java @@ -0,0 +1,148 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.transfer.s3.internal; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.google.common.jimfs.Jimfs; +import java.io.IOException; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Function; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import software.amazon.awssdk.core.exception.SdkClientException; +import software.amazon.awssdk.services.s3.model.PutObjectResponse; +import software.amazon.awssdk.transfer.s3.CompletedUpload; +import software.amazon.awssdk.transfer.s3.CompletedUploadDirectory; +import software.amazon.awssdk.transfer.s3.Upload; +import software.amazon.awssdk.transfer.s3.UploadDirectory; +import software.amazon.awssdk.transfer.s3.UploadDirectoryRequest; +import software.amazon.awssdk.transfer.s3.UploadRequest; + +public class UploadDirectoryManagerTest { + private static FileSystem jimfs; + private static Path directory; + private Function singleUploadFunction; + private UploadDirectoryManager tm; + + @BeforeClass + public static void setUp() throws IOException { + jimfs = Jimfs.newFileSystem(); + directory = jimfs.getPath("test"); + Files.createDirectory(directory); + Files.createFile(jimfs.getPath("test/1")); + Files.createFile(jimfs.getPath("test/2")); + } + + @AfterClass + public static void tearDown() throws IOException { + jimfs.close(); + } + + @Before + public void methodSetup() { + singleUploadFunction = mock(Function.class); + tm = new UploadDirectoryManager(TransferConfiguration.builder().build(), singleUploadFunction); + } + + @Test + public void uploadDirectory_cancel_shouldCancelAllFutures() { + CompletableFuture future = new CompletableFuture<>(); + Upload upload = new DefaultUpload(future); + + CompletableFuture future2 = new CompletableFuture<>(); + Upload upload2 = new DefaultUpload(future2); + + when(singleUploadFunction.apply(any(UploadRequest.class))).thenReturn(upload, upload2); + + UploadDirectory uploadDirectory = + tm.uploadDirectory(UploadDirectoryRequest.builder() + .sourceDirectory(directory) + .bucket("bucket") + .build()); + + uploadDirectory.completionFuture().cancel(true); + assertThat(future).isCancelled(); + assertThat(future2).isCancelled(); + } + + @Test + public void uploadDirectory_allUploadSucceed_failedUploadShouldBeEmpty() throws ExecutionException, InterruptedException, + TimeoutException { + CompletedUpload completedUpload = DefaultCompletedUpload.builder().response(PutObjectResponse.builder().build()).build(); + CompletableFuture successfulFuture = new CompletableFuture<>(); + Upload upload = new DefaultUpload(successfulFuture); + successfulFuture.complete(completedUpload); + + CompletedUpload completedUpload2 = DefaultCompletedUpload.builder().response(PutObjectResponse.builder().build()).build(); + CompletableFuture failedFuture = new CompletableFuture<>(); + Upload upload2 = new DefaultUpload(failedFuture); + failedFuture.complete(completedUpload2); + + when(singleUploadFunction.apply(any(UploadRequest.class))).thenReturn(upload, upload2); + + UploadDirectory uploadDirectory = + tm.uploadDirectory(UploadDirectoryRequest.builder() + .sourceDirectory(directory) + .bucket("bucket") + .build()); + + CompletedUploadDirectory completedUploadDirectory = uploadDirectory.completionFuture().get(5, TimeUnit.SECONDS); + + assertThat(completedUploadDirectory.failedUploads()).isEmpty(); + assertThat(completedUploadDirectory.successfulObjects()).hasSize(2).containsOnly(completedUpload, completedUpload2); + } + + @Test + public void uploadDirectory_partialSuccess_shouldProvideFailedUploads() throws ExecutionException, InterruptedException, + TimeoutException { + CompletedUpload completedUpload = DefaultCompletedUpload.builder().response(PutObjectResponse.builder().build()).build(); + CompletableFuture successfulFuture = new CompletableFuture<>(); + Upload upload = new DefaultUpload(successfulFuture); + successfulFuture.complete(completedUpload); + + SdkClientException exception = SdkClientException.create("failed"); + CompletableFuture failedFuture = new CompletableFuture<>(); + Upload upload2 = new DefaultUpload(failedFuture); + failedFuture.completeExceptionally(exception); + + when(singleUploadFunction.apply(any(UploadRequest.class))).thenReturn(upload, upload2); + + UploadDirectory uploadDirectory = + tm.uploadDirectory(UploadDirectoryRequest.builder() + .sourceDirectory(directory) + .bucket("bucket") + .build()); + + CompletedUploadDirectory completedUploadDirectory = uploadDirectory.completionFuture().get(5, TimeUnit.SECONDS); + + assertThat(completedUploadDirectory.failedUploads()).hasSize(1); + assertThat(completedUploadDirectory.failedUploads().get(0).exception()).isEqualTo(exception); + assertThat(completedUploadDirectory.failedUploads().get(0).path().toString()).isEqualTo("test/2"); + assertThat(completedUploadDirectory.successfulObjects()).hasSize(1).containsOnly(completedUpload); + } +} diff --git a/test/test-utils/src/main/java/software/amazon/awssdk/testutils/FileUtils.java b/test/test-utils/src/main/java/software/amazon/awssdk/testutils/FileUtils.java new file mode 100644 index 000000000000..2b61a68e3d05 --- /dev/null +++ b/test/test-utils/src/main/java/software/amazon/awssdk/testutils/FileUtils.java @@ -0,0 +1,44 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.testutils; + +import java.io.File; +import java.io.IOException; +import java.nio.file.FileVisitOption; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.stream.Stream; + +public final class FileUtils { + private FileUtils() { + + } + + public static void cleanUpTestDirectory(Path directory) { + try { + try (Stream paths = Files.walk(directory, Integer.MAX_VALUE, FileVisitOption.FOLLOW_LINKS)) { + paths.sorted(Comparator.reverseOrder()) + .map(Path::toFile) + .forEach(File::delete); + } + + } catch (IOException e) { + // ignore + e.printStackTrace(); + } + } +} From 144b16d9a6ae19d56fd1db2b23fdcf5ec01d55bc Mon Sep 17 00:00:00 2001 From: Zoe Wang <33073555+zoewangg@users.noreply.github.com> Date: Wed, 29 Sep 2021 20:38:07 -0700 Subject: [PATCH 2/8] address part of the feedback --- ...ManagerUploadDirectoryIntegrationTest.java | 76 ++++++-- ...Upload.java => CompletedFileTransfer.java} | 12 +- .../transfer/s3/CompletedUploadDirectory.java | 99 +++++++++- .../transfer/s3/FailedSingleFileTransfer.java | 67 +++++++ .../transfer/s3/FailedSingleFileUpload.java | 136 ++++++++++++++ .../transfer/s3/S3ClientConfiguration.java | 76 +------- .../awssdk/transfer/s3/S3TransferManager.java | 108 ++++++++++- ...3TransferManagerOverrideConfiguration.java | 177 ++++++++++++++++++ .../awssdk/transfer/s3/UploadDirectory.java | 29 --- ...UploadDirectoryOverrideConfiguration.java} | 53 ++++-- .../transfer/s3/UploadDirectoryRequest.java | 70 +++++-- .../transfer/s3/UploadDirectoryTransfer.java | 79 ++++++++ .../awssdk/transfer/s3/UploadRequest.java | 9 + .../DefaultCompletedUploadDirectory.java | 116 ------------ .../s3/internal/DefaultFailedUpload.java | 99 ---------- .../s3/internal/DefaultS3TransferManager.java | 84 ++++++--- .../s3/internal/DefaultUploadDirectory.java | 35 ---- .../s3/internal/TransferConfiguration.java | 81 -------- .../internal/TransferConfigurationOption.java | 8 + .../TransferManagerConfiguration.java | 137 ++++++++++++++ ...anager.java => UploadDirectoryHelper.java} | 124 +++++++----- .../s3/S3ClientConfigurationTest.java | 6 - .../s3/UploadDirectoryConfigurationTest.java | 10 +- .../CompletedUploadDirectoryTest.java | 39 ++-- .../internal/FailedSingleFileUploadTest.java | 74 ++++++++ .../s3/internal/FailedUploadTest.java | 52 ----- .../s3/internal/S3TransferManagerTest.java | 42 ++--- .../TransferManagerConfigurationTest.java | 103 ++++++++++ ...loadDirectoryHelperParameterizedTest.java} | 45 +++-- ...st.java => UploadDirectoryHelperTest.java} | 42 +++-- 30 files changed, 1374 insertions(+), 714 deletions(-) rename services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/{FailedUpload.java => CompletedFileTransfer.java} (78%) create mode 100644 services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/FailedSingleFileTransfer.java create mode 100644 services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/FailedSingleFileUpload.java create mode 100644 services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/S3TransferManagerOverrideConfiguration.java delete mode 100644 services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectory.java rename services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/{UploadDirectoryConfiguration.java => UploadDirectoryOverrideConfiguration.java} (74%) create mode 100644 services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryTransfer.java delete mode 100644 services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultCompletedUploadDirectory.java delete mode 100644 services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultFailedUpload.java delete mode 100644 services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultUploadDirectory.java delete mode 100644 services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/TransferConfiguration.java create mode 100644 services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/TransferManagerConfiguration.java rename services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/{UploadDirectoryManager.java => UploadDirectoryHelper.java} (56%) create mode 100644 services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/FailedSingleFileUploadTest.java delete mode 100644 services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/FailedUploadTest.java create mode 100644 services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/TransferManagerConfigurationTest.java rename services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/{UploadDirectoryManagerParameterizedTest.java => UploadDirectoryHelperParameterizedTest.java} (86%) rename services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/{UploadDirectoryManagerTest.java => UploadDirectoryHelperTest.java} (78%) diff --git a/services-custom/s3-transfer-manager/src/it/java/software/amazon/awssdk/transfer/s3/S3TransferManagerUploadDirectoryIntegrationTest.java b/services-custom/s3-transfer-manager/src/it/java/software/amazon/awssdk/transfer/s3/S3TransferManagerUploadDirectoryIntegrationTest.java index dbbddb02f00a..9fa74864e7e8 100644 --- a/services-custom/s3-transfer-manager/src/it/java/software/amazon/awssdk/transfer/s3/S3TransferManagerUploadDirectoryIntegrationTest.java +++ b/services-custom/s3-transfer-manager/src/it/java/software/amazon/awssdk/transfer/s3/S3TransferManagerUploadDirectoryIntegrationTest.java @@ -17,6 +17,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static software.amazon.awssdk.testutils.service.S3BucketUtils.temporaryBucketName; +import static software.amazon.awssdk.utils.IoUtils.closeQuietly; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -25,25 +26,30 @@ import java.nio.file.Paths; import java.util.List; import java.util.stream.Collectors; +import org.apache.commons.lang3.RandomStringUtils; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; +import software.amazon.awssdk.core.sync.ResponseTransformer; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.S3Object; import software.amazon.awssdk.testutils.FileUtils; +import software.amazon.awssdk.utils.Logger; public class S3TransferManagerUploadDirectoryIntegrationTest extends S3IntegrationTestBase { + private static final Logger log = Logger.loggerFor(S3TransferManagerUploadDirectoryIntegrationTest.class); private static final String TEST_BUCKET = temporaryBucketName(S3TransferManagerUploadIntegrationTest.class); private static S3TransferManager tm; private static Path directory; private static S3Client s3Client; + private static String randomString; @BeforeClass public static void setUp() throws Exception { S3IntegrationTestBase.setUp(); createBucket(TEST_BUCKET); - + randomString = RandomStringUtils.random(100); directory = createLocalTestDirectory(); tm = S3TransferManager.builder() @@ -59,27 +65,63 @@ public static void setUp() throws Exception { @AfterClass public static void teardown() { - tm.close(); - s3Client.close(); - deleteBucketAndAllContents(TEST_BUCKET); - FileUtils.cleanUpTestDirectory(directory); + try { + FileUtils.cleanUpTestDirectory(directory); + } catch (Exception exception) { + log.warn(() -> "Failed to clean up test directory " + directory, exception); + } + + try { + deleteBucketAndAllContents(TEST_BUCKET); + } catch (Exception exception) { + log.warn(() -> "Failed to delete s3 bucket " + TEST_BUCKET, exception); + } + + closeQuietly(tm, log.logger()); + closeQuietly(s3Client, log.logger()); S3IntegrationTestBase.cleanUp(); } @Test public void uploadDirectory_filesSentCorrectly() { String prefix = "yolo"; - UploadDirectory uploadDirectory = tm.uploadDirectory(u -> u.sourceDirectory(directory) - .bucket(TEST_BUCKET) - .prefix(prefix) - .overrideConfiguration(o -> o.recursive(true))); - uploadDirectory.completionFuture().join(); + UploadDirectoryTransfer uploadDirectory = tm.uploadDirectory(u -> u.sourceDirectory(directory) + .bucket(TEST_BUCKET) + .prefix(prefix) + .overrideConfiguration(o -> o.recursive(true))); + CompletedUploadDirectory completedUploadDirectory = uploadDirectory.completionFuture().join(); + assertThat(completedUploadDirectory.failedUploads()).isEmpty(); List keys = s3Client.listObjectsV2Paginator(b -> b.bucket(TEST_BUCKET).prefix(prefix)).contents().stream().map(S3Object::key) .collect(Collectors.toList()); assertThat(keys).containsOnly(prefix + "/bar.txt", prefix + "/foo/1.txt", prefix + "/foo/2.txt"); + + keys.forEach(k -> verifyContent(k, k.substring(prefix.length() + 1) + randomString)); + } + + @Test + public void uploadDirectory_withDelimiter_filesSentCorrectly() { + String prefix = "hello"; + String delimiter = "0"; + UploadDirectoryTransfer uploadDirectory = tm.uploadDirectory(u -> u.sourceDirectory(directory) + .bucket(TEST_BUCKET) + .delimiter(delimiter) + .prefix(prefix) + .overrideConfiguration(o -> o.recursive(true))); + CompletedUploadDirectory completedUploadDirectory = uploadDirectory.completionFuture().join(); + assertThat(completedUploadDirectory.failedUploads()).isEmpty(); + + List keys = + s3Client.listObjectsV2Paginator(b -> b.bucket(TEST_BUCKET).prefix(prefix)).contents().stream().map(S3Object::key) + .collect(Collectors.toList()); + + assertThat(keys).containsOnly(prefix + "0bar.txt", prefix + "0foo01.txt", prefix + "0foo02.txt"); + keys.forEach(k -> { + String path = k.replace(delimiter, "/"); + verifyContent(k, path.substring(prefix.length() + 1) + randomString); + }); } private static Path createLocalTestDirectory() throws IOException { @@ -88,11 +130,17 @@ private static Path createLocalTestDirectory() throws IOException { String directoryName = directory.toString(); Files.createDirectory(Paths.get(directory + "/foo")); - - Files.write(Paths.get(directoryName, "bar.txt"), "bar".getBytes(StandardCharsets.UTF_8)); - Files.write(Paths.get(directoryName, "foo/1.txt"), "1".getBytes(StandardCharsets.UTF_8)); - Files.write(Paths.get(directoryName, "foo/2.txt"), "2".getBytes(StandardCharsets.UTF_8)); + Files.write(Paths.get(directoryName, "bar.txt"), ("bar.txt" + randomString).getBytes(StandardCharsets.UTF_8)); + Files.write(Paths.get(directoryName, "foo/1.txt"), ("foo/1.txt" + randomString).getBytes(StandardCharsets.UTF_8)); + Files.write(Paths.get(directoryName, "foo/2.txt"), ("foo/2.txt" + randomString).getBytes(StandardCharsets.UTF_8)); return directory; } + + private static void verifyContent(String key, String expectedContent) { + String actualContent = s3.getObject(r -> r.bucket(TEST_BUCKET).key(key), + ResponseTransformer.toBytes()).asUtf8String(); + + assertThat(actualContent).isEqualTo(expectedContent); + } } diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/FailedUpload.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/CompletedFileTransfer.java similarity index 78% rename from services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/FailedUpload.java rename to services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/CompletedFileTransfer.java index 16ea0ce16007..92eb1b4d0393 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/FailedUpload.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/CompletedFileTransfer.java @@ -20,19 +20,15 @@ import software.amazon.awssdk.annotations.SdkPublicApi; /** - * A failed single file upload transfer. + * Represents a completed file transfer. */ @SdkPublicApi @SdkPreviewApi -public interface FailedUpload { +public interface CompletedFileTransfer extends CompletedTransfer { /** - * @return the exception thrown - */ - Throwable exception(); - - /** - * @return the path to the file that the transfer manager failed to upload + * Returns the path of the file that was upload from or downloaded to + * @return the path */ Path path(); } diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/CompletedUploadDirectory.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/CompletedUploadDirectory.java index e1ce79afee2e..506100b755e5 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/CompletedUploadDirectory.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/CompletedUploadDirectory.java @@ -15,26 +15,109 @@ package software.amazon.awssdk.transfer.s3; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.List; import software.amazon.awssdk.annotations.SdkPreviewApi; import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.utils.ToString; +import software.amazon.awssdk.utils.Validate; /** - * A completed download directory transfer. + * Represents a completed upload directory transfer to Amazon S3. It can be used to track + * failed single file uploads. + * + * @see S3TransferManager#uploadDirectory(UploadDirectoryRequest) */ @SdkPublicApi @SdkPreviewApi -public interface CompletedUploadDirectory extends CompletedTransfer { +public final class CompletedUploadDirectory implements CompletedTransfer { + private final List failedUploads; + + private CompletedUploadDirectory(DefaultBuilder builder) { + this.failedUploads = Collections.unmodifiableList(new ArrayList<>(Validate.paramNotNull(builder.failedUploads, + "failedUploads"))); + } /** - * A list of failed single file uploads associated with one upload directory transfer - * @return A list of failed uploads + * Return failed uploads with error details, request metadata about each file that is failed to upload. + * + * @return a list of failed uploads */ - List failedUploads(); + public Collection failedUploads() { + return failedUploads; + } /** - * A list of successful single file uploads associated with one upload directory transfer - * @return a list of successful uploads + * Creates a default builder for {@link CompletedUpload}. */ - List successfulObjects(); + public static Builder builder() { + return new DefaultBuilder(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + CompletedUploadDirectory that = (CompletedUploadDirectory) o; + + return failedUploads.equals(that.failedUploads); + } + + @Override + public int hashCode() { + return failedUploads.hashCode(); + } + + @Override + public String toString() { + return ToString.builder("CompletedUploadDirectory") + .add("failedUploads", failedUploads) + .build(); + } + + public interface Builder { + + /** + * Sets a collection of {@link FailedSingleFileUpload}s + * + * @param failedUploads failed uploads + * @return This builder for method chaining. + */ + Builder failedUploads(Collection failedUploads); + + /** + * Builds a {@link CompletedUploadDirectory} based on the properties supplied to this builder + * @return An initialized {@link CompletedUploadDirectory} + */ + CompletedUploadDirectory build(); + } + + private static final class DefaultBuilder implements Builder { + private Collection failedUploads = Collections.emptyList(); + + private DefaultBuilder() { + } + + @Override + public Builder failedUploads(Collection failedUploads) { + this.failedUploads = failedUploads; + return this; + } + + public void setFailedUploads(Collection failedUploads) { + failedUploads(failedUploads); + } + + @Override + public CompletedUploadDirectory build() { + return new CompletedUploadDirectory(this); + } + } } diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/FailedSingleFileTransfer.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/FailedSingleFileTransfer.java new file mode 100644 index 000000000000..30662f1bfd39 --- /dev/null +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/FailedSingleFileTransfer.java @@ -0,0 +1,67 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.transfer.s3; + +import software.amazon.awssdk.annotations.SdkPreviewApi; +import software.amazon.awssdk.annotations.SdkPublicApi; + +/** + * Represents a failed single file transfer in a multi-file transfer operation such as + * {@link S3TransferManager#uploadDirectory} + */ +@SdkPublicApi +@SdkPreviewApi +public interface FailedSingleFileTransfer { + + /** + * The exception thrown from a specific single file transfer + * + * @return the exception thrown + */ + Throwable exception(); + + /** + * The failed {@link TransferRequest}. + * + * @return the failed request + */ + T request(); + + interface Builder { + /** + * Specify the exception thrown from a specific single file transfer + * + * @param exception the exception thrown + * @return this builder for method chaining. + */ + Builder exception(Throwable exception); + + /** + * Specify the failed request + * + * @param request the failed request + * @return this builder for method chaining. + */ + Builder request(T request); + + /** + * Builds a {@link FailedSingleFileTransfer} based on the properties supplied to this builder + * + * @return An initialized {@link FailedSingleFileTransfer} + */ + FailedSingleFileTransfer build(); + } +} diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/FailedSingleFileUpload.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/FailedSingleFileUpload.java new file mode 100644 index 000000000000..2dc893eeed04 --- /dev/null +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/FailedSingleFileUpload.java @@ -0,0 +1,136 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.transfer.s3; + +import software.amazon.awssdk.annotations.SdkPreviewApi; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.utils.ToString; +import software.amazon.awssdk.utils.Validate; +import software.amazon.awssdk.utils.builder.CopyableBuilder; +import software.amazon.awssdk.utils.builder.ToCopyableBuilder; + +/** + * Represents a failed single file upload from {@link S3TransferManager#uploadDirectory}. It + * has detailed description of the result + */ +@SdkPublicApi +@SdkPreviewApi +public final class FailedSingleFileUpload implements FailedSingleFileTransfer, + ToCopyableBuilder { + private final Throwable exception; + private final UploadRequest request; + + FailedSingleFileUpload(DefaultBuilder builder) { + this.exception = Validate.paramNotNull(builder.exception, "exception"); + this.request = Validate.paramNotNull(builder.request, "request"); + } + + @Override + public Throwable exception() { + return exception; + } + + @Override + public UploadRequest request() { + return request; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + FailedSingleFileUpload that = (FailedSingleFileUpload) o; + + if (!exception.equals(that.exception)) { + return false; + } + return request.equals(that.request); + } + + @Override + public int hashCode() { + int result = exception.hashCode(); + result = 31 * result + request.hashCode(); + return result; + } + + @Override + public String toString() { + return ToString.builder("FailedUpload") + .add("exception", exception) + .add("request", request) + .build(); + } + + public static Builder builder() { + return new DefaultBuilder(); + } + + @Override + public Builder toBuilder() { + return new DefaultBuilder(this); + } + + public interface Builder extends CopyableBuilder, + FailedSingleFileTransfer.Builder { + + @Override + Builder exception(Throwable exception); + + @Override + Builder request(UploadRequest request); + + @Override + FailedSingleFileUpload build(); + } + + private static final class DefaultBuilder implements Builder { + private UploadRequest request; + private Throwable exception; + + private DefaultBuilder(FailedSingleFileUpload failedSingleFileUpload) { + this.request = failedSingleFileUpload.request; + this.exception = failedSingleFileUpload.exception; + } + + private DefaultBuilder() { + + } + + @Override + public Builder exception(Throwable exception) { + this.exception = exception; + return this; + } + + @Override + public Builder request(UploadRequest request) { + this.request = request; + return this; + } + + @Override + public FailedSingleFileUpload build() { + return new FailedSingleFileUpload(this); + } + } +} diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/S3ClientConfiguration.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/S3ClientConfiguration.java index fcb54c8e980a..c6b6328024e8 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/S3ClientConfiguration.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/S3ClientConfiguration.java @@ -17,11 +17,9 @@ import java.util.Objects; import java.util.Optional; -import java.util.function.Consumer; import software.amazon.awssdk.annotations.SdkPreviewApi; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; -import software.amazon.awssdk.core.client.config.ClientAsyncConfiguration; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.utils.Validate; import software.amazon.awssdk.utils.builder.CopyableBuilder; @@ -41,8 +39,6 @@ public final class S3ClientConfiguration implements ToCopyableBuilder maxConcurrency() { return Optional.ofNullable(maxConcurrency); } - /** - * @return the optional SDK async configuration specified - */ - public Optional asyncConfiguration() { - return Optional.ofNullable(asyncConfiguration); - } - - /** - * @return the optional upload directory configuration specified - */ - public Optional uploadDirectoryConfiguration() { - return Optional.ofNullable(uploadDirectoryConfiguration); - } - @Override public Builder toBuilder() { return new DefaultBuilder(this); @@ -132,13 +112,7 @@ public boolean equals(Object o) { if (!Objects.equals(targetThroughputInGbps, that.targetThroughputInGbps)) { return false; } - if (!Objects.equals(maxConcurrency, that.maxConcurrency)) { - return false; - } - if (!Objects.equals(asyncConfiguration, that.asyncConfiguration)) { - return false; - } - return Objects.equals(uploadDirectoryConfiguration, that.uploadDirectoryConfiguration); + return Objects.equals(maxConcurrency, that.maxConcurrency); } @Override @@ -148,8 +122,6 @@ public int hashCode() { result = 31 * result + (minimumPartSizeInBytes != null ? minimumPartSizeInBytes.hashCode() : 0); result = 31 * result + (targetThroughputInGbps != null ? targetThroughputInGbps.hashCode() : 0); result = 31 * result + (maxConcurrency != null ? maxConcurrency.hashCode() : 0); - result = 31 * result + (asyncConfiguration != null ? asyncConfiguration.hashCode() : 0); - result = 31 * result + (uploadDirectoryConfiguration != null ? uploadDirectoryConfiguration.hashCode() : 0); return result; } @@ -244,37 +216,6 @@ public interface Builder extends CopyableBuilder * @see #targetThroughputInGbps(Double) */ Builder maxConcurrency(Integer maxConcurrency); - - /** - * Specify overrides to the default SDK async configuration that should be used for clients created by this builder. - * - * @param asyncConfiguration the async configuration - * @return this builder for method chaining. - * @see #asyncConfiguration(Consumer) - */ - Builder asyncConfiguration(ClientAsyncConfiguration asyncConfiguration); - - /** - * Similar to {@link #asyncConfiguration(ClientAsyncConfiguration)}, but takes a lambda to configure a new - * {@link ClientAsyncConfiguration.Builder}. This removes the need to call {@link ClientAsyncConfiguration#builder()} - * and {@link ClientAsyncConfiguration.Builder#build()}. - * - * @param configuration the async configuration - * @return this builder for method chaining. - * @see #asyncConfiguration(ClientAsyncConfiguration) - */ - default Builder asyncConfiguration(Consumer configuration) { - return asyncConfiguration(ClientAsyncConfiguration.builder().applyMutation(configuration).build()); - } - - Builder uploadDirectoryConfiguration(UploadDirectoryConfiguration uploadDirectoryConfiguration); - - default Builder uploadDirectoryConfiguration(Consumer uploadConfigurationBuilder) { - Validate.paramNotNull(uploadConfigurationBuilder, "uploadConfigurationBuilder"); - return uploadDirectoryConfiguration(UploadDirectoryConfiguration.builder() - .applyMutation(uploadConfigurationBuilder) - .build()); - } } private static final class DefaultBuilder implements Builder { @@ -283,8 +224,6 @@ private static final class DefaultBuilder implements Builder { private Long minimumPartSizeInBytes; private Double targetThroughputInGbps; private Integer maxConcurrency; - private ClientAsyncConfiguration asyncConfiguration; - private UploadDirectoryConfiguration uploadDirectoryConfiguration; private DefaultBuilder() { } @@ -295,7 +234,6 @@ private DefaultBuilder(S3ClientConfiguration configuration) { this.minimumPartSizeInBytes = configuration.minimumPartSizeInBytes; this.targetThroughputInGbps = configuration.targetThroughputInGbps; this.maxConcurrency = configuration.maxConcurrency; - this.asyncConfiguration = configuration.asyncConfiguration; } @Override @@ -328,18 +266,6 @@ public Builder maxConcurrency(Integer maxConcurrency) { return this; } - @Override - public Builder asyncConfiguration(ClientAsyncConfiguration asyncConfiguration) { - this.asyncConfiguration = asyncConfiguration; - return this; - } - - @Override - public Builder uploadDirectoryConfiguration(UploadDirectoryConfiguration uploadDirectoryConfiguration) { - this.uploadDirectoryConfiguration = uploadDirectoryConfiguration; - return this; - } - @Override public S3ClientConfiguration build() { return new S3ClientConfiguration(this); diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/S3TransferManager.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/S3TransferManager.java index d8b061b33b18..557388db828f 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/S3TransferManager.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/S3TransferManager.java @@ -15,6 +15,7 @@ package software.amazon.awssdk.transfer.s3; +import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; import software.amazon.awssdk.annotations.SdkPreviewApi; import software.amazon.awssdk.annotations.SdkPublicApi; @@ -161,7 +162,24 @@ default Upload upload(Consumer request) { } /** - * Upload the given directory to the S3 bucket under the given prefix. + * Upload all files under the given directory to the provided S3 bucket. The key name transformation depends on the optional + * prefix and delimiter provided in the {@link UploadDirectoryRequest}. By default, all subdirectories will be uploaded + * recursively, and symbolic links are not followed automatically. This behavior can be configured in + * {@link UploadDirectoryOverrideConfiguration} + * at request level via {@link UploadDirectoryRequest.Builder#overrideConfiguration(UploadDirectoryOverrideConfiguration)} or + * client level via {@link S3TransferManager.Builder#transferConfiguration(S3TransferManagerOverrideConfiguration)} Note + * that request-level configuration takes precedence over client-level configuration. + * + *

+ * The returned {@link CompletableFuture} only completes exceptionally if the request cannot be attempted as a whole (the + * source directory provided does not exist for example). The future completes successfully for partial successful + * requests, i.e., there might be failed uploads in the successfully completed response. As a result, + * you should check for errors in the response via {@link CompletedUploadDirectory#failedUploads()} + * even when the future completes successfully. Failed single file uploads can be retried by calling + * {@link S3TransferManager#upload(UploadRequest)} + * + *

+ * The current user must have read access to all directories and files * *

* Usage Example: @@ -174,19 +192,49 @@ default Upload upload(Consumer request) { * .prefix("prefix") * .build()); * // Wait for the transfer to complete - * uploadDirectory.completionFuture().join(); - * } + * CompletedUploadDirectory completedUploadDirectory = uploadDirectory.completionFuture().join(); + * + * // Print out the failed uploads + * completedUploadDirectory.failedUploads().forEach(System.out::println); + * + * // Retrying failed uploads if the exceptions are retryable + * List> futures = + * completedUploadDirectory.failedUploads() + * .stream() + * .filter(failedSingleFileUpload -> isRetryable(failedSingleFileUpload.exception())) + * .map(failedSingleFileUpload -> + * tm.upload(failedSingleFileUpload.request()).completionFuture()) + * .collect(Collectors.toList()); + * CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); * * * @param uploadDirectoryRequest the upload directory request - * @see #uploadDirectory(UploadDirectoryRequest) + * @see #uploadDirectory(Consumer) + * @see UploadDirectoryOverrideConfiguration */ - default UploadDirectory uploadDirectory(UploadDirectoryRequest uploadDirectoryRequest) { + default UploadDirectoryTransfer uploadDirectory(UploadDirectoryRequest uploadDirectoryRequest) { throw new UnsupportedOperationException(); } /** - * Upload the given directory to the S3 bucket under the given prefix. + * Upload all files under the given directory to the provided S3 bucket. The key name transformation depends on the optional + * prefix and delimiter provided in the {@link UploadDirectoryRequest}. By default, all subdirectories will be uploaded + * recursively, and symbolic links are not followed automatically. This behavior can be configured in + * {@link UploadDirectoryOverrideConfiguration} + * at request level via {@link UploadDirectoryRequest.Builder#overrideConfiguration(UploadDirectoryOverrideConfiguration)} or + * client level via {@link S3TransferManager.Builder#transferConfiguration(S3TransferManagerOverrideConfiguration)} Note + * that request-level configuration takes precedence over client-level configuration. + * + *

+ * The returned {@link CompletableFuture} only completes exceptionally if the request cannot be attempted as a whole (the + * source directory provided does not exist for example). The future completes successfully for partial successful + * requests, i.e., there might be failed uploads in the successfully completed response. As a result, + * you should check for errors in the response via {@link CompletedUploadDirectory#failedUploads()} + * even when the future completes successfully. Failed single file uploads can be retried by calling + * {@link S3TransferManager#upload(UploadRequest)} + * + *

+ * The current user must have read access to all directories and files * *

* This is a convenience method that creates an instance of the {@link UploadDirectoryRequest} builder avoiding the @@ -200,13 +248,24 @@ default UploadDirectory uploadDirectory(UploadDirectoryRequest uploadDirectoryRe * transferManager.uploadDirectory(b -> b.sourceDirectory(Paths.get(".")) * .bucket("key") * .prefix("prefix")); - * // Wait for the transfer to complete - * uploadDirectory.completionFuture().join(); - * } + * // Print out the failed uploads + * completedUploadDirectory.failedUploads().forEach(System.out::println); + * + * // Retrying failed uploads if the exceptions are retryable + * List> futures = + * completedUploadDirectory.failedUploads() + * .stream() + * .filter(failedSingleFileUpload -> isRetryable(failedSingleFileUpload.exception())) + * .map(failedSingleFileUpload -> + * tm.upload(failedSingleFileUpload.request()).completionFuture()) + * .collect(Collectors.toList()); + * CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); * * @param requestBuilder the upload directory request builder + * @see #uploadDirectory(UploadDirectoryRequest) + * @see UploadDirectoryOverrideConfiguration */ - default UploadDirectory uploadDirectory(Consumer requestBuilder) { + default UploadDirectoryTransfer uploadDirectory(Consumer requestBuilder) { return uploadDirectory(UploadDirectoryRequest.builder().applyMutation(requestBuilder).build()); } @@ -258,6 +317,35 @@ default Builder s3ClientConfiguration(Consumer co return this; } + /** + * Configuration settings for how {@link S3TransferManager} should process the request. The + * {@link S3TransferManager} already provides sensible defaults. All values are optional. + * + * @param transferConfiguration the configuration to use + * @return Returns a reference to this object so that method calls can be chained together. + * @see #transferConfiguration(Consumer) + */ + Builder transferConfiguration(S3TransferManagerOverrideConfiguration transferConfiguration); + + /** + * Configuration settings for how {@link S3TransferManager} should process the request. The + * {@link S3TransferManager} already provides sensible defaults. All values are optional. + * + *

+ * This is a convenience method that creates an instance of the {@link S3TransferManagerOverrideConfiguration} builder + * avoiding the need to create one manually via {@link S3TransferManagerOverrideConfiguration#builder()}. + * + * @param configuration the configuration to use + * @return Returns a reference to this object so that method calls can be chained together. + * @see #transferConfiguration(S3TransferManagerOverrideConfiguration) + */ + default Builder transferConfiguration(Consumer configuration) { + S3TransferManagerOverrideConfiguration.Builder builder = S3TransferManagerOverrideConfiguration.builder(); + configuration.accept(builder); + transferConfiguration(builder.build()); + return this; + } + /** * Build an instance of {@link S3TransferManager} based on the settings supplied to this builder * diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/S3TransferManagerOverrideConfiguration.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/S3TransferManagerOverrideConfiguration.java new file mode 100644 index 000000000000..eabbe5c9704b --- /dev/null +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/S3TransferManagerOverrideConfiguration.java @@ -0,0 +1,177 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.transfer.s3; + +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.Executor; +import java.util.function.Consumer; +import software.amazon.awssdk.annotations.SdkPreviewApi; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.utils.Validate; +import software.amazon.awssdk.utils.builder.CopyableBuilder; +import software.amazon.awssdk.utils.builder.ToCopyableBuilder; + +/** + * Configuration options for how {@link S3TransferManager} processes requests. + *

+ * All values are optional, and not specifying them will use default values provided bt the SDK. + * + *

Use {@link #builder()} to create a set of options.

+ */ + +@SdkPublicApi +@SdkPreviewApi +public final class S3TransferManagerOverrideConfiguration implements + ToCopyableBuilder { + private final Executor executor; + private final UploadDirectoryOverrideConfiguration uploadDirectoryConfiguration; + + private S3TransferManagerOverrideConfiguration(DefaultBuilder builder) { + this.executor = builder.executor; + this.uploadDirectoryConfiguration = builder.uploadDirectoryConfiguration; + } + + /** + * @return the optional SDK executor specified + */ + public Optional executor() { + return Optional.ofNullable(executor); + } + + /** + * @return the optional upload directory configuration specified + */ + public Optional uploadDirectoryConfiguration() { + return Optional.ofNullable(uploadDirectoryConfiguration); + } + + @Override + public Builder toBuilder() { + return new DefaultBuilder(this); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + S3TransferManagerOverrideConfiguration that = (S3TransferManagerOverrideConfiguration) o; + + if (!Objects.equals(executor, that.executor)) { + return false; + } + return Objects.equals(uploadDirectoryConfiguration, that.uploadDirectoryConfiguration); + } + + @Override + public int hashCode() { + int result = executor != null ? executor.hashCode() : 0; + result = 31 * result + (uploadDirectoryConfiguration != null ? uploadDirectoryConfiguration.hashCode() : 0); + return result; + } + + /** + * Creates a default builder for {@link S3TransferManagerOverrideConfiguration}. + */ + public static Builder builder() { + return new DefaultBuilder(); + } + + /** + * The builder definition for a {@link S3TransferManagerOverrideConfiguration}. + */ + public interface Builder extends CopyableBuilder { + + /** + * Specify the executor that will be used in {@link S3TransferManager} + * + * @param executor the async configuration + * @return this builder for method chaining. + */ + Builder executor(Executor executor); + + /** + * Specify the configuration options for upload directory operation + * + * @param uploadDirectoryConfiguration the configuration for upload directory + * @return this builder for method chaining. + * @see S3TransferManager#uploadDirectory(UploadDirectoryRequest) + */ + Builder uploadDirectoryConfiguration(UploadDirectoryOverrideConfiguration uploadDirectoryConfiguration); + + /** + * Similar to {@link #uploadDirectoryConfiguration}, but takes a lambda to configure a new + * {@link UploadDirectoryOverrideConfiguration.Builder}. This removes the need to call + * {@link UploadDirectoryOverrideConfiguration#builder()} and + * {@link UploadDirectoryOverrideConfiguration.Builder#build()}. + * + * @param uploadConfigurationBuilder the configuration for upload directory + * @return this builder for method chaining. + * @see #uploadDirectoryConfiguration(UploadDirectoryOverrideConfiguration) + */ + default Builder uploadDirectoryConfiguration(Consumer + uploadConfigurationBuilder) { + Validate.paramNotNull(uploadConfigurationBuilder, "uploadConfigurationBuilder"); + return uploadDirectoryConfiguration(UploadDirectoryOverrideConfiguration.builder() + .applyMutation(uploadConfigurationBuilder) + .build()); + } + } + + private static final class DefaultBuilder implements Builder { + private Executor executor; + private UploadDirectoryOverrideConfiguration uploadDirectoryConfiguration; + + private DefaultBuilder() { + } + + private DefaultBuilder(S3TransferManagerOverrideConfiguration configuration) { + this.executor = configuration.executor; + this.uploadDirectoryConfiguration = configuration.uploadDirectoryConfiguration; + } + + @Override + public Builder executor(Executor executor) { + this.executor = executor; + return this; + } + + public void setExecutor(Executor executor) { + executor(executor); + } + + @Override + public Builder uploadDirectoryConfiguration(UploadDirectoryOverrideConfiguration uploadDirectoryConfiguration) { + this.uploadDirectoryConfiguration = uploadDirectoryConfiguration; + return this; + } + + public void setUploadDirectoryConfiguration(UploadDirectoryOverrideConfiguration uploadDirectoryConfiguration) { + uploadDirectoryConfiguration(uploadDirectoryConfiguration); + } + + @Override + public S3TransferManagerOverrideConfiguration build() { + return new S3TransferManagerOverrideConfiguration(this); + } + } +} diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectory.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectory.java deleted file mode 100644 index 03e245dd954c..000000000000 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectory.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.transfer.s3; - -import java.util.concurrent.CompletableFuture; -import software.amazon.awssdk.annotations.SdkPublicApi; - -/** - * An upload transfer of an directory to S3. - */ -@SdkPublicApi -public interface UploadDirectory extends Transfer { - - @Override - CompletableFuture completionFuture(); -} diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryConfiguration.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryOverrideConfiguration.java similarity index 74% rename from services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryConfiguration.java rename to services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryOverrideConfiguration.java index e4ae7b4f2988..62c5ffba1fe0 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryConfiguration.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryOverrideConfiguration.java @@ -20,6 +20,7 @@ import software.amazon.awssdk.annotations.SdkPreviewApi; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.utils.ToString; +import software.amazon.awssdk.utils.Validate; import software.amazon.awssdk.utils.builder.CopyableBuilder; import software.amazon.awssdk.utils.builder.ToCopyableBuilder; @@ -31,16 +32,16 @@ */ @SdkPublicApi @SdkPreviewApi -public final class UploadDirectoryConfiguration implements ToCopyableBuilder { +public final class UploadDirectoryOverrideConfiguration implements ToCopyableBuilder { private final Boolean followSymbolicLinks; private final Integer maxDepth; private final Boolean recursive; - public UploadDirectoryConfiguration(DefaultBuilder builder) { + public UploadDirectoryOverrideConfiguration(DefaultBuilder builder) { this.followSymbolicLinks = builder.followSymbolicLinks; - this.maxDepth = builder.maxDepth; + this.maxDepth = Validate.isPositiveOrNull(builder.maxDepth, "maxDepth"); this.recursive = builder.recursive; } @@ -79,7 +80,7 @@ public boolean equals(Object o) { return false; } - UploadDirectoryConfiguration that = (UploadDirectoryConfiguration) o; + UploadDirectoryOverrideConfiguration that = (UploadDirectoryOverrideConfiguration) o; if (!Objects.equals(followSymbolicLinks, that.followSymbolicLinks)) { return false; @@ -111,18 +112,21 @@ public static Builder builder() { return new DefaultBuilder(); } - public interface Builder extends CopyableBuilder { + public interface Builder extends CopyableBuilder { /** * Specify whether to recursively upload all files under the specified directory * + *

+ * Default to true + * * @param recursive whether enable recursive upload * @return This builder for method chaining. */ Builder recursive(Boolean recursive); /** - * Specify whether to follow Follow symbolic links when traverse the directory tree. + * Specify whether to follow symbolic links when traversing the file tree. *

* Default to false * @@ -132,14 +136,19 @@ public interface Builder extends CopyableBuilder + * Default to {@code Integer.MAX_VALUE} + * + * @param maxDepth the maximum number of directory levels to visit * @return This builder for method chaining. */ Builder maxDepth(Integer maxDepth); @Override - UploadDirectoryConfiguration build(); + UploadDirectoryOverrideConfiguration build(); } private static final class DefaultBuilder implements Builder { @@ -147,7 +156,7 @@ private static final class DefaultBuilder implements Builder { private Integer maxDepth; private Boolean recursive; - private DefaultBuilder(UploadDirectoryConfiguration configuration) { + private DefaultBuilder(UploadDirectoryOverrideConfiguration configuration) { this.followSymbolicLinks = configuration.followSymbolicLinks; this.maxDepth = configuration.maxDepth; this.recursive = configuration.recursive; @@ -157,26 +166,38 @@ private DefaultBuilder() { } @Override - public DefaultBuilder recursive(Boolean recursive) { + public Builder recursive(Boolean recursive) { this.recursive = recursive; return this; } + public void setRecursive(Boolean recursive) { + recursive(recursive); + } + @Override - public DefaultBuilder followSymbolicLinks(Boolean followSymbolicLinks) { + public Builder followSymbolicLinks(Boolean followSymbolicLinks) { this.followSymbolicLinks = followSymbolicLinks; return this; } + public void setFollowSymbolicLinks(Boolean followSymbolicLinks) { + followSymbolicLinks(followSymbolicLinks); + } + @Override - public DefaultBuilder maxDepth(Integer maxDepth) { + public Builder maxDepth(Integer maxDepth) { this.maxDepth = maxDepth; return this; } + public void setMaxDepth(Integer maxDepth) { + maxDepth(maxDepth); + } + @Override - public UploadDirectoryConfiguration build() { - return new UploadDirectoryConfiguration(this); + public UploadDirectoryOverrideConfiguration build() { + return new UploadDirectoryOverrideConfiguration(this); } } diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryRequest.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryRequest.java index d0ed8dca84d2..adbe438408e1 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryRequest.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryRequest.java @@ -28,6 +28,8 @@ /** * Request object to upload a local directory to S3 using the Transfer Manager. + * + * @see S3TransferManager#uploadDirectory(UploadDirectoryRequest) */ @SdkPublicApi @SdkPreviewApi @@ -37,13 +39,15 @@ public final class UploadDirectoryRequest implements TransferRequest, ToCopyable private final Path sourceDirectory; private final String bucket; private final String prefix; - private final UploadDirectoryConfiguration overrideConfiguration; + private final UploadDirectoryOverrideConfiguration overrideConfiguration; + private final String delimiter; public UploadDirectoryRequest(DefaultBuilder builder) { this.sourceDirectory = Validate.paramNotNull(builder.sourceDirectory, "sourceDirectory"); - this.bucket = Validate.paramNotNull(builder.bucket, "bucketName"); + this.bucket = Validate.paramNotNull(builder.bucket, "bucket"); this.prefix = builder.prefix; this.overrideConfiguration = builder.configuration; + this.delimiter = builder.delimiter; } /** @@ -65,16 +69,20 @@ public String bucket() { } /** - * @return the key prefix of the virtual directory to upload to + * @return the optional key prefix */ - public String prefix() { - return prefix; + public Optional prefix() { + return Optional.ofNullable(prefix); + } + + public Optional delimiter() { + return Optional.ofNullable(delimiter); } /** * @return the optional override configuration */ - public Optional overrideConfiguration() { + public Optional overrideConfiguration() { return Optional.ofNullable(overrideConfiguration); } @@ -138,36 +146,53 @@ public interface Builder extends CopyableBuilder + * See Organizing objects using + * prefixes * * @param prefix the key prefix * @return This builder for method chaining. */ Builder prefix(String prefix); + /** + * Specify the delimiter. A delimiter causes a list operation to roll up all the keys that share a common prefix into a + * single summary list result. If not provided, {@code "/"} will be used. + * + * See Organizing objects using + * prefixes + * + * @param delimiter the delimiter + * @return This builder for method chaining. + */ + Builder delimiter(String delimiter); + /** * Add an optional request override configuration. * * @param configuration The override configuration. * @return This builder for method chaining. */ - Builder overrideConfiguration(UploadDirectoryConfiguration configuration); + Builder overrideConfiguration(UploadDirectoryOverrideConfiguration configuration); /** - * Similar to {@link #overrideConfiguration(UploadDirectoryConfiguration)}, but takes a lambda to configure a new - * {@link UploadDirectoryConfiguration.Builder}. This removes the need to call - * {@link UploadDirectoryConfiguration#builder()} and {@link UploadDirectoryConfiguration.Builder#build()}. + * Similar to {@link #overrideConfiguration(UploadDirectoryOverrideConfiguration)}, but takes a lambda to configure a new + * {@link UploadDirectoryOverrideConfiguration.Builder}. This removes the need to call + * {@link UploadDirectoryOverrideConfiguration#builder()} and + * {@link UploadDirectoryOverrideConfiguration.Builder#build()}. * * @param uploadConfigurationBuilder the upload configuration * @return this builder for method chaining. - * @see #overrideConfiguration(UploadDirectoryConfiguration) + * @see #overrideConfiguration(UploadDirectoryOverrideConfiguration) */ - default Builder overrideConfiguration(Consumer uploadConfigurationBuilder) { + default Builder overrideConfiguration(Consumer uploadConfigurationBuilder) { Validate.paramNotNull(uploadConfigurationBuilder, "uploadConfigurationBuilder"); - return overrideConfiguration(UploadDirectoryConfiguration.builder() - .applyMutation(uploadConfigurationBuilder) - .build()); + return overrideConfiguration(UploadDirectoryOverrideConfiguration.builder() + .applyMutation(uploadConfigurationBuilder) + .build()); } @Override @@ -179,7 +204,8 @@ private static final class DefaultBuilder implements Builder { private Path sourceDirectory; private String bucket; private String prefix; - private UploadDirectoryConfiguration configuration; + private UploadDirectoryOverrideConfiguration configuration; + private String delimiter; private DefaultBuilder() { } @@ -210,7 +236,13 @@ public Builder prefix(String prefix) { } @Override - public Builder overrideConfiguration(UploadDirectoryConfiguration configuration) { + public Builder delimiter(String delimiter) { + this.delimiter = delimiter; + return this; + } + + @Override + public Builder overrideConfiguration(UploadDirectoryOverrideConfiguration configuration) { this.configuration = configuration; return this; } diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryTransfer.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryTransfer.java new file mode 100644 index 000000000000..488b2ea73e42 --- /dev/null +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryTransfer.java @@ -0,0 +1,79 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.transfer.s3; + +import java.util.concurrent.CompletableFuture; +import software.amazon.awssdk.annotations.SdkPreviewApi; +import software.amazon.awssdk.annotations.SdkPublicApi; + +@SdkPublicApi +@SdkPreviewApi +public final class UploadDirectoryTransfer implements Transfer { + private final CompletableFuture completionFuture; + + private UploadDirectoryTransfer(DefaultBuilder builder) { + this.completionFuture = builder.completionFuture; + } + + @Override + public CompletableFuture completionFuture() { + return completionFuture; + } + + public static Builder builder() { + return new DefaultBuilder(); + } + + public interface Builder { + + /** + * Specifies the future that will be completed when this transfer is complete. + * + * @param completionFuture the future that will be completed when this transfer is complete. + * @return This builder for method chaining. + */ + Builder completionFuture(CompletableFuture completionFuture); + + /** + * Builds a {@link UploadDirectoryTransfer} based on the properties supplied to this builder + * + * @return An initialized {@link UploadDirectoryTransfer} + */ + UploadDirectoryTransfer build(); + } + + private static final class DefaultBuilder implements Builder { + private CompletableFuture completionFuture; + + private DefaultBuilder() { + } + + @Override + public DefaultBuilder completionFuture(CompletableFuture completionFuture) { + this.completionFuture = completionFuture; + return this; + } + + public void setCompletionFuture(CompletableFuture completionFuture) { + completionFuture(completionFuture); + } + + @Override + public UploadDirectoryTransfer build() { + return new UploadDirectoryTransfer(this); + } + } +} diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadRequest.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadRequest.java index ac230fe17ee3..170e4e696fe1 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadRequest.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadRequest.java @@ -25,6 +25,7 @@ import software.amazon.awssdk.annotations.SdkPreviewApi; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.utils.ToString; import software.amazon.awssdk.utils.Validate; import software.amazon.awssdk.utils.builder.CopyableBuilder; import software.amazon.awssdk.utils.builder.ToCopyableBuilder; @@ -97,6 +98,14 @@ public int hashCode() { return result; } + @Override + public String toString() { + return ToString.builder("UploadRequest") + .add("putObjectRequest", putObjectRequest) + .add("source", source) + .build(); + } + /** * A builder for a {@link UploadRequest}, created with {@link #builder()} */ diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultCompletedUploadDirectory.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultCompletedUploadDirectory.java deleted file mode 100644 index b44d9158effb..000000000000 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultCompletedUploadDirectory.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.transfer.s3.internal; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import software.amazon.awssdk.annotations.SdkInternalApi; -import software.amazon.awssdk.transfer.s3.CompletedUpload; -import software.amazon.awssdk.transfer.s3.CompletedUploadDirectory; -import software.amazon.awssdk.transfer.s3.FailedUpload; -import software.amazon.awssdk.utils.ToString; - -@SdkInternalApi -public final class DefaultCompletedUploadDirectory implements CompletedUploadDirectory { - private final List completedUploads; - private final List failedUploads; - - private DefaultCompletedUploadDirectory(DefaultBuilder builder) { - this.completedUploads = Collections.unmodifiableList(new ArrayList<>(builder.completedUploads)); - this.failedUploads = Collections.unmodifiableList(builder.failedObjects); - } - - @Override - public List failedUploads() { - return failedUploads; - } - - @Override - public List successfulObjects() { - return completedUploads; - } - - /** - * Creates a default builder for {@link CompletedUpload}. - */ - public static Builder builder() { - return new DefaultBuilder(); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - - DefaultCompletedUploadDirectory that = (DefaultCompletedUploadDirectory) o; - - if (!completedUploads.equals(that.completedUploads)) { - return false; - } - return failedUploads.equals(that.failedUploads); - } - - @Override - public int hashCode() { - int result = completedUploads.hashCode(); - result = 31 * result + failedUploads.hashCode(); - return result; - } - - @Override - public String toString() { - return ToString.builder("CompletedUploadDirectory") - .add("failedUploads", failedUploads) - .add("successfulUploads", completedUploads) - .build(); - } - - interface Builder { - - Builder successfulUploads(List successfulUploads); - - Builder failedUploads(List failedUploads); - - CompletedUploadDirectory build(); - } - - private static class DefaultBuilder implements Builder { - private List completedUploads = Collections.emptyList(); - private List failedObjects = Collections.emptyList(); - - @Override - public Builder successfulUploads(List completedUploads) { - this.completedUploads = completedUploads; - return this; - } - - @Override - public Builder failedUploads(List failedUploads) { - this.failedObjects = failedUploads; - return this; - } - - @Override - public CompletedUploadDirectory build() { - return new DefaultCompletedUploadDirectory(this); - } - } -} diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultFailedUpload.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultFailedUpload.java deleted file mode 100644 index a8bcd28ca0e1..000000000000 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultFailedUpload.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.transfer.s3.internal; - -import java.nio.file.Path; -import java.util.Objects; -import software.amazon.awssdk.annotations.SdkInternalApi; -import software.amazon.awssdk.transfer.s3.FailedUpload; -import software.amazon.awssdk.utils.ToString; - -@SdkInternalApi -final class DefaultFailedUpload implements FailedUpload { - private final Throwable throwable; - private final Path path; - - DefaultFailedUpload(Builder builder) { - this.throwable = builder.throwable; - this.path = builder.path; - } - - public Throwable exception() { - return throwable; - } - - public Path path() { - return path; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - - DefaultFailedUpload that = (DefaultFailedUpload) o; - - if (!Objects.equals(throwable, that.throwable)) { - return false; - } - return path.equals(that.path); - } - - @Override - public int hashCode() { - int result = throwable != null ? throwable.hashCode() : 0; - result = 31 * result + path.hashCode(); - return result; - } - - @Override - public String toString() { - return ToString.builder("FailedUpload") - .add("exception", throwable) - .add("path", path) - .build(); - } - - public static Builder builder() { - return new Builder(); - } - - public static final class Builder { - private Throwable throwable; - private Path path; - - private Builder() { - } - - public Builder exception(Throwable throwable) { - this.throwable = throwable; - return this; - } - - public Builder path(Path path) { - this.path = path; - return this; - } - - public DefaultFailedUpload build() { - return new DefaultFailedUpload(this); - } - } -} diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultS3TransferManager.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultS3TransferManager.java index ac8c541f586e..2f2fc204a1e0 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultS3TransferManager.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultS3TransferManager.java @@ -22,6 +22,8 @@ import software.amazon.awssdk.annotations.SdkTestInternalApi; import software.amazon.awssdk.core.async.AsyncRequestBody; import software.amazon.awssdk.core.async.AsyncResponseTransformer; +import software.amazon.awssdk.core.client.config.ClientAsyncConfiguration; +import software.amazon.awssdk.core.client.config.SdkAdvancedAsyncClientOption; import software.amazon.awssdk.services.s3.model.GetObjectResponse; import software.amazon.awssdk.services.s3.model.PutObjectRequest; import software.amazon.awssdk.services.s3.model.PutObjectResponse; @@ -31,44 +33,60 @@ import software.amazon.awssdk.transfer.s3.DownloadRequest; 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.Upload; -import software.amazon.awssdk.transfer.s3.UploadDirectory; import software.amazon.awssdk.transfer.s3.UploadDirectoryRequest; +import software.amazon.awssdk.transfer.s3.UploadDirectoryTransfer; import software.amazon.awssdk.transfer.s3.UploadRequest; import software.amazon.awssdk.utils.CompletableFutureUtils; +import software.amazon.awssdk.utils.Validate; @SdkInternalApi public final class DefaultS3TransferManager implements S3TransferManager { private final S3CrtAsyncClient s3CrtAsyncClient; - private final TransferConfiguration transferConfiguration; - private final UploadDirectoryManager uploadDirectoryManager; + private final TransferManagerConfiguration transferConfiguration; + private final UploadDirectoryHelper uploadDirectoryManager; public DefaultS3TransferManager(DefaultBuilder tmBuilder) { - S3CrtAsyncClient.S3CrtAsyncClientBuilder clientBuilder = S3CrtAsyncClient.builder(); - TransferConfiguration.Builder transferConfigBuilder = TransferConfiguration.builder(); - if (tmBuilder.s3ClientConfiguration != null) { - tmBuilder.s3ClientConfiguration.credentialsProvider().ifPresent(clientBuilder::credentialsProvider); - tmBuilder.s3ClientConfiguration.maxConcurrency().ifPresent(clientBuilder::maxConcurrency); - tmBuilder.s3ClientConfiguration.minimumPartSizeInBytes().ifPresent(clientBuilder::minimumPartSizeInBytes); - tmBuilder.s3ClientConfiguration.region().ifPresent(clientBuilder::region); - tmBuilder.s3ClientConfiguration.targetThroughputInGbps().ifPresent(clientBuilder::targetThroughputInGbps); - tmBuilder.s3ClientConfiguration.asyncConfiguration().ifPresent(clientBuilder::asyncConfiguration); - - tmBuilder.s3ClientConfiguration.uploadDirectoryConfiguration().ifPresent(transferConfigBuilder::configuration); - } - - s3CrtAsyncClient = clientBuilder.build(); - transferConfiguration = transferConfigBuilder.build(); - uploadDirectoryManager = new UploadDirectoryManager(transferConfiguration, this::upload); + transferConfiguration = resolveTransferManagerConfiguration(tmBuilder); + s3CrtAsyncClient = initializeS3CrtClient(tmBuilder); + uploadDirectoryManager = new UploadDirectoryHelper(transferConfiguration, this::upload); } @SdkTestInternalApi - DefaultS3TransferManager(S3CrtAsyncClient s3CrtAsyncClient, UploadDirectoryManager uploadDirectoryManager) { + DefaultS3TransferManager(S3CrtAsyncClient s3CrtAsyncClient, + UploadDirectoryHelper uploadDirectoryManager, + TransferManagerConfiguration configuration) { this.s3CrtAsyncClient = s3CrtAsyncClient; - this.transferConfiguration = TransferConfiguration.builder().build(); + this.transferConfiguration = configuration; this.uploadDirectoryManager = uploadDirectoryManager; } + private TransferManagerConfiguration resolveTransferManagerConfiguration(DefaultBuilder tmBuilder) { + TransferManagerConfiguration.Builder transferConfigBuilder = TransferManagerConfiguration.builder(); + tmBuilder.transferManagerConfiguration.uploadDirectoryConfiguration() + .ifPresent(transferConfigBuilder::uploadDirectoryConfiguration); + tmBuilder.transferManagerConfiguration.executor().ifPresent(transferConfigBuilder::executor); + return transferConfigBuilder.build(); + } + + private S3CrtAsyncClient initializeS3CrtClient(DefaultBuilder tmBuilder) { + S3CrtAsyncClient.S3CrtAsyncClientBuilder clientBuilder = S3CrtAsyncClient.builder(); + tmBuilder.s3ClientConfiguration.credentialsProvider().ifPresent(clientBuilder::credentialsProvider); + tmBuilder.s3ClientConfiguration.maxConcurrency().ifPresent(clientBuilder::maxConcurrency); + tmBuilder.s3ClientConfiguration.minimumPartSizeInBytes().ifPresent(clientBuilder::minimumPartSizeInBytes); + tmBuilder.s3ClientConfiguration.region().ifPresent(clientBuilder::region); + tmBuilder.s3ClientConfiguration.targetThroughputInGbps().ifPresent(clientBuilder::targetThroughputInGbps); + ClientAsyncConfiguration clientAsyncConfiguration = + ClientAsyncConfiguration.builder() + .advancedOption(SdkAdvancedAsyncClientOption.FUTURE_COMPLETION_EXECUTOR, + transferConfiguration.option(TransferConfigurationOption.EXECUTOR)) + .build(); + clientBuilder.asyncConfiguration(clientAsyncConfiguration); + + return clientBuilder.build(); + } + @Override public Upload upload(UploadRequest uploadRequest) { try { @@ -89,18 +107,18 @@ public Upload upload(UploadRequest uploadRequest) { } @Override - public UploadDirectory uploadDirectory(UploadDirectoryRequest uploadDirectoryRequest) { + public UploadDirectoryTransfer uploadDirectory(UploadDirectoryRequest uploadDirectoryRequest) { try { Path directory = uploadDirectoryRequest.sourceDirectory(); - if (!Files.exists(directory) || Files.isRegularFile(directory)) { - throw new IllegalArgumentException("The source directory provided either does not exist or is not a directory"); - } + Validate.isTrue(Files.exists(directory), "The source directory (%s) provided does not exist", directory); + Validate.isFalse(Files.isRegularFile(directory), "The source directory (%s) provided is not a directory", directory); + assertNotObjectLambdaArn(uploadDirectoryRequest.bucket(), "downloadDirectory"); return uploadDirectoryManager.uploadDirectory(uploadDirectoryRequest); } catch (Throwable throwable) { - return new DefaultUploadDirectory(CompletableFutureUtils.failedFuture(throwable)); + return UploadDirectoryTransfer.builder().completionFuture(CompletableFutureUtils.failedFuture(throwable)).build(); } } @@ -124,6 +142,7 @@ public Download download(DownloadRequest downloadRequest) { @Override public void close() { s3CrtAsyncClient.close(); + transferConfiguration.close(); } public static Builder builder() { @@ -150,7 +169,12 @@ private AsyncRequestBody requestBodyFor(UploadRequest uploadRequest) { } private static class DefaultBuilder implements S3TransferManager.Builder { - private S3ClientConfiguration s3ClientConfiguration; + private S3ClientConfiguration s3ClientConfiguration = S3ClientConfiguration.builder().build(); + private S3TransferManagerOverrideConfiguration transferManagerConfiguration = + S3TransferManagerOverrideConfiguration.builder().build(); + + private DefaultBuilder() { + } @Override public Builder s3ClientConfiguration(S3ClientConfiguration configuration) { @@ -158,6 +182,12 @@ public Builder s3ClientConfiguration(S3ClientConfiguration configuration) { return this; } + @Override + public Builder transferConfiguration(S3TransferManagerOverrideConfiguration transferManagerConfiguration) { + this.transferManagerConfiguration = transferManagerConfiguration; + return this; + } + @Override public S3TransferManager build() { return new DefaultS3TransferManager(this); diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultUploadDirectory.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultUploadDirectory.java deleted file mode 100644 index 872b0d3a093a..000000000000 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultUploadDirectory.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.transfer.s3.internal; - -import java.util.concurrent.CompletableFuture; -import software.amazon.awssdk.annotations.SdkInternalApi; -import software.amazon.awssdk.transfer.s3.CompletedUploadDirectory; -import software.amazon.awssdk.transfer.s3.UploadDirectory; - -@SdkInternalApi -public class DefaultUploadDirectory implements UploadDirectory { - private final CompletableFuture completionFuture; - - public DefaultUploadDirectory(CompletableFuture completionFuture) { - this.completionFuture = completionFuture; - } - - @Override - public CompletableFuture completionFuture() { - return completionFuture; - } -} diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/TransferConfiguration.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/TransferConfiguration.java deleted file mode 100644 index 2d8839d439d9..000000000000 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/TransferConfiguration.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.transfer.s3.internal; - -import static software.amazon.awssdk.transfer.s3.internal.TransferConfigurationOption.UPLOAD_DIRECTORY_FOLLOW_SYMBOLIC_LINKS; -import static software.amazon.awssdk.transfer.s3.internal.TransferConfigurationOption.UPLOAD_DIRECTORY_MAX_DEPTH; -import static software.amazon.awssdk.transfer.s3.internal.TransferConfigurationOption.UPLOAD_DIRECTORY_RECURSIVE; - -import software.amazon.awssdk.annotations.SdkInternalApi; -import software.amazon.awssdk.transfer.s3.UploadDirectoryConfiguration; -import software.amazon.awssdk.transfer.s3.UploadDirectoryRequest; -import software.amazon.awssdk.utils.AttributeMap; - -/** - * Internal transfer manager related configuration - */ -@SdkInternalApi -public final class TransferConfiguration { - private final AttributeMap options; - - private TransferConfiguration(AttributeMap mergedOptions) { - this.options = mergedOptions; - } - - public AttributeMap options() { - return options; - } - - public boolean resolveUploadDirectoryRecursive(UploadDirectoryRequest request) { - return request.overrideConfiguration() - .flatMap(UploadDirectoryConfiguration::recursive) - .orElse(options().get(UPLOAD_DIRECTORY_RECURSIVE)); - } - - public boolean resolveUploadDirectoryFollowSymbolicLinks(UploadDirectoryRequest request) { - return request.overrideConfiguration() - .flatMap(UploadDirectoryConfiguration::followSymbolicLinks) - .orElse(options().get(UPLOAD_DIRECTORY_FOLLOW_SYMBOLIC_LINKS)); - } - - public int resolveUploadDirectoryMaxDepth(UploadDirectoryRequest request) { - return request.overrideConfiguration() - .flatMap(UploadDirectoryConfiguration::maxDepth) - .orElse(options().get(UPLOAD_DIRECTORY_MAX_DEPTH)); - } - - public static Builder builder() { - return new Builder(); - } - - public static final class Builder { - private final AttributeMap.Builder standardOptions = AttributeMap.builder(); - - public Builder configuration(UploadDirectoryConfiguration configuration) { - standardOptions.put(TransferConfigurationOption.UPLOAD_DIRECTORY_FOLLOW_SYMBOLIC_LINKS, - configuration.followSymbolicLinks().orElse(null)); - standardOptions.put(TransferConfigurationOption.UPLOAD_DIRECTORY_MAX_DEPTH, configuration.maxDepth().orElse(null)); - standardOptions.put(TransferConfigurationOption.UPLOAD_DIRECTORY_RECURSIVE, configuration.recursive().orElse(null)); - return this; - } - - public TransferConfiguration build() { - AttributeMap mergedOptions = standardOptions.build().merge(TransferConfigurationOption.TRANSFER_DEFAULTS); - - return new TransferConfiguration(mergedOptions); - } - } -} diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/TransferConfigurationOption.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/TransferConfigurationOption.java index 43a86f1ba5f3..905cd752ffba 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/TransferConfigurationOption.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/TransferConfigurationOption.java @@ -15,9 +15,14 @@ package software.amazon.awssdk.transfer.s3.internal; +import java.util.concurrent.Executor; import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.transfer.s3.S3TransferManager; import software.amazon.awssdk.utils.AttributeMap; +/** + * A set of internal options required by the {@link S3TransferManager} via {@link TransferManagerConfiguration}. + */ @SdkInternalApi public final class TransferConfigurationOption extends AttributeMap.Key { public static final TransferConfigurationOption UPLOAD_DIRECTORY_MAX_DEPTH = @@ -29,7 +34,10 @@ public final class TransferConfigurationOption extends AttributeMap.Key { public static final TransferConfigurationOption UPLOAD_DIRECTORY_FOLLOW_SYMBOLIC_LINKS = new TransferConfigurationOption<>("UploadDirectoryFileVisitOption", Boolean.class); + public static final TransferConfigurationOption EXECUTOR = + new TransferConfigurationOption<>("Executor", Executor.class); + // TODO: revisit private static final int DEFAULT_UPLOAD_DIRECTORY_MAX_DEPTH = Integer.MAX_VALUE; private static final Boolean DEFAULT_UPLOAD_DIRECTORY_RECURSIVE = Boolean.TRUE; // TODO: revisit diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/TransferManagerConfiguration.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/TransferManagerConfiguration.java new file mode 100644 index 000000000000..97d149122764 --- /dev/null +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/TransferManagerConfiguration.java @@ -0,0 +1,137 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.transfer.s3.internal; + +import static software.amazon.awssdk.transfer.s3.internal.TransferConfigurationOption.TRANSFER_DEFAULTS; +import static software.amazon.awssdk.transfer.s3.internal.TransferConfigurationOption.UPLOAD_DIRECTORY_FOLLOW_SYMBOLIC_LINKS; +import static software.amazon.awssdk.transfer.s3.internal.TransferConfigurationOption.UPLOAD_DIRECTORY_MAX_DEPTH; +import static software.amazon.awssdk.transfer.s3.internal.TransferConfigurationOption.UPLOAD_DIRECTORY_RECURSIVE; + +import java.util.concurrent.Executor; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.transfer.s3.S3TransferManager; +import software.amazon.awssdk.transfer.s3.UploadDirectoryOverrideConfiguration; +import software.amazon.awssdk.transfer.s3.UploadDirectoryRequest; +import software.amazon.awssdk.utils.AttributeMap; +import software.amazon.awssdk.utils.ExecutorUtils; +import software.amazon.awssdk.utils.SdkAutoCloseable; +import software.amazon.awssdk.utils.ThreadFactoryBuilder; +import software.amazon.awssdk.utils.Validate; + +/** + * Contains resolved configuration settings for {@link S3TransferManager}. + * This configuration object can be {@link #close()}d to release all closeable resources configured within it. + */ +@SdkInternalApi +public class TransferManagerConfiguration implements SdkAutoCloseable { + private final AttributeMap options; + + private TransferManagerConfiguration(Builder builder) { + UploadDirectoryOverrideConfiguration uploadDirectoryConfiguration = + Validate.paramNotNull(builder.uploadDirectoryOverrideConfiguration, "uploadDirectoryOverrideConfiguration"); + AttributeMap.Builder standardOptions = AttributeMap.builder(); + + standardOptions.put(TransferConfigurationOption.UPLOAD_DIRECTORY_FOLLOW_SYMBOLIC_LINKS, + uploadDirectoryConfiguration.followSymbolicLinks().orElse(null)); + standardOptions.put(TransferConfigurationOption.UPLOAD_DIRECTORY_MAX_DEPTH, + uploadDirectoryConfiguration.maxDepth().orElse(null)); + standardOptions.put(TransferConfigurationOption.UPLOAD_DIRECTORY_RECURSIVE, + uploadDirectoryConfiguration.recursive().orElse(null)); + finalizeExecutor(builder, standardOptions); + + options = standardOptions.build().merge(TRANSFER_DEFAULTS); + } + + private void finalizeExecutor(Builder builder, AttributeMap.Builder standardOptions) { + if (builder.executor != null) { + standardOptions.put(TransferConfigurationOption.EXECUTOR, ExecutorUtils.unmanagedExecutor(builder.executor)); + } else { + + standardOptions.put(TransferConfigurationOption.EXECUTOR, defaultExecutor()); + } + } + + /** + * Retrieve the value of a specific option. + */ + public T option(TransferConfigurationOption option) { + return options.get(option); + } + + public boolean resolveUploadDirectoryRecursive(UploadDirectoryRequest request) { + return request.overrideConfiguration() + .flatMap(UploadDirectoryOverrideConfiguration::recursive) + .orElse(options.get(UPLOAD_DIRECTORY_RECURSIVE)); + } + + public boolean resolveUploadDirectoryFollowSymbolicLinks(UploadDirectoryRequest request) { + return request.overrideConfiguration() + .flatMap(UploadDirectoryOverrideConfiguration::followSymbolicLinks) + .orElse(options.get(UPLOAD_DIRECTORY_FOLLOW_SYMBOLIC_LINKS)); + } + + public int resolveUploadDirectoryMaxDepth(UploadDirectoryRequest request) { + return request.overrideConfiguration() + .flatMap(UploadDirectoryOverrideConfiguration::maxDepth) + .orElse(options.get(UPLOAD_DIRECTORY_MAX_DEPTH)); + } + + @Override + public void close() { + options.close(); + } + + private Executor defaultExecutor() { + int processors = Runtime.getRuntime().availableProcessors(); + int corePoolSize = Math.max(8, processors); + int maxPoolSize = Math.max(64, processors * 2); + ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maxPoolSize, + 10, TimeUnit.SECONDS, + new LinkedBlockingQueue<>(1_000), + new ThreadFactoryBuilder() + .threadNamePrefix("s3-transfer-manager").build()); + // Allow idle core threads to time out + executor.allowCoreThreadTimeOut(true); + return executor; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private UploadDirectoryOverrideConfiguration uploadDirectoryOverrideConfiguration = + UploadDirectoryOverrideConfiguration.builder().build(); + private Executor executor; + + public Builder uploadDirectoryConfiguration(UploadDirectoryOverrideConfiguration configuration) { + this.uploadDirectoryOverrideConfiguration = configuration; + return this; + } + + public Builder executor(Executor executor) { + this.executor = executor; + return this; + } + + public TransferManagerConfiguration build() { + return new TransferManagerConfiguration(this); + } + } +} diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryManager.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryHelper.java similarity index 56% rename from services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryManager.java rename to services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryHelper.java index 7b58aac8c112..f3d1545f5533 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryManager.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryHelper.java @@ -17,89 +17,113 @@ import java.io.IOException; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; import java.nio.file.FileVisitOption; import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Phaser; import java.util.function.Function; -import java.util.stream.Collectors; import java.util.stream.Stream; import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.annotations.SdkTestInternalApi; import software.amazon.awssdk.core.exception.SdkClientException; import software.amazon.awssdk.services.s3.model.PutObjectRequest; import software.amazon.awssdk.transfer.s3.CompletedUpload; import software.amazon.awssdk.transfer.s3.CompletedUploadDirectory; -import software.amazon.awssdk.transfer.s3.FailedUpload; +import software.amazon.awssdk.transfer.s3.FailedSingleFileUpload; import software.amazon.awssdk.transfer.s3.S3TransferManager; import software.amazon.awssdk.transfer.s3.Upload; -import software.amazon.awssdk.transfer.s3.UploadDirectory; import software.amazon.awssdk.transfer.s3.UploadDirectoryRequest; +import software.amazon.awssdk.transfer.s3.UploadDirectoryTransfer; import software.amazon.awssdk.transfer.s3.UploadRequest; import software.amazon.awssdk.utils.CompletableFutureUtils; import software.amazon.awssdk.utils.Logger; import software.amazon.awssdk.utils.StringUtils; +/** + * An internal helper class that traverses the file tree and send the upload request + * for each file. + */ @SdkInternalApi -public class UploadDirectoryManager { +public class UploadDirectoryHelper { private static final Logger log = Logger.loggerFor(S3TransferManager.class); + private static final String DEFAULT_DELIMITER = "/"; - private final TransferConfiguration transferConfiguration; + private final TransferManagerConfiguration transferConfiguration; private final Function uploadFunction; + private final FileSystem fileSystem; + + public UploadDirectoryHelper(TransferManagerConfiguration transferConfiguration, + Function uploadFunction) { + + this.transferConfiguration = transferConfiguration; + this.uploadFunction = uploadFunction; + this.fileSystem = FileSystems.getDefault(); + } - public UploadDirectoryManager(TransferConfiguration transferConfiguration, - Function uploadFunction) { + @SdkTestInternalApi + UploadDirectoryHelper(TransferManagerConfiguration transferConfiguration, + Function uploadFunction, + FileSystem fileSystem) { this.transferConfiguration = transferConfiguration; this.uploadFunction = uploadFunction; + this.fileSystem = fileSystem; } - public UploadDirectory uploadDirectory(UploadDirectoryRequest uploadDirectoryRequest) { + public UploadDirectoryTransfer uploadDirectory(UploadDirectoryRequest uploadDirectoryRequest) { + CompletableFuture returnFuture = new CompletableFuture<>(); - Path directory = uploadDirectoryRequest.sourceDirectory(); + CompletableFuture.runAsync(() -> doUploadDirectory(returnFuture, uploadDirectoryRequest), + transferConfiguration.option(TransferConfigurationOption.EXECUTOR)); - List uploads = new ArrayList<>(); - List failedUploads = new ArrayList<>(); - List> uploadFutures; + return UploadDirectoryTransfer.builder().completionFuture(returnFuture).build(); + } + + private void doUploadDirectory(CompletableFuture returnFuture, + UploadDirectoryRequest uploadDirectoryRequest) { + + Path directory = uploadDirectoryRequest.sourceDirectory(); + List failedUploads = Collections.synchronizedList(new ArrayList<>()); + Phaser phaser = new Phaser(1); try (Stream entries = listFiles(directory, uploadDirectoryRequest)) { - uploadFutures = - entries.map(path -> { - CompletableFuture future = - uploadSingleFile(uploadDirectoryRequest, uploads, failedUploads, path); - // Forward cancellation of the return future to all individual futures. - CompletableFutureUtils.forwardExceptionTo(returnFuture, future); - return future; - }).collect(Collectors.toList()); + entries.forEach(path -> { + CompletableFuture future = + uploadSingleFile(uploadDirectoryRequest, failedUploads, path, phaser); + + // Forward cancellation of the return future to all individual futures. + CompletableFutureUtils.forwardExceptionTo(returnFuture, future); + }); } - CompletableFuture.allOf(uploadFutures.toArray(new CompletableFuture[0])) - .whenComplete((r, t) -> returnFuture.complete( - DefaultCompletedUploadDirectory.builder() - .failedUploads(failedUploads) - .successfulUploads(uploads) - .build())); - return new DefaultUploadDirectory(returnFuture); + phaser.arriveAndAwaitAdvance(); + phaser.arriveAndDeregister(); + returnFuture.complete(CompletedUploadDirectory.builder().failedUploads(failedUploads).build()); } private CompletableFuture uploadSingleFile(UploadDirectoryRequest uploadDirectoryRequest, - List uploads, - List failedUploads, - Path path) { + List failedUploads, + Path path, + Phaser phaser) { + phaser.register(); int nameCount = uploadDirectoryRequest.sourceDirectory().getNameCount(); UploadRequest uploadRequest = constructUploadRequest(uploadDirectoryRequest, nameCount, path); - CompletableFuture future = uploadFunction.apply(uploadRequest) - .completionFuture(); + log.debug(() -> "Sending upload request: " + uploadRequest); + CompletableFuture future = uploadFunction.apply(uploadRequest).completionFuture(); future.whenComplete((r, t) -> { + phaser.arriveAndDeregister(); if (t != null) { - failedUploads.add(DefaultFailedUpload.builder() - .exception(t) - .path(path) - .build()); - } else { - uploads.add(r); + failedUploads.add(FailedSingleFileUpload.builder() + .exception(t) + .request(uploadRequest) + .build()); } }); return future; @@ -133,25 +157,33 @@ private Stream listFiles(Path directory, UploadDirectoryRequest request) { } } - private static String processPrefix(String prefix) { + private static String processPrefix(String prefix, String delimiter) { if (StringUtils.isEmpty(prefix)) { return ""; } - return prefix.endsWith("/") ? prefix : prefix + "/"; + return prefix.endsWith(delimiter) ? prefix : prefix + delimiter; } - private static String getRelativePathName(int directoryNameCount, Path path) { + private String getRelativePathName(int directoryNameCount, Path path, String delimiter) { String relativePathName = path.subpath(directoryNameCount, path.getNameCount()).toString(); - // Replace "\" (Windows FS) with "/" - return relativePathName.replace('\\', '/'); + String separator = fileSystem.getSeparator(); + return relativePathName.replace(separator, delimiter); } - private static UploadRequest constructUploadRequest(UploadDirectoryRequest uploadDirectoryRequest, int directoryNameCount, + private UploadRequest constructUploadRequest(UploadDirectoryRequest uploadDirectoryRequest, int directoryNameCount, Path path) { - String prefix = processPrefix(uploadDirectoryRequest.prefix()); - String relativePathName = getRelativePathName(directoryNameCount, path); + String delimiter = + uploadDirectoryRequest.delimiter() + .filter(s -> !s.isEmpty()) + .orElse(DEFAULT_DELIMITER); + + String prefix = uploadDirectoryRequest.prefix() + .map(s -> processPrefix(s, delimiter)) + .orElse(""); + + String relativePathName = getRelativePathName(directoryNameCount, path, delimiter); String key = prefix + relativePathName; PutObjectRequest putObjectRequest = PutObjectRequest.builder() diff --git a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/S3ClientConfigurationTest.java b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/S3ClientConfigurationTest.java index 39b87f178dd1..f8742bb2a0c1 100644 --- a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/S3ClientConfigurationTest.java +++ b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/S3ClientConfigurationTest.java @@ -23,7 +23,6 @@ import software.amazon.awssdk.auth.credentials.AnonymousCredentialsProvider; import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; -import software.amazon.awssdk.core.client.config.SdkAdvancedAsyncClientOption; import software.amazon.awssdk.regions.Region; public class S3ClientConfigurationTest { @@ -105,7 +104,6 @@ public void equalsHashCode() { .maxConcurrency(100) .targetThroughputInGbps(10.0) .region(Region.US_WEST_2) - .uploadDirectoryConfiguration(b -> b.recursive(true)) .minimumPartSizeInBytes(5 * MB) .build(); @@ -115,16 +113,12 @@ public void equalsHashCode() { .targetThroughputInGbps(10.0) .region(Region.US_WEST_2) .minimumPartSizeInBytes(5 * MB) - .uploadDirectoryConfiguration(b -> b.recursive(true)) .build(); S3ClientConfiguration configuration3 = configuration1.toBuilder() .credentialsProvider(AnonymousCredentialsProvider.create()) .maxConcurrency(50) .targetThroughputInGbps(1.0) - .uploadDirectoryConfiguration(b -> b.recursive(false)) - .asyncConfiguration(c -> c.advancedOption(SdkAdvancedAsyncClientOption.FUTURE_COMPLETION_EXECUTOR, - Runnable::run)) .build(); assertThat(configuration1).isEqualTo(configuration2); diff --git a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/UploadDirectoryConfigurationTest.java b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/UploadDirectoryConfigurationTest.java index 378cb4bf63d4..2f1f5143ed67 100644 --- a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/UploadDirectoryConfigurationTest.java +++ b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/UploadDirectoryConfigurationTest.java @@ -24,7 +24,7 @@ public class UploadDirectoryConfigurationTest { @Test public void getters_allPropertiesSet() { - UploadDirectoryConfiguration object = UploadDirectoryConfiguration + UploadDirectoryOverrideConfiguration object = UploadDirectoryOverrideConfiguration .builder() .maxDepth(100) .followSymbolicLinks(true) @@ -38,7 +38,7 @@ public void getters_allPropertiesSet() { @Test public void getters_allPropertiesAbsent() { - UploadDirectoryConfiguration object = UploadDirectoryConfiguration + UploadDirectoryOverrideConfiguration object = UploadDirectoryOverrideConfiguration .builder().build(); assertThat(object.followSymbolicLinks()).isEmpty(); @@ -48,16 +48,16 @@ public void getters_allPropertiesAbsent() { @Test public void equalsHashCode() { - UploadDirectoryConfiguration object1 = UploadDirectoryConfiguration + UploadDirectoryOverrideConfiguration object1 = UploadDirectoryOverrideConfiguration .builder() .maxDepth(100) .followSymbolicLinks(true) .recursive(false) .build(); - UploadDirectoryConfiguration object2 = object1.toBuilder().build(); + UploadDirectoryOverrideConfiguration object2 = object1.toBuilder().build(); - UploadDirectoryConfiguration object3 = object1.toBuilder().recursive(true).build(); + UploadDirectoryOverrideConfiguration object3 = object1.toBuilder().recursive(true).build(); assertThat(object1).isEqualTo(object2); assertThat(object1.hashCode()).isEqualTo(object2.hashCode()); assertThat(object1).isNotEqualTo(object3); diff --git a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/CompletedUploadDirectoryTest.java b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/CompletedUploadDirectoryTest.java index 6c30689652cf..fb060b79ee37 100644 --- a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/CompletedUploadDirectoryTest.java +++ b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/CompletedUploadDirectoryTest.java @@ -21,29 +21,34 @@ import java.util.Arrays; import java.util.List; import org.junit.Test; -import software.amazon.awssdk.services.s3.model.PutObjectResponse; -import software.amazon.awssdk.transfer.s3.CompletedUpload; +import software.amazon.awssdk.core.exception.SdkClientException; import software.amazon.awssdk.transfer.s3.CompletedUploadDirectory; -import software.amazon.awssdk.transfer.s3.FailedUpload; +import software.amazon.awssdk.transfer.s3.FailedSingleFileUpload; +import software.amazon.awssdk.transfer.s3.UploadRequest; public class CompletedUploadDirectoryTest { @Test public void equalsHashcode() { - List failedUploads = Arrays.asList(DefaultFailedUpload.builder().path(Paths.get(".")).build()); - List completedUploads = Arrays.asList(DefaultCompletedUpload.builder().response(PutObjectResponse.builder().build()).build()); - CompletedUploadDirectory completedUploadDirectory = DefaultCompletedUploadDirectory.builder() - .failedUploads(failedUploads) - .successfulUploads(completedUploads) - .build(); - - CompletedUploadDirectory completedUploadDirectory2 = DefaultCompletedUploadDirectory.builder() - .failedUploads(failedUploads) - .successfulUploads(completedUploads) - .build(); - - CompletedUploadDirectory completedUploadDirectory3 = DefaultCompletedUploadDirectory.builder() - .build(); + List failedUploads = Arrays.asList(FailedSingleFileUpload.builder() + .request(UploadRequest.builder() + .source(Paths.get(".")) + .putObjectRequest(b -> b.bucket("bucket").key("key") + ) + .build()) + .exception(SdkClientException.create( + "helloworld")) + .build()); + CompletedUploadDirectory completedUploadDirectory = CompletedUploadDirectory.builder() + .failedUploads(failedUploads) + .build(); + + CompletedUploadDirectory completedUploadDirectory2 = CompletedUploadDirectory.builder() + .failedUploads(failedUploads) + .build(); + + CompletedUploadDirectory completedUploadDirectory3 = CompletedUploadDirectory.builder() + .build(); assertThat(completedUploadDirectory).isEqualTo(completedUploadDirectory2); assertThat(completedUploadDirectory.hashCode()).isEqualTo(completedUploadDirectory2.hashCode()); diff --git a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/FailedSingleFileUploadTest.java b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/FailedSingleFileUploadTest.java new file mode 100644 index 000000000000..b33c739a79f5 --- /dev/null +++ b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/FailedSingleFileUploadTest.java @@ -0,0 +1,74 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.transfer.s3.internal; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.nio.file.Path; +import java.nio.file.Paths; +import org.junit.Test; +import software.amazon.awssdk.core.exception.SdkClientException; +import software.amazon.awssdk.transfer.s3.FailedSingleFileUpload; +import software.amazon.awssdk.transfer.s3.UploadRequest; + +public class FailedSingleFileUploadTest { + + @Test + public void requestNull_mustThrowException() { + assertThatThrownBy(() -> FailedSingleFileUpload.builder() + .exception(SdkClientException.create("xxx")).build()) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("request must not be null"); + } + + @Test + public void exceptionNull_mustThrowException() { + UploadRequest uploadRequest = + UploadRequest.builder().source(Paths.get(".")).putObjectRequest(p -> p.bucket("bucket").key("key")).build(); + assertThatThrownBy(() -> FailedSingleFileUpload.builder() + .request(uploadRequest).build()) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("exception must not be null"); + } + + @Test + public void equalsHashcode() { + SdkClientException exception = SdkClientException.create("test"); + Path path = Paths.get("."); + UploadRequest uploadRequest = + UploadRequest.builder().source(Paths.get(".")).putObjectRequest(p -> p.bucket("bucket").key("key")).build(); + FailedSingleFileUpload failedUpload1 = FailedSingleFileUpload.builder() + .request(uploadRequest) + .exception(exception) + .build(); + + FailedSingleFileUpload failedUpload2 = FailedSingleFileUpload.builder() + .request(uploadRequest) + .exception(exception) + .build(); + + FailedSingleFileUpload failedUpload3 = FailedSingleFileUpload.builder() + .request(uploadRequest) + .exception(SdkClientException.create("blah")) + .build(); + + assertThat(failedUpload1).isEqualTo(failedUpload2); + assertThat(failedUpload1.hashCode()).isEqualTo(failedUpload2.hashCode()); + assertThat(failedUpload1).isNotEqualTo(failedUpload3); + assertThat(failedUpload1.hashCode()).isNotEqualTo(failedUpload3); + } +} diff --git a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/FailedUploadTest.java b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/FailedUploadTest.java deleted file mode 100644 index 80486b8d2114..000000000000 --- a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/FailedUploadTest.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.transfer.s3.internal; - -import static org.assertj.core.api.Assertions.assertThat; - -import java.nio.file.Path; -import java.nio.file.Paths; -import org.junit.Test; -import software.amazon.awssdk.core.exception.SdkClientException; -import software.amazon.awssdk.transfer.s3.FailedUpload; - -public class FailedUploadTest { - - @Test - public void equalsHashcode() { - SdkClientException exception = SdkClientException.create("test"); - Path path = Paths.get("."); - FailedUpload failedUpload1 = DefaultFailedUpload.builder() - .exception(exception) - .path(path) - .build(); - - FailedUpload failedUpload2 = DefaultFailedUpload.builder() - .exception(exception) - .path(path) - .build(); - - FailedUpload failedUpload3 = DefaultFailedUpload.builder() - .exception(SdkClientException.create("blah")) - .path(Paths.get("..")) - .build(); - - assertThat(failedUpload1).isEqualTo(failedUpload2); - assertThat(failedUpload1.hashCode()).isEqualTo(failedUpload2.hashCode()); - assertThat(failedUpload1).isNotEqualTo(failedUpload3); - assertThat(failedUpload1.hashCode()).isNotEqualTo(failedUpload3); - } -} diff --git a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/S3TransferManagerTest.java b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/S3TransferManagerTest.java index ba4373e06a4b..3696de68b2a1 100644 --- a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/S3TransferManagerTest.java +++ b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/S3TransferManagerTest.java @@ -19,51 +19,39 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import com.google.common.jimfs.Configuration; -import com.google.common.jimfs.Jimfs; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.FileSystem; -import java.nio.file.Files; -import java.nio.file.Path; import java.nio.file.Paths; -import java.util.List; import java.util.concurrent.CompletableFuture; import org.junit.After; -import org.junit.AfterClass; import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.mockito.ArgumentCaptor; import software.amazon.awssdk.core.async.AsyncRequestBody; import software.amazon.awssdk.core.async.AsyncResponseTransformer; -import software.amazon.awssdk.metrics.MetricCollection; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.GetObjectResponse; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.services.s3.model.PutObjectResponse; import software.amazon.awssdk.transfer.s3.CompletedDownload; import software.amazon.awssdk.transfer.s3.CompletedUpload; import software.amazon.awssdk.transfer.s3.DownloadRequest; import software.amazon.awssdk.transfer.s3.S3TransferManager; -import software.amazon.awssdk.transfer.s3.UploadDirectory; import software.amazon.awssdk.transfer.s3.UploadDirectoryRequest; import software.amazon.awssdk.transfer.s3.UploadRequest; -import software.amazon.awssdk.services.s3.model.GetObjectRequest; -import software.amazon.awssdk.services.s3.model.GetObjectResponse; -import software.amazon.awssdk.services.s3.model.PutObjectRequest; -import software.amazon.awssdk.services.s3.model.PutObjectResponse; public class S3TransferManagerTest { private S3CrtAsyncClient mockS3Crt; private S3TransferManager tm; - private UploadDirectoryManager uploadDirectoryManager; + private UploadDirectoryHelper uploadDirectoryManager; + private TransferManagerConfiguration configuration; @Before public void methodSetup() { mockS3Crt = mock(S3CrtAsyncClient.class); - uploadDirectoryManager = mock(UploadDirectoryManager.class); - tm = new DefaultS3TransferManager(mockS3Crt, uploadDirectoryManager); + uploadDirectoryManager = mock(UploadDirectoryHelper.class); + configuration = mock(TransferManagerConfiguration.class); + tm = new DefaultS3TransferManager(mockS3Crt, uploadDirectoryManager, configuration); } @After @@ -163,7 +151,7 @@ public void objectLambdaArnBucketProvided_shouldThrowException() { public void uploadDirectory_directoryNotExist_shouldCompleteFutureExceptionally() { assertThatThrownBy(() -> tm.uploadDirectory(u -> u.sourceDirectory(Paths.get("randomstringneverexistas234ersaf1231")) .bucket("bucketName")).completionFuture().join()) - .hasMessageContaining("The source directory provided either").hasCauseInstanceOf(IllegalArgumentException.class); + .hasMessageContaining("does not exist").hasCauseInstanceOf(IllegalArgumentException.class); } @Test @@ -175,4 +163,12 @@ public void uploadDirectory_throwException_shouldCompleteFutureExceptionally() { .bucket("bucketName")).completionFuture().join()) .hasCause(exception); } + + @Test + public void close_shouldCloseResources() { + S3TransferManager transferManager = new DefaultS3TransferManager(mockS3Crt, uploadDirectoryManager, configuration); + transferManager.close(); + verify(mockS3Crt).close(); + verify(configuration).close(); + } } diff --git a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/TransferManagerConfigurationTest.java b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/TransferManagerConfigurationTest.java new file mode 100644 index 000000000000..443dd4539cfa --- /dev/null +++ b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/TransferManagerConfigurationTest.java @@ -0,0 +1,103 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.transfer.s3.internal; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static software.amazon.awssdk.transfer.s3.internal.TransferConfigurationOption.EXECUTOR; +import static software.amazon.awssdk.transfer.s3.internal.TransferConfigurationOption.UPLOAD_DIRECTORY_FOLLOW_SYMBOLIC_LINKS; +import static software.amazon.awssdk.transfer.s3.internal.TransferConfigurationOption.UPLOAD_DIRECTORY_MAX_DEPTH; +import static software.amazon.awssdk.transfer.s3.internal.TransferConfigurationOption.UPLOAD_DIRECTORY_RECURSIVE; + +import java.nio.file.Paths; +import java.util.concurrent.ExecutorService; +import org.junit.Test; +import org.mockito.Mockito; +import software.amazon.awssdk.transfer.s3.UploadDirectoryOverrideConfiguration; +import software.amazon.awssdk.transfer.s3.UploadDirectoryRequest; + +public class TransferManagerConfigurationTest { + private TransferManagerConfiguration transferManagerConfiguration; + + @Test + public void resolveUploadDirectoryRecursive_requestOverride_requestOverrideShouldTakePrecedence() { + transferManagerConfiguration = TransferManagerConfiguration.builder() + .uploadDirectoryConfiguration(UploadDirectoryOverrideConfiguration.builder().recursive(true).build()) + .build(); + UploadDirectoryRequest uploadDirectoryRequest = UploadDirectoryRequest.builder() + .bucket("bucket") + .sourceDirectory(Paths.get(".")) + .overrideConfiguration(o -> o.recursive(false)) + .build(); + assertThat(transferManagerConfiguration.resolveUploadDirectoryRecursive(uploadDirectoryRequest)).isFalse(); + } + + @Test + public void resolveMaxDepth_requestOverride_requestOverrideShouldTakePrecedence() { + transferManagerConfiguration = TransferManagerConfiguration.builder() + .uploadDirectoryConfiguration(UploadDirectoryOverrideConfiguration.builder() + .maxDepth(1) + .build()) + .build(); + UploadDirectoryRequest uploadDirectoryRequest = UploadDirectoryRequest.builder() + .bucket("bucket") + .sourceDirectory(Paths.get(".")) + .overrideConfiguration(o -> o.maxDepth(2)) + .build(); + assertThat(transferManagerConfiguration.resolveUploadDirectoryMaxDepth(uploadDirectoryRequest)).isEqualTo(2); + } + + @Test + public void resolveFollowSymlinks_requestOverride_requestOverrideShouldTakePrecedence() { + transferManagerConfiguration = TransferManagerConfiguration.builder() + .uploadDirectoryConfiguration(UploadDirectoryOverrideConfiguration.builder() + .followSymbolicLinks(false) + .build()) + .build(); + UploadDirectoryRequest uploadDirectoryRequest = UploadDirectoryRequest.builder() + .bucket("bucket") + .sourceDirectory(Paths.get(".")) + .overrideConfiguration(o -> o.followSymbolicLinks(true)) + .build(); + assertThat(transferManagerConfiguration.resolveUploadDirectoryFollowSymbolicLinks(uploadDirectoryRequest)).isTrue(); + } + + @Test + public void noOverride_shouldUseDefaults() { + transferManagerConfiguration = TransferManagerConfiguration.builder().build(); + assertThat(transferManagerConfiguration.option(UPLOAD_DIRECTORY_FOLLOW_SYMBOLIC_LINKS)).isFalse(); + assertThat(transferManagerConfiguration.option(UPLOAD_DIRECTORY_MAX_DEPTH)).isEqualTo(Integer.MAX_VALUE); + assertThat(transferManagerConfiguration.option(UPLOAD_DIRECTORY_RECURSIVE)).isTrue(); + assertThat(transferManagerConfiguration.option(EXECUTOR)).isNotNull(); + } + + @Test + public void close_noCustomExecutor_shouldCloseDefaultOne() { + transferManagerConfiguration = TransferManagerConfiguration.builder().build(); + transferManagerConfiguration.close(); + ExecutorService executor = (ExecutorService) transferManagerConfiguration.option(EXECUTOR); + assertThat(executor.isShutdown()).isTrue(); + } + + @Test + public void close_customExecutor_shouldNotCloseCustomExecutor() { + ExecutorService executorService = Mockito.mock(ExecutorService.class); + transferManagerConfiguration = TransferManagerConfiguration.builder().executor(executorService).build(); + transferManagerConfiguration.close(); + verify(executorService, never()).shutdown(); + } +} diff --git a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryManagerParameterizedTest.java b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryHelperParameterizedTest.java similarity index 86% rename from services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryManagerParameterizedTest.java rename to services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryHelperParameterizedTest.java index b94db21b308b..e6f5d4f66fe3 100644 --- a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryManagerParameterizedTest.java +++ b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryHelperParameterizedTest.java @@ -45,22 +45,22 @@ import software.amazon.awssdk.services.s3.model.PutObjectResponse; import software.amazon.awssdk.testutils.FileUtils; import software.amazon.awssdk.transfer.s3.Upload; -import software.amazon.awssdk.transfer.s3.UploadDirectory; import software.amazon.awssdk.transfer.s3.UploadDirectoryRequest; +import software.amazon.awssdk.transfer.s3.UploadDirectoryTransfer; import software.amazon.awssdk.transfer.s3.UploadRequest; import software.amazon.awssdk.utils.IoUtils; /** - * Testing {@link UploadDirectoryManager} with different file systems. + * Testing {@link UploadDirectoryHelper} with different file systems. */ @RunWith(Parameterized.class) -public class UploadDirectoryManagerParameterizedTest { +public class UploadDirectoryHelperParameterizedTest { private static final Set FILE_SYSTEMS = Sets.newHashSet(Arrays.asList(Configuration.unix(), Configuration.osX(), Configuration.windows(), Configuration.forCurrentPlatform())); private Function singleUploadFunction; - private UploadDirectoryManager tm; + private UploadDirectoryHelper tm; private Path directory; @Parameterized.Parameter @@ -76,12 +76,13 @@ public static Collection fileSystems() { @Before public void methodSetup() throws IOException { singleUploadFunction = mock(Function.class); - tm = new UploadDirectoryManager(TransferConfiguration.builder().build(), singleUploadFunction); if (!configuration.equals(Configuration.forCurrentPlatform())) { jimfs = Jimfs.newFileSystem(configuration); + tm = new UploadDirectoryHelper(TransferManagerConfiguration.builder().build(), singleUploadFunction, jimfs); + } else { + tm = new UploadDirectoryHelper(TransferManagerConfiguration.builder().build(), singleUploadFunction); } - directory = createTestDirectory(); } @@ -100,7 +101,7 @@ public void uploadDirectory_defaultSetting_shouldRecursivelyUpload() { when(singleUploadFunction.apply(requestArgumentCaptor.capture())) .thenReturn(completedUpload()); - UploadDirectory uploadDirectory = + UploadDirectoryTransfer uploadDirectory = tm.uploadDirectory(UploadDirectoryRequest.builder() .sourceDirectory(directory) .bucket("bucket") @@ -125,7 +126,7 @@ public void uploadDirectory_recursiveFalse_shouldOnlyUploadTopLevel() { ArgumentCaptor requestArgumentCaptor = ArgumentCaptor.forClass(UploadRequest.class); when(singleUploadFunction.apply(requestArgumentCaptor.capture())).thenReturn(completedUpload()); - UploadDirectory uploadDirectory = + UploadDirectoryTransfer uploadDirectory = tm.uploadDirectory(UploadDirectoryRequest.builder() .sourceDirectory(directory) .bucket("bucket") @@ -149,7 +150,7 @@ public void uploadDirectory_FollowSymlinkTrue_shouldIncludeLinkedFiles() { ArgumentCaptor requestArgumentCaptor = ArgumentCaptor.forClass(UploadRequest.class); when(singleUploadFunction.apply(requestArgumentCaptor.capture())).thenReturn(completedUpload()); - UploadDirectory uploadDirectory = + UploadDirectoryTransfer uploadDirectory = tm.uploadDirectory(UploadDirectoryRequest.builder() .sourceDirectory(directory) .bucket("bucket") @@ -173,7 +174,7 @@ public void uploadDirectory_withPrefix_keysShouldHavePrefix() { ArgumentCaptor requestArgumentCaptor = ArgumentCaptor.forClass(UploadRequest.class); when(singleUploadFunction.apply(requestArgumentCaptor.capture())).thenReturn(completedUpload()); - UploadDirectory uploadDirectory = + UploadDirectoryTransfer uploadDirectory = tm.uploadDirectory(UploadDirectoryRequest.builder() .sourceDirectory(directory) .bucket("bucket") @@ -189,13 +190,35 @@ public void uploadDirectory_withPrefix_keysShouldHavePrefix() { keys.forEach(r -> assertThat(r).startsWith("yolo/")); } + @Test + public void uploadDirectory_withDelimiter_shouldHonor() { + ArgumentCaptor requestArgumentCaptor = ArgumentCaptor.forClass(UploadRequest.class); + + when(singleUploadFunction.apply(requestArgumentCaptor.capture())).thenReturn(completedUpload()); + UploadDirectoryTransfer uploadDirectory = + tm.uploadDirectory(UploadDirectoryRequest.builder() + .sourceDirectory(directory) + .bucket("bucket") + .delimiter(",") + .prefix("yolo") + .build()); + uploadDirectory.completionFuture().join(); + + List keys = + requestArgumentCaptor.getAllValues().stream().map(u -> u.putObjectRequest().key()) + .collect(Collectors.toList()); + + assertThat(keys.size()).isEqualTo(3); + assertThat(keys).containsOnly("yolo,foo,2.txt", "yolo,foo,1.txt", "yolo,bar.txt"); + } + @Test public void uploadDirectory_maxLengthOne_shouldOnlyUploadTopLevel() { ArgumentCaptor requestArgumentCaptor = ArgumentCaptor.forClass(UploadRequest.class); when(singleUploadFunction.apply(requestArgumentCaptor.capture())) .thenReturn(completedUpload()); - UploadDirectory uploadDirectory = + UploadDirectoryTransfer uploadDirectory = tm.uploadDirectory(UploadDirectoryRequest.builder() .sourceDirectory(directory) .bucket("bucket") diff --git a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryManagerTest.java b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryHelperTest.java similarity index 78% rename from services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryManagerTest.java rename to services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryHelperTest.java index 0890949b2641..9bd0820bf2d9 100644 --- a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryManagerTest.java +++ b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryHelperTest.java @@ -16,6 +16,7 @@ package software.amazon.awssdk.transfer.s3.internal; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -25,6 +26,7 @@ import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.Path; +import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; @@ -39,15 +41,15 @@ import software.amazon.awssdk.transfer.s3.CompletedUpload; import software.amazon.awssdk.transfer.s3.CompletedUploadDirectory; import software.amazon.awssdk.transfer.s3.Upload; -import software.amazon.awssdk.transfer.s3.UploadDirectory; import software.amazon.awssdk.transfer.s3.UploadDirectoryRequest; +import software.amazon.awssdk.transfer.s3.UploadDirectoryTransfer; import software.amazon.awssdk.transfer.s3.UploadRequest; -public class UploadDirectoryManagerTest { +public class UploadDirectoryHelperTest { private static FileSystem jimfs; private static Path directory; private Function singleUploadFunction; - private UploadDirectoryManager tm; + private UploadDirectoryHelper tm; @BeforeClass public static void setUp() throws IOException { @@ -66,7 +68,7 @@ public static void tearDown() throws IOException { @Before public void methodSetup() { singleUploadFunction = mock(Function.class); - tm = new UploadDirectoryManager(TransferConfiguration.builder().build(), singleUploadFunction); + tm = new UploadDirectoryHelper(TransferManagerConfiguration.builder().build(), singleUploadFunction); } @Test @@ -79,33 +81,40 @@ public void uploadDirectory_cancel_shouldCancelAllFutures() { when(singleUploadFunction.apply(any(UploadRequest.class))).thenReturn(upload, upload2); - UploadDirectory uploadDirectory = + UploadDirectoryTransfer uploadDirectory = tm.uploadDirectory(UploadDirectoryRequest.builder() .sourceDirectory(directory) .bucket("bucket") .build()); uploadDirectory.completionFuture().cancel(true); - assertThat(future).isCancelled(); - assertThat(future2).isCancelled(); + + assertThatThrownBy(() -> future.get(1, TimeUnit.SECONDS)) + .isInstanceOf(CancellationException.class); + + assertThatThrownBy(() -> future2.get(1, TimeUnit.SECONDS)) + .isInstanceOf(CancellationException.class); } @Test - public void uploadDirectory_allUploadSucceed_failedUploadShouldBeEmpty() throws ExecutionException, InterruptedException, + public void uploadDirectory_allUploadsSucceed_failedUploadsShouldBeEmpty() throws ExecutionException, InterruptedException, TimeoutException { - CompletedUpload completedUpload = DefaultCompletedUpload.builder().response(PutObjectResponse.builder().build()).build(); + PutObjectResponse putObjectResponse = PutObjectResponse.builder().eTag("1234").build(); + CompletedUpload completedUpload = DefaultCompletedUpload.builder().response(putObjectResponse).build(); CompletableFuture successfulFuture = new CompletableFuture<>(); + Upload upload = new DefaultUpload(successfulFuture); successfulFuture.complete(completedUpload); - CompletedUpload completedUpload2 = DefaultCompletedUpload.builder().response(PutObjectResponse.builder().build()).build(); + PutObjectResponse putObjectResponse2 = PutObjectResponse.builder().eTag("5678").build(); + CompletedUpload completedUpload2 = DefaultCompletedUpload.builder().response(putObjectResponse2).build(); CompletableFuture failedFuture = new CompletableFuture<>(); Upload upload2 = new DefaultUpload(failedFuture); failedFuture.complete(completedUpload2); when(singleUploadFunction.apply(any(UploadRequest.class))).thenReturn(upload, upload2); - UploadDirectory uploadDirectory = + UploadDirectoryTransfer uploadDirectory = tm.uploadDirectory(UploadDirectoryRequest.builder() .sourceDirectory(directory) .bucket("bucket") @@ -114,13 +123,13 @@ public void uploadDirectory_allUploadSucceed_failedUploadShouldBeEmpty() throws CompletedUploadDirectory completedUploadDirectory = uploadDirectory.completionFuture().get(5, TimeUnit.SECONDS); assertThat(completedUploadDirectory.failedUploads()).isEmpty(); - assertThat(completedUploadDirectory.successfulObjects()).hasSize(2).containsOnly(completedUpload, completedUpload2); } @Test public void uploadDirectory_partialSuccess_shouldProvideFailedUploads() throws ExecutionException, InterruptedException, TimeoutException { - CompletedUpload completedUpload = DefaultCompletedUpload.builder().response(PutObjectResponse.builder().build()).build(); + PutObjectResponse putObjectResponse = PutObjectResponse.builder().eTag("1234").build(); + CompletedUpload completedUpload = DefaultCompletedUpload.builder().response(putObjectResponse).build(); CompletableFuture successfulFuture = new CompletableFuture<>(); Upload upload = new DefaultUpload(successfulFuture); successfulFuture.complete(completedUpload); @@ -132,7 +141,7 @@ public void uploadDirectory_partialSuccess_shouldProvideFailedUploads() throws E when(singleUploadFunction.apply(any(UploadRequest.class))).thenReturn(upload, upload2); - UploadDirectory uploadDirectory = + UploadDirectoryTransfer uploadDirectory = tm.uploadDirectory(UploadDirectoryRequest.builder() .sourceDirectory(directory) .bucket("bucket") @@ -141,8 +150,7 @@ public void uploadDirectory_partialSuccess_shouldProvideFailedUploads() throws E CompletedUploadDirectory completedUploadDirectory = uploadDirectory.completionFuture().get(5, TimeUnit.SECONDS); assertThat(completedUploadDirectory.failedUploads()).hasSize(1); - assertThat(completedUploadDirectory.failedUploads().get(0).exception()).isEqualTo(exception); - assertThat(completedUploadDirectory.failedUploads().get(0).path().toString()).isEqualTo("test/2"); - assertThat(completedUploadDirectory.successfulObjects()).hasSize(1).containsOnly(completedUpload); + assertThat(completedUploadDirectory.failedUploads().iterator().next().exception()).isEqualTo(exception); + assertThat(completedUploadDirectory.failedUploads().iterator().next().request().source().toString()).isEqualTo("test/2"); } } From 1b4fa194a01893941ca109f61a75cc79e8743d97 Mon Sep 17 00:00:00 2001 From: Zoe Wang <33073555+zoewangg@users.noreply.github.com> Date: Mon, 4 Oct 2021 12:46:42 -0700 Subject: [PATCH 3/8] Address part of the feedback --- pom.xml | 2 +- services-custom/s3-transfer-manager/pom.xml | 5 ++ .../transfer/s3/CompletedFileTransfer.java | 34 --------- .../transfer/s3/CompletedUploadDirectory.java | 7 +- .../awssdk/transfer/s3/DownloadRequest.java | 16 ++++ .../transfer/s3/FailedSingleFileUpload.java | 17 ++++- .../awssdk/transfer/s3/S3TransferManager.java | 2 + ...3TransferManagerOverrideConfiguration.java | 15 +++- .../UploadDirectoryOverrideConfiguration.java | 18 ++++- .../transfer/s3/UploadDirectoryRequest.java | 44 +++++++++++ .../awssdk/transfer/s3/UploadRequest.java | 16 ++++ .../s3/internal/DefaultS3TransferManager.java | 5 +- .../internal/TransferConfigurationOption.java | 11 +-- .../TransferManagerConfiguration.java | 4 +- .../transfer/s3/DownloadRequestTest.java | 30 +------- .../s3/FailedSingleFileUploadTest.java | 51 +++++++++++++ .../s3/S3ClientConfigurationTest.java | 32 +------- ...nsferManagerOverrideConfigurationTest.java | 57 ++++++++++++++ .../s3/UploadDirectoryConfigurationTest.java | 66 ----------------- ...oadDirectoryOverrideConfigurationTest.java | 65 ++++++++++++++++ .../s3/UploadDirectoryRequestTest.java | 25 ++----- .../awssdk/transfer/s3/UploadRequestTest.java | 29 +------- .../CompletedUploadDirectoryTest.java | 36 +-------- .../internal/FailedSingleFileUploadTest.java | 74 ------------------- .../s3/internal/S3TransferManagerTest.java | 24 +++++- 25 files changed, 363 insertions(+), 322 deletions(-) delete mode 100644 services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/CompletedFileTransfer.java create mode 100644 services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/FailedSingleFileUploadTest.java create mode 100644 services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/S3TransferManagerOverrideConfigurationTest.java delete mode 100644 services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/UploadDirectoryConfigurationTest.java create mode 100644 services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/UploadDirectoryOverrideConfigurationTest.java delete mode 100644 services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/FailedSingleFileUploadTest.java diff --git a/pom.xml b/pom.xml index 96b2af8313ef..aefad4272888 100644 --- a/pom.xml +++ b/pom.xml @@ -98,7 +98,7 @@ 1.7.30 1.2.17 2.5 - 3.5 + 3.7.1 4.1.68.Final diff --git a/services-custom/s3-transfer-manager/pom.xml b/services-custom/s3-transfer-manager/pom.xml index 84b598f1a52c..1fcd1ccffcb2 100644 --- a/services-custom/s3-transfer-manager/pom.xml +++ b/services-custom/s3-transfer-manager/pom.xml @@ -158,6 +158,11 @@ jimfs test + + nl.jqno.equalsverifier + equalsverifier + test + diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/CompletedFileTransfer.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/CompletedFileTransfer.java deleted file mode 100644 index 92eb1b4d0393..000000000000 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/CompletedFileTransfer.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.transfer.s3; - -import java.nio.file.Path; -import software.amazon.awssdk.annotations.SdkPreviewApi; -import software.amazon.awssdk.annotations.SdkPublicApi; - -/** - * Represents a completed file transfer. - */ -@SdkPublicApi -@SdkPreviewApi -public interface CompletedFileTransfer extends CompletedTransfer { - - /** - * Returns the path of the file that was upload from or downloaded to - * @return the path - */ - Path path(); -} diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/CompletedUploadDirectory.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/CompletedUploadDirectory.java index 506100b755e5..62379fa0b65c 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/CompletedUploadDirectory.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/CompletedUploadDirectory.java @@ -41,7 +41,8 @@ private CompletedUploadDirectory(DefaultBuilder builder) { } /** - * Return failed uploads with error details, request metadata about each file that is failed to upload. + * An immutable collection of failed uploads with error details, request metadata about each file that is failed to + * upload. * * @return a list of failed uploads */ @@ -111,6 +112,10 @@ public Builder failedUploads(Collection failedUploads) { return this; } + public Collection getFailedUploads() { + return failedUploads; + } + public void setFailedUploads(Collection failedUploads) { failedUploads(failedUploads); } diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/DownloadRequest.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/DownloadRequest.java index 70ee7ba092c5..94bb07c4c30a 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/DownloadRequest.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/DownloadRequest.java @@ -172,12 +172,28 @@ public Builder destination(Path destination) { return this; } + public Path getDestination() { + return destination; + } + + public void setDestination(Path destination) { + destination(destination); + } + @Override public Builder getObjectRequest(GetObjectRequest getObjectRequest) { this.getObjectRequest = getObjectRequest; return this; } + public GetObjectRequest getGetObjectRequest() { + return getObjectRequest; + } + + public void setGetObjectRequest(GetObjectRequest getObjectRequest) { + getObjectRequest(getObjectRequest); + } + @Override public DownloadRequest build() { return new DownloadRequest(this); diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/FailedSingleFileUpload.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/FailedSingleFileUpload.java index 2dc893eeed04..10cbbe3ff3d4 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/FailedSingleFileUpload.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/FailedSingleFileUpload.java @@ -113,7 +113,6 @@ private DefaultBuilder(FailedSingleFileUpload failedSingleFileUpload) { } private DefaultBuilder() { - } @Override @@ -122,12 +121,28 @@ public Builder exception(Throwable exception) { return this; } + public void setException(Throwable exception) { + exception(exception); + } + + public Throwable getException() { + return exception; + } + @Override public Builder request(UploadRequest request) { this.request = request; return this; } + public void setRequest(UploadRequest request) { + request(request); + } + + public UploadRequest getRequest() { + return request; + } + @Override public FailedSingleFileUpload build() { return new FailedSingleFileUpload(this); diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/S3TransferManager.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/S3TransferManager.java index 557388db828f..9e4c8d4a2a29 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/S3TransferManager.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/S3TransferManager.java @@ -21,6 +21,7 @@ import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.transfer.s3.internal.DefaultS3TransferManager; import software.amazon.awssdk.utils.SdkAutoCloseable; +import software.amazon.awssdk.utils.Validate; /** * The S3 Transfer Manager is a library that allows users to easily and @@ -266,6 +267,7 @@ default UploadDirectoryTransfer uploadDirectory(UploadDirectoryRequest uploadDir * @see UploadDirectoryOverrideConfiguration */ default UploadDirectoryTransfer uploadDirectory(Consumer requestBuilder) { + Validate.paramNotNull(requestBuilder, "requestBuilder"); return uploadDirectory(UploadDirectoryRequest.builder().applyMutation(requestBuilder).build()); } diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/S3TransferManagerOverrideConfiguration.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/S3TransferManagerOverrideConfiguration.java index eabbe5c9704b..329f3164a582 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/S3TransferManagerOverrideConfiguration.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/S3TransferManagerOverrideConfiguration.java @@ -30,9 +30,9 @@ *

* All values are optional, and not specifying them will use default values provided bt the SDK. * - *

Use {@link #builder()} to create a set of options.

+ *

Use {@link #builder()} to create a set of options. + * @see S3TransferManager.Builder#transferConfiguration(S3TransferManagerOverrideConfiguration) */ - @SdkPublicApi @SdkPreviewApi public final class S3TransferManagerOverrideConfiguration implements @@ -102,7 +102,8 @@ public static Builder builder() { public interface Builder extends CopyableBuilder { /** - * Specify the executor that will be used in {@link S3TransferManager} + * Specify the executor that will be used in {@link S3TransferManager} to offload + * execution * * @param executor the async configuration * @return this builder for method chaining. @@ -159,6 +160,10 @@ public void setExecutor(Executor executor) { executor(executor); } + public Executor getExecutor() { + return executor; + } + @Override public Builder uploadDirectoryConfiguration(UploadDirectoryOverrideConfiguration uploadDirectoryConfiguration) { this.uploadDirectoryConfiguration = uploadDirectoryConfiguration; @@ -169,6 +174,10 @@ public void setUploadDirectoryConfiguration(UploadDirectoryOverrideConfiguration uploadDirectoryConfiguration(uploadDirectoryConfiguration); } + public UploadDirectoryOverrideConfiguration getUploadDirectoryConfiguration() { + return uploadDirectoryConfiguration; + } + @Override public S3TransferManagerOverrideConfiguration build() { return new S3TransferManagerOverrideConfiguration(this); diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryOverrideConfiguration.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryOverrideConfiguration.java index 62c5ffba1fe0..a143d9d9d207 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryOverrideConfiguration.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryOverrideConfiguration.java @@ -28,7 +28,8 @@ * Configuration options for {@link S3TransferManager#uploadDirectory}. All values are optional, and not specifying them will * use the SDK default values. * - *

Use {@link #builder()} to create a set of options.

+ *

Use {@link #builder()} to create a set of options. + * @see S3TransferManager#uploadDirectory(UploadDirectoryRequest) */ @SdkPublicApi @SdkPreviewApi @@ -47,6 +48,7 @@ public UploadDirectoryOverrideConfiguration(DefaultBuilder builder) { /** * @return whether to follow symbolic links + * @see Builder#followSymbolicLinks(Boolean) */ public Optional followSymbolicLinks() { return Optional.ofNullable(followSymbolicLinks); @@ -54,6 +56,7 @@ public Optional followSymbolicLinks() { /** * @return the maximum number of directory levels to traverse + * @see Builder#maxDepth(Integer) */ public Optional maxDepth() { return Optional.ofNullable(maxDepth); @@ -61,6 +64,7 @@ public Optional maxDepth() { /** * @return whether to recursively upload all files under the specified directory + * @see Builder#recursive(Boolean) */ public Optional recursive() { return Optional.ofNullable(recursive); @@ -171,6 +175,10 @@ public Builder recursive(Boolean recursive) { return this; } + public Boolean getRecursive() { + return recursive; + } + public void setRecursive(Boolean recursive) { recursive(recursive); } @@ -185,6 +193,10 @@ public void setFollowSymbolicLinks(Boolean followSymbolicLinks) { followSymbolicLinks(followSymbolicLinks); } + public Boolean getFollowSymbolicLinks() { + return followSymbolicLinks; + } + @Override public Builder maxDepth(Integer maxDepth) { this.maxDepth = maxDepth; @@ -195,6 +207,10 @@ public void setMaxDepth(Integer maxDepth) { maxDepth(maxDepth); } + public Integer getMaxDepth() { + return maxDepth; + } + @Override public UploadDirectoryOverrideConfiguration build() { return new UploadDirectoryOverrideConfiguration(this); diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryRequest.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryRequest.java index adbe438408e1..856c2ca22ad6 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryRequest.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryRequest.java @@ -115,6 +115,9 @@ public boolean equals(Object o) { if (!Objects.equals(prefix, that.prefix)) { return false; } + if (!Objects.equals(delimiter, that.delimiter)) { + return false; + } return Objects.equals(overrideConfiguration, that.overrideConfiguration); } @@ -123,6 +126,7 @@ public int hashCode() { int result = sourceDirectory.hashCode(); result = 31 * result + bucket.hashCode(); result = 31 * result + (prefix != null ? prefix.hashCode() : 0); + result = 31 * result + (delimiter != null ? delimiter.hashCode() : 0); result = 31 * result + (overrideConfiguration != null ? overrideConfiguration.hashCode() : 0); return result; } @@ -223,30 +227,70 @@ public Builder sourceDirectory(Path sourceDirectory) { return this; } + public void setSourceDirectory(Path sourceDirectory) { + sourceDirectory(sourceDirectory); + } + + public Path getSourceDirectory() { + return sourceDirectory; + } + @Override public Builder bucket(String bucket) { this.bucket = bucket; return this; } + public void setBucket(String bucket) { + bucket(bucket); + } + + public String getBucket() { + return bucket; + } + @Override public Builder prefix(String prefix) { this.prefix = prefix; return this; } + public void setPrefix(String prefix) { + prefix(prefix); + } + + public String getPrefix() { + return prefix; + } + @Override public Builder delimiter(String delimiter) { this.delimiter = delimiter; return this; } + public void setDelimiter(String delimiter) { + delimiter(delimiter); + } + + public String getDelimiter() { + return delimiter; + } + @Override public Builder overrideConfiguration(UploadDirectoryOverrideConfiguration configuration) { this.configuration = configuration; return this; } + public void setOverrideConfiguration(UploadDirectoryOverrideConfiguration configuration) { + overrideConfiguration(configuration); + } + + public UploadDirectoryOverrideConfiguration getOverrideConfiguration() { + return configuration; + } + @Override public UploadDirectoryRequest build() { return new UploadDirectoryRequest(this); diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadRequest.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadRequest.java index 170e4e696fe1..57d1ae13c277 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadRequest.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadRequest.java @@ -179,12 +179,28 @@ public Builder source(Path source) { return this; } + public Path getSource() { + return source; + } + + public void setSource(Path source) { + source(source); + } + @Override public Builder putObjectRequest(PutObjectRequest putObjectRequest) { this.putObjectRequest = putObjectRequest; return this; } + public PutObjectRequest getPutObjectRequest() { + return putObjectRequest; + } + + public void setPutObjectRequest(PutObjectRequest putObjectRequest) { + putObjectRequest(putObjectRequest); + } + @Override public UploadRequest build() { return new UploadRequest(this); diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultS3TransferManager.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultS3TransferManager.java index 2f2fc204a1e0..a66a4190c55e 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultS3TransferManager.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultS3TransferManager.java @@ -90,6 +90,7 @@ private S3CrtAsyncClient initializeS3CrtClient(DefaultBuilder tmBuilder) { @Override public Upload upload(UploadRequest uploadRequest) { try { + Validate.paramNotNull(uploadRequest, "uploadRequest"); assertNotObjectLambdaArn(uploadRequest.putObjectRequest().bucket(), "upload"); PutObjectRequest putObjectRequest = uploadRequest.putObjectRequest(); @@ -109,12 +110,13 @@ public Upload upload(UploadRequest uploadRequest) { @Override public UploadDirectoryTransfer uploadDirectory(UploadDirectoryRequest uploadDirectoryRequest) { try { + Validate.paramNotNull(uploadDirectoryRequest, "uploadDirectoryRequest"); Path directory = uploadDirectoryRequest.sourceDirectory(); Validate.isTrue(Files.exists(directory), "The source directory (%s) provided does not exist", directory); Validate.isFalse(Files.isRegularFile(directory), "The source directory (%s) provided is not a directory", directory); - assertNotObjectLambdaArn(uploadDirectoryRequest.bucket(), "downloadDirectory"); + assertNotObjectLambdaArn(uploadDirectoryRequest.bucket(), "uploadDirectory"); return uploadDirectoryManager.uploadDirectory(uploadDirectoryRequest); } catch (Throwable throwable) { @@ -125,6 +127,7 @@ public UploadDirectoryTransfer uploadDirectory(UploadDirectoryRequest uploadDire @Override public Download download(DownloadRequest downloadRequest) { try { + Validate.paramNotNull(downloadRequest, "downloadRequest"); assertNotObjectLambdaArn(downloadRequest.getObjectRequest().bucket(), "download"); CompletableFuture getObjectFuture = diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/TransferConfigurationOption.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/TransferConfigurationOption.java index 905cd752ffba..8e30e837ed53 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/TransferConfigurationOption.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/TransferConfigurationOption.java @@ -17,11 +17,12 @@ import java.util.concurrent.Executor; import software.amazon.awssdk.annotations.SdkInternalApi; -import software.amazon.awssdk.transfer.s3.S3TransferManager; import software.amazon.awssdk.utils.AttributeMap; /** - * A set of internal options required by the {@link S3TransferManager} via {@link TransferManagerConfiguration}. + * A set of internal options required by the {@link DefaultS3TransferManager} via {@link TransferManagerConfiguration}. + * It contains the default settings + * */ @SdkInternalApi public final class TransferConfigurationOption extends AttributeMap.Key { @@ -37,13 +38,13 @@ public final class TransferConfigurationOption extends AttributeMap.Key { public static final TransferConfigurationOption EXECUTOR = new TransferConfigurationOption<>("Executor", Executor.class); - // TODO: revisit private static final int DEFAULT_UPLOAD_DIRECTORY_MAX_DEPTH = Integer.MAX_VALUE; private static final Boolean DEFAULT_UPLOAD_DIRECTORY_RECURSIVE = Boolean.TRUE; - // TODO: revisit + private static final Boolean DEFAULT_UPLOAD_DIRECTORY_FOLLOW_SYMBOLIC_LINKS = Boolean.FALSE; - public static final AttributeMap TRANSFER_DEFAULTS = AttributeMap + // TODO: revisit default settings before GA + public static final AttributeMap TRANSFER_MANAGER_DEFAULTS = AttributeMap .builder() .put(UPLOAD_DIRECTORY_MAX_DEPTH, DEFAULT_UPLOAD_DIRECTORY_MAX_DEPTH) .put(UPLOAD_DIRECTORY_RECURSIVE, DEFAULT_UPLOAD_DIRECTORY_RECURSIVE) diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/TransferManagerConfiguration.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/TransferManagerConfiguration.java index 97d149122764..05bc70f92674 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/TransferManagerConfiguration.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/TransferManagerConfiguration.java @@ -15,7 +15,7 @@ package software.amazon.awssdk.transfer.s3.internal; -import static software.amazon.awssdk.transfer.s3.internal.TransferConfigurationOption.TRANSFER_DEFAULTS; +import static software.amazon.awssdk.transfer.s3.internal.TransferConfigurationOption.TRANSFER_MANAGER_DEFAULTS; import static software.amazon.awssdk.transfer.s3.internal.TransferConfigurationOption.UPLOAD_DIRECTORY_FOLLOW_SYMBOLIC_LINKS; import static software.amazon.awssdk.transfer.s3.internal.TransferConfigurationOption.UPLOAD_DIRECTORY_MAX_DEPTH; import static software.amazon.awssdk.transfer.s3.internal.TransferConfigurationOption.UPLOAD_DIRECTORY_RECURSIVE; @@ -55,7 +55,7 @@ private TransferManagerConfiguration(Builder builder) { uploadDirectoryConfiguration.recursive().orElse(null)); finalizeExecutor(builder, standardOptions); - options = standardOptions.build().merge(TRANSFER_DEFAULTS); + options = standardOptions.build().merge(TRANSFER_MANAGER_DEFAULTS); } private void finalizeExecutor(Builder builder, AttributeMap.Builder standardOptions) { diff --git a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/DownloadRequestTest.java b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/DownloadRequestTest.java index 36a293320d5f..6a54cd7e90e2 100644 --- a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/DownloadRequestTest.java +++ b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/DownloadRequestTest.java @@ -20,10 +20,10 @@ import java.io.File; import java.nio.file.Path; import java.nio.file.Paths; +import nl.jqno.equalsverifier.EqualsVerifier; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; -import software.amazon.awssdk.services.s3.model.GetObjectRequest; public class DownloadRequestTest { @@ -75,30 +75,8 @@ public void usingFile_null_shouldThrowException() { @Test public void equals_hashcode() { - GetObjectRequest getObjectRequest = GetObjectRequest.builder() - .bucket("bucket") - .key("key") - .build(); - - DownloadRequest request1 = DownloadRequest.builder() - .getObjectRequest(b -> b.bucket("bucket").key("key")) - .destination(Paths.get(".")) - .build(); - - DownloadRequest request2 = DownloadRequest.builder() - .getObjectRequest(getObjectRequest) - .destination(Paths.get(".")) - .build(); - - DownloadRequest request3 = DownloadRequest.builder() - .getObjectRequest(b -> b.bucket("bucket1").key("key1")) - .destination(Paths.get(".")) - .build(); - - assertThat(request1).isEqualTo(request2); - assertThat(request1.hashCode()).isEqualTo(request2.hashCode()); - - assertThat(request1.hashCode()).isNotEqualTo(request3.hashCode()); - assertThat(request1).isNotEqualTo(request3); + EqualsVerifier.forClass(DownloadRequest.class) + .withNonnullFields("destination", "getObjectRequest") + .verify(); } } diff --git a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/FailedSingleFileUploadTest.java b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/FailedSingleFileUploadTest.java new file mode 100644 index 000000000000..b7cab9bc1951 --- /dev/null +++ b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/FailedSingleFileUploadTest.java @@ -0,0 +1,51 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.transfer.s3; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.nio.file.Paths; +import nl.jqno.equalsverifier.EqualsVerifier; +import org.junit.Test; +import software.amazon.awssdk.core.exception.SdkClientException; + +public class FailedSingleFileUploadTest { + + @Test + public void requestNull_mustThrowException() { + assertThatThrownBy(() -> FailedSingleFileUpload.builder() + .exception(SdkClientException.create("xxx")).build()) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("request must not be null"); + } + + @Test + public void exceptionNull_mustThrowException() { + UploadRequest uploadRequest = + UploadRequest.builder().source(Paths.get(".")).putObjectRequest(p -> p.bucket("bucket").key("key")).build(); + assertThatThrownBy(() -> FailedSingleFileUpload.builder() + .request(uploadRequest).build()) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("exception must not be null"); + } + + @Test + public void equalsHashcode() { + EqualsVerifier.forClass(FailedSingleFileUpload.class) + .withNonnullFields("exception", "request") + .verify(); + } +} diff --git a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/S3ClientConfigurationTest.java b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/S3ClientConfigurationTest.java index f8742bb2a0c1..ec2a2b67de6e 100644 --- a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/S3ClientConfigurationTest.java +++ b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/S3ClientConfigurationTest.java @@ -19,8 +19,8 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static software.amazon.awssdk.transfer.s3.SizeConstant.MB; +import nl.jqno.equalsverifier.EqualsVerifier; import org.junit.Test; -import software.amazon.awssdk.auth.credentials.AnonymousCredentialsProvider; import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; import software.amazon.awssdk.regions.Region; @@ -97,33 +97,7 @@ public void build_emptyBuilder() { @Test public void equalsHashCode() { - AwsCredentialsProvider credentials = () -> AwsBasicCredentials.create("test" - , "test"); - S3ClientConfiguration configuration1 = S3ClientConfiguration.builder() - .credentialsProvider(credentials) - .maxConcurrency(100) - .targetThroughputInGbps(10.0) - .region(Region.US_WEST_2) - .minimumPartSizeInBytes(5 * MB) - .build(); - - S3ClientConfiguration configuration2 = S3ClientConfiguration.builder() - .credentialsProvider(credentials) - .maxConcurrency(100) - .targetThroughputInGbps(10.0) - .region(Region.US_WEST_2) - .minimumPartSizeInBytes(5 * MB) - .build(); - - S3ClientConfiguration configuration3 = configuration1.toBuilder() - .credentialsProvider(AnonymousCredentialsProvider.create()) - .maxConcurrency(50) - .targetThroughputInGbps(1.0) - .build(); - - assertThat(configuration1).isEqualTo(configuration2); - assertThat(configuration1.hashCode()).isEqualTo(configuration2.hashCode()); - assertThat(configuration1).isNotEqualTo(configuration3); - assertThat(configuration1.hashCode()).isNotEqualTo(configuration3.hashCode()); + EqualsVerifier.forClass(S3ClientConfiguration.class) + .verify(); } } diff --git a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/S3TransferManagerOverrideConfigurationTest.java b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/S3TransferManagerOverrideConfigurationTest.java new file mode 100644 index 000000000000..00fa560be571 --- /dev/null +++ b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/S3TransferManagerOverrideConfigurationTest.java @@ -0,0 +1,57 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.transfer.s3; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.concurrent.Executor; +import nl.jqno.equalsverifier.EqualsVerifier; +import org.junit.Test; +import org.mockito.Mockito; + +public class S3TransferManagerOverrideConfigurationTest { + + @Test + public void build_allProperties() { + Executor executor = Mockito.mock(Executor.class); + UploadDirectoryOverrideConfiguration directoryOverrideConfiguration = + UploadDirectoryOverrideConfiguration.builder() + .build(); + S3TransferManagerOverrideConfiguration configuration = + S3TransferManagerOverrideConfiguration.builder() + .uploadDirectoryConfiguration(directoryOverrideConfiguration) + .executor(executor) + .build(); + + assertThat(configuration.executor()).contains(executor); + assertThat(configuration.uploadDirectoryConfiguration()).contains(directoryOverrideConfiguration); + } + + @Test + public void build_emptyBuilder() { + S3TransferManagerOverrideConfiguration configuration = S3TransferManagerOverrideConfiguration.builder() + .build(); + + assertThat(configuration.executor()).isEmpty(); + assertThat(configuration.uploadDirectoryConfiguration()).isEmpty(); + } + + @Test + public void equalsHashCode() { + EqualsVerifier.forClass(S3TransferManagerOverrideConfiguration.class) + .verify(); + } +} diff --git a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/UploadDirectoryConfigurationTest.java b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/UploadDirectoryConfigurationTest.java deleted file mode 100644 index 2f1f5143ed67..000000000000 --- a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/UploadDirectoryConfigurationTest.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.transfer.s3; - - -import static org.assertj.core.api.Assertions.assertThat; - -import org.junit.Test; - -public class UploadDirectoryConfigurationTest { - - @Test - public void getters_allPropertiesSet() { - UploadDirectoryOverrideConfiguration object = UploadDirectoryOverrideConfiguration - .builder() - .maxDepth(100) - .followSymbolicLinks(true) - .recursive(false) - .build(); - - assertThat(object.followSymbolicLinks()).contains(true); - assertThat(object.recursive()).contains(false); - assertThat(object.maxDepth()).contains(100); - } - - @Test - public void getters_allPropertiesAbsent() { - UploadDirectoryOverrideConfiguration object = UploadDirectoryOverrideConfiguration - .builder().build(); - - assertThat(object.followSymbolicLinks()).isEmpty(); - assertThat(object.recursive()).isEmpty(); - assertThat(object.maxDepth()).isEmpty(); - } - - @Test - public void equalsHashCode() { - UploadDirectoryOverrideConfiguration object1 = UploadDirectoryOverrideConfiguration - .builder() - .maxDepth(100) - .followSymbolicLinks(true) - .recursive(false) - .build(); - - UploadDirectoryOverrideConfiguration object2 = object1.toBuilder().build(); - - UploadDirectoryOverrideConfiguration object3 = object1.toBuilder().recursive(true).build(); - assertThat(object1).isEqualTo(object2); - assertThat(object1.hashCode()).isEqualTo(object2.hashCode()); - assertThat(object1).isNotEqualTo(object3); - assertThat(object1.hashCode()).isNotEqualTo(object3.hashCode()); - } -} diff --git a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/UploadDirectoryOverrideConfigurationTest.java b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/UploadDirectoryOverrideConfigurationTest.java new file mode 100644 index 000000000000..09a8fa590867 --- /dev/null +++ b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/UploadDirectoryOverrideConfigurationTest.java @@ -0,0 +1,65 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.transfer.s3; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import nl.jqno.equalsverifier.EqualsVerifier; +import org.junit.Test; + +public class UploadDirectoryOverrideConfigurationTest { + + @Test + public void maxDepthNonNegative_shouldThrowException() { + assertThatThrownBy(() -> UploadDirectoryOverrideConfiguration.builder() + .maxDepth(0) + .build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("positive"); + + assertThatThrownBy(() -> UploadDirectoryOverrideConfiguration.builder() + .maxDepth(-1) + .build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("positive"); + } + + @Test + public void defaultBuilder() { + UploadDirectoryOverrideConfiguration configuration = UploadDirectoryOverrideConfiguration.builder().build(); + assertThat(configuration.followSymbolicLinks()).isEmpty(); + assertThat(configuration.recursive()).isEmpty(); + assertThat(configuration.maxDepth()).isEmpty(); + } + + @Test + public void defaultBuilderWithPropertySet() { + UploadDirectoryOverrideConfiguration configuration = UploadDirectoryOverrideConfiguration.builder() + .maxDepth(10) + .recursive(true) + .followSymbolicLinks(false) + .build(); + assertThat(configuration.followSymbolicLinks()).contains(false); + assertThat(configuration.recursive()).contains(true); + assertThat(configuration.maxDepth()).contains(10); + } + + @Test + public void equalsHashCode() { + EqualsVerifier.forClass(UploadDirectoryOverrideConfiguration.class).verify(); + } +} diff --git a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/UploadDirectoryRequestTest.java b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/UploadDirectoryRequestTest.java index 29278c2b4f53..896bfc8d67f7 100644 --- a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/UploadDirectoryRequestTest.java +++ b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/UploadDirectoryRequestTest.java @@ -15,10 +15,10 @@ package software.amazon.awssdk.transfer.s3; -import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.nio.file.Paths; +import nl.jqno.equalsverifier.EqualsVerifier; import org.junit.Test; public class UploadDirectoryRequestTest { @@ -26,7 +26,7 @@ public class UploadDirectoryRequestTest { @Test public void noSourceDirectory_throws() { assertThatThrownBy(() -> - UploadDirectoryRequest.builder().bucket("bucket").build() + UploadDirectoryRequest.builder().bucket("bucket").build() ).isInstanceOf(NullPointerException.class).hasMessageContaining("sourceDirectory"); } @@ -39,23 +39,8 @@ public void noBucket_throws() { @Test public void equals_hashcode() { - UploadDirectoryRequest request1 = UploadDirectoryRequest - .builder() - .bucket("bucket") - .sourceDirectory(Paths.get(".")) - .overrideConfiguration(o -> o.recursive(true)) - .build(); - - UploadDirectoryRequest request2 = request1.toBuilder() - .build(); - - UploadDirectoryRequest request3 = request1.toBuilder() - .bucket("anotherBucket") - .build(); - - assertThat(request1).isEqualTo(request2); - assertThat(request1.hashCode()).isEqualTo(request2.hashCode()); - assertThat(request1).isNotEqualTo(request3); - assertThat(request1.hashCode()).isNotEqualTo(request3.hashCode()); + EqualsVerifier.forClass(UploadDirectoryRequest.class) + .withNonnullFields("sourceDirectory", "bucket") + .verify(); } } diff --git a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/UploadRequestTest.java b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/UploadRequestTest.java index 7ba0e7fc58f5..08debe7b8c1a 100644 --- a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/UploadRequestTest.java +++ b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/UploadRequestTest.java @@ -20,6 +20,7 @@ import java.io.File; import java.nio.file.Path; import java.nio.file.Paths; +import nl.jqno.equalsverifier.EqualsVerifier; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -71,31 +72,9 @@ public void sourceUsingFile_null_shouldThrowException() { @Test public void equals_hashcode() { - PutObjectRequest getObjectRequest = PutObjectRequest.builder() - .bucket("bucket") - .key("key") - .build(); - - UploadRequest request1 = UploadRequest.builder() - .putObjectRequest(b -> b.bucket("bucket").key("key")) - .source(Paths.get(".")) - .build(); - - UploadRequest request2 = UploadRequest.builder() - .putObjectRequest(getObjectRequest) - .source(Paths.get(".")) - .build(); - - UploadRequest request3 = UploadRequest.builder() - .putObjectRequest(b -> b.bucket("bucket1").key("key1")) - .source(Paths.get(".")) - .build(); - - assertThat(request1).isEqualTo(request2); - assertThat(request1.hashCode()).isEqualTo(request2.hashCode()); - - assertThat(request1.hashCode()).isNotEqualTo(request3.hashCode()); - assertThat(request1).isNotEqualTo(request3); + EqualsVerifier.forClass(UploadRequest.class) + .withNonnullFields("source", "putObjectRequest") + .verify(); } } diff --git a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/CompletedUploadDirectoryTest.java b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/CompletedUploadDirectoryTest.java index fb060b79ee37..64da9ab51145 100644 --- a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/CompletedUploadDirectoryTest.java +++ b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/CompletedUploadDirectoryTest.java @@ -15,44 +15,16 @@ package software.amazon.awssdk.transfer.s3.internal; -import static org.assertj.core.api.Assertions.assertThat; - -import java.nio.file.Paths; -import java.util.Arrays; -import java.util.List; +import nl.jqno.equalsverifier.EqualsVerifier; import org.junit.Test; -import software.amazon.awssdk.core.exception.SdkClientException; import software.amazon.awssdk.transfer.s3.CompletedUploadDirectory; -import software.amazon.awssdk.transfer.s3.FailedSingleFileUpload; -import software.amazon.awssdk.transfer.s3.UploadRequest; public class CompletedUploadDirectoryTest { @Test public void equalsHashcode() { - List failedUploads = Arrays.asList(FailedSingleFileUpload.builder() - .request(UploadRequest.builder() - .source(Paths.get(".")) - .putObjectRequest(b -> b.bucket("bucket").key("key") - ) - .build()) - .exception(SdkClientException.create( - "helloworld")) - .build()); - CompletedUploadDirectory completedUploadDirectory = CompletedUploadDirectory.builder() - .failedUploads(failedUploads) - .build(); - - CompletedUploadDirectory completedUploadDirectory2 = CompletedUploadDirectory.builder() - .failedUploads(failedUploads) - .build(); - - CompletedUploadDirectory completedUploadDirectory3 = CompletedUploadDirectory.builder() - .build(); - - assertThat(completedUploadDirectory).isEqualTo(completedUploadDirectory2); - assertThat(completedUploadDirectory.hashCode()).isEqualTo(completedUploadDirectory2.hashCode()); - assertThat(completedUploadDirectory2).isNotEqualTo(completedUploadDirectory3); - assertThat(completedUploadDirectory2.hashCode()).isNotEqualTo(completedUploadDirectory3); + EqualsVerifier.forClass(CompletedUploadDirectory.class) + .withNonnullFields("failedUploads") + .verify(); } } diff --git a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/FailedSingleFileUploadTest.java b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/FailedSingleFileUploadTest.java deleted file mode 100644 index b33c739a79f5..000000000000 --- a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/FailedSingleFileUploadTest.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.transfer.s3.internal; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import java.nio.file.Path; -import java.nio.file.Paths; -import org.junit.Test; -import software.amazon.awssdk.core.exception.SdkClientException; -import software.amazon.awssdk.transfer.s3.FailedSingleFileUpload; -import software.amazon.awssdk.transfer.s3.UploadRequest; - -public class FailedSingleFileUploadTest { - - @Test - public void requestNull_mustThrowException() { - assertThatThrownBy(() -> FailedSingleFileUpload.builder() - .exception(SdkClientException.create("xxx")).build()) - .isInstanceOf(NullPointerException.class) - .hasMessageContaining("request must not be null"); - } - - @Test - public void exceptionNull_mustThrowException() { - UploadRequest uploadRequest = - UploadRequest.builder().source(Paths.get(".")).putObjectRequest(p -> p.bucket("bucket").key("key")).build(); - assertThatThrownBy(() -> FailedSingleFileUpload.builder() - .request(uploadRequest).build()) - .isInstanceOf(NullPointerException.class) - .hasMessageContaining("exception must not be null"); - } - - @Test - public void equalsHashcode() { - SdkClientException exception = SdkClientException.create("test"); - Path path = Paths.get("."); - UploadRequest uploadRequest = - UploadRequest.builder().source(Paths.get(".")).putObjectRequest(p -> p.bucket("bucket").key("key")).build(); - FailedSingleFileUpload failedUpload1 = FailedSingleFileUpload.builder() - .request(uploadRequest) - .exception(exception) - .build(); - - FailedSingleFileUpload failedUpload2 = FailedSingleFileUpload.builder() - .request(uploadRequest) - .exception(exception) - .build(); - - FailedSingleFileUpload failedUpload3 = FailedSingleFileUpload.builder() - .request(uploadRequest) - .exception(SdkClientException.create("blah")) - .build(); - - assertThat(failedUpload1).isEqualTo(failedUpload2); - assertThat(failedUpload1.hashCode()).isEqualTo(failedUpload2.hashCode()); - assertThat(failedUpload1).isNotEqualTo(failedUpload3); - assertThat(failedUpload1.hashCode()).isNotEqualTo(failedUpload3); - } -} diff --git a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/S3TransferManagerTest.java b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/S3TransferManagerTest.java index 3696de68b2a1..b65bafcc65f3 100644 --- a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/S3TransferManagerTest.java +++ b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/S3TransferManagerTest.java @@ -165,10 +165,32 @@ public void uploadDirectory_throwException_shouldCompleteFutureExceptionally() { } @Test - public void close_shouldCloseResources() { + public void close_shouldCloseUnderlyingResources() { S3TransferManager transferManager = new DefaultS3TransferManager(mockS3Crt, uploadDirectoryManager, configuration); transferManager.close(); verify(mockS3Crt).close(); verify(configuration).close(); } + + @Test + public void uploadDirectory_requestNull_shouldThrowException() { + UploadDirectoryRequest request = null; + assertThatThrownBy(() -> tm.uploadDirectory(request).completionFuture().join()) + .hasCauseInstanceOf(NullPointerException.class) + .hasMessageContaining("must not be null"); + } + + @Test + public void upload_requestNull_shouldThrowException() { + UploadRequest request = null; + assertThatThrownBy(() -> tm.upload(request).completionFuture().join()).hasCauseInstanceOf(NullPointerException.class) + .hasMessageContaining("must not be null"); + } + + @Test + public void download_requestNull_shouldThrowException() { + DownloadRequest request = null; + assertThatThrownBy(() -> tm.download(request).completionFuture().join()).hasCauseInstanceOf(NullPointerException.class) + .hasMessageContaining("must not be null"); + } } From 0228ea576ba91ccaf4fa85e85408e3f4bc773538 Mon Sep 17 00:00:00 2001 From: Zoe Wang <33073555+zoewangg@users.noreply.github.com> Date: Mon, 4 Oct 2021 16:45:06 -0700 Subject: [PATCH 4/8] Address remaining feedback --- .../awssdk/transfer/s3/CompletedDownload.java | 83 ++++++++++++++++- .../awssdk/transfer/s3/CompletedUpload.java | 86 +++++++++++++++++- .../transfer/s3/CompletedUploadDirectory.java | 19 +++- .../awssdk/transfer/s3/S3TransferManager.java | 7 +- ...3TransferManagerOverrideConfiguration.java | 11 ++- .../transfer/s3/UploadDirectoryRequest.java | 16 +++- .../awssdk/transfer/s3/UploadRequest.java | 1 + .../s3/internal/DefaultCompletedDownload.java | 51 ----------- .../s3/internal/DefaultCompletedUpload.java | 90 ------------------- .../s3/internal/DefaultS3TransferManager.java | 19 ++-- .../s3/internal/UploadDirectoryHelper.java | 28 ++++-- .../transfer/s3/CompletedDownloadTest.java | 37 ++++++++ .../transfer/s3/CompletedUploadTest.java | 37 ++++++++ ...ploadDirectoryHelperParameterizedTest.java | 45 ++++++++-- .../internal/UploadDirectoryHelperTest.java | 6 +- 15 files changed, 358 insertions(+), 178 deletions(-) delete mode 100644 services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultCompletedDownload.java delete mode 100644 services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultCompletedUpload.java create mode 100644 services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/CompletedDownloadTest.java create mode 100644 services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/CompletedUploadTest.java diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/CompletedDownload.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/CompletedDownload.java index 1fb176f93cd3..f2ff4c81fc46 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/CompletedDownload.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/CompletedDownload.java @@ -17,18 +17,95 @@ import software.amazon.awssdk.annotations.SdkPreviewApi; import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.services.s3.S3AsyncClient; import software.amazon.awssdk.services.s3.model.GetObjectResponse; +import software.amazon.awssdk.utils.Validate; /** - * A completed download transfer. + * Represents a completed download transfer from Amazon S3. It can be used to track + * the underlying {@link GetObjectResponse} + * + * @see S3TransferManager#download(DownloadRequest) */ @SdkPublicApi @SdkPreviewApi -public interface CompletedDownload extends CompletedTransfer { +public final class CompletedDownload implements CompletedTransfer { + private final GetObjectResponse response; + + private CompletedDownload(DefaultBuilder builder) { + this.response = Validate.paramNotNull(builder.response, "response"); + } /** * Returns the API response from the {@link S3TransferManager#download(DownloadRequest)} * @return the response */ - GetObjectResponse response(); + public GetObjectResponse response() { + return response; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + CompletedDownload that = (CompletedDownload) o; + + return response.equals(that.response); + } + + @Override + public int hashCode() { + return response.hashCode(); + } + + public static Builder builder() { + return new DefaultBuilder(); + } + + public interface Builder { + /** + * Specify the {@link GetObjectResponse} from {@link S3AsyncClient#getObject} + * + * @param response the response + * @return This builder for method chaining. + */ + Builder response(GetObjectResponse response); + + /** + * Builds a {@link CompletedUpload} based on the properties supplied to this builder + * @return An initialized {@link CompletedUpload} + */ + CompletedDownload build(); + } + + private static final class DefaultBuilder implements Builder { + private GetObjectResponse response; + + private DefaultBuilder() { + } + + @Override + public Builder response(GetObjectResponse response) { + this.response = response; + return this; + } + + public void setResponse(GetObjectResponse response) { + response(response); + } + + public GetObjectResponse getResponse() { + return response; + } + + @Override + public CompletedDownload build() { + return new CompletedDownload(this); + } + } } diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/CompletedUpload.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/CompletedUpload.java index 5633084018a0..9dbbbc721e22 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/CompletedUpload.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/CompletedUpload.java @@ -17,18 +17,98 @@ import software.amazon.awssdk.annotations.SdkPreviewApi; import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.services.s3.S3AsyncClient; import software.amazon.awssdk.services.s3.model.PutObjectResponse; +import software.amazon.awssdk.utils.ToString; +import software.amazon.awssdk.utils.Validate; /** - * A completed upload transfer. + * Represents a completed upload transfer to Amazon S3. It can be used to track + * the underlying {@link PutObjectResponse} + * + * @see S3TransferManager#upload(UploadRequest) */ @SdkPublicApi @SdkPreviewApi -public interface CompletedUpload extends CompletedTransfer { +public final class CompletedUpload implements CompletedTransfer { + private final PutObjectResponse response; + + private CompletedUpload(DefaultBuilder builder) { + this.response = Validate.paramNotNull(builder.response, "response"); + } /** * Returns the API response from the {@link S3TransferManager#upload(UploadRequest)} * @return the response */ - PutObjectResponse response(); + public PutObjectResponse response() { + return response; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + CompletedUpload that = (CompletedUpload) o; + + return response.equals(that.response); + } + + @Override + public int hashCode() { + return response.hashCode(); + } + + @Override + public String toString() { + return ToString.builder("CompletedUpload") + .add("response", response) + .build(); + } + + /** + * Creates a default builder for {@link CompletedUpload}. + */ + public static Builder builder() { + return new DefaultBuilder(); + } + + public interface Builder { + /** + * Specify the {@link PutObjectResponse} from {@link S3AsyncClient#putObject} + * + * @param response the response + * @return This builder for method chaining. + */ + Builder response(PutObjectResponse response); + + /** + * Builds a {@link CompletedUpload} based on the properties supplied to this builder + * @return An initialized {@link CompletedUpload} + */ + CompletedUpload build(); + } + + private static class DefaultBuilder implements Builder { + private PutObjectResponse response; + + private DefaultBuilder() { + } + + @Override + public Builder response(PutObjectResponse response) { + this.response = response; + return this; + } + + @Override + public CompletedUpload build() { + return new CompletedUpload(this); + } + } } diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/CompletedUploadDirectory.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/CompletedUploadDirectory.java index 62379fa0b65c..91c9f2118cab 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/CompletedUploadDirectory.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/CompletedUploadDirectory.java @@ -44,6 +44,23 @@ private CompletedUploadDirectory(DefaultBuilder builder) { * An immutable collection of failed uploads with error details, request metadata about each file that is failed to * upload. * + *

+ * Failed single file uploads can be retried by calling {@link S3TransferManager#upload(UploadRequest)} + * + *

+     * {@code
+     * // Retrying failed uploads if the exception is retryable
+     * List> futures =
+     *     completedUploadDirectory.failedUploads()
+     *                             .stream()
+     *                             .filter(failedSingleFileUpload -> isRetryable(failedSingleFileUpload.exception()))
+     *                             .map(failedSingleFileUpload ->
+     *                                  tm.upload(failedSingleFileUpload.request()).completionFuture())
+     *                             .collect(Collectors.toList());
+     * CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
+     * }
+     * 
+ * * @return a list of failed uploads */ public Collection failedUploads() { @@ -51,7 +68,7 @@ public Collection failedUploads() { } /** - * Creates a default builder for {@link CompletedUpload}. + * Creates a default builder for {@link CompletedUploadDirectory}. */ public static Builder builder() { return new DefaultBuilder(); diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/S3TransferManager.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/S3TransferManager.java index 9e4c8d4a2a29..3e142f1763d7 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/S3TransferManager.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/S3TransferManager.java @@ -198,7 +198,7 @@ default Upload upload(Consumer request) { * // Print out the failed uploads * completedUploadDirectory.failedUploads().forEach(System.out::println); * - * // Retrying failed uploads if the exceptions are retryable + * // Retrying failed uploads if the exception is retryable * List> futures = * completedUploadDirectory.failedUploads() * .stream() @@ -207,6 +207,7 @@ default Upload upload(Consumer request) { * tm.upload(failedSingleFileUpload.request()).completionFuture()) * .collect(Collectors.toList()); * CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); + * } * * * @param uploadDirectoryRequest the upload directory request @@ -252,7 +253,7 @@ default UploadDirectoryTransfer uploadDirectory(UploadDirectoryRequest uploadDir * // Print out the failed uploads * completedUploadDirectory.failedUploads().forEach(System.out::println); * - * // Retrying failed uploads if the exceptions are retryable + * // Retrying failed uploads if the exception is retryable * List> futures = * completedUploadDirectory.failedUploads() * .stream() @@ -261,6 +262,7 @@ default UploadDirectoryTransfer uploadDirectory(UploadDirectoryRequest uploadDir * tm.upload(failedSingleFileUpload.request()).completionFuture()) * .collect(Collectors.toList()); * CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); + * } * * @param requestBuilder the upload directory request builder * @see #uploadDirectory(UploadDirectoryRequest) @@ -342,6 +344,7 @@ default Builder s3ClientConfiguration(Consumer co * @see #transferConfiguration(S3TransferManagerOverrideConfiguration) */ default Builder transferConfiguration(Consumer configuration) { + Validate.paramNotNull(configuration, "configuration"); S3TransferManagerOverrideConfiguration.Builder builder = S3TransferManagerOverrideConfiguration.builder(); configuration.accept(builder); transferConfiguration(builder.build()); diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/S3TransferManagerOverrideConfiguration.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/S3TransferManagerOverrideConfiguration.java index 329f3164a582..1ce29dbb5ea5 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/S3TransferManagerOverrideConfiguration.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/S3TransferManagerOverrideConfiguration.java @@ -102,10 +102,15 @@ public static Builder builder() { public interface Builder extends CopyableBuilder { /** - * Specify the executor that will be used in {@link S3TransferManager} to offload - * execution + * Specify the executor that {@link S3TransferManager} will use to execute + * the request that might take some time to process before handing it off to the underlying S3 async client such as + * visiting file tree in {@link S3TransferManager#uploadDirectory(UploadDirectoryRequest)} operation. * - * @param executor the async configuration + *

+ * This executor must be shut down by the user when it is ready to be disposed. The SDK will not close the executor + * when the s3 transfer manager is closed. + * + * @param executor the executor to use * @return this builder for method chaining. */ Builder executor(Executor executor); diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryRequest.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryRequest.java index 856c2ca22ad6..3e29405cb4ae 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryRequest.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryRequest.java @@ -54,6 +54,7 @@ public UploadDirectoryRequest(DefaultBuilder builder) { * The source directory to upload * * @return the source directory + * @see Builder#sourceDirectory(Path) */ public Path sourceDirectory() { return sourceDirectory; @@ -63,6 +64,7 @@ public Path sourceDirectory() { * The name of the bucket to upload objects to. * * @return bucket name + * @see Builder#bucket(String) */ public String bucket() { return bucket; @@ -70,17 +72,23 @@ public String bucket() { /** * @return the optional key prefix + * @see Builder#prefix(String) */ public Optional prefix() { return Optional.ofNullable(prefix); } + /** + * @return the optional delimiter + * @see Builder#delimiter(String) + */ public Optional delimiter() { return Optional.ofNullable(delimiter); } /** * @return the optional override configuration + * @see Builder#overrideConfiguration(UploadDirectoryOverrideConfiguration) */ public Optional overrideConfiguration() { return Optional.ofNullable(overrideConfiguration); @@ -134,10 +142,16 @@ public int hashCode() { public interface Builder extends CopyableBuilder { /** - * Specify the source directory to upload + * Specify the source directory to upload. The source directory must exist. + * Fle wildcards are not supported and treated literally. Hidden files/directories are visited. + * + *

+ * Note that the current user must have read access to all directories and files, + * otherwise {@link SecurityException} will be thrown. * * @param sourceDirectory the source directory * @return This builder for method chaining. + * @see UploadDirectoryOverrideConfiguration */ Builder sourceDirectory(Path sourceDirectory); diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadRequest.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadRequest.java index 57d1ae13c277..c45162040a8c 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadRequest.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadRequest.java @@ -32,6 +32,7 @@ /** * Upload an object to S3 using {@link S3TransferManager}. + * @see S3TransferManager#upload(UploadRequest) */ @SdkPublicApi @SdkPreviewApi diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultCompletedDownload.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultCompletedDownload.java deleted file mode 100644 index 46a9716f61bd..000000000000 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultCompletedDownload.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.transfer.s3.internal; - -import software.amazon.awssdk.annotations.SdkInternalApi; -import software.amazon.awssdk.services.s3.model.GetObjectResponse; -import software.amazon.awssdk.transfer.s3.CompletedDownload; - -@SdkInternalApi -public final class DefaultCompletedDownload implements CompletedDownload { - private final GetObjectResponse response; - - private DefaultCompletedDownload(Builder builder) { - this.response = builder.response; - } - - @Override - public GetObjectResponse response() { - return response; - } - - public static Builder builder() { - return new Builder(); - } - - public static final class Builder { - private GetObjectResponse response; - - public Builder response(GetObjectResponse response) { - this.response = response; - return this; - } - - public CompletedDownload build() { - return new DefaultCompletedDownload(this); - } - } -} diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultCompletedUpload.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultCompletedUpload.java deleted file mode 100644 index 0a7371706116..000000000000 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultCompletedUpload.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.transfer.s3.internal; - -import software.amazon.awssdk.annotations.SdkInternalApi; -import software.amazon.awssdk.services.s3.model.PutObjectResponse; -import software.amazon.awssdk.transfer.s3.CompletedUpload; -import software.amazon.awssdk.utils.ToString; -import software.amazon.awssdk.utils.Validate; - -@SdkInternalApi -public final class DefaultCompletedUpload implements CompletedUpload { - private final PutObjectResponse response; - - private DefaultCompletedUpload(BuilderImpl builder) { - this.response = Validate.paramNotNull(builder.response, "response"); - } - - @Override - public PutObjectResponse response() { - return response; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - - DefaultCompletedUpload that = (DefaultCompletedUpload) o; - - return response.equals(that.response); - } - - @Override - public int hashCode() { - return response.hashCode(); - } - - @Override - public String toString() { - return ToString.builder("CompletedUpload") - .add("response", response) - .build(); - } - - /** - * Creates a default builder for {@link CompletedUpload}. - */ - public static Builder builder() { - return new DefaultCompletedUpload.BuilderImpl(); - } - - interface Builder { - Builder response(PutObjectResponse response); - - CompletedUpload build(); - } - - private static class BuilderImpl implements Builder { - private PutObjectResponse response; - - @Override - public Builder response(PutObjectResponse response) { - this.response = response; - return this; - } - - @Override - public CompletedUpload build() { - return new DefaultCompletedUpload(this); - } - } -} diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultS3TransferManager.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultS3TransferManager.java index a66a4190c55e..16cf7f3d6c44 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultS3TransferManager.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultS3TransferManager.java @@ -16,6 +16,7 @@ package software.amazon.awssdk.transfer.s3.internal; import java.nio.file.Files; +import java.nio.file.LinkOption; import java.nio.file.Path; import java.util.concurrent.CompletableFuture; import software.amazon.awssdk.annotations.SdkInternalApi; @@ -98,9 +99,9 @@ public Upload upload(UploadRequest uploadRequest) { CompletableFuture putObjFuture = s3CrtAsyncClient.putObject(putObjectRequest, requestBody); - CompletableFuture future = putObjFuture.thenApply(r -> DefaultCompletedUpload.builder() - .response(r) - .build()); + CompletableFuture future = putObjFuture.thenApply(r -> CompletedUpload.builder() + .response(r) + .build()); return new DefaultUpload(CompletableFutureUtils.forwardExceptionTo(future, putObjFuture)); } catch (Throwable throwable) { return new DefaultUpload(CompletableFutureUtils.failedFuture(throwable)); @@ -114,7 +115,15 @@ public UploadDirectoryTransfer uploadDirectory(UploadDirectoryRequest uploadDire Path directory = uploadDirectoryRequest.sourceDirectory(); Validate.isTrue(Files.exists(directory), "The source directory (%s) provided does not exist", directory); - Validate.isFalse(Files.isRegularFile(directory), "The source directory (%s) provided is not a directory", directory); + boolean followSymbolicLinks = transferConfiguration.resolveUploadDirectoryFollowSymbolicLinks(uploadDirectoryRequest); + if (followSymbolicLinks) { + Validate.isFalse(Files.isRegularFile(directory), "The source directory (%s) provided is not a " + + "directory", directory); + } else { + Validate.isFalse(Files.isRegularFile(directory, LinkOption.NOFOLLOW_LINKS), "The source directory (%s) provided" + + " is not a " + + "directory", directory); + } assertNotObjectLambdaArn(uploadDirectoryRequest.bucket(), "uploadDirectory"); @@ -134,7 +143,7 @@ public Download download(DownloadRequest downloadRequest) { s3CrtAsyncClient.getObject(downloadRequest.getObjectRequest(), AsyncResponseTransformer.toFile(downloadRequest.destination())); CompletableFuture future = - getObjectFuture.thenApply(r -> DefaultCompletedDownload.builder().response(r).build()); + getObjectFuture.thenApply(r -> CompletedDownload.builder().response(r).build()); return new DefaultDownload(CompletableFutureUtils.forwardExceptionTo(future, getObjectFuture)); } catch (Throwable throwable) { diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryHelper.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryHelper.java index f3d1545f5533..e95717482d06 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryHelper.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryHelper.java @@ -15,7 +15,6 @@ package software.amazon.awssdk.transfer.s3.internal; - import java.io.IOException; import java.nio.file.FileSystem; import java.nio.file.FileSystems; @@ -80,6 +79,8 @@ public UploadDirectoryHelper(TransferManagerConfiguration transferConfiguration, public UploadDirectoryTransfer uploadDirectory(UploadDirectoryRequest uploadDirectoryRequest) { CompletableFuture returnFuture = new CompletableFuture<>(); + + // offload the execution to the transfer manager executor CompletableFuture.runAsync(() -> doUploadDirectory(returnFuture, uploadDirectoryRequest), transferConfiguration.option(TransferConfigurationOption.EXECUTOR)); @@ -103,6 +104,7 @@ private void doUploadDirectory(CompletableFuture retur }); } + // Wait for all sub transfers to complete phaser.arriveAndAwaitAdvance(); phaser.arriveAndDeregister(); returnFuture.complete(CompletedUploadDirectory.builder().failedUploads(failedUploads).build()); @@ -115,10 +117,12 @@ private CompletableFuture uploadSingleFile(UploadDirectoryReque phaser.register(); int nameCount = uploadDirectoryRequest.sourceDirectory().getNameCount(); UploadRequest uploadRequest = constructUploadRequest(uploadDirectoryRequest, nameCount, path); - log.debug(() -> "Sending upload request: " + uploadRequest); + log.debug(() -> String.format("Sending upload request (%s) for path (%s)", uploadRequest, path)); CompletableFuture future = uploadFunction.apply(uploadRequest).completionFuture(); future.whenComplete((r, t) -> { + // notify the future is completed phaser.arriveAndDeregister(); + if (t != null) { failedUploads.add(FailedSingleFileUpload.builder() .exception(t) @@ -133,30 +137,36 @@ private Stream listFiles(Path directory, UploadDirectoryRequest request) { try { boolean recursive = transferConfiguration.resolveUploadDirectoryRecursive(request); + boolean followSymbolicLinks = transferConfiguration.resolveUploadDirectoryFollowSymbolicLinks(request); if (!recursive) { - return Files.list(directory).filter(Files::isRegularFile); + return Files.list(directory) + .filter(p -> isRegularFile(p, followSymbolicLinks)); } - boolean followSymbolicLinks = transferConfiguration.resolveUploadDirectoryFollowSymbolicLinks(request); - int maxDepth = transferConfiguration.resolveUploadDirectoryMaxDepth(request); if (followSymbolicLinks) { return Files.walk(directory, maxDepth, FileVisitOption.FOLLOW_LINKS) - .peek(p -> log.trace(() -> "Processing path: " + p)) - .filter(Files::isRegularFile); + .filter(path -> isRegularFile(path, true)); } return Files.walk(directory, maxDepth) - .peek(p -> log.trace(() -> "Processing path: " + p)) - .filter(path -> Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)); + .filter(path -> isRegularFile(path, false)); } catch (IOException e) { throw SdkClientException.create("Failed to list files under the provided directory", e); } } + private boolean isRegularFile(Path path, boolean followSymlinks) { + if (followSymlinks) { + return Files.isRegularFile(path); + } + + return Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS); + } + private static String processPrefix(String prefix, String delimiter) { if (StringUtils.isEmpty(prefix)) { return ""; diff --git a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/CompletedDownloadTest.java b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/CompletedDownloadTest.java new file mode 100644 index 000000000000..a815e1ea4081 --- /dev/null +++ b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/CompletedDownloadTest.java @@ -0,0 +1,37 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.transfer.s3; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import nl.jqno.equalsverifier.EqualsVerifier; +import org.junit.Test; + +public class CompletedDownloadTest { + + @Test + public void responseNull_shouldThrowException() { + assertThatThrownBy(() -> CompletedDownload.builder().build()).isInstanceOf(NullPointerException.class) + .hasMessageContaining("must not be null"); + } + + @Test + public void equalsHashcode() { + EqualsVerifier.forClass(CompletedDownload.class) + .withNonnullFields("response") + .verify(); + } +} diff --git a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/CompletedUploadTest.java b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/CompletedUploadTest.java new file mode 100644 index 000000000000..2e79f3fbe39f --- /dev/null +++ b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/CompletedUploadTest.java @@ -0,0 +1,37 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.transfer.s3; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import nl.jqno.equalsverifier.EqualsVerifier; +import org.junit.Test; + +public class CompletedUploadTest { + + @Test + public void responseNull_shouldThrowException() { + assertThatThrownBy(() -> CompletedUpload.builder().build()).isInstanceOf(NullPointerException.class) + .hasMessageContaining("must not be null"); + } + + @Test + public void equalsHashcode() { + EqualsVerifier.forClass(CompletedUpload.class) + .withNonnullFields("response") + .verify(); + } +} diff --git a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryHelperParameterizedTest.java b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryHelperParameterizedTest.java index e6f5d4f66fe3..fe0396581f25 100644 --- a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryHelperParameterizedTest.java +++ b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryHelperParameterizedTest.java @@ -44,6 +44,7 @@ import org.mockito.ArgumentCaptor; import software.amazon.awssdk.services.s3.model.PutObjectResponse; import software.amazon.awssdk.testutils.FileUtils; +import software.amazon.awssdk.transfer.s3.CompletedUpload; import software.amazon.awssdk.transfer.s3.Upload; import software.amazon.awssdk.transfer.s3.UploadDirectoryRequest; import software.amazon.awssdk.transfer.s3.UploadDirectoryTransfer; @@ -142,6 +143,30 @@ public void uploadDirectory_recursiveFalse_shouldOnlyUploadTopLevel() { assertThat(actualRequests.get(0).putObjectRequest().key()).isEqualTo("bar.txt"); } + @Test + public void uploadDirectory_recursiveFalseFollowSymlinkTrue_shouldOnlyUploadTopLevel() { + // skip the test if we are using jimfs because it doesn't work well with symlink + assumeTrue(configuration.equals(Configuration.forCurrentPlatform())); + ArgumentCaptor requestArgumentCaptor = ArgumentCaptor.forClass(UploadRequest.class); + + when(singleUploadFunction.apply(requestArgumentCaptor.capture())).thenReturn(completedUpload()); + UploadDirectoryTransfer uploadDirectory = + tm.uploadDirectory(UploadDirectoryRequest.builder() + .sourceDirectory(directory) + .bucket("bucket") + .overrideConfiguration(o -> o.recursive(false).followSymbolicLinks(true)) + .build()); + uploadDirectory.completionFuture().join(); + + List actualRequests = requestArgumentCaptor.getAllValues(); + List keys = + actualRequests.stream().map(u -> u.putObjectRequest().key()) + .collect(Collectors.toList()); + + assertThat(keys.size()).isEqualTo(2); + assertThat(keys).containsOnly("bar.txt", "symlink2"); + } + @Test public void uploadDirectory_FollowSymlinkTrue_shouldIncludeLinkedFiles() { // skip the test if we are using jimfs because it doesn't work well with symlink @@ -165,8 +190,8 @@ public void uploadDirectory_FollowSymlinkTrue_shouldIncludeLinkedFiles() { actualRequests.stream().map(u -> u.putObjectRequest().key()) .collect(Collectors.toList()); - assertThat(keys.size()).isEqualTo(4); - assertThat(keys).containsOnly("bar.txt", "foo/1.txt", "foo/2.txt", "symlink/3.txt"); + assertThat(keys.size()).isEqualTo(5); + assertThat(keys).containsOnly("bar.txt", "foo/1.txt", "foo/2.txt", "symlink/2.txt", "symlink2"); } @Test @@ -239,9 +264,9 @@ public void uploadDirectory_maxLengthOne_shouldOnlyUploadTopLevel() { } private DefaultUpload completedUpload() { - return new DefaultUpload(CompletableFuture.completedFuture(DefaultCompletedUpload.builder() - .response(PutObjectResponse.builder().build()) - .build())); + return new DefaultUpload(CompletableFuture.completedFuture(CompletedUpload.builder() + .response(PutObjectResponse.builder().build()) + .build())); } private Path createTestDirectory() throws IOException { @@ -261,12 +286,16 @@ private Path createTestDirectory() throws IOException { * - 2.txt * - bar.txt * - symlink -> test2 + * - symlink2 -> test3/4.txt * - test2 - * - 3.txt + * - 2.txt + * - test3 + * - 4.txt */ private Path createLocalTestDirectoryWithSymLink() throws IOException { Path directory = Files.createTempDirectory("test1"); Path anotherDirectory = Files.createTempDirectory("test2"); + Path thirdDirectory = Files.createTempDirectory("test3"); String directoryName = directory.toString(); String anotherDirectoryName = anotherDirectory.toString(); @@ -277,9 +306,11 @@ private Path createLocalTestDirectoryWithSymLink() throws IOException { Files.write(Paths.get(directoryName, "foo/1.txt"), "1".getBytes(StandardCharsets.UTF_8)); Files.write(Paths.get(directoryName, "foo/2.txt"), "2".getBytes(StandardCharsets.UTF_8)); - Files.write(Paths.get(anotherDirectoryName, "3.txt"), "3".getBytes(StandardCharsets.UTF_8)); + Files.write(Paths.get(anotherDirectoryName, "2.txt"), "2".getBytes(StandardCharsets.UTF_8)); + Files.write(Paths.get(thirdDirectory.toString(), "3.txt"), "3".getBytes(StandardCharsets.UTF_8)); Files.createSymbolicLink(Paths.get(directoryName, "symlink"), anotherDirectory); + Files.createSymbolicLink(Paths.get(directoryName, "symlink2"), Paths.get(thirdDirectory.toString(), "3.txt")); return directory; } diff --git a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryHelperTest.java b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryHelperTest.java index 9bd0820bf2d9..fac56157b265 100644 --- a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryHelperTest.java +++ b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryHelperTest.java @@ -100,14 +100,14 @@ public void uploadDirectory_cancel_shouldCancelAllFutures() { public void uploadDirectory_allUploadsSucceed_failedUploadsShouldBeEmpty() throws ExecutionException, InterruptedException, TimeoutException { PutObjectResponse putObjectResponse = PutObjectResponse.builder().eTag("1234").build(); - CompletedUpload completedUpload = DefaultCompletedUpload.builder().response(putObjectResponse).build(); + CompletedUpload completedUpload = CompletedUpload.builder().response(putObjectResponse).build(); CompletableFuture successfulFuture = new CompletableFuture<>(); Upload upload = new DefaultUpload(successfulFuture); successfulFuture.complete(completedUpload); PutObjectResponse putObjectResponse2 = PutObjectResponse.builder().eTag("5678").build(); - CompletedUpload completedUpload2 = DefaultCompletedUpload.builder().response(putObjectResponse2).build(); + CompletedUpload completedUpload2 = CompletedUpload.builder().response(putObjectResponse2).build(); CompletableFuture failedFuture = new CompletableFuture<>(); Upload upload2 = new DefaultUpload(failedFuture); failedFuture.complete(completedUpload2); @@ -129,7 +129,7 @@ public void uploadDirectory_allUploadsSucceed_failedUploadsShouldBeEmpty() throw public void uploadDirectory_partialSuccess_shouldProvideFailedUploads() throws ExecutionException, InterruptedException, TimeoutException { PutObjectResponse putObjectResponse = PutObjectResponse.builder().eTag("1234").build(); - CompletedUpload completedUpload = DefaultCompletedUpload.builder().response(putObjectResponse).build(); + CompletedUpload completedUpload = CompletedUpload.builder().response(putObjectResponse).build(); CompletableFuture successfulFuture = new CompletableFuture<>(); Upload upload = new DefaultUpload(successfulFuture); successfulFuture.complete(completedUpload); From afae96baf292a0f0199987f3338221c52c302024 Mon Sep 17 00:00:00 2001 From: Zoe Wang <33073555+zoewangg@users.noreply.github.com> Date: Tue, 12 Oct 2021 11:40:17 -0700 Subject: [PATCH 5/8] Address part of the feedback --- .../awssdk/transfer/s3/CompletedUpload.java | 4 + .../transfer/s3/CompletedUploadDirectory.java | 11 +- .../awssdk/transfer/s3/DownloadRequest.java | 4 + .../transfer/s3/FailedSingleFileUpload.java | 4 + .../awssdk/transfer/s3/S3TransferManager.java | 28 +--- ...3TransferManagerOverrideConfiguration.java | 13 +- .../UploadDirectoryOverrideConfiguration.java | 6 +- .../transfer/s3/UploadDirectoryRequest.java | 4 + .../transfer/s3/UploadDirectoryTransfer.java | 4 + .../awssdk/transfer/s3/UploadRequest.java | 4 + .../s3/internal/DefaultS3TransferManager.java | 16 --- .../internal/TransferConfigurationOption.java | 4 + .../s3/internal/UploadDirectoryHelper.java | 86 ++++++++---- .../s3/internal/S3TransferManagerTest.java | 7 - ...ploadDirectoryHelperParameterizedTest.java | 123 ++++++++++++------ .../internal/UploadDirectoryHelperTest.java | 28 ++-- 16 files changed, 209 insertions(+), 137 deletions(-) diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/CompletedUpload.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/CompletedUpload.java index 9dbbbc721e22..5b101b9db41d 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/CompletedUpload.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/CompletedUpload.java @@ -71,6 +71,10 @@ public String toString() { .build(); } + public static Class serializableBuilderClass() { + return DefaultBuilder.class; + } + /** * Creates a default builder for {@link CompletedUpload}. */ diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/CompletedUploadDirectory.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/CompletedUploadDirectory.java index 91c9f2118cab..ec0a07656c30 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/CompletedUploadDirectory.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/CompletedUploadDirectory.java @@ -15,10 +15,8 @@ package software.amazon.awssdk.transfer.s3; -import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.List; import software.amazon.awssdk.annotations.SdkPreviewApi; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.utils.ToString; @@ -33,11 +31,10 @@ @SdkPublicApi @SdkPreviewApi public final class CompletedUploadDirectory implements CompletedTransfer { - private final List failedUploads; + private final Collection failedUploads; private CompletedUploadDirectory(DefaultBuilder builder) { - this.failedUploads = Collections.unmodifiableList(new ArrayList<>(Validate.paramNotNull(builder.failedUploads, - "failedUploads"))); + this.failedUploads = Collections.unmodifiableCollection(Validate.paramNotNull(builder.failedUploads, "failedUploads")); } /** @@ -100,6 +97,10 @@ public String toString() { .build(); } + public static Class serializableBuilderClass() { + return DefaultBuilder.class; + } + public interface Builder { /** diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/DownloadRequest.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/DownloadRequest.java index 94bb07c4c30a..aaf01c4e8834 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/DownloadRequest.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/DownloadRequest.java @@ -96,6 +96,10 @@ public int hashCode() { return result; } + public static Class serializableBuilderClass() { + return BuilderImpl.class; + } + /** * A builder for a {@link DownloadRequest}, created with {@link #builder()} */ diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/FailedSingleFileUpload.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/FailedSingleFileUpload.java index 10cbbe3ff3d4..40812a2f139d 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/FailedSingleFileUpload.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/FailedSingleFileUpload.java @@ -85,6 +85,10 @@ public static Builder builder() { return new DefaultBuilder(); } + public static Class serializableBuilderClass() { + return DefaultBuilder.class; + } + @Override public Builder toBuilder() { return new DefaultBuilder(this); diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/S3TransferManager.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/S3TransferManager.java index 3e142f1763d7..8c61b9ea0a6b 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/S3TransferManager.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/S3TransferManager.java @@ -176,8 +176,7 @@ default Upload upload(Consumer request) { * source directory provided does not exist for example). The future completes successfully for partial successful * requests, i.e., there might be failed uploads in the successfully completed response. As a result, * you should check for errors in the response via {@link CompletedUploadDirectory#failedUploads()} - * even when the future completes successfully. Failed single file uploads can be retried by calling - * {@link S3TransferManager#upload(UploadRequest)} + * even when the future completes successfully. * *

* The current user must have read access to all directories and files @@ -186,7 +185,7 @@ default Upload upload(Consumer request) { * Usage Example: *

      * {@code
-     * UploadDirectory uploadDirectory =
+     * UploadDirectoryTransfer uploadDirectory =
      *       transferManager.uploadDirectory(UploadDirectoryRequest.builder()
      *                                                             .sourceDirectory(Paths.get("."))
      *                                                             .bucket("bucket")
@@ -198,15 +197,6 @@ default Upload upload(Consumer request) {
      * // Print out the failed uploads
      * completedUploadDirectory.failedUploads().forEach(System.out::println);
      *
-     * // Retrying failed uploads if the exception is retryable
-     * List> futures =
-     *     completedUploadDirectory.failedUploads()
-     *                             .stream()
-     *                             .filter(failedSingleFileUpload -> isRetryable(failedSingleFileUpload.exception()))
-     *                             .map(failedSingleFileUpload ->
-     *                                  tm.upload(failedSingleFileUpload.request()).completionFuture())
-     *                             .collect(Collectors.toList());
-     * CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
      * }
      * 
* @@ -232,8 +222,7 @@ default UploadDirectoryTransfer uploadDirectory(UploadDirectoryRequest uploadDir * source directory provided does not exist for example). The future completes successfully for partial successful * requests, i.e., there might be failed uploads in the successfully completed response. As a result, * you should check for errors in the response via {@link CompletedUploadDirectory#failedUploads()} - * even when the future completes successfully. Failed single file uploads can be retried by calling - * {@link S3TransferManager#upload(UploadRequest)} + * even when the future completes successfully. * *

* The current user must have read access to all directories and files @@ -246,22 +235,13 @@ default UploadDirectoryTransfer uploadDirectory(UploadDirectoryRequest uploadDir * Usage Example: *

      * {@code
-     * UploadDirectory uploadDirectory =
+     * UploadDirectoryTransfer uploadDirectory =
      *       transferManager.uploadDirectory(b -> b.sourceDirectory(Paths.get("."))
      *                                             .bucket("key")
      *                                             .prefix("prefix"));
      * // Print out the failed uploads
      * completedUploadDirectory.failedUploads().forEach(System.out::println);
      *
-     * // Retrying failed uploads if the exception is retryable
-     * List> futures =
-     *     completedUploadDirectory.failedUploads()
-     *                             .stream()
-     *                             .filter(failedSingleFileUpload -> isRetryable(failedSingleFileUpload.exception()))
-     *                             .map(failedSingleFileUpload ->
-     *                                  tm.upload(failedSingleFileUpload.request()).completionFuture())
-     *                             .collect(Collectors.toList());
-     * CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
      * }
      * 
* @param requestBuilder the upload directory request builder diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/S3TransferManagerOverrideConfiguration.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/S3TransferManagerOverrideConfiguration.java index 1ce29dbb5ea5..4986973cdb14 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/S3TransferManagerOverrideConfiguration.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/S3TransferManagerOverrideConfiguration.java @@ -96,15 +96,22 @@ public static Builder builder() { return new DefaultBuilder(); } + public static Class serializableBuilderClass() { + return DefaultBuilder.class; + } + /** * The builder definition for a {@link S3TransferManagerOverrideConfiguration}. */ public interface Builder extends CopyableBuilder { /** - * Specify the executor that {@link S3TransferManager} will use to execute - * the request that might take some time to process before handing it off to the underlying S3 async client such as - * visiting file tree in {@link S3TransferManager#uploadDirectory(UploadDirectoryRequest)} operation. + * Specify the executor that {@link S3TransferManager} will use to execute background tasks before handing them off to + * the underlying S3 async client, such as visiting file tree in a + * {@link S3TransferManager#uploadDirectory(UploadDirectoryRequest)} operation + * + *

+ * The SDK will create an executor if not provided * *

* This executor must be shut down by the user when it is ready to be disposed. The SDK will not close the executor diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryOverrideConfiguration.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryOverrideConfiguration.java index a143d9d9d207..22c2c45dd9aa 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryOverrideConfiguration.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryOverrideConfiguration.java @@ -116,6 +116,10 @@ public static Builder builder() { return new DefaultBuilder(); } + public static Class serializableBuilderClass() { + return DefaultBuilder.class; + } + public interface Builder extends CopyableBuilder { /** @@ -141,7 +145,7 @@ public interface Builder extends CopyableBuilder * Default to {@code Integer.MAX_VALUE} diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryRequest.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryRequest.java index 3e29405cb4ae..ecdca160aa2e 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryRequest.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryRequest.java @@ -98,6 +98,10 @@ public static Builder builder() { return new DefaultBuilder(); } + public static Class serializableBuilderClass() { + return DefaultBuilder.class; + } + @Override public Builder toBuilder() { return new DefaultBuilder(this); diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryTransfer.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryTransfer.java index 488b2ea73e42..39ef2d7b6736 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryTransfer.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryTransfer.java @@ -37,6 +37,10 @@ public static Builder builder() { return new DefaultBuilder(); } + public static Class serializableBuilderClass() { + return DefaultBuilder.class; + } + public interface Builder { /** diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadRequest.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadRequest.java index c45162040a8c..d475f38f20f7 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadRequest.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadRequest.java @@ -70,6 +70,10 @@ public static Builder builder() { return new BuilderImpl(); } + public static Class serializableBuilderClass() { + return BuilderImpl.class; + } + @Override public Builder toBuilder() { return new BuilderImpl(); diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultS3TransferManager.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultS3TransferManager.java index 16cf7f3d6c44..c0f1c447e249 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultS3TransferManager.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultS3TransferManager.java @@ -15,9 +15,6 @@ package software.amazon.awssdk.transfer.s3.internal; -import java.nio.file.Files; -import java.nio.file.LinkOption; -import java.nio.file.Path; import java.util.concurrent.CompletableFuture; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.annotations.SdkTestInternalApi; @@ -112,19 +109,6 @@ public Upload upload(UploadRequest uploadRequest) { public UploadDirectoryTransfer uploadDirectory(UploadDirectoryRequest uploadDirectoryRequest) { try { Validate.paramNotNull(uploadDirectoryRequest, "uploadDirectoryRequest"); - Path directory = uploadDirectoryRequest.sourceDirectory(); - - Validate.isTrue(Files.exists(directory), "The source directory (%s) provided does not exist", directory); - boolean followSymbolicLinks = transferConfiguration.resolveUploadDirectoryFollowSymbolicLinks(uploadDirectoryRequest); - if (followSymbolicLinks) { - Validate.isFalse(Files.isRegularFile(directory), "The source directory (%s) provided is not a " - + "directory", directory); - } else { - Validate.isFalse(Files.isRegularFile(directory, LinkOption.NOFOLLOW_LINKS), "The source directory (%s) provided" - + " is not a " - + "directory", directory); - } - assertNotObjectLambdaArn(uploadDirectoryRequest.bucket(), "uploadDirectory"); return uploadDirectoryManager.uploadDirectory(uploadDirectoryRequest); diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/TransferConfigurationOption.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/TransferConfigurationOption.java index 8e30e837ed53..cfff0f42040e 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/TransferConfigurationOption.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/TransferConfigurationOption.java @@ -38,6 +38,10 @@ public final class TransferConfigurationOption extends AttributeMap.Key { public static final TransferConfigurationOption EXECUTOR = new TransferConfigurationOption<>("Executor", Executor.class); + + public static final Character DEFAULT_DELIMITER_CHARACTER = '/'; + public static final String DEFAULT_DELIMITER = DEFAULT_DELIMITER_CHARACTER.toString(); + private static final int DEFAULT_UPLOAD_DIRECTORY_MAX_DEPTH = Integer.MAX_VALUE; private static final Boolean DEFAULT_UPLOAD_DIRECTORY_RECURSIVE = Boolean.TRUE; diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryHelper.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryHelper.java index e95717482d06..8eecf4f84da6 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryHelper.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryHelper.java @@ -15,6 +15,9 @@ package software.amazon.awssdk.transfer.s3.internal; +import static software.amazon.awssdk.transfer.s3.internal.TransferConfigurationOption.DEFAULT_DELIMITER; +import static software.amazon.awssdk.transfer.s3.internal.TransferConfigurationOption.DEFAULT_DELIMITER_CHARACTER; + import java.io.IOException; import java.nio.file.FileSystem; import java.nio.file.FileSystems; @@ -22,12 +25,12 @@ import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; +import java.util.Queue; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Phaser; +import java.util.concurrent.ConcurrentLinkedQueue; import java.util.function.Function; +import java.util.stream.Collectors; import java.util.stream.Stream; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.annotations.SdkTestInternalApi; @@ -44,6 +47,7 @@ import software.amazon.awssdk.utils.CompletableFutureUtils; import software.amazon.awssdk.utils.Logger; import software.amazon.awssdk.utils.StringUtils; +import software.amazon.awssdk.utils.Validate; /** * An internal helper class that traverses the file tree and send the upload request @@ -52,7 +56,6 @@ @SdkInternalApi public class UploadDirectoryHelper { private static final Logger log = Logger.loggerFor(S3TransferManager.class); - private static final String DEFAULT_DELIMITER = "/"; private final TransferManagerConfiguration transferConfiguration; private final Function uploadFunction; @@ -82,7 +85,12 @@ public UploadDirectoryTransfer uploadDirectory(UploadDirectoryRequest uploadDire // offload the execution to the transfer manager executor CompletableFuture.runAsync(() -> doUploadDirectory(returnFuture, uploadDirectoryRequest), - transferConfiguration.option(TransferConfigurationOption.EXECUTOR)); + transferConfiguration.option(TransferConfigurationOption.EXECUTOR)) + .handle((ignore, t) -> { + // should never execute this + returnFuture.completeExceptionally(t); + return ignore; + }); return UploadDirectoryTransfer.builder().completionFuture(returnFuture).build(); } @@ -90,39 +98,55 @@ public UploadDirectoryTransfer uploadDirectory(UploadDirectoryRequest uploadDire private void doUploadDirectory(CompletableFuture returnFuture, UploadDirectoryRequest uploadDirectoryRequest) { - Path directory = uploadDirectoryRequest.sourceDirectory(); - List failedUploads = Collections.synchronizedList(new ArrayList<>()); - Phaser phaser = new Phaser(1); + try { + Path directory = uploadDirectoryRequest.sourceDirectory(); - try (Stream entries = listFiles(directory, uploadDirectoryRequest)) { - entries.forEach(path -> { - CompletableFuture future = - uploadSingleFile(uploadDirectoryRequest, failedUploads, path, phaser); + validateDirectory(uploadDirectoryRequest); + + Queue failedUploads = new ConcurrentLinkedQueue<>(); + List> futures; + + try (Stream entries = listFiles(directory, uploadDirectoryRequest)) { + futures = entries.map(path -> { + CompletableFuture future = uploadSingleFile(uploadDirectoryRequest, + failedUploads, path); + + // Forward cancellation of the return future to all individual futures. + CompletableFutureUtils.forwardExceptionTo(returnFuture, future); + return future; + }).collect(Collectors.toList()); + } - // Forward cancellation of the return future to all individual futures. - CompletableFutureUtils.forwardExceptionTo(returnFuture, future); + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).whenComplete((r, t) -> { + returnFuture.complete(CompletedUploadDirectory.builder().failedUploads(failedUploads).build()); }); + } catch (Throwable throwable) { + returnFuture.completeExceptionally(throwable); } + } - // Wait for all sub transfers to complete - phaser.arriveAndAwaitAdvance(); - phaser.arriveAndDeregister(); - returnFuture.complete(CompletedUploadDirectory.builder().failedUploads(failedUploads).build()); + private void validateDirectory(UploadDirectoryRequest uploadDirectoryRequest) { + Path directory = uploadDirectoryRequest.sourceDirectory(); + Validate.isTrue(Files.exists(directory), "The source directory (%s) provided does not exist", directory); + boolean followSymbolicLinks = transferConfiguration.resolveUploadDirectoryFollowSymbolicLinks(uploadDirectoryRequest); + if (followSymbolicLinks) { + Validate.isTrue(Files.isDirectory(directory), "The source directory (%s) provided is not a " + + "directory", directory); + } else { + Validate.isTrue(Files.isDirectory(directory, LinkOption.NOFOLLOW_LINKS), "The source directory (%s) provided" + + " is not a " + + "directory", directory); + } } private CompletableFuture uploadSingleFile(UploadDirectoryRequest uploadDirectoryRequest, - List failedUploads, - Path path, - Phaser phaser) { - phaser.register(); + Queue failedUploads, + Path path) { int nameCount = uploadDirectoryRequest.sourceDirectory().getNameCount(); UploadRequest uploadRequest = constructUploadRequest(uploadDirectoryRequest, nameCount, path); log.debug(() -> String.format("Sending upload request (%s) for path (%s)", uploadRequest, path)); CompletableFuture future = uploadFunction.apply(uploadRequest).completionFuture(); future.whenComplete((r, t) -> { - // notify the future is completed - phaser.arriveAndDeregister(); - if (t != null) { failedUploads.add(FailedSingleFileUpload.builder() .exception(t) @@ -155,7 +179,7 @@ private Stream listFiles(Path directory, UploadDirectoryRequest request) { .filter(path -> isRegularFile(path, false)); } catch (IOException e) { - throw SdkClientException.create("Failed to list files under the provided directory", e); + throw SdkClientException.create("Failed to list files within the provided directory: " + directory, e); } } @@ -167,7 +191,7 @@ private boolean isRegularFile(Path path, boolean followSymlinks) { return Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS); } - private static String processPrefix(String prefix, String delimiter) { + private static String normalizePrefix(String prefix, String delimiter) { if (StringUtils.isEmpty(prefix)) { return ""; } @@ -179,6 +203,12 @@ private String getRelativePathName(int directoryNameCount, Path path, String del path.getNameCount()).toString(); String separator = fileSystem.getSeparator(); + + if (delimiter.equals(DEFAULT_DELIMITER)) { + // Optimizing for the default case: if delimiter is not overridden, skip replacing unless it's Windows + return separator.equals("\\") ? relativePathName.replace('\\', DEFAULT_DELIMITER_CHARACTER) : relativePathName; + } + return relativePathName.replace(separator, delimiter); } @@ -190,7 +220,7 @@ private UploadRequest constructUploadRequest(UploadDirectoryRequest uploadDirect .orElse(DEFAULT_DELIMITER); String prefix = uploadDirectoryRequest.prefix() - .map(s -> processPrefix(s, delimiter)) + .map(s -> normalizePrefix(s, delimiter)) .orElse(""); String relativePathName = getRelativePathName(directoryNameCount, path, delimiter); diff --git a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/S3TransferManagerTest.java b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/S3TransferManagerTest.java index b65bafcc65f3..b12fdcfe107b 100644 --- a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/S3TransferManagerTest.java +++ b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/S3TransferManagerTest.java @@ -147,13 +147,6 @@ public void objectLambdaArnBucketProvided_shouldThrowException() { .hasMessageContaining("support S3 Object Lambda resources").hasCauseInstanceOf(IllegalArgumentException.class); } - @Test - public void uploadDirectory_directoryNotExist_shouldCompleteFutureExceptionally() { - assertThatThrownBy(() -> tm.uploadDirectory(u -> u.sourceDirectory(Paths.get("randomstringneverexistas234ersaf1231")) - .bucket("bucketName")).completionFuture().join()) - .hasMessageContaining("does not exist").hasCauseInstanceOf(IllegalArgumentException.class); - } - @Test public void uploadDirectory_throwException_shouldCompleteFutureExceptionally() { RuntimeException exception = new RuntimeException("test"); diff --git a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryHelperParameterizedTest.java b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryHelperParameterizedTest.java index fe0396581f25..a41061c2c14c 100644 --- a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryHelperParameterizedTest.java +++ b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryHelperParameterizedTest.java @@ -16,6 +16,7 @@ package software.amazon.awssdk.transfer.s3.internal; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.Assume.assumeTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -61,7 +62,7 @@ public class UploadDirectoryHelperParameterizedTest { Configuration.windows(), Configuration.forCurrentPlatform())); private Function singleUploadFunction; - private UploadDirectoryHelper tm; + private UploadDirectoryHelper uploadDirectoryHelper; private Path directory; @Parameterized.Parameter @@ -80,9 +81,9 @@ public void methodSetup() throws IOException { if (!configuration.equals(Configuration.forCurrentPlatform())) { jimfs = Jimfs.newFileSystem(configuration); - tm = new UploadDirectoryHelper(TransferManagerConfiguration.builder().build(), singleUploadFunction, jimfs); + uploadDirectoryHelper = new UploadDirectoryHelper(TransferManagerConfiguration.builder().build(), singleUploadFunction, jimfs); } else { - tm = new UploadDirectoryHelper(TransferManagerConfiguration.builder().build(), singleUploadFunction); + uploadDirectoryHelper = new UploadDirectoryHelper(TransferManagerConfiguration.builder().build(), singleUploadFunction); } directory = createTestDirectory(); } @@ -103,11 +104,11 @@ public void uploadDirectory_defaultSetting_shouldRecursivelyUpload() { when(singleUploadFunction.apply(requestArgumentCaptor.capture())) .thenReturn(completedUpload()); UploadDirectoryTransfer uploadDirectory = - tm.uploadDirectory(UploadDirectoryRequest.builder() - .sourceDirectory(directory) - .bucket("bucket") - .overrideConfiguration(o -> o.followSymbolicLinks(false)) - .build()); + uploadDirectoryHelper.uploadDirectory(UploadDirectoryRequest.builder() + .sourceDirectory(directory) + .bucket("bucket") + .overrideConfiguration(o -> o.followSymbolicLinks(false)) + .build()); uploadDirectory.completionFuture().join(); List actualRequests = requestArgumentCaptor.getAllValues(); @@ -128,11 +129,11 @@ public void uploadDirectory_recursiveFalse_shouldOnlyUploadTopLevel() { when(singleUploadFunction.apply(requestArgumentCaptor.capture())).thenReturn(completedUpload()); UploadDirectoryTransfer uploadDirectory = - tm.uploadDirectory(UploadDirectoryRequest.builder() - .sourceDirectory(directory) - .bucket("bucket") - .overrideConfiguration(o -> o.recursive(false)) - .build()); + uploadDirectoryHelper.uploadDirectory(UploadDirectoryRequest.builder() + .sourceDirectory(directory) + .bucket("bucket") + .overrideConfiguration(o -> o.recursive(false)) + .build()); uploadDirectory.completionFuture().join(); List actualRequests = requestArgumentCaptor.getAllValues(); @@ -151,11 +152,11 @@ public void uploadDirectory_recursiveFalseFollowSymlinkTrue_shouldOnlyUploadTopL when(singleUploadFunction.apply(requestArgumentCaptor.capture())).thenReturn(completedUpload()); UploadDirectoryTransfer uploadDirectory = - tm.uploadDirectory(UploadDirectoryRequest.builder() - .sourceDirectory(directory) - .bucket("bucket") - .overrideConfiguration(o -> o.recursive(false).followSymbolicLinks(true)) - .build()); + uploadDirectoryHelper.uploadDirectory(UploadDirectoryRequest.builder() + .sourceDirectory(directory) + .bucket("bucket") + .overrideConfiguration(o -> o.recursive(false).followSymbolicLinks(true)) + .build()); uploadDirectory.completionFuture().join(); List actualRequests = requestArgumentCaptor.getAllValues(); @@ -176,11 +177,11 @@ public void uploadDirectory_FollowSymlinkTrue_shouldIncludeLinkedFiles() { when(singleUploadFunction.apply(requestArgumentCaptor.capture())).thenReturn(completedUpload()); UploadDirectoryTransfer uploadDirectory = - tm.uploadDirectory(UploadDirectoryRequest.builder() - .sourceDirectory(directory) - .bucket("bucket") - .overrideConfiguration(o -> o.followSymbolicLinks(true)) - .build()); + uploadDirectoryHelper.uploadDirectory(UploadDirectoryRequest.builder() + .sourceDirectory(directory) + .bucket("bucket") + .overrideConfiguration(o -> o.followSymbolicLinks(true)) + .build()); uploadDirectory.completionFuture().join(); List actualRequests = requestArgumentCaptor.getAllValues(); @@ -200,11 +201,11 @@ public void uploadDirectory_withPrefix_keysShouldHavePrefix() { when(singleUploadFunction.apply(requestArgumentCaptor.capture())).thenReturn(completedUpload()); UploadDirectoryTransfer uploadDirectory = - tm.uploadDirectory(UploadDirectoryRequest.builder() - .sourceDirectory(directory) - .bucket("bucket") - .prefix("yolo") - .build()); + uploadDirectoryHelper.uploadDirectory(UploadDirectoryRequest.builder() + .sourceDirectory(directory) + .bucket("bucket") + .prefix("yolo") + .build()); uploadDirectory.completionFuture().join(); List keys = @@ -221,12 +222,12 @@ public void uploadDirectory_withDelimiter_shouldHonor() { when(singleUploadFunction.apply(requestArgumentCaptor.capture())).thenReturn(completedUpload()); UploadDirectoryTransfer uploadDirectory = - tm.uploadDirectory(UploadDirectoryRequest.builder() - .sourceDirectory(directory) - .bucket("bucket") - .delimiter(",") - .prefix("yolo") - .build()); + uploadDirectoryHelper.uploadDirectory(UploadDirectoryRequest.builder() + .sourceDirectory(directory) + .bucket("bucket") + .delimiter(",") + .prefix("yolo") + .build()); uploadDirectory.completionFuture().join(); List keys = @@ -244,11 +245,11 @@ public void uploadDirectory_maxLengthOne_shouldOnlyUploadTopLevel() { when(singleUploadFunction.apply(requestArgumentCaptor.capture())) .thenReturn(completedUpload()); UploadDirectoryTransfer uploadDirectory = - tm.uploadDirectory(UploadDirectoryRequest.builder() - .sourceDirectory(directory) - .bucket("bucket") - .overrideConfiguration(o -> o.maxDepth(1)) - .build()); + uploadDirectoryHelper.uploadDirectory(UploadDirectoryRequest.builder() + .sourceDirectory(directory) + .bucket("bucket") + .overrideConfiguration(o -> o.maxDepth(1)) + .build()); uploadDirectory.completionFuture().join(); List actualRequests = requestArgumentCaptor.getAllValues(); @@ -263,6 +264,50 @@ public void uploadDirectory_maxLengthOne_shouldOnlyUploadTopLevel() { assertThat(keys).containsOnly("bar.txt"); } + + @Test + public void uploadDirectory_directoryNotExist_shouldCompleteFutureExceptionally() { + assertThatThrownBy(() -> uploadDirectoryHelper.uploadDirectory(UploadDirectoryRequest.builder().sourceDirectory(Paths.get( + "randomstringneverexistas234ersaf1231")) + .bucket("bucketName").build()).completionFuture().join()) + .hasMessageContaining("does not exist").hasCauseInstanceOf(IllegalArgumentException.class); + } + + @Test + public void uploadDirectory_notDirectory_shouldCompleteFutureExceptionally() { + // skip the test if we are using jimfs because it doesn't work well with symlink + assumeTrue(configuration.equals(Configuration.forCurrentPlatform())); + assertThatThrownBy(() -> uploadDirectoryHelper.uploadDirectory(UploadDirectoryRequest.builder().sourceDirectory(Paths.get(directory.toString(), "symlink")) + .bucket("bucketName").build()).completionFuture().join()) + .hasMessageContaining("is not a directory").hasCauseInstanceOf(IllegalArgumentException.class); + } + + @Test + public void uploadDirectory_notDirectoryFollowSymlinkTrue_shouldCompleteSuccessfully() { + // skip the test if we are using jimfs because it doesn't work well with symlink + assumeTrue(configuration.equals(Configuration.forCurrentPlatform())); + ArgumentCaptor requestArgumentCaptor = ArgumentCaptor.forClass(UploadRequest.class); + + when(singleUploadFunction.apply(requestArgumentCaptor.capture())).thenReturn(completedUpload()); + UploadDirectoryTransfer uploadDirectory = uploadDirectoryHelper.uploadDirectory(UploadDirectoryRequest.builder() + .overrideConfiguration(o -> o.followSymbolicLinks(true)) + .sourceDirectory(Paths.get(directory.toString(), "symlink")) + .bucket("bucket").build()); + + uploadDirectory.completionFuture().join(); + + List actualRequests = requestArgumentCaptor.getAllValues(); + actualRequests.forEach(r -> assertThat(r.putObjectRequest().bucket()).isEqualTo("bucket")); + + assertThat(actualRequests.size()).isEqualTo(1); + + List keys = + actualRequests.stream().map(u -> u.putObjectRequest().key()) + .collect(Collectors.toList()); + + assertThat(keys).containsOnly("2.txt"); + } + private DefaultUpload completedUpload() { return new DefaultUpload(CompletableFuture.completedFuture(CompletedUpload.builder() .response(PutObjectResponse.builder().build()) diff --git a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryHelperTest.java b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryHelperTest.java index fac56157b265..205bbb54dd0d 100644 --- a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryHelperTest.java +++ b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryHelperTest.java @@ -49,7 +49,7 @@ public class UploadDirectoryHelperTest { private static FileSystem jimfs; private static Path directory; private Function singleUploadFunction; - private UploadDirectoryHelper tm; + private UploadDirectoryHelper uploadDirectoryHelper; @BeforeClass public static void setUp() throws IOException { @@ -68,7 +68,7 @@ public static void tearDown() throws IOException { @Before public void methodSetup() { singleUploadFunction = mock(Function.class); - tm = new UploadDirectoryHelper(TransferManagerConfiguration.builder().build(), singleUploadFunction); + uploadDirectoryHelper = new UploadDirectoryHelper(TransferManagerConfiguration.builder().build(), singleUploadFunction); } @Test @@ -82,10 +82,10 @@ public void uploadDirectory_cancel_shouldCancelAllFutures() { when(singleUploadFunction.apply(any(UploadRequest.class))).thenReturn(upload, upload2); UploadDirectoryTransfer uploadDirectory = - tm.uploadDirectory(UploadDirectoryRequest.builder() - .sourceDirectory(directory) - .bucket("bucket") - .build()); + uploadDirectoryHelper.uploadDirectory(UploadDirectoryRequest.builder() + .sourceDirectory(directory) + .bucket("bucket") + .build()); uploadDirectory.completionFuture().cancel(true); @@ -115,10 +115,10 @@ public void uploadDirectory_allUploadsSucceed_failedUploadsShouldBeEmpty() throw when(singleUploadFunction.apply(any(UploadRequest.class))).thenReturn(upload, upload2); UploadDirectoryTransfer uploadDirectory = - tm.uploadDirectory(UploadDirectoryRequest.builder() - .sourceDirectory(directory) - .bucket("bucket") - .build()); + uploadDirectoryHelper.uploadDirectory(UploadDirectoryRequest.builder() + .sourceDirectory(directory) + .bucket("bucket") + .build()); CompletedUploadDirectory completedUploadDirectory = uploadDirectory.completionFuture().get(5, TimeUnit.SECONDS); @@ -142,10 +142,10 @@ public void uploadDirectory_partialSuccess_shouldProvideFailedUploads() throws E when(singleUploadFunction.apply(any(UploadRequest.class))).thenReturn(upload, upload2); UploadDirectoryTransfer uploadDirectory = - tm.uploadDirectory(UploadDirectoryRequest.builder() - .sourceDirectory(directory) - .bucket("bucket") - .build()); + uploadDirectoryHelper.uploadDirectory(UploadDirectoryRequest.builder() + .sourceDirectory(directory) + .bucket("bucket") + .build()); CompletedUploadDirectory completedUploadDirectory = uploadDirectory.completionFuture().get(5, TimeUnit.SECONDS); From 27d2ef665ca369de439d06ea88474a624e64ae0a Mon Sep 17 00:00:00 2001 From: Zoe Wang <33073555+zoewangg@users.noreply.github.com> Date: Tue, 12 Oct 2021 15:14:29 -0700 Subject: [PATCH 6/8] address feedback --- services-custom/s3-transfer-manager/pom.xml | 5 +++ .../UploadDirectoryOverrideConfiguration.java | 1 + .../transfer/s3/UploadDirectoryRequest.java | 28 +++++++++++++ .../s3/internal/DefaultS3TransferManager.java | 40 ++++++++++++++----- .../TransferManagerConfiguration.java | 9 ++--- .../s3/internal/UploadDirectoryHelper.java | 11 +++-- .../s3/internal/S3TransferManagerTest.java | 16 ++++++++ 7 files changed, 92 insertions(+), 18 deletions(-) diff --git a/services-custom/s3-transfer-manager/pom.xml b/services-custom/s3-transfer-manager/pom.xml index 1fcd1ccffcb2..f4a0601c159f 100644 --- a/services-custom/s3-transfer-manager/pom.xml +++ b/services-custom/s3-transfer-manager/pom.xml @@ -86,6 +86,11 @@ http-client-spi ${awsjavasdk.version} + + software.amazon.awssdk + arns + ${awsjavasdk.version} + software.amazon.awssdk auth diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryOverrideConfiguration.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryOverrideConfiguration.java index 22c2c45dd9aa..a4bf7135ebb1 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryOverrideConfiguration.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryOverrideConfiguration.java @@ -120,6 +120,7 @@ public static Class serializableBuilderClass() { return DefaultBuilder.class; } + // TODO: consider consolidating maxDepth and recursive public interface Builder extends CopyableBuilder { /** diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryRequest.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryRequest.java index ecdca160aa2e..fcf9cbb1c46c 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryRequest.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/UploadDirectoryRequest.java @@ -174,8 +174,22 @@ public interface Builder extends CopyableBuilderOrganizing objects using * prefixes * + *

+ * Note: if the provided prefix ends with the same string as delimiter, it will get "normalized" when generating the key + * name. For example, assuming the prefix provided is "foo/" and the delimiter is "/" and the source directory has the + * following structure: + * + *

+         * |- test
+         *     |- obj1.txt
+         *     |- obj2.txt
+         * 
+ * + * the object keys will be "foo/obj1.txt" and "foo/obj2.txt" as apposed to "foo//obj1.txt" and "foo//obj2.txt" + * * @param prefix the key prefix * @return This builder for method chaining. + * @see #delimiter(String) */ Builder prefix(String prefix); @@ -187,8 +201,22 @@ public interface Builder extends CopyableBuilderOrganizing objects using * prefixes * + *

+ * Note: if the provided prefix ends with the same string as delimiter, it will get "normalized" when generating the key + * name. For example, assuming the prefix provided is "foo/" and the delimiter is "/" and the source directory has the + * following structure: + * + *

+         * |- test
+         *     |- obj1.txt
+         *     |- obj2.txt
+         * 
+ * + * the object keys will be "foo/obj1.txt" and "foo/obj2.txt" as apposed to "foo//obj1.txt" and "foo//obj2.txt" + * * @param delimiter the delimiter * @return This builder for method chaining. + * @see #prefix(String) */ Builder delimiter(String delimiter); diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultS3TransferManager.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultS3TransferManager.java index c0f1c447e249..4f33172bc1b4 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultS3TransferManager.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/DefaultS3TransferManager.java @@ -18,10 +18,14 @@ import java.util.concurrent.CompletableFuture; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.annotations.SdkTestInternalApi; +import software.amazon.awssdk.arns.Arn; import software.amazon.awssdk.core.async.AsyncRequestBody; import software.amazon.awssdk.core.async.AsyncResponseTransformer; import software.amazon.awssdk.core.client.config.ClientAsyncConfiguration; import software.amazon.awssdk.core.client.config.SdkAdvancedAsyncClientOption; +import software.amazon.awssdk.services.s3.internal.resource.S3AccessPointResource; +import software.amazon.awssdk.services.s3.internal.resource.S3ArnConverter; +import software.amazon.awssdk.services.s3.internal.resource.S3Resource; import software.amazon.awssdk.services.s3.model.GetObjectResponse; import software.amazon.awssdk.services.s3.model.PutObjectRequest; import software.amazon.awssdk.services.s3.model.PutObjectResponse; @@ -89,7 +93,7 @@ private S3CrtAsyncClient initializeS3CrtClient(DefaultBuilder tmBuilder) { public Upload upload(UploadRequest uploadRequest) { try { Validate.paramNotNull(uploadRequest, "uploadRequest"); - assertNotObjectLambdaArn(uploadRequest.putObjectRequest().bucket(), "upload"); + assertNotUnsupportedArn(uploadRequest.putObjectRequest().bucket(), "upload"); PutObjectRequest putObjectRequest = uploadRequest.putObjectRequest(); AsyncRequestBody requestBody = requestBodyFor(uploadRequest); @@ -109,7 +113,7 @@ public Upload upload(UploadRequest uploadRequest) { public UploadDirectoryTransfer uploadDirectory(UploadDirectoryRequest uploadDirectoryRequest) { try { Validate.paramNotNull(uploadDirectoryRequest, "uploadDirectoryRequest"); - assertNotObjectLambdaArn(uploadDirectoryRequest.bucket(), "uploadDirectory"); + assertNotUnsupportedArn(uploadDirectoryRequest.bucket(), "uploadDirectory"); return uploadDirectoryManager.uploadDirectory(uploadDirectoryRequest); } catch (Throwable throwable) { @@ -121,7 +125,7 @@ public UploadDirectoryTransfer uploadDirectory(UploadDirectoryRequest uploadDire public Download download(DownloadRequest downloadRequest) { try { Validate.paramNotNull(downloadRequest, "downloadRequest"); - assertNotObjectLambdaArn(downloadRequest.getObjectRequest().bucket(), "download"); + assertNotUnsupportedArn(downloadRequest.getObjectRequest().bucket(), "download"); CompletableFuture getObjectFuture = s3CrtAsyncClient.getObject(downloadRequest.getObjectRequest(), @@ -145,19 +149,37 @@ public static Builder builder() { return new DefaultBuilder(); } - private static void assertNotObjectLambdaArn(String arn, String operation) { - if (isObjectLambdaArn(arn)) { + private static void assertNotUnsupportedArn(String bucket, String operation) { + if (!bucket.startsWith("arn:")) { + return; + } + + if (isObjectLambdaArn(bucket)) { String error = String.format("%s does not support S3 Object Lambda resources", operation); throw new IllegalArgumentException(error); } + + Arn arn = Arn.fromString(bucket); + + if (isMrapArn(arn)) { + String error = String.format("%s does not support S3 multi-region access point ARN", operation); + throw new IllegalArgumentException(error); + } } private static boolean isObjectLambdaArn(String arn) { - if (arn == null) { - return false; - } + return arn.contains(":s3-object-lambda"); + } + + private static boolean isMrapArn(Arn arn) { + S3Resource s3Resource = S3ArnConverter.create().convertArn(arn); + + S3AccessPointResource s3EndpointResource = + Validate.isInstanceOf(S3AccessPointResource.class, s3Resource, + "An ARN was passed as a bucket parameter to an S3 operation, however it does not " + + "appear to be a valid S3 access point ARN."); - return arn.startsWith("arn:") && arn.contains(":s3-object-lambda"); + return !s3EndpointResource.region().isPresent(); } private AsyncRequestBody requestBodyFor(UploadRequest uploadRequest) { diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/TransferManagerConfiguration.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/TransferManagerConfiguration.java index 05bc70f92674..05ca8a64d5e1 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/TransferManagerConfiguration.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/TransferManagerConfiguration.java @@ -97,12 +97,11 @@ public void close() { options.close(); } + // TODO: revisit this before GA private Executor defaultExecutor() { - int processors = Runtime.getRuntime().availableProcessors(); - int corePoolSize = Math.max(8, processors); - int maxPoolSize = Math.max(64, processors * 2); - ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maxPoolSize, - 10, TimeUnit.SECONDS, + int maxPoolSize = 100; + ThreadPoolExecutor executor = new ThreadPoolExecutor(0, maxPoolSize, + 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1_000), new ThreadFactoryBuilder() .threadNamePrefix("s3-transfer-manager").build()); diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryHelper.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryHelper.java index 8eecf4f84da6..8419e5c27a22 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryHelper.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryHelper.java @@ -16,7 +16,6 @@ package software.amazon.awssdk.transfer.s3.internal; import static software.amazon.awssdk.transfer.s3.internal.TransferConfigurationOption.DEFAULT_DELIMITER; -import static software.amazon.awssdk.transfer.s3.internal.TransferConfigurationOption.DEFAULT_DELIMITER_CHARACTER; import java.io.IOException; import java.nio.file.FileSystem; @@ -191,6 +190,9 @@ private boolean isRegularFile(Path path, boolean followSymlinks) { return Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS); } + /** + * If the prefix already ends with the same string as delimiter, there is no need to add delimiter. + */ private static String normalizePrefix(String prefix, String delimiter) { if (StringUtils.isEmpty(prefix)) { return ""; @@ -204,9 +206,10 @@ private String getRelativePathName(int directoryNameCount, Path path, String del String separator = fileSystem.getSeparator(); - if (delimiter.equals(DEFAULT_DELIMITER)) { - // Optimizing for the default case: if delimiter is not overridden, skip replacing unless it's Windows - return separator.equals("\\") ? relativePathName.replace('\\', DEFAULT_DELIMITER_CHARACTER) : relativePathName; + // Optimization for the case where separator equals to the delimiter: there is no need to call String#replace which + // invokes Pattern#compile in Java 8 + if (delimiter.equals(separator)) { + return relativePathName; } return relativePathName.replace(separator, delimiter); diff --git a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/S3TransferManagerTest.java b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/S3TransferManagerTest.java index b12fdcfe107b..a1bad44562d4 100644 --- a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/S3TransferManagerTest.java +++ b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/S3TransferManagerTest.java @@ -147,6 +147,22 @@ public void objectLambdaArnBucketProvided_shouldThrowException() { .hasMessageContaining("support S3 Object Lambda resources").hasCauseInstanceOf(IllegalArgumentException.class); } + @Test + public void mrapArnProvided_shouldThrowException() { + String mrapArn = "arn:aws:s3::123456789012:accesspoint:mfzwi23gnjvgw.mrap"; + assertThatThrownBy(() -> tm.upload(b -> b.putObjectRequest(p -> p.bucket(mrapArn) + .key("key")).source(Paths.get("."))) + .completionFuture().join()) + .hasMessageContaining("multi-region access point ARN").hasCauseInstanceOf(IllegalArgumentException.class); + + assertThatThrownBy(() -> tm.download(b -> b.getObjectRequest(p -> p.bucket(mrapArn) + .key("key")).destination(Paths.get("."))).completionFuture().join()) + .hasMessageContaining("multi-region access point ARN").hasCauseInstanceOf(IllegalArgumentException.class); + + assertThatThrownBy(() -> tm.uploadDirectory(b -> b.bucket(mrapArn).sourceDirectory(Paths.get("."))).completionFuture().join()) + .hasMessageContaining("multi-region access point ARN").hasCauseInstanceOf(IllegalArgumentException.class); + } + @Test public void uploadDirectory_throwException_shouldCompleteFutureExceptionally() { RuntimeException exception = new RuntimeException("test"); From 61f0ad3ea2ae2631ab8139a071e23376e9e53a96 Mon Sep 17 00:00:00 2001 From: Zoe Wang <33073555+zoewangg@users.noreply.github.com> Date: Tue, 12 Oct 2021 17:13:33 -0700 Subject: [PATCH 7/8] Address feedback --- .../transfer/s3/CompletedUploadDirectory.java | 16 ++--- ...eFileUpload.java => FailedFileUpload.java} | 20 +++--- .../internal/TransferConfigurationOption.java | 4 +- .../s3/internal/UploadDirectoryHelper.java | 63 +++++++++---------- .../s3/FailedSingleFileUploadTest.java | 10 +-- 5 files changed, 54 insertions(+), 59 deletions(-) rename services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/{FailedSingleFileUpload.java => FailedFileUpload.java} (85%) diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/CompletedUploadDirectory.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/CompletedUploadDirectory.java index ec0a07656c30..db7fe32992f0 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/CompletedUploadDirectory.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/CompletedUploadDirectory.java @@ -31,7 +31,7 @@ @SdkPublicApi @SdkPreviewApi public final class CompletedUploadDirectory implements CompletedTransfer { - private final Collection failedUploads; + private final Collection failedUploads; private CompletedUploadDirectory(DefaultBuilder builder) { this.failedUploads = Collections.unmodifiableCollection(Validate.paramNotNull(builder.failedUploads, "failedUploads")); @@ -60,7 +60,7 @@ private CompletedUploadDirectory(DefaultBuilder builder) { * * @return a list of failed uploads */ - public Collection failedUploads() { + public Collection failedUploads() { return failedUploads; } @@ -104,12 +104,12 @@ public static Class serializableBuilderClass() { public interface Builder { /** - * Sets a collection of {@link FailedSingleFileUpload}s + * Sets a collection of {@link FailedFileUpload}s * * @param failedUploads failed uploads * @return This builder for method chaining. */ - Builder failedUploads(Collection failedUploads); + Builder failedUploads(Collection failedUploads); /** * Builds a {@link CompletedUploadDirectory} based on the properties supplied to this builder @@ -119,22 +119,22 @@ public interface Builder { } private static final class DefaultBuilder implements Builder { - private Collection failedUploads = Collections.emptyList(); + private Collection failedUploads = Collections.emptyList(); private DefaultBuilder() { } @Override - public Builder failedUploads(Collection failedUploads) { + public Builder failedUploads(Collection failedUploads) { this.failedUploads = failedUploads; return this; } - public Collection getFailedUploads() { + public Collection getFailedUploads() { return failedUploads; } - public void setFailedUploads(Collection failedUploads) { + public void setFailedUploads(Collection failedUploads) { failedUploads(failedUploads); } diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/FailedSingleFileUpload.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/FailedFileUpload.java similarity index 85% rename from services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/FailedSingleFileUpload.java rename to services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/FailedFileUpload.java index 40812a2f139d..973b01ed24d5 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/FailedSingleFileUpload.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/FailedFileUpload.java @@ -28,13 +28,13 @@ */ @SdkPublicApi @SdkPreviewApi -public final class FailedSingleFileUpload implements FailedSingleFileTransfer, - ToCopyableBuilder { +public final class FailedFileUpload implements FailedSingleFileTransfer, + ToCopyableBuilder { private final Throwable exception; private final UploadRequest request; - FailedSingleFileUpload(DefaultBuilder builder) { + FailedFileUpload(DefaultBuilder builder) { this.exception = Validate.paramNotNull(builder.exception, "exception"); this.request = Validate.paramNotNull(builder.request, "request"); } @@ -58,7 +58,7 @@ public boolean equals(Object o) { return false; } - FailedSingleFileUpload that = (FailedSingleFileUpload) o; + FailedFileUpload that = (FailedFileUpload) o; if (!exception.equals(that.exception)) { return false; @@ -94,7 +94,7 @@ public Builder toBuilder() { return new DefaultBuilder(this); } - public interface Builder extends CopyableBuilder, + public interface Builder extends CopyableBuilder, FailedSingleFileTransfer.Builder { @Override @@ -104,14 +104,14 @@ public interface Builder extends CopyableBuilder extends AttributeMap.Key { public static final TransferConfigurationOption EXECUTOR = new TransferConfigurationOption<>("Executor", Executor.class); - - public static final Character DEFAULT_DELIMITER_CHARACTER = '/'; - public static final String DEFAULT_DELIMITER = DEFAULT_DELIMITER_CHARACTER.toString(); + public static final String DEFAULT_DELIMITER = "/"; private static final int DEFAULT_UPLOAD_DIRECTORY_MAX_DEPTH = Integer.MAX_VALUE; private static final Boolean DEFAULT_UPLOAD_DIRECTORY_RECURSIVE = Boolean.TRUE; diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryHelper.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryHelper.java index 8419e5c27a22..803d465872ae 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryHelper.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryHelper.java @@ -24,8 +24,8 @@ import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; +import java.util.Collection; import java.util.List; -import java.util.Queue; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.function.Function; @@ -37,7 +37,7 @@ import software.amazon.awssdk.services.s3.model.PutObjectRequest; import software.amazon.awssdk.transfer.s3.CompletedUpload; import software.amazon.awssdk.transfer.s3.CompletedUploadDirectory; -import software.amazon.awssdk.transfer.s3.FailedSingleFileUpload; +import software.amazon.awssdk.transfer.s3.FailedFileUpload; import software.amazon.awssdk.transfer.s3.S3TransferManager; import software.amazon.awssdk.transfer.s3.Upload; import software.amazon.awssdk.transfer.s3.UploadDirectoryRequest; @@ -85,11 +85,11 @@ public UploadDirectoryTransfer uploadDirectory(UploadDirectoryRequest uploadDire // offload the execution to the transfer manager executor CompletableFuture.runAsync(() -> doUploadDirectory(returnFuture, uploadDirectoryRequest), transferConfiguration.option(TransferConfigurationOption.EXECUTOR)) - .handle((ignore, t) -> { - // should never execute this - returnFuture.completeExceptionally(t); - return ignore; - }); + .whenComplete((r, t) -> { + if (t != null) { + returnFuture.completeExceptionally(t); + } + }); return UploadDirectoryTransfer.builder().completionFuture(returnFuture).build(); } @@ -97,31 +97,28 @@ public UploadDirectoryTransfer uploadDirectory(UploadDirectoryRequest uploadDire private void doUploadDirectory(CompletableFuture returnFuture, UploadDirectoryRequest uploadDirectoryRequest) { - try { - Path directory = uploadDirectoryRequest.sourceDirectory(); + Path directory = uploadDirectoryRequest.sourceDirectory(); - validateDirectory(uploadDirectoryRequest); + validateDirectory(uploadDirectoryRequest); - Queue failedUploads = new ConcurrentLinkedQueue<>(); - List> futures; + Collection failedUploads = new ConcurrentLinkedQueue<>(); + List> futures; - try (Stream entries = listFiles(directory, uploadDirectoryRequest)) { - futures = entries.map(path -> { - CompletableFuture future = uploadSingleFile(uploadDirectoryRequest, - failedUploads, path); + try (Stream entries = listFiles(directory, uploadDirectoryRequest)) { + futures = entries.map(path -> { + CompletableFuture future = uploadSingleFile(uploadDirectoryRequest, + failedUploads, path); - // Forward cancellation of the return future to all individual futures. - CompletableFutureUtils.forwardExceptionTo(returnFuture, future); - return future; - }).collect(Collectors.toList()); - } - - CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).whenComplete((r, t) -> { - returnFuture.complete(CompletedUploadDirectory.builder().failedUploads(failedUploads).build()); - }); - } catch (Throwable throwable) { - returnFuture.completeExceptionally(throwable); + // Forward cancellation of the return future to all individual futures. + CompletableFutureUtils.forwardExceptionTo(returnFuture, future); + return future; + }).collect(Collectors.toList()); } + + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) + .whenComplete((r, t) -> returnFuture.complete(CompletedUploadDirectory.builder() + .failedUploads(failedUploads) + .build())); } private void validateDirectory(UploadDirectoryRequest uploadDirectoryRequest) { @@ -129,7 +126,7 @@ private void validateDirectory(UploadDirectoryRequest uploadDirectoryRequest) { Validate.isTrue(Files.exists(directory), "The source directory (%s) provided does not exist", directory); boolean followSymbolicLinks = transferConfiguration.resolveUploadDirectoryFollowSymbolicLinks(uploadDirectoryRequest); if (followSymbolicLinks) { - Validate.isTrue(Files.isDirectory(directory), "The source directory (%s) provided is not a " + Validate.isTrue(Files.isDirectory(directory), "The source directory provided (%s) is not a " + "directory", directory); } else { Validate.isTrue(Files.isDirectory(directory, LinkOption.NOFOLLOW_LINKS), "The source directory (%s) provided" @@ -139,7 +136,7 @@ private void validateDirectory(UploadDirectoryRequest uploadDirectoryRequest) { } private CompletableFuture uploadSingleFile(UploadDirectoryRequest uploadDirectoryRequest, - Queue failedUploads, + Collection failedUploads, Path path) { int nameCount = uploadDirectoryRequest.sourceDirectory().getNameCount(); UploadRequest uploadRequest = constructUploadRequest(uploadDirectoryRequest, nameCount, path); @@ -147,10 +144,10 @@ private CompletableFuture uploadSingleFile(UploadDirectoryReque CompletableFuture future = uploadFunction.apply(uploadRequest).completionFuture(); future.whenComplete((r, t) -> { if (t != null) { - failedUploads.add(FailedSingleFileUpload.builder() - .exception(t) - .request(uploadRequest) - .build()); + failedUploads.add(FailedFileUpload.builder() + .exception(t) + .request(uploadRequest) + .build()); } }); return future; diff --git a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/FailedSingleFileUploadTest.java b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/FailedSingleFileUploadTest.java index b7cab9bc1951..a2bd8112b9e4 100644 --- a/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/FailedSingleFileUploadTest.java +++ b/services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/FailedSingleFileUploadTest.java @@ -26,8 +26,8 @@ public class FailedSingleFileUploadTest { @Test public void requestNull_mustThrowException() { - assertThatThrownBy(() -> FailedSingleFileUpload.builder() - .exception(SdkClientException.create("xxx")).build()) + assertThatThrownBy(() -> FailedFileUpload.builder() + .exception(SdkClientException.create("xxx")).build()) .isInstanceOf(NullPointerException.class) .hasMessageContaining("request must not be null"); } @@ -36,15 +36,15 @@ public void requestNull_mustThrowException() { public void exceptionNull_mustThrowException() { UploadRequest uploadRequest = UploadRequest.builder().source(Paths.get(".")).putObjectRequest(p -> p.bucket("bucket").key("key")).build(); - assertThatThrownBy(() -> FailedSingleFileUpload.builder() - .request(uploadRequest).build()) + assertThatThrownBy(() -> FailedFileUpload.builder() + .request(uploadRequest).build()) .isInstanceOf(NullPointerException.class) .hasMessageContaining("exception must not be null"); } @Test public void equalsHashcode() { - EqualsVerifier.forClass(FailedSingleFileUpload.class) + EqualsVerifier.forClass(FailedFileUpload.class) .withNonnullFields("exception", "request") .verify(); } From 693b6b9e11044407ea5e41c5500d5f7f3bcd4e61 Mon Sep 17 00:00:00 2001 From: Zoe Wang <33073555+zoewangg@users.noreply.github.com> Date: Wed, 13 Oct 2021 10:02:34 -0700 Subject: [PATCH 8/8] address feedback --- ...SingleFileTransfer.java => FailedFileTransfer.java} | 8 ++++---- .../amazon/awssdk/transfer/s3/FailedFileUpload.java | 4 ++-- .../transfer/s3/internal/UploadDirectoryHelper.java | 10 +++++----- 3 files changed, 11 insertions(+), 11 deletions(-) rename services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/{FailedSingleFileTransfer.java => FailedFileTransfer.java} (85%) diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/FailedSingleFileTransfer.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/FailedFileTransfer.java similarity index 85% rename from services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/FailedSingleFileTransfer.java rename to services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/FailedFileTransfer.java index 30662f1bfd39..dcb240b15954 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/FailedSingleFileTransfer.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/FailedFileTransfer.java @@ -24,7 +24,7 @@ */ @SdkPublicApi @SdkPreviewApi -public interface FailedSingleFileTransfer { +public interface FailedFileTransfer { /** * The exception thrown from a specific single file transfer @@ -58,10 +58,10 @@ interface Builder { Builder request(T request); /** - * Builds a {@link FailedSingleFileTransfer} based on the properties supplied to this builder + * Builds a {@link FailedFileTransfer} based on the properties supplied to this builder * - * @return An initialized {@link FailedSingleFileTransfer} + * @return An initialized {@link FailedFileTransfer} */ - FailedSingleFileTransfer build(); + FailedFileTransfer build(); } } diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/FailedFileUpload.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/FailedFileUpload.java index 973b01ed24d5..75dc86a7fff4 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/FailedFileUpload.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/FailedFileUpload.java @@ -28,7 +28,7 @@ */ @SdkPublicApi @SdkPreviewApi -public final class FailedFileUpload implements FailedSingleFileTransfer, +public final class FailedFileUpload implements FailedFileTransfer, ToCopyableBuilder { private final Throwable exception; @@ -95,7 +95,7 @@ public Builder toBuilder() { } public interface Builder extends CopyableBuilder, - FailedSingleFileTransfer.Builder { + FailedFileTransfer.Builder { @Override Builder exception(Throwable exception); diff --git a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryHelper.java b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryHelper.java index 803d465872ae..64d64bddacc7 100644 --- a/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryHelper.java +++ b/services-custom/s3-transfer-manager/src/main/java/software/amazon/awssdk/transfer/s3/internal/UploadDirectoryHelper.java @@ -123,15 +123,15 @@ private void doUploadDirectory(CompletableFuture retur private void validateDirectory(UploadDirectoryRequest uploadDirectoryRequest) { Path directory = uploadDirectoryRequest.sourceDirectory(); - Validate.isTrue(Files.exists(directory), "The source directory (%s) provided does not exist", directory); + Validate.isTrue(Files.exists(directory), "The source directory provided (%s) does not exist", directory); boolean followSymbolicLinks = transferConfiguration.resolveUploadDirectoryFollowSymbolicLinks(uploadDirectoryRequest); if (followSymbolicLinks) { Validate.isTrue(Files.isDirectory(directory), "The source directory provided (%s) is not a " - + "directory", directory); + + "directory", directory); } else { - Validate.isTrue(Files.isDirectory(directory, LinkOption.NOFOLLOW_LINKS), "The source directory (%s) provided" - + " is not a " - + "directory", directory); + Validate.isTrue(Files.isDirectory(directory, LinkOption.NOFOLLOW_LINKS), "The source directory provided (%s)" + + " is not a " + + "directory", directory); } }