|
5 | 5 | package aws.sdk.kotlin.e2etest |
6 | 6 |
|
7 | 7 | import aws.sdk.kotlin.services.s3.S3Client |
| 8 | +import aws.sdk.kotlin.services.s3.model.CompletedPart |
8 | 9 | import aws.sdk.kotlin.services.s3.model.GetObjectRequest |
9 | 10 | import aws.sdk.kotlin.testing.PRINTABLE_CHARS |
10 | 11 | import aws.sdk.kotlin.testing.withAllEngines |
11 | 12 | import aws.smithy.kotlin.runtime.content.ByteStream |
| 13 | +import aws.smithy.kotlin.runtime.content.asByteStream |
12 | 14 | import aws.smithy.kotlin.runtime.content.decodeToString |
13 | 15 | import aws.smithy.kotlin.runtime.content.fromFile |
| 16 | +import aws.smithy.kotlin.runtime.content.toByteArray |
| 17 | +import aws.smithy.kotlin.runtime.hashing.sha256 |
14 | 18 | import aws.smithy.kotlin.runtime.testing.RandomTempFile |
15 | | -import kotlinx.coroutines.ExperimentalCoroutinesApi |
16 | | -import kotlinx.coroutines.runBlocking |
17 | | -import kotlinx.coroutines.withTimeout |
| 19 | +import aws.smithy.kotlin.runtime.util.encodeToHex |
| 20 | +import kotlinx.coroutines.* |
18 | 21 | import org.junit.jupiter.api.AfterAll |
19 | 22 | import org.junit.jupiter.api.BeforeAll |
20 | 23 | import org.junit.jupiter.api.TestInstance |
| 24 | +import java.io.File |
| 25 | +import java.util.UUID |
21 | 26 | import kotlin.test.Test |
22 | 27 | import kotlin.test.assertEquals |
23 | 28 | import kotlin.time.Duration.Companion.seconds |
@@ -150,8 +155,71 @@ class S3BucketOpsIntegrationTest { |
150 | 155 | } |
151 | 156 | } |
152 | 157 | } |
| 158 | + |
| 159 | + @Test |
| 160 | + fun testMultipartUpload(): Unit = runBlocking { |
| 161 | + s3WithAllEngines { s3 -> |
| 162 | + val objKey = "test-multipart-${UUID.randomUUID()}" |
| 163 | + val contentSize: Long = 8 * 1024 * 1024 // 2 parts |
| 164 | + val file = RandomTempFile(sizeInBytes = contentSize) |
| 165 | + val partSize = 5 * 1024 * 1024 // 5 MB - min part size |
| 166 | + |
| 167 | + val expectedSha256 = file.readBytes().sha256().encodeToHex() |
| 168 | + |
| 169 | + val resp = s3.createMultipartUpload { |
| 170 | + bucket = testBucket |
| 171 | + key = objKey |
| 172 | + } |
| 173 | + |
| 174 | + val completedParts = file.chunk(partSize) |
| 175 | + .mapIndexed { idx, chunk -> |
| 176 | + async { |
| 177 | + val uploadResp = s3.uploadPart { |
| 178 | + bucket = testBucket |
| 179 | + key = objKey |
| 180 | + uploadId = resp.uploadId |
| 181 | + body = file.asByteStream(chunk) |
| 182 | + partNumber = idx + 1 |
| 183 | + } |
| 184 | + |
| 185 | + CompletedPart { |
| 186 | + partNumber = idx + 1 |
| 187 | + eTag = uploadResp.eTag |
| 188 | + } |
| 189 | + } |
| 190 | + } |
| 191 | + .toList() |
| 192 | + .awaitAll() |
| 193 | + |
| 194 | + s3.completeMultipartUpload { |
| 195 | + bucket = testBucket |
| 196 | + key = objKey |
| 197 | + uploadId = resp.uploadId |
| 198 | + multipartUpload { |
| 199 | + parts = completedParts |
| 200 | + } |
| 201 | + } |
| 202 | + |
| 203 | + // TOOD - eventually make use of s3 checksums |
| 204 | + val getRequest = GetObjectRequest { |
| 205 | + bucket = testBucket |
| 206 | + key = objKey |
| 207 | + } |
| 208 | + val actualSha256 = s3.getObject(getRequest) { resp -> |
| 209 | + resp.body!!.toByteArray().sha256().encodeToHex() |
| 210 | + } |
| 211 | + |
| 212 | + assertEquals(expectedSha256, actualSha256) |
| 213 | + } |
| 214 | + } |
153 | 215 | } |
154 | 216 |
|
| 217 | +// generate sequence of "chunks" where each range defines the inclusive start and end bytes |
| 218 | +private fun File.chunk(partSize: Int): Sequence<LongRange> = |
| 219 | + (0 until length() step partSize.toLong()).asSequence().map { |
| 220 | + it until minOf(it + partSize, length()) |
| 221 | + } |
| 222 | + |
155 | 223 | internal suspend fun s3WithAllEngines(block: suspend (S3Client) -> Unit) { |
156 | 224 | withAllEngines { engine -> |
157 | 225 | S3Client { |
|
0 commit comments