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" ;
24import type { HttpRequest , HttpResponse } from "@smithy/types" ;
3- import { headStream } from "@smithy/util-stream" ;
5+ import { ChecksumStream , headStream } from "@smithy/util-stream" ;
46import { 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" ;
68
79import { getIntegTestResources } from "../../../tests/e2e/get-integ-test-resources" ;
810
911describe ( "S3 checksums" , ( ) => {
1012 let s3 : S3 ;
1113 let s3_noChecksum : S3 ;
14+ let s3_noRequestBuffer : S3 ;
1215 let Bucket : string ;
1316 let Key : string ;
1417 let region : string ;
1518 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+ }
1639
1740 beforeAll ( async ( ) => {
1841 const integTestResourcesEnv = await getIntegTestResources ( ) ;
@@ -21,7 +44,8 @@ describe("S3 checksums", () => {
2144 region = process ?. env ?. AWS_SMOKE_TEST_REGION as string ;
2245 Bucket = process ?. env ?. AWS_SMOKE_TEST_BUCKET as string ;
2346
24- s3 = new S3 ( { region } ) ;
47+ s3 = new S3 ( { logger, region, requestStreamBufferSize : 8 * 1024 } ) ;
48+ s3_noRequestBuffer = new S3 ( { logger, region } ) ;
2549 s3_noChecksum = new S3 ( {
2650 region,
2751 requestChecksumCalculation : "WHEN_REQUIRED" ,
@@ -38,7 +62,7 @@ describe("S3 checksums", () => {
3862 expect ( reqHeader ) . toEqual ( "CRC32" ) ;
3963 }
4064 if ( resHeader ) {
41- expect ( resHeader ) . toEqual ( "7YLNEQ==" ) ;
65+ expect ( resHeader . length ) . toBeGreaterThanOrEqual ( 8 ) ;
4266 }
4367 return r ;
4468 } ,
@@ -52,10 +76,152 @@ describe("S3 checksums", () => {
5276 await s3 . putObject ( { Bucket, Key, Body : "abcd" } ) ;
5377 } ) ;
5478
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+
5590 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 ) ;
5793 } ) ;
5894
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+
59225 describe ( "the stream returned by S3::getObject should function interchangeably between ChecksumStream and default streams" , ( ) => {
60226 it ( "when collecting the stream" , async ( ) => {
61227 const defaultStream = ( await s3_noChecksum . getObject ( { Bucket, Key } ) ) . Body as Readable ;
0 commit comments