1
- import { S3 } from "@aws-sdk/client-s3" ;
1
+ import { S3 , UploadPartCommandOutput } from "@aws-sdk/client-s3" ;
2
+ import { Upload } from "@aws-sdk/lib-storage" ;
3
+ import { FetchHttpHandler } from "@smithy/fetch-http-handler" ;
2
4
import type { HttpRequest , HttpResponse } from "@smithy/types" ;
3
- import { headStream } from "@smithy/util-stream" ;
5
+ import { ChecksumStream , headStream } from "@smithy/util-stream" ;
4
6
import { Readable } from "node:stream" ;
5
- import { beforeAll , describe , expect , test as it } from "vitest" ;
7
+ import { beforeAll , describe , expect , test as it , vi } from "vitest" ;
6
8
7
9
import { getIntegTestResources } from "../../../tests/e2e/get-integ-test-resources" ;
8
10
9
11
describe ( "S3 checksums" , ( ) => {
10
12
let s3 : S3 ;
11
13
let s3_noChecksum : S3 ;
14
+ let s3_noRequestBuffer : S3 ;
12
15
let Bucket : string ;
13
16
let Key : string ;
14
17
let region : string ;
15
18
const expected = new Uint8Array ( [ 97 , 98 , 99 , 100 ] ) ;
19
+ const logger = {
20
+ debug : vi . fn ( ) ,
21
+ info : vi . fn ( ) ,
22
+ warn : vi . fn ( ) ,
23
+ error : vi . fn ( ) ,
24
+ } ;
25
+
26
+ function stream ( size : number , chunkSize : number ) {
27
+ async function * generate ( ) {
28
+ while ( size > 0 ) {
29
+ const z = Math . min ( size , chunkSize ) ;
30
+ yield "a" . repeat ( z ) ;
31
+ size -= z ;
32
+ }
33
+ }
34
+ return Readable . from ( generate ( ) ) ;
35
+ }
36
+ function webStream ( size : number , chunkSize : number ) {
37
+ return Readable . toWeb ( stream ( size , chunkSize ) ) as unknown as ReadableStream ;
38
+ }
16
39
17
40
beforeAll ( async ( ) => {
18
41
const integTestResourcesEnv = await getIntegTestResources ( ) ;
@@ -21,7 +44,8 @@ describe("S3 checksums", () => {
21
44
region = process ?. env ?. AWS_SMOKE_TEST_REGION as string ;
22
45
Bucket = process ?. env ?. AWS_SMOKE_TEST_BUCKET as string ;
23
46
24
- s3 = new S3 ( { region } ) ;
47
+ s3 = new S3 ( { logger, region, requestStreamBufferSize : 8 * 1024 } ) ;
48
+ s3_noRequestBuffer = new S3 ( { logger, region } ) ;
25
49
s3_noChecksum = new S3 ( {
26
50
region,
27
51
requestChecksumCalculation : "WHEN_REQUIRED" ,
@@ -38,7 +62,7 @@ describe("S3 checksums", () => {
38
62
expect ( reqHeader ) . toEqual ( "CRC32" ) ;
39
63
}
40
64
if ( resHeader ) {
41
- expect ( resHeader ) . toEqual ( "7YLNEQ==" ) ;
65
+ expect ( resHeader . length ) . toBeGreaterThanOrEqual ( 8 ) ;
42
66
}
43
67
return r ;
44
68
} ,
@@ -52,10 +76,152 @@ describe("S3 checksums", () => {
52
76
await s3 . putObject ( { Bucket, Key, Body : "abcd" } ) ;
53
77
} ) ;
54
78
79
+ it ( "checksums work with empty objects" , async ( ) => {
80
+ await s3 . putObject ( {
81
+ Bucket,
82
+ Key : Key + "empty" ,
83
+ Body : stream ( 0 , 0 ) ,
84
+ ContentLength : 0 ,
85
+ } ) ;
86
+ const get = await s3 . getObject ( { Bucket, Key : Key + "empty" } ) ;
87
+ expect ( get . Body ) . toBeInstanceOf ( ChecksumStream ) ;
88
+ } ) ;
89
+
55
90
it ( "an object should have checksum by default" , async ( ) => {
56
- await s3 . getObject ( { Bucket, Key } ) ;
91
+ const get = await s3 . getObject ( { Bucket, Key } ) ;
92
+ expect ( get . Body ) . toBeInstanceOf ( ChecksumStream ) ;
57
93
} ) ;
58
94
95
+ describe ( "PUT operations" , ( ) => {
96
+ it ( "S3 throws an error if chunks are too small, because request buffering is off by default" , async ( ) => {
97
+ await s3_noRequestBuffer
98
+ . putObject ( {
99
+ Bucket,
100
+ Key : Key + "small-chunks" ,
101
+ Body : stream ( 24 * 1024 , 8 ) ,
102
+ ContentLength : 24 * 1024 ,
103
+ } )
104
+ . catch ( ( e ) => {
105
+ expect ( String ( e ) ) . toContain (
106
+ "InvalidChunkSizeError: Only the last chunk is allowed to have a size less than 8192 bytes"
107
+ ) ;
108
+ } ) ;
109
+ expect . hasAssertions ( ) ;
110
+ } ) ;
111
+ it ( "should assist user input streams by buffering to the minimum 8kb required by S3" , async ( ) => {
112
+ await s3 . putObject ( {
113
+ Bucket,
114
+ Key : Key + "small-chunks" ,
115
+ Body : stream ( 24 * 1024 , 8 ) ,
116
+ ContentLength : 24 * 1024 ,
117
+ } ) ;
118
+ expect ( logger . warn ) . toHaveBeenCalledWith (
119
+ `@smithy/util-stream - stream chunk size 8 is below threshold of 8192, automatically buffering.`
120
+ ) ;
121
+ const get = await s3 . getObject ( {
122
+ Bucket,
123
+ Key : Key + "small-chunks" ,
124
+ } ) ;
125
+ expect ( ( await get . Body ?. transformToByteArray ( ) ) ?. byteLength ) . toEqual ( 24 * 1024 ) ;
126
+ } ) ;
127
+ it ( "should be able to write an object with a webstream body (using fetch handler without checksum)" , async ( ) => {
128
+ const handler = s3_noChecksum . config . requestHandler ;
129
+ s3_noChecksum . config . requestHandler = new FetchHttpHandler ( ) ;
130
+ await s3_noChecksum . putObject ( {
131
+ Bucket,
132
+ Key : Key + "small-chunks-webstream" ,
133
+ Body : webStream ( 24 * 1024 , 512 ) ,
134
+ ContentLength : 24 * 1024 ,
135
+ } ) ;
136
+ s3_noChecksum . config . requestHandler = handler ;
137
+ const get = await s3 . getObject ( {
138
+ Bucket,
139
+ Key : Key + "small-chunks-webstream" ,
140
+ } ) ;
141
+ expect ( ( await get . Body ?. transformToByteArray ( ) ) ?. byteLength ) . toEqual ( 24 * 1024 ) ;
142
+ } ) ;
143
+ it ( "@aws-sdk/lib-storage Upload should allow webstreams to be used" , async ( ) => {
144
+ await new Upload ( {
145
+ client : s3 ,
146
+ params : {
147
+ Bucket,
148
+ Key : Key + "small-chunks-webstream-mpu" ,
149
+ Body : webStream ( 6 * 1024 * 1024 , 512 ) ,
150
+ } ,
151
+ } ) . done ( ) ;
152
+ const get = await s3 . getObject ( {
153
+ Bucket,
154
+ Key : Key + "small-chunks-webstream-mpu" ,
155
+ } ) ;
156
+ expect ( ( await get . Body ?. transformToByteArray ( ) ) ?. byteLength ) . toEqual ( 6 * 1024 * 1024 ) ;
157
+ } ) ;
158
+ it ( "should allow streams to be used in a manually orchestrated MPU" , async ( ) => {
159
+ const cmpu = await s3 . createMultipartUpload ( {
160
+ Bucket,
161
+ Key : Key + "-mpu" ,
162
+ } ) ;
163
+
164
+ const MB = 1024 * 1024 ;
165
+ const up = [ ] as UploadPartCommandOutput [ ] ;
166
+
167
+ try {
168
+ up . push (
169
+ await s3 . uploadPart ( {
170
+ Bucket,
171
+ Key : Key + "-mpu" ,
172
+ UploadId : cmpu . UploadId ,
173
+ Body : stream ( 5 * MB , 1024 ) ,
174
+ PartNumber : 1 ,
175
+ ContentLength : 5 * MB ,
176
+ } ) ,
177
+ await s3 . uploadPart ( {
178
+ Bucket,
179
+ Key : Key + "-mpu" ,
180
+ UploadId : cmpu . UploadId ,
181
+ Body : stream ( MB , 64 ) ,
182
+ PartNumber : 2 ,
183
+ ContentLength : MB ,
184
+ } )
185
+ ) ;
186
+ expect ( logger . warn ) . toHaveBeenCalledWith (
187
+ `@smithy/util-stream - stream chunk size 1024 is below threshold of 8192, automatically buffering.`
188
+ ) ;
189
+ expect ( logger . warn ) . toHaveBeenCalledWith (
190
+ `@smithy/util-stream - stream chunk size 64 is below threshold of 8192, automatically buffering.`
191
+ ) ;
192
+
193
+ await s3 . completeMultipartUpload ( {
194
+ Bucket,
195
+ Key : Key + "-mpu" ,
196
+ UploadId : cmpu . UploadId ,
197
+ MultipartUpload : {
198
+ Parts : up . map ( ( part , i ) => {
199
+ return {
200
+ PartNumber : i + 1 ,
201
+ ETag : part . ETag ,
202
+ } ;
203
+ } ) ,
204
+ } ,
205
+ } ) ;
206
+
207
+ const go = await s3 . getObject ( {
208
+ Bucket,
209
+ Key : Key + "-mpu" ,
210
+ } ) ;
211
+ expect ( ( await go . Body ?. transformToByteArray ( ) ) ?. byteLength ) . toEqual ( 6 * MB ) ;
212
+
213
+ expect ( go . $metadata . httpStatusCode ) . toEqual ( 200 ) ;
214
+ } catch ( e ) {
215
+ await s3 . abortMultipartUpload ( {
216
+ UploadId : cmpu . UploadId ,
217
+ Bucket,
218
+ Key : Key + "-mpu" ,
219
+ } ) ;
220
+ throw e ;
221
+ }
222
+ } ) ;
223
+ } , 45_000 ) ;
224
+
59
225
describe ( "the stream returned by S3::getObject should function interchangeably between ChecksumStream and default streams" , ( ) => {
60
226
it ( "when collecting the stream" , async ( ) => {
61
227
const defaultStream = ( await s3_noChecksum . getObject ( { Bucket, Key } ) ) . Body as Readable ;
0 commit comments