@@ -70,6 +70,15 @@ const storageRetryOptions: IStorageRetryOptions = {
7070 retryPolicyType : StorageRetryPolicyType . EXPONENTIAL
7171} ;
7272
73+ function _isWebClientResponse ( response : IWebClientResponseBase ) : response is IWebClientResponse {
74+ const candidate : Partial < IWebClientResponse > = response ;
75+ return (
76+ typeof candidate . getTextAsync === 'function' &&
77+ typeof candidate . getJsonAsync === 'function' &&
78+ typeof candidate . getBufferAsync === 'function'
79+ ) ;
80+ }
81+
7382/**
7483 * Computes the SHA-256 hash of a file on disk using streaming reads.
7584 */
@@ -216,6 +225,7 @@ export class AmazonS3Client {
216225
217226 // Compute SHA-256 hash of the file before uploading so we can sign the payload
218227 const contentHash : string = await _hashFileAsync ( localFilePath ) ;
228+ const { size } = await FileSystem . getStatisticsAsync ( localFilePath ) ;
219229 const entryStream : FileSystemReadStream = FileSystem . createReadStream ( localFilePath ) ;
220230
221231 // Streaming uploads cannot be retried because the stream is consumed after the first attempt.
@@ -224,7 +234,8 @@ export class AmazonS3Client {
224234 objectName ,
225235 entryStream as Readable ,
226236 true ,
227- contentHash
237+ contentHash ,
238+ size
228239 ) ;
229240 response . stream . resume ( ) ;
230241 if ( ! response . ok ) {
@@ -260,7 +271,7 @@ export class AmazonS3Client {
260271 getSuccessResult : ( ) => T | Promise < T > ,
261272 cleanup ?: ( ) => void
262273 ) : Promise < RetryableRequestResponse < T | undefined > > {
263- const { ok, status, statusText } = response ;
274+ const { ok, status } = response ;
264275 if ( ok ) {
265276 return {
266277 hasNetworkError : false ,
@@ -288,16 +299,25 @@ export class AmazonS3Client {
288299 } ;
289300 } else if ( status === 400 || status === 401 || status === 403 ) {
290301 cleanup ?.( ) ;
291- throw new Error ( `Amazon S3 responded with status code ${ status } ( ${ statusText } )` ) ;
302+ throw await this . _getGetResponseErrorAsync ( response ) ;
292303 } else {
293304 cleanup ?.( ) ;
294305 return {
295306 hasNetworkError : true ,
296- error : new Error ( `Amazon S3 responded with status code ${ status } ( ${ statusText } )` )
307+ error : await this . _getGetResponseErrorAsync ( response )
297308 } ;
298309 }
299310 }
300311
312+ private async _getGetResponseErrorAsync ( response : IWebClientResponseBase ) : Promise < Error > {
313+ if ( _isWebClientResponse ( response ) ) {
314+ return await this . _getS3ErrorAsync ( response ) ;
315+ }
316+
317+ const { status, statusText } = response ;
318+ return new Error ( `Amazon S3 responded with status code ${ status } (${ statusText } )` ) ;
319+ }
320+
301321 private async _makeSignedRequestAsync (
302322 verb : 'GET' | 'PUT' ,
303323 objectName : string ,
@@ -308,27 +328,33 @@ export class AmazonS3Client {
308328 objectName : string ,
309329 body : Readable | undefined ,
310330 stream : true ,
311- contentHash ?: string
331+ contentHash ?: string ,
332+ contentLength ?: number
312333 ) : Promise < IWebClientStreamResponse > ;
313334 private async _makeSignedRequestAsync (
314335 verb : 'GET' | 'PUT' ,
315336 objectName : string ,
316337 body ?: Buffer | Readable ,
317338 stream ?: boolean ,
318- contentHash ?: string
339+ contentHash ?: string ,
340+ contentLength ?: number
319341 ) : Promise < IWebClientResponse | IWebClientStreamResponse > {
320342 // Use the provided content hash if available (e.g. pre-computed from a file on disk),
321343 // otherwise compute from the buffer body, or use the empty hash for GET requests.
322344 const bodyHash : string = contentHash ?? this . _getBufferSha256 ( Buffer . isBuffer ( body ) ? body : undefined ) ;
323- const { url, headers } = this . _buildSignedRequest ( verb , objectName , bodyHash ) ;
345+ const { url, headers } = this . _buildSignedRequest ( verb , objectName , bodyHash , contentLength ) ;
324346
325- const webFetchOptions : IGetFetchOptions | IFetchOptionsWithBody = {
326- verb,
327- headers
328- } ;
329- if ( verb === 'PUT' && body ) {
330- ( webFetchOptions as IFetchOptionsWithBody ) . body = body ;
331- }
347+ const webFetchOptions : IGetFetchOptions | IFetchOptionsWithBody =
348+ verb === 'GET'
349+ ? {
350+ verb : 'GET' ,
351+ headers
352+ }
353+ : {
354+ verb : 'PUT' ,
355+ headers,
356+ body
357+ } ;
332358
333359 if ( stream ) {
334360 return await this . _webClient . fetchStreamAsync ( url , webFetchOptions ) ;
@@ -343,12 +369,16 @@ export class AmazonS3Client {
343369 private _buildSignedRequest (
344370 verb : 'GET' | 'PUT' ,
345371 objectName : string ,
346- bodyHash : string
372+ bodyHash : string ,
373+ contentLength ?: number
347374 ) : { url : string ; headers : Record < string , string > } {
348375 const isoDateString : IIsoDateString = this . _getIsoDateString ( ) ;
349376 const headers : Record < string , string > = { } ;
350377 headers [ DATE_HEADER_NAME ] = isoDateString . dateTime ;
351378 headers [ CONTENT_HASH_HEADER_NAME ] = bodyHash ;
379+ if ( verb === 'PUT' && contentLength !== undefined ) {
380+ headers [ 'Content-Length' ] = `${ contentLength } ` ;
381+ }
352382
353383 // the host can be e.g. https://s3.aws.com or http://localhost:9000
354384 const host : string = this . _s3Endpoint . replace ( protocolRegex , '' ) ;
0 commit comments