Skip to content

Commit 4ff2f7c

Browse files
authored
Fix file name mapping in downloadDirectory (#3386)
1 parent 7b1aee5 commit 4ff2f7c

File tree

4 files changed

+77
-10
lines changed

4 files changed

+77
-10
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": "Fixed the file name mapping issue in downloadDirectory. See [#3366](https://github.com/aws/aws-sdk-java-v2/issues/3366)"
6+
}

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
import org.junit.jupiter.api.BeforeEach;
3636
import org.junit.jupiter.api.Test;
3737
import org.junit.ComparisonFailure;
38+
import org.junit.jupiter.params.ParameterizedTest;
39+
import org.junit.jupiter.params.provider.ValueSource;
3840
import software.amazon.awssdk.testutils.FileUtils;
3941
import software.amazon.awssdk.utils.Logger;
4042

@@ -131,7 +133,7 @@ public void downloadDirectory() throws Exception {
131133
}
132134

133135
/**
134-
* The destination directory structure should be the following with prefix "notes"
136+
* With prefix = "notes", the destination directory structure should be the following:
135137
* <pre>
136138
* {@code
137139
* - destination
@@ -144,9 +146,9 @@ public void downloadDirectory() throws Exception {
144146
* }
145147
* </pre>
146148
*/
147-
@Test
148-
public void downloadDirectory_withPrefix() throws Exception {
149-
String prefix = "notes";
149+
@ParameterizedTest
150+
@ValueSource(strings = {"notes/2021", "notes/2021/", "notes"})
151+
public void downloadDirectory_withPrefix(String prefix) throws Exception {
150152
DirectoryDownload downloadDirectory = tm.downloadDirectory(u -> u.destinationDirectory(destinationDirectory)
151153
.prefix(prefix)
152154
.bucket(TEST_BUCKET));

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

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ private void doDownloadDirectory(CompletableFuture<CompletedDirectoryDownload> r
9494
DownloadDirectoryRequest downloadDirectoryRequest) {
9595
validateDirectoryIfExists(downloadDirectoryRequest.destinationDirectory());
9696
String bucket = downloadDirectoryRequest.bucket();
97+
// Delimiter is null by default. See https://github.com/aws/aws-sdk-java/issues/1215
9798
String delimiter = downloadDirectoryRequest.delimiter().orElse(null);
9899
String prefix = downloadDirectoryRequest.prefix().orElse(DEFAULT_PREFIX);
99100

@@ -146,7 +147,7 @@ private Function<DownloadFileContext, CompletableFuture<?>> downloadSingleFile(
146147

147148
private DownloadFileContext determineDestinationPath(DownloadDirectoryRequest downloadDirectoryRequest, S3Object s3Object) {
148149
FileSystem fileSystem = downloadDirectoryRequest.destinationDirectory().getFileSystem();
149-
String delimiter = downloadDirectoryRequest.delimiter().orElse(null);
150+
String delimiter = downloadDirectoryRequest.delimiter().orElse(DEFAULT_DELIMITER);
150151
String key = normalizeKey(downloadDirectoryRequest, s3Object, delimiter);
151152
String relativePath = getRelativePath(fileSystem, delimiter, key);
152153
Path destinationPath = downloadDirectoryRequest.destinationDirectory().resolve(relativePath);
@@ -194,17 +195,28 @@ private CompletableFuture<CompletedFileDownload> doDownloadSingleFile(DownloadDi
194195
}
195196

196197
/**
197-
* Normalizing the key by stripping the prefix from the s3 object key if the prefix is not empty. For example: given a request
198-
* with prefix = "notes/2021", delimiter = "/" and key = "notes/2021/1.txt", the returned string should be "1.txt". If a
199-
* delimiter is null (not provided by user), use "/" by default.
198+
* Normalizing the key by stripping the prefix from the s3 object key if the prefix is not empty.
199+
*
200+
* If a delimiter is null (not provided by user), use "/" by default.
201+
*
202+
* For example: given a request with prefix = "notes/2021" or "notes/2021/", delimiter = "/" and key = "notes/2021/1.txt",
203+
* the normalized key should be "1.txt".
200204
*/
201205
private static String normalizeKey(DownloadDirectoryRequest downloadDirectoryRequest,
202206
S3Object s3Object,
203207
String delimiter) {
204-
int delimiterLength = delimiter == null ? DEFAULT_DELIMITER.length() : delimiter.length();
208+
int delimiterLength = delimiter.length();
205209
return downloadDirectoryRequest.prefix()
206210
.filter(prefix -> !prefix.isEmpty())
207-
.map(prefix -> s3Object.key().substring(prefix.length() + delimiterLength))
211+
.map(prefix -> {
212+
String normalizedKey;
213+
if (prefix.endsWith(delimiter)) {
214+
normalizedKey = s3Object.key().substring(prefix.length());
215+
} else {
216+
normalizedKey = s3Object.key().substring(prefix.length() + delimiterLength);
217+
}
218+
return normalizedKey;
219+
})
208220
.orElseGet(s3Object::key);
209221
}
210222

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

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,53 @@ void downloadDirectory_shouldRecursivelyDownload(FileSystem jimfs) {
103103
verifyDestinationPathForSingleDownload(jimfs, "/", keys, actualRequests);
104104
}
105105

106+
/**
107+
* The S3 bucket has the following keys:
108+
* abc/def/image.jpg
109+
* abc/def/title.jpg
110+
* abc/def/ghi/xyz.txt
111+
*
112+
* if the prefix is "abc/def/", the structure should like this:
113+
* image.jpg
114+
* title.jpg
115+
* ghi
116+
* - xyz.txt
117+
*/
118+
@ParameterizedTest
119+
@MethodSource("fileSystems")
120+
void downloadDirectory_withPrefix_shouldStripPrefixInDestinationPath(FileSystem jimfs) {
121+
directory = jimfs.getPath("test");
122+
String[] keys = {"abc/def/image.jpg", "abc/def/title.jpg", "abc/def/ghi/xyz.txt"};
123+
stubSuccessfulListObjects(listObjectsHelper, keys);
124+
ArgumentCaptor<DownloadFileRequest> requestArgumentCaptor = ArgumentCaptor.forClass(DownloadFileRequest.class);
125+
126+
when(singleDownloadFunction.apply(requestArgumentCaptor.capture()))
127+
.thenReturn(completedDownload());
128+
DirectoryDownload downloadDirectory =
129+
downloadDirectoryHelper.downloadDirectory(DownloadDirectoryRequest.builder()
130+
.destinationDirectory(directory)
131+
.bucket("bucket")
132+
.prefix("abc/def/")
133+
.build());
134+
CompletedDirectoryDownload completedDirectoryDownload = downloadDirectory.completionFuture().join();
135+
assertThat(completedDirectoryDownload.failedTransfers()).isEmpty();
136+
137+
List<DownloadFileRequest> actualRequests = requestArgumentCaptor.getAllValues();
138+
139+
assertThat(actualRequests.size()).isEqualTo(keys.length);
140+
141+
List<String> destinations =
142+
actualRequests.stream().map(u -> u.destination().toString())
143+
.collect(Collectors.toList());
144+
145+
String jimfsSeparator = jimfs.getSeparator();
146+
147+
List<String> expectedPaths =
148+
Arrays.asList("image.jpg", "title.jpg", "ghi/xyz.txt").stream()
149+
.map(k -> DIRECTORY_NAME + jimfsSeparator + k.replace("/",jimfsSeparator)).collect(Collectors.toList());
150+
assertThat(destinations).isEqualTo(expectedPaths);
151+
}
152+
106153
@ParameterizedTest
107154
@MethodSource("fileSystems")
108155
void downloadDirectory_withDelimiter_shouldHonor(FileSystem jimfs) {

0 commit comments

Comments
 (0)