@@ -37,6 +37,8 @@ import software.amazon.awssdk.services.s3.S3AsyncClient
3737import software.amazon.awssdk.services.s3.S3Client
3838import software.amazon.awssdk.services.s3.model.ChecksumAlgorithm
3939import software.amazon.awssdk.services.s3.model.ChecksumMode
40+ import software.amazon.awssdk.services.s3.model.ChecksumType
41+ import software.amazon.awssdk.services.s3.model.CompleteMultipartUploadRequest
4042import software.amazon.awssdk.services.s3.model.CompletedPart
4143import software.amazon.awssdk.services.s3.model.ListPartsRequest
4244import software.amazon.awssdk.services.s3.model.NoSuchBucketException
@@ -255,13 +257,9 @@ internal class MultiPartIT : S3TestBase() {
255257 .isEqualTo(" ${serviceEndpoint} /$bucketName /${UriUtils .encode(UPLOAD_FILE_NAME , StandardCharsets .UTF_8 )} " )
256258 }
257259
258-
259- /* *
260- * Tests if a multipart upload with the last part being smaller than 5MB works.
261- */
262260 @Test
263261 @S3VerifiedSuccess(year = 2025 )
264- fun testMultipartUpload_checksum (testInfo : TestInfo ) {
262+ fun `multipartupload send checksum in create and complete` (testInfo : TestInfo ) {
265263 val bucketName = givenBucket(testInfo)
266264 val uploadFile = File (TEST_IMAGE_TIFF )
267265 // construct uploadfile >5MB
@@ -277,7 +275,10 @@ internal class MultiPartIT : S3TestBase() {
277275 it.bucket(bucketName)
278276 it.key(TEST_IMAGE_TIFF )
279277 it.checksumAlgorithm(ChecksumAlgorithm .CRC32 )
278+ it.checksumType(ChecksumType .COMPOSITE )
280279 }
280+ assertThat(initiateMultipartUploadResult.checksumAlgorithm()).isEqualTo(ChecksumAlgorithm .CRC32 )
281+ assertThat(initiateMultipartUploadResult.checksumType()).isEqualTo(ChecksumType .COMPOSITE )
281282 val uploadId = initiateMultipartUploadResult.uploadId()
282283 // upload part 1, <5MB
283284 val partResponse1 = s3Client.uploadPart(
@@ -289,7 +290,6 @@ internal class MultiPartIT : S3TestBase() {
289290 it.partNumber(1 )
290291 it.contentLength(tempFile.length())
291292 },
292- // .lastPart(true)
293293 RequestBody .fromFile(tempFile),
294294 )
295295 val etag1 = partResponse1.eTag()
@@ -303,7 +303,6 @@ internal class MultiPartIT : S3TestBase() {
303303 it.partNumber(2 )
304304 it.contentLength(uploadFile.length())
305305 },
306- // .lastPart(true)
307306 RequestBody .fromFile(uploadFile),
308307 )
309308 val etag2 = partResponse2.eTag()
@@ -354,11 +353,87 @@ internal class MultiPartIT : S3TestBase() {
354353 .isEqualTo(" ${serviceEndpoint} /$bucketName /${UriUtils .encode(TEST_IMAGE_TIFF , StandardCharsets .UTF_8 )} " )
355354 }
356355
356+ @Test
357+ @S3VerifiedSuccess(year = 2025 )
358+ fun `multipartupload send checksum in create only` (testInfo : TestInfo ) {
359+ val bucketName = givenBucket(testInfo)
360+ val uploadFile = File (TEST_IMAGE_TIFF )
361+ // construct uploadfile >5MB
362+ val tempFile = Files .newTemporaryFile().also {
363+ (readStreamIntoByteArray(uploadFile.inputStream()) +
364+ readStreamIntoByteArray(uploadFile.inputStream()) +
365+ readStreamIntoByteArray(uploadFile.inputStream()))
366+ .inputStream()
367+ .copyTo(it.outputStream())
368+ }
369+
370+ val initiateMultipartUploadResult = s3Client.createMultipartUpload {
371+ it.bucket(bucketName)
372+ it.key(TEST_IMAGE_TIFF )
373+ it.checksumAlgorithm(ChecksumAlgorithm .CRC32 )
374+ }
375+ val uploadId = initiateMultipartUploadResult.uploadId()
376+ // upload part 1, <5MB
377+ val partResponse1 = s3Client.uploadPart(
378+ {
379+ it.bucket(initiateMultipartUploadResult.bucket())
380+ it.key(initiateMultipartUploadResult.key())
381+ it.uploadId(uploadId)
382+ it.checksumAlgorithm(ChecksumAlgorithm .CRC32 )
383+ it.partNumber(1 )
384+ it.contentLength(tempFile.length())
385+ },
386+ RequestBody .fromFile(tempFile),
387+ )
388+ val etag1 = partResponse1.eTag()
389+ val checksum1 = partResponse1.checksumCRC32()
390+ // upload part 2, <5MB
391+ val partResponse2 = s3Client.uploadPart({
392+ it.bucket(initiateMultipartUploadResult.bucket())
393+ it.key(initiateMultipartUploadResult.key())
394+ it.uploadId(uploadId)
395+ it.checksumAlgorithm(ChecksumAlgorithm .CRC32 )
396+ it.partNumber(2 )
397+ it.contentLength(uploadFile.length())
398+ },
399+ RequestBody .fromFile(uploadFile),
400+ )
401+ val etag2 = partResponse2.eTag()
402+ val checksum2 = partResponse2.checksumCRC32()
403+ val localChecksum1 = DigestUtil .checksumFor(tempFile.toPath(), DefaultChecksumAlgorithm .CRC32 )
404+ assertThat(checksum1).isEqualTo(localChecksum1)
405+ val localChecksum2 = DigestUtil .checksumFor(uploadFile.toPath(), DefaultChecksumAlgorithm .CRC32 )
406+ assertThat(checksum2).isEqualTo(localChecksum2)
407+
408+ assertThatThrownBy {
409+ s3Client.completeMultipartUpload {
410+ it.bucket(initiateMultipartUploadResult.bucket())
411+ it.key(initiateMultipartUploadResult.key())
412+ it.uploadId(initiateMultipartUploadResult.uploadId())
413+ it.multipartUpload {
414+ it.parts(
415+ {
416+ it.eTag(etag1)
417+ it.partNumber(1 )
418+ },
419+ {
420+ it.eTag(etag2)
421+ it.partNumber(2 )
422+ }
423+ )
424+ }
425+ }
426+ }.isInstanceOf(S3Exception ::class .java)
427+ .hasMessageContaining(" Service: S3, Status Code: 400" )
428+ .hasMessageContaining(" The upload was created using a crc32 checksum. The complete request must include the " +
429+ " checksum for each part. It was missing for part 1 in the request." )
430+ }
431+
357432
358433 @S3VerifiedSuccess(year = 2025 )
359434 @ParameterizedTest
360435 @MethodSource(value = [" checksumAlgorithms" ])
361- fun testUploadPart_checksumAlgorithm (checksumAlgorithm : software.amazon.awssdk.checksums.spi.ChecksumAlgorithm ,
436+ fun testUploadPart_checksumAlgorithm_initiate (checksumAlgorithm : software.amazon.awssdk.checksums.spi.ChecksumAlgorithm ,
362437 testInfo : TestInfo ) {
363438 val bucketName = givenBucket(testInfo)
364439 val uploadFile = File (UPLOAD_FILE_NAME )
@@ -377,7 +452,6 @@ internal class MultiPartIT : S3TestBase() {
377452 it.checksumAlgorithm(checksumAlgorithm.toAlgorithm())
378453 it.partNumber(1 )
379454 it.contentLength(uploadFile.length()).build()
380- // .lastPart(true)
381455 },
382456 RequestBody .fromFile(uploadFile),
383457 ).also {
@@ -392,6 +466,45 @@ internal class MultiPartIT : S3TestBase() {
392466 }
393467 }
394468
469+ @S3VerifiedSuccess(year = 2025 )
470+ @ParameterizedTest
471+ @MethodSource(value = [" checksumAlgorithms" ])
472+ fun testUploadPart_checksumAlgorithm_complete (
473+ checksumAlgorithm : software.amazon.awssdk.checksums.spi.ChecksumAlgorithm ,
474+ testInfo : TestInfo
475+ ) {
476+ val bucketName = givenBucket(testInfo)
477+ val uploadFile = File (UPLOAD_FILE_NAME )
478+ val initiateMultipartUploadResult = s3Client.createMultipartUpload {
479+ it.bucket(bucketName)
480+ it.key(UPLOAD_FILE_NAME )
481+ }
482+ val uploadId = initiateMultipartUploadResult.uploadId()
483+
484+ s3Client.uploadPart({
485+ it.bucket(initiateMultipartUploadResult.bucket())
486+ it.key(initiateMultipartUploadResult.key())
487+ it.uploadId(uploadId)
488+ it.partNumber(1 )
489+ it.contentLength(uploadFile.length()).build()
490+ // .lastPart(true)
491+ },
492+ RequestBody .fromFile(uploadFile),
493+ )
494+
495+ assertThatThrownBy {
496+ s3Client.completeMultipartUpload {
497+ it.bucket(bucketName)
498+ it.key(UPLOAD_FILE_NAME )
499+ it.uploadId(uploadId)
500+ it.checksumType(ChecksumType .COMPOSITE )
501+ it.checksum(" WRONG CHECKSUM" , checksumAlgorithm.toAlgorithm())
502+ }
503+ }
504+ .isInstanceOf(S3Exception ::class .java)
505+ .hasMessageContaining(" Service: S3, Status Code: 400" )
506+ }
507+
395508 @S3VerifiedSuccess(year = 2025 )
396509 @ParameterizedTest
397510 @MethodSource(value = [" checksumAlgorithms" ])
@@ -473,6 +586,19 @@ internal class MultiPartIT : S3TestBase() {
473586 else -> error(" Unknown checksum algorithm" )
474587 }
475588
589+ private fun CompleteMultipartUploadRequest.Builder.checksum (
590+ checksum : String ,
591+ checksumAlgorithm : ChecksumAlgorithm
592+ ): CompleteMultipartUploadRequest .Builder =
593+ when (checksumAlgorithm) {
594+ ChecksumAlgorithm .SHA1 -> this .checksumSHA1(checksum)
595+ ChecksumAlgorithm .SHA256 -> this .checksumSHA256(checksum)
596+ ChecksumAlgorithm .CRC32 -> this .checksumCRC32(checksum)
597+ ChecksumAlgorithm .CRC32_C -> this .checksumCRC32C(checksum)
598+ ChecksumAlgorithm .CRC64_NVME -> this .checksumCRC64NVME(checksum)
599+ else -> error(" Unknown checksum algorithm" )
600+ }
601+
476602 @Test
477603 @S3VerifiedSuccess(year = 2025 )
478604 fun testInitiateMultipartAndRetrieveParts (testInfo : TestInfo ) {
@@ -1372,6 +1498,6 @@ internal class MultiPartIT : S3TestBase() {
13721498 companion object {
13731499 private const val NO_SUCH_BUCKET = " The specified bucket does not exist"
13741500 private const val INVALID_PART_NUMBER = " Part number must be an integer between 1 and 10000, inclusive"
1375- private const val INVALID_PART = " One or more of the specified parts could not be found. The part might not have been uploaded, or the specified entity tagSet might not have matched the part's entity tagSet ."
1501+ private const val INVALID_PART = " One or more of the specified parts could not be found. The part might not have been uploaded, or the specified entity tag may not match the part's entity tag ."
13761502 }
13771503}
0 commit comments