Skip to content

Commit 7499be3

Browse files
authored
Skip downloading S3 folders (0-content-length folders created in the S3 console) in downloadDirectory in the S3 Transfer Manager. (#5225)
1 parent fd73a6a commit 7499be3

File tree

5 files changed

+98
-6
lines changed

5 files changed

+98
-6
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "bugfix",
3+
"category": "S3 Transfer Manager",
4+
"contributor": "",
5+
"description": "Skip downloading S3 folders (0-content-length folders created in the S3 console) in downloadDirectory in the S3 Transfer Manager."
6+
}

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

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
package software.amazon.awssdk.transfer.s3.config;
1717

1818
import java.util.function.Predicate;
19-
import software.amazon.awssdk.annotations.SdkPreviewApi;
2019
import software.amazon.awssdk.annotations.SdkPublicApi;
2120
import software.amazon.awssdk.services.s3.model.S3Object;
2221
import software.amazon.awssdk.transfer.s3.model.DownloadDirectoryRequest;
@@ -28,7 +27,6 @@
2827
* {@link #or(Predicate)} methods.
2928
*/
3029
@SdkPublicApi
31-
@SdkPreviewApi
3230
public interface DownloadFilter extends Predicate<S3Object> {
3331

3432
/**
@@ -41,10 +39,18 @@ public interface DownloadFilter extends Predicate<S3Object> {
4139
boolean test(S3Object s3Object);
4240

4341
/**
44-
* A {@link DownloadFilter} that downloads all objects. This is the default behavior if no filter is provided.
42+
* A {@link DownloadFilter} that downloads all non-folder objects. A folder is a 0-byte object created when a customer
43+
* uses S3 console to create a folder, and it always ends with "/".
44+
*
45+
* <p>
46+
* This is the default behavior if no filter is provided.
4547
*/
46-
@SdkPreviewApi
4748
static DownloadFilter allObjects() {
48-
return ctx -> true;
49+
return s3Object -> {
50+
boolean isFolder = s3Object.key().endsWith("/") &&
51+
s3Object.size() != null &&
52+
s3Object.size() == 0;
53+
return !isFolder;
54+
};
4955
}
5056
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.transfer.s3.config;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
20+
import java.util.stream.Stream;
21+
import org.junit.jupiter.params.ParameterizedTest;
22+
import org.junit.jupiter.params.provider.Arguments;
23+
import org.junit.jupiter.params.provider.MethodSource;
24+
import software.amazon.awssdk.services.s3.model.S3Object;
25+
26+
public class DownloadFilterTest {
27+
28+
public static Stream<Arguments> s3Objects() {
29+
return Stream.of(
30+
Arguments.of(S3Object.builder().key("no-slash-zero-content").size(0L).build(), true),
31+
Arguments.of(S3Object.builder().key("slash-zero-content/").size(0L).build(), false),
32+
Arguments.of(S3Object.builder().key("key").size(10L).build(), true)
33+
);
34+
}
35+
36+
@ParameterizedTest
37+
@MethodSource("s3Objects")
38+
void allObjectsFilter_shouldWork(S3Object s3Object, boolean result) {
39+
assertThat(DownloadFilter.allObjects().test(s3Object)).isEqualTo(result);
40+
}
41+
}

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
5555
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
5656
import software.amazon.awssdk.services.s3.model.ListObjectsV2Request;
57+
import software.amazon.awssdk.services.s3.model.S3Object;
5758
import software.amazon.awssdk.transfer.s3.internal.model.DefaultFileDownload;
5859
import software.amazon.awssdk.transfer.s3.internal.progress.DefaultTransferProgress;
5960
import software.amazon.awssdk.transfer.s3.internal.progress.DefaultTransferProgressSnapshot;
@@ -124,6 +125,37 @@ void downloadDirectory_allDownloadsSucceed_failedDownloadsShouldBeEmpty() throws
124125
"key2"));
125126
}
126127

128+
@Test
129+
void downloadDirectory_containsFolderObjects_shouldSkip() throws Exception {
130+
stubSuccessfulListObjects(listObjectsHelper, S3Object.builder().key("key1").size(10L).build(),
131+
S3Object.builder().key("key2").size(0L).build(),
132+
S3Object.builder().key("folder/").size(0L).build());
133+
134+
FileDownload fileDownload = newSuccessfulDownload();
135+
FileDownload fileDownload2 = newSuccessfulDownload();
136+
137+
when(singleDownloadFunction.apply(any(DownloadFileRequest.class))).thenReturn(fileDownload, fileDownload2);
138+
139+
DirectoryDownload downloadDirectory =
140+
downloadDirectoryHelper.downloadDirectory(DownloadDirectoryRequest.builder()
141+
.destination(directory)
142+
.bucket("bucket")
143+
.build());
144+
145+
CompletedDirectoryDownload completedDirectoryDownload = downloadDirectory.completionFuture().get(5, TimeUnit.SECONDS);
146+
147+
ArgumentCaptor<DownloadFileRequest> argumentCaptor = ArgumentCaptor.forClass(DownloadFileRequest.class);
148+
verify(singleDownloadFunction, times(2)).apply(argumentCaptor.capture());
149+
150+
assertThat(completedDirectoryDownload.failedTransfers()).isEmpty();
151+
List<DownloadFileRequest> allValues = argumentCaptor.getAllValues();
152+
assertThat(allValues).size().isEqualTo(2);
153+
assertThat(allValues).element(0).satisfies(d -> assertThat(d.getObjectRequest().key()).isEqualTo(
154+
"key1"));
155+
assertThat(allValues).element(1).satisfies(d -> assertThat(d.getObjectRequest().key()).isEqualTo(
156+
"key2"));
157+
}
158+
127159
@ParameterizedTest
128160
@ValueSource(strings = {"/blah",
129161
"../blah/object.dat",

services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/util/S3ApiCallMockUtils.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.Arrays;
2323
import java.util.List;
2424
import java.util.stream.Collectors;
25+
import org.apache.commons.lang3.RandomStringUtils;
2526
import software.amazon.awssdk.core.async.SdkPublisher;
2627
import software.amazon.awssdk.services.s3.model.ListObjectsV2Request;
2728
import software.amazon.awssdk.services.s3.model.S3Object;
@@ -33,8 +34,14 @@ private S3ApiCallMockUtils() {
3334
}
3435

3536
public static void stubSuccessfulListObjects(ListObjectsHelper helper, String... keys) {
36-
List<S3Object> s3Objects = Arrays.stream(keys).map(k -> S3Object.builder().key(k).build()).collect(Collectors.toList());
37+
List<S3Object> s3Objects =
38+
Arrays.stream(keys).map(k -> S3Object.builder().key(k).size(100L).build()).collect(Collectors.toList());
3739
when(helper.listS3ObjectsRecursively(any(ListObjectsV2Request.class))).thenReturn(SdkPublisher.adapt(Flowable.fromIterable(s3Objects)));
3840
}
3941

42+
public static void stubSuccessfulListObjects(ListObjectsHelper helper, S3Object... s3Objects) {
43+
when(helper.listS3ObjectsRecursively(any(ListObjectsV2Request.class)))
44+
.thenReturn(SdkPublisher.adapt(Flowable.fromIterable(Arrays.asList(s3Objects))));
45+
}
46+
4047
}

0 commit comments

Comments
 (0)