Skip to content

Commit ed1e4f1

Browse files
committed
added part count and content range validation for download
1 parent 5d2b640 commit ed1e4f1

File tree

3 files changed

+116
-0
lines changed

3 files changed

+116
-0
lines changed

services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/multipart/MultipartDownloaderSubscriber.java

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.reactivestreams.Subscription;
2424
import software.amazon.awssdk.annotations.SdkInternalApi;
2525
import software.amazon.awssdk.core.async.AsyncResponseTransformer;
26+
import software.amazon.awssdk.core.exception.SdkClientException;
2627
import software.amazon.awssdk.services.s3.S3AsyncClient;
2728
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
2829
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
@@ -76,6 +77,16 @@ public class MultipartDownloaderSubscriber implements Subscriber<AsyncResponseTr
7677
*/
7778
private volatile String eTag;
7879

80+
/**
81+
* The size of each part of the object being downloaded.
82+
*/
83+
private volatile Long partSize;
84+
85+
/**
86+
* The total size of the object being downloaded.
87+
*/
88+
private volatile Long totalContentLength;
89+
7990
/**
8091
* The Subscription lock
8192
*/
@@ -117,6 +128,7 @@ public void onNext(AsyncResponseTransformer<GetObjectResponse, GetObjectResponse
117128

118129
synchronized (lock) {
119130
if (totalParts != null && nextPartToGet > totalParts) {
131+
validatePartsCount(completedParts.get());
120132
log.debug(() -> String.format("Completing multipart download after a total of %d parts downloaded.", totalParts));
121133
subscription.cancel();
122134
return;
@@ -162,10 +174,20 @@ private void requestMoreIfNeeded(GetObjectResponse response) {
162174
totalParts = partCount;
163175
}
164176

177+
String actualContentRange = response.contentRange();
178+
if (actualContentRange != null && partSize == null) {
179+
getRangeInfo(actualContentRange);
180+
log.debug(() -> String.format("Part size of the object to download: " + partSize));
181+
log.debug(() -> String.format("Total Content Length of the object to download: " + totalContentLength));
182+
}
183+
184+
validateContentRange(totalComplete, actualContentRange);
185+
165186
synchronized (lock) {
166187
if (totalParts != null && totalParts > 1 && totalComplete < totalParts) {
167188
subscription.request(1);
168189
} else {
190+
validatePartsCount(completedParts.get());
169191
log.debug(() -> String.format("Completing multipart download after a total of %d parts downloaded.", totalParts));
170192
subscription.cancel();
171193
}
@@ -198,4 +220,45 @@ private GetObjectRequest nextRequest(int nextPartToGet) {
198220
}
199221
});
200222
}
223+
224+
private void validatePartsCount(int currentGetCount) {
225+
if (totalParts != null && currentGetCount != totalParts) {
226+
String errorMessage = "PartsCount validation failed. Expected " + totalParts + ", downloaded"
227+
+ " " + currentGetCount + " parts.";
228+
log.error(() -> errorMessage);
229+
subscription.cancel();
230+
SdkClientException exception = SdkClientException.create(errorMessage);
231+
onError(exception);
232+
}
233+
}
234+
235+
private void validateContentRange(int partNumber, String contentRange) {
236+
if (contentRange == null) {
237+
return;
238+
}
239+
240+
long expectedStart = (partNumber - 1) * partSize;
241+
long expectedEnd = partNumber == totalParts ? totalContentLength - 1 : expectedStart + partSize - 1;
242+
243+
String expectedContentRange = String.format("bytes %d-%d/%d", expectedStart, expectedEnd, totalContentLength);
244+
245+
if (!expectedContentRange.equals(contentRange)) {
246+
String errorMessage = String.format(
247+
"Content-Range validation failed for part %d. Expected: %s, Actual: %s",
248+
partNumber, expectedContentRange, contentRange);
249+
log.error(() -> errorMessage);
250+
onError(SdkClientException.create(errorMessage));
251+
}
252+
}
253+
254+
private void getRangeInfo(String contentRange) {
255+
String rangeInfo = contentRange.substring(6);
256+
String[] parts = rangeInfo.split("/");
257+
258+
this.totalContentLength = Long.parseLong(parts[1]);
259+
String[] rangeParts = parts[0].split("-");
260+
long startByte = Long.parseLong(rangeParts[0]);
261+
long endByte = Long.parseLong(rangeParts[1]);
262+
this.partSize = endByte - startByte + 1;
263+
}
201264
}

services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/multipart/MultipartDownloadTestUtil.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,21 @@ public byte[] stubForPart(String testBucket, String testKey,int part, int totalP
7070
aResponse()
7171
.withHeader("x-amz-mp-parts-count", totalPart + "")
7272
.withHeader("ETag", eTag)
73+
.withHeader("Content-Length", String.valueOf(body.length))
74+
.withHeader("Content-Range", contentRange(part, totalPart, partSize))
75+
.withBody(body)));
76+
return body;
77+
}
78+
79+
public byte[] stubForPartwithWrongContentRange(String testBucket, String testKey,int part, int totalPart, int partSize) {
80+
byte[] body = new byte[partSize];
81+
random.nextBytes(body);
82+
stubFor(get(urlEqualTo(String.format("/%s/%s?partNumber=%d", testBucket, testKey, part))).willReturn(
83+
aResponse()
84+
.withHeader("x-amz-mp-parts-count", totalPart + "")
85+
.withHeader("ETag", eTag)
86+
.withHeader("Content-Length", String.valueOf(body.length))
87+
.withHeader("Content-Range", contentRange(part, totalPart, partSize + 1))
7388
.withBody(body)));
7489
return body;
7590
}
@@ -95,4 +110,16 @@ public byte[] stubForPartSuccess(int part, int totalPart, int partSize) {
95110
.withBody(body)));
96111
return body;
97112
}
113+
114+
private String contentRange(int part, int totalPart, int partSize) {
115+
long totalObjectSize = (long) totalPart * partSize;
116+
long startByte = (long) (part - 1) * partSize;
117+
long endByte = startByte + partSize - 1;
118+
119+
if (part == totalPart) {
120+
endByte = totalObjectSize - 1;
121+
}
122+
123+
return String.format("bytes %d-%d/%d", startByte, endByte, totalObjectSize);
124+
}
98125
}

services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/multipart/MultipartDownloaderSubscriberWiremockTest.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,32 @@ <T> void errorOnThirdRequest_shouldCompleteExceptionallyOnlyPartsGreaterThanTwo(
160160
}
161161
}
162162

163+
@ParameterizedTest
164+
@MethodSource("argumentsProvider")
165+
<T> void wrongContentRangeOnSecondRequest_should(AsyncResponseTransformerTestSupplier<T> supplier,
166+
int amountOfPartToTest,
167+
int partSize) {
168+
util.stubForPart(testBucket, testKey, 1, 3, partSize);
169+
util.stubForPartwithWrongContentRange(testBucket, testKey, 2, 3, partSize);
170+
util.stubForPart(testBucket, testKey, 3, 3, partSize);
171+
//byte[] expectedBody = util.stubAllParts(testBucket, testKey, amountOfPartToTest, partSize);
172+
AsyncResponseTransformer<GetObjectResponse, T> transformer = supplier.transformer();
173+
AsyncResponseTransformer.SplitResult<GetObjectResponse, T> split = transformer.split(
174+
SplittingTransformerConfiguration.builder()
175+
.bufferSizeInBytes(1024 * 32L)
176+
.build());
177+
Subscriber<AsyncResponseTransformer<GetObjectResponse, GetObjectResponse>> subscriber = new MultipartDownloaderSubscriber(
178+
s3AsyncClient,
179+
GetObjectRequest.builder()
180+
.bucket(testBucket)
181+
.key(testKey)
182+
.build());
183+
184+
split.publisher().subscribe(subscriber);
185+
T response = split.resultFuture().join();
186+
187+
}
188+
163189
private static Stream<Arguments> argumentsProvider() {
164190
// amount of part, individual part size
165191
List<Pair<Integer, Integer>> partSizes = Arrays.asList(

0 commit comments

Comments
 (0)