Skip to content

Commit 2374eb2

Browse files
authored
Fixed an issue in Transfer Manager downloadDirectory that could cause… (#3625)
* Fixed an issue in Transfer Manager downloadDirectory that could cause objects that contain prefix to fail to download
1 parent 8058985 commit 2374eb2

File tree

3 files changed

+84
-22
lines changed

3 files changed

+84
-22
lines changed

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

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -125,11 +125,24 @@ public void downloadDirectory() throws Exception {
125125
assertTwoDirectoriesHaveSameStructure(sourceDirectory, directory);
126126
}
127127

128+
@ParameterizedTest
129+
@ValueSource(strings = {"notes/2021", "notes/2021/"})
130+
void downloadDirectory_withPrefix(String prefix) throws Exception {
131+
DirectoryDownload downloadDirectory = tm.downloadDirectory(u -> u.destination(directory)
132+
.listObjectsV2RequestTransformer(r -> r.prefix(prefix))
133+
.bucket(TEST_BUCKET));
134+
CompletedDirectoryDownload completedDirectoryDownload = downloadDirectory.completionFuture().get(5, TimeUnit.SECONDS);
135+
assertThat(completedDirectoryDownload.failedTransfers()).isEmpty();
136+
137+
assertTwoDirectoriesHaveSameStructure(sourceDirectory.resolve(prefix), directory);
138+
}
139+
128140
/**
129141
* With prefix = "notes", the destination directory structure should be the following:
130142
* <pre>
131143
* {@code
132144
* - destination
145+
* - notesMemo.txt
133146
* - 2021
134147
* - 1.txt
135148
* - 2.txt
@@ -139,16 +152,24 @@ public void downloadDirectory() throws Exception {
139152
* }
140153
* </pre>
141154
*/
142-
@ParameterizedTest
143-
@ValueSource(strings = {"notes/2021", "notes/2021/", "notes"})
144-
public void downloadDirectory_withPrefix(String prefix) throws Exception {
155+
@Test
156+
void downloadDirectory_containsObjectWithPrefixInTheKey_shouldResolveCorrectly() throws Exception {
157+
String prefix = "notes";
145158
DirectoryDownload downloadDirectory = tm.downloadDirectory(u -> u.destination(directory)
146159
.listObjectsV2RequestTransformer(r -> r.prefix(prefix))
147160
.bucket(TEST_BUCKET));
148161
CompletedDirectoryDownload completedDirectoryDownload = downloadDirectory.completionFuture().get(5, TimeUnit.SECONDS);
149162
assertThat(completedDirectoryDownload.failedTransfers()).isEmpty();
150163

151-
assertTwoDirectoriesHaveSameStructure(sourceDirectory.resolve(prefix), directory);
164+
Path expectedDirectory = Files.createTempDirectory("expectedDirectory");
165+
166+
try {
167+
FileUtils.copyDirectory(sourceDirectory.resolve(prefix), expectedDirectory);
168+
Files.copy(sourceDirectory.resolve("notesMemo.txt"), expectedDirectory.resolve("notesMemo.txt"));
169+
assertTwoDirectoriesHaveSameStructure(expectedDirectory, directory);
170+
} finally {
171+
FileUtils.cleanUpTestDirectory(expectedDirectory);
172+
}
152173
}
153174

154175
/**
@@ -203,6 +224,7 @@ public void downloadDirectory_withFilter() throws Exception {
203224
Files.delete(expectedDirectory.resolve("notes/2022"));
204225
Files.delete(expectedDirectory.resolve("notes/important.txt"));
205226
Files.delete(expectedDirectory.resolve("notes/2021/1.txt"));
227+
Files.delete(expectedDirectory.resolve("notesMemo.txt"));
206228

207229
assertTwoDirectoriesHaveSameStructure(expectedDirectory, directory);
208230
} finally {
@@ -242,6 +264,7 @@ private static void assertLeftHasRight(Path left, Path right) {
242264
* - source
243265
* - README.md
244266
* - CHANGELOG.md
267+
* - notesMemo.txt
245268
* - notes
246269
* - 2021
247270
* - 1.txt
@@ -262,6 +285,7 @@ private static Path createLocalTestDirectory() throws IOException {
262285
Files.createDirectory(Paths.get(directoryName, "notes", "2022"));
263286
Files.write(Paths.get(directoryName, "README.md"), RandomStringUtils.random(100).getBytes(StandardCharsets.UTF_8));
264287
Files.write(Paths.get(directoryName, "CHANGELOG.md"), RandomStringUtils.random(100).getBytes(StandardCharsets.UTF_8));
288+
Files.write(Paths.get(directoryName, "notesMemo.txt"), RandomStringUtils.random(100).getBytes(StandardCharsets.UTF_8));
265289
Files.write(Paths.get(directoryName, "notes", "2021", "1.txt"),
266290
RandomStringUtils.random(100).getBytes(StandardCharsets.UTF_8));
267291
Files.write(Paths.get(directoryName, "notes", "2021", "2.txt"),

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

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -147,12 +147,10 @@ private Path determineDestinationPath(DownloadDirectoryRequest downloadDirectory
147147
ListObjectsV2Request listRequest,
148148
S3Object s3Object) {
149149
FileSystem fileSystem = downloadDirectoryRequest.destination().getFileSystem();
150-
// listOBjectv2requests.delimiter
151150
String delimiter = listRequest.delimiter() == null ? DEFAULT_DELIMITER : listRequest.delimiter();
152-
String key = normalizeKey(downloadDirectoryRequest, listRequest, s3Object, delimiter);
151+
String key = normalizeKey(listRequest, s3Object.key(), delimiter);
153152
String relativePath = getRelativePath(fileSystem, delimiter, key);
154153
Path destinationPath = downloadDirectoryRequest.destination().resolve(relativePath);
155-
156154
validatePath(downloadDirectoryRequest.destination(), destinationPath, s3Object.key());
157155
return destinationPath;
158156
}
@@ -200,31 +198,35 @@ private CompletableFuture<CompletedFileDownload> doDownloadSingleFile(DownloadDi
200198
}
201199

202200
/**
203-
* Normalizing the key by stripping the prefix from the s3 object key if the prefix is not empty.
201+
* If the prefix is not empty AND the key contains the delimiter, normalize the key by stripping the prefix from the key.
204202
*
205203
* If a delimiter is null (not provided by user), use "/" by default.
206204
*
207205
* For example: given a request with prefix = "notes/2021" or "notes/2021/", delimiter = "/" and key = "notes/2021/1.txt",
208206
* the normalized key should be "1.txt".
209207
*/
210-
private static String normalizeKey(DownloadDirectoryRequest downloadDirectoryRequest,
211-
ListObjectsV2Request listObjectsRequest,
212-
S3Object s3Object,
208+
private static String normalizeKey(ListObjectsV2Request listObjectsRequest,
209+
String key,
213210
String delimiter) {
214-
int delimiterLength = delimiter.length();
211+
if (StringUtils.isEmpty(listObjectsRequest.prefix())) {
212+
return key;
213+
}
215214

216-
if (!StringUtils.isEmpty(listObjectsRequest.prefix())) {
217-
String prefix = listObjectsRequest.prefix();
218-
String normalizedKey;
219-
if (prefix.endsWith(delimiter)) {
220-
normalizedKey = s3Object.key().substring(prefix.length());
221-
} else {
222-
normalizedKey = s3Object.key().substring(prefix.length() + delimiterLength);
223-
}
224-
return normalizedKey;
215+
String prefix = listObjectsRequest.prefix();
216+
217+
if (!key.contains(delimiter)) {
218+
return key;
219+
}
220+
221+
String normalizedKey;
222+
223+
if (prefix.endsWith(delimiter)) {
224+
normalizedKey = key.substring(prefix.length());
225+
} else {
226+
normalizedKey = key.substring(prefix.length() + delimiter.length());
225227
}
228+
return normalizedKey;
226229

227-
return s3Object.key();
228230
}
229231

230232
private static String getRelativePath(FileSystem fileSystem, String delimiter, String key) {

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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,42 @@ void downloadDirectory_withPrefix_shouldStripPrefixInDestinationPath(FileSystem
366366
assertThat(destinations).isEqualTo(expectedPaths);
367367
}
368368

369+
@ParameterizedTest
370+
@MethodSource("fileSystems")
371+
void downloadDirectory_containsObjectWithPrefixInIt_shouldInclude(FileSystem jimfs) {
372+
String prefix = "abc";
373+
directory = jimfs.getPath("test");
374+
String[] keys = {"abc/def/image.jpg", "abc/def/title.jpg", "abcd"};
375+
stubSuccessfulListObjects(listObjectsHelper, keys);
376+
ArgumentCaptor<DownloadFileRequest> requestArgumentCaptor = ArgumentCaptor.forClass(DownloadFileRequest.class);
377+
378+
when(singleDownloadFunction.apply(requestArgumentCaptor.capture()))
379+
.thenReturn(completedDownload());
380+
DirectoryDownload downloadDirectory =
381+
downloadDirectoryHelper.downloadDirectory(DownloadDirectoryRequest.builder()
382+
.destination(directory)
383+
.bucket("bucket")
384+
.listObjectsV2RequestTransformer(l -> l.prefix(prefix))
385+
.build());
386+
CompletedDirectoryDownload completedDirectoryDownload = downloadDirectory.completionFuture().join();
387+
assertThat(completedDirectoryDownload.failedTransfers()).isEmpty();
388+
389+
List<DownloadFileRequest> actualRequests = requestArgumentCaptor.getAllValues();
390+
391+
assertThat(actualRequests.size()).isEqualTo(keys.length);
392+
393+
List<String> destinations =
394+
actualRequests.stream().map(u -> u.destination().toString())
395+
.collect(Collectors.toList());
396+
397+
String jimfsSeparator = jimfs.getSeparator();
398+
399+
List<String> expectedPaths =
400+
Arrays.asList("def/image.jpg", "def/title.jpg", "abcd").stream()
401+
.map(k -> DIRECTORY_NAME + jimfsSeparator + k.replace("/",jimfsSeparator)).collect(Collectors.toList());
402+
assertThat(destinations).isEqualTo(expectedPaths);
403+
}
404+
369405
@ParameterizedTest
370406
@MethodSource("fileSystems")
371407
void downloadDirectory_withDelimiter_shouldHonor(FileSystem jimfs) {

0 commit comments

Comments
 (0)