import { URLBuilder } from "@azure/ms-rest-js" ; import axios , { AxiosResponse } from "axios" ; import { URL } from "url" ; import IExtentStore from "../../common/persistence/IExtentStore" ; import { convertRawHeadersToMetadata , getMD5FromStream } from "../../common/utils/utils" ; import BlobStorageContext from "../context/BlobStorageContext" ; import NotImplementedError from "../errors/NotImplementedError" ; import StorageErrorFactory from "../errors/StorageErrorFactory" ; import * as Models from "../generated/artifacts/models" ; import Context from "../generated/Context" ; import IBlobHandler from "../generated/handlers/IBlobHandler" ; import ILogger from "../generated/utils/ILogger" ; import { parseXML } from "../generated/utils/xml" ; import { extractStoragePartsFromPath } from "../middlewares/blobStorageContext.middleware" ; import IBlobMetadataStore , { BlobModel } from "../persistence/IBlobMetadataStore" ; import { BLOB_API_VERSION , EMULATOR_ACCOUNT_KIND , EMULATOR_ACCOUNT_SKUNAME , HeaderConstants } from "../utils/constants" ; import { deserializePageBlobRangeHeader , deserializeRangeHeader } from "../utils/utils" ; import BaseHandler from "./BaseHandler" ; import IPageBlobRangesManager from "./IPageBlobRangesManager" ; export class extends BaseHandler implements IBlobHandler export default class BlobHandler extends BaseHandler implements IBlobHandler { constructor ( metadataStore , extentStore , logger , loose , private readonly rangesManager ) { super ( metadataStore , extentStore , logger , loose ) ; } public setAccessControl ( options , context ) { throw new NotImplementedError ( context . contextId ) ; } public getAccessControl ( options , context ) { throw new NotImplementedError ( context . contextId ) ; } public rename ( renameSource , options , context ) { throw new NotImplementedError ( context . contextId ) ; } param { Models . BlobDownloadOptionalParams } options param { Context } context returns { Promise < Models . BlobDownloadResponse > } memberof public async download ( options , context ) { const blobCtx = new BlobStorageContext ( context ) ; const accountName = blobCtx . account ! ; const containerName = blobCtx . container ! ; const blobName = blobCtx . blob ! ; const blob = await this . metadataStore . downloadBlob ( context , accountName , containerName , blobName , options . snapshot , options . leaseAccessConditions , options . modifiedAccessConditions ) ; if ( blob . properties . blobType === Models . BlobType . BlockBlob ) { return this . downloadBlockBlobOrAppendBlob ( options , context , blob ) ; } else if ( blob . properties . blobType === Models . BlobType . PageBlob ) { return this . downloadPageBlob ( options , context , blob ) ; } else if ( blob . properties . blobType === Models . BlobType . AppendBlob ) { return this . downloadBlockBlobOrAppendBlob ( options , context , blob ) ; } else { throw StorageErrorFactory . getInvalidOperation ( context . contextId ! ) ; } } param { Models . BlobGetPropertiesOptionalParams } options param { Context } context returns { Promise < Models . BlobGetPropertiesResponse > } memberof public async getProperties ( options , context ) { const blobCtx = new BlobStorageContext ( context ) ; const account = blobCtx . account ! ; const container = blobCtx . container ! ; const blob = blobCtx . blob ! ; const res = await this . metadataStore . getBlobProperties ( context , account , container , blob , options . snapshot , options . leaseAccessConditions , options . modifiedAccessConditions ) ; const againstMetadata = context . request ! . getQuery ( "comp" ) === "metadata" ; const response = againstMetadata ? { statusCode : 200 , metadata : res . metadata , eTag : res . properties . etag , requestId : context . contextId , version : BLOB_API_VERSION , date : context . startTime , clientRequestId : options . requestId , contentLength : res . properties . contentLength , lastModified : res . properties . lastModified } : { statusCode : 200 , metadata : res . metadata , isIncrementalCopy : res . properties . incrementalCopy , eTag : res . properties . etag , requestId : context . contextId , version : BLOB_API_VERSION , date : context . startTime , acceptRanges : "bytes" , blobCommittedBlockCount : res . properties . blobType === Models . BlobType . AppendBlob ? res . blobCommittedBlockCount : undefined , isServerEncrypted : true , clientRequestId : options . requestId , ... res . properties , cacheControl : context . request ! . getQuery ( "rscc" ) ?? res . properties . cacheControl , contentDisposition : context . request ! . getQuery ( "rscd" ) ?? res . properties . contentDisposition , contentEncoding : context . request ! . getQuery ( "rsce" ) ?? res . properties . contentEncoding , contentLanguage : context . request ! . getQuery ( "rscl" ) ?? res . properties . contentLanguage , contentType : context . request ! . getQuery ( "rsct" ) ?? res . properties . contentType , } ; return response ; } param { Models . BlobDeleteMethodOptionalParams } options param { Context } context returns { Promise < Models . BlobDeleteResponse > } memberof public async delete ( options , context ) { const blobCtx = new BlobStorageContext ( context ) ; const account = blobCtx . account ! ; const container = blobCtx . container ! ; const blob = blobCtx . blob ! ; await this . metadataStore . deleteBlob ( context , account , container , blob , options ) ; const response = { statusCode : 202 , requestId : context . contextId , date : context . startTime , version : BLOB_API_VERSION , clientRequestId : options . requestId } ; return response ; } param { Models . BlobUndeleteOptionalParams } options param { Context } context returns { Promise < Models . BlobUndeleteResponse > } memberof public async undelete ( options , context ) { throw new NotImplementedError ( context . contextId ) ; } param { Models . BlobSetHTTPHeadersOptionalParams } options param { Context } context returns { Promise < Models . BlobSetHTTPHeadersResponse > } memberof public async setHTTPHeaders ( options , context ) { const blobCtx = new BlobStorageContext ( context ) ; const account = blobCtx . account ! ; const container = blobCtx . container ! ; const blob = blobCtx . blob ! ; let res ; const sequenceNumberAction = context . request ! . getHeader ( HeaderConstants . X_MS_SEQUENCE_NUMBER_ACTION ) ; const sequenceNumber = context . request ! . getHeader ( HeaderConstants . X_MS_BLOB_SEQUENCE_NUMBER ) ; if ( sequenceNumberAction !== undefined ) { this . logger . verbose ( "BlobHandler:setHTTPHeaders() Redirect to updateSequenceNumber..." , context . contextId ) ; res = await this . metadataStore . updateSequenceNumber ( context , account , container , blob , sequenceNumberAction . toLowerCase ( ) as Models . SequenceNumberActionType , sequenceNumber === undefined ? undefined : parseInt ( sequenceNumber , 10 ) , options . leaseAccessConditions , options . modifiedAccessConditions ) ; } else { res = await this . metadataStore . setBlobHTTPHeaders ( context , account , container , blob , options . leaseAccessConditions , options . blobHTTPHeaders , options . modifiedAccessConditions ) ; } const response = { statusCode : 200 , eTag : res . etag , lastModified : res . lastModified , blobSequenceNumber : res . blobSequenceNumber , requestId : context . contextId , version : BLOB_API_VERSION , date : context . startTime , clientRequestId : options . requestId } ; return response ; } param { Models . BlobSetMetadataOptionalParams } options param { Context } context returns { Promise < Models . BlobSetMetadataResponse > } memberof public async setMetadata ( options , context ) { const blobCtx = new BlobStorageContext ( context ) ; const account = blobCtx . account ! ; const container = blobCtx . container ! ; const blob = blobCtx . blob ! ; const metadata = convertRawHeadersToMetadata ( blobCtx . request ! . getRawHeaders ( ) ) ; const res = await this . metadataStore . setBlobMetadata ( context , account , container , blob , options . leaseAccessConditions , metadata , options . modifiedAccessConditions ) ; const response = { statusCode : 200 , eTag : res . etag , lastModified : res . lastModified , isServerEncrypted : true , requestId : context . contextId , date : context . startTime , version : BLOB_API_VERSION , clientRequestId : options . requestId } ; return response ; } param { Models . BlobAcquireLeaseOptionalParams } options param { Context } context returns { Promise < Models . BlobAcquireLeaseResponse > } memberof public async acquireLease ( options , context ) { const blobCtx = new BlobStorageContext ( context ) ; const account = blobCtx . account ! ; const container = blobCtx . container ! ; const blob = blobCtx . blob ! ; const snapshot = blobCtx . request ! . getQuery ( "snapshot" ) ; if ( snapshot !== undefined && snapshot !== "" ) { throw StorageErrorFactory . getInvalidOperation ( context . contextId , "A lease cannot be granted for a blob snapshot" ) ; } const res = await this . metadataStore . acquireBlobLease ( context , account , container , blob , options . duration ! , options . proposedLeaseId , options ) ; const response = { date : blobCtx . startTime ! , eTag : res . properties . etag , lastModified : res . properties . lastModified , leaseId : res . leaseId , requestId : context . contextId , version : BLOB_API_VERSION , statusCode : 201 , clientRequestId : options . requestId } ; return response ; } param { string } leaseId param { Models . BlobReleaseLeaseOptionalParams } options param { Context } context returns { Promise < Models . BlobReleaseLeaseResponse > } memberof public async releaseLease ( leaseId , options , context ) { const blobCtx = new BlobStorageContext ( context ) ; const account = blobCtx . account ! ; const container = blobCtx . container ! ; const blob = blobCtx . blob ! ; const res = await this . metadataStore . releaseBlobLease ( context , account , container , blob , leaseId , options ) ; const response = { date : blobCtx . startTime ! , eTag : res . etag , lastModified : res . lastModified , requestId : context . contextId , version : BLOB_API_VERSION , statusCode : 200 , clientRequestId : options . requestId } ; return response ; } param { string } leaseId param { Models . BlobRenewLeaseOptionalParams } options param { Context } context returns { Promise < Models . BlobRenewLeaseResponse > } memberof public async renewLease ( leaseId , options , context ) { const blobCtx = new BlobStorageContext ( context ) ; const account = blobCtx . account ! ; const container = blobCtx . container ! ; const blob = blobCtx . blob ! ; const res = await this . metadataStore . renewBlobLease ( context , account , container , blob , leaseId , options ) ; const response = { date : blobCtx . startTime ! , eTag : res . properties . etag , lastModified : res . properties . lastModified , leaseId : res . leaseId , requestId : context . contextId , version : BLOB_API_VERSION , statusCode : 200 , clientRequestId : options . requestId } ; return response ; } param { string } leaseId param { string } proposedLeaseId param { Models . BlobChangeLeaseOptionalParams } options param { Context } context returns { Promise < Models . BlobChangeLeaseResponse > } memberof public async changeLease ( leaseId , proposedLeaseId , options , context ) { const blobCtx = new BlobStorageContext ( context ) ; const account = blobCtx . account ! ; const container = blobCtx . container ! ; const blob = blobCtx . blob ! ; const res = await this . metadataStore . changeBlobLease ( context , account , container , blob , leaseId , proposedLeaseId , options ) ; const response = { date : blobCtx . startTime ! , eTag : res . properties . etag , lastModified : res . properties . lastModified , leaseId : res . leaseId , requestId : context . contextId , version : BLOB_API_VERSION , statusCode : 200 , clientRequestId : options . requestId } ; return response ; } param { Models . BlobBreakLeaseOptionalParams } options param { Context } context returns { Promise < Models . BlobBreakLeaseResponse > } memberof public async breakLease ( options , context ) { const blobCtx = new BlobStorageContext ( context ) ; const account = blobCtx . account ! ; const container = blobCtx . container ! ; const blob = blobCtx . blob ! ; const res = await this . metadataStore . breakBlobLease ( context , account , container , blob , options . breakPeriod , options ) ; const response = { date : blobCtx . startTime ! , eTag : res . properties . etag , lastModified : res . properties . lastModified , leaseTime : res . leaseTime , requestId : context . contextId , version : BLOB_API_VERSION , statusCode : 202 , clientRequestId : options . requestId } ; return response ; } see https param { Models . BlobCreateSnapshotOptionalParams } options param { Context } context returns { Promise < Models . BlobCreateSnapshotResponse > } memberof public async createSnapshot ( options , context ) { const blobCtx = new BlobStorageContext ( context ) ; const account = blobCtx . account ! ; const container = blobCtx . container ! ; const blob = blobCtx . blob ! ; const metadata = convertRawHeadersToMetadata ( blobCtx . request ! . getRawHeaders ( ) ) ; const res = await this . metadataStore . createSnapshot ( context , account , container , blob , options . leaseAccessConditions , ! options . metadata || JSON . stringify ( options . metadata ) === "{}" ? undefined : metadata , options . modifiedAccessConditions ) ; const response = { statusCode : 201 , eTag : res . properties . etag , lastModified : res . properties . lastModified , requestId : context . contextId , date : context . startTime ! , version : BLOB_API_VERSION , snapshot : res . snapshot , clientRequestId : options . requestId } ; return response ; } param { string } copySource param { Models . BlobStartCopyFromURLOptionalParams } options param { Context } context returns { Promise < Models . BlobStartCopyFromURLResponse > } memberof public async startCopyFromURL ( copySource , options , context ) { const blobCtx = new BlobStorageContext ( context ) ; const account = blobCtx . account ! ; const container = blobCtx . container ! ; const blob = blobCtx . blob ! ; const url = new URL ( copySource ) ; const [ sourceAccount , sourceContainer , sourceBlob ] = extractStoragePartsFromPath ( url . hostname , url . pathname ) ; const snapshot = url . searchParams . get ( "snapshot" ) || "" ; if ( sourceAccount === undefined || sourceContainer === undefined || sourceBlob === undefined ) { throw StorageErrorFactory . getBlobNotFound ( context . contextId ! ) ; } if ( sourceAccount !== blobCtx . account ) { const currentServer = blobCtx . request ! . getHeader ( "Host" ) || "" ; if ( currentServer !== url . host ) { this . logger . error ( `BlobHandler:startCopyFromURL() Source account ${ url } is not on the same Azurite instance as target account ${ account }` , context . contextId ) ; throw StorageErrorFactory . getCannotVerifyCopySource ( context . contextId ! , 404 , "The specified resource does not exist" ) ; } this . logger . debug ( `BlobHandler:startCopyFromURL() Validating access to the source account ${ sourceAccount }` , context . contextId ) ; const metadataUrl = URLBuilder . parse ( copySource ) ; metadataUrl . setQueryParameter ( "comp" , "metadata" ) ; const validationResponse = await axios . get ( metadataUrl . toString ( ) , { validateStatus : ( ) => true } ) ; if ( validationResponse . status === 200 ) { this . logger . debug ( `BlobHandler:startCopyFromURL() Successfully validated access to source account ${ sourceAccount }` , context . contextId ) ; } else { this . logger . debug ( `BlobHandler:startCopyFromURL() Access denied to source account ${ sourceAccount } StatusCode=${ validationResponse . status }, AuthenticationErrorDetail=${ validationResponse . data }` , context . contextId ) ; if ( validationResponse . status === 404 ) { throw StorageErrorFactory . getCannotVerifyCopySource ( context . contextId ! , validationResponse . status , "The specified resource does not exist" ) ; } else { let message = "Could not verify the copy source within the specified time." ; if ( validationResponse . headers [ HeaderConstants . CONTENT_TYPE ] === "application/xml" ) { const authenticationError = await parseXML ( validationResponse . data ) ; if ( authenticationError . Message !== undefined ) { message = authenticationError . Message . replace ( /\n+/gm , "" ) ; } } throw StorageErrorFactory . getCannotVerifyCopySource ( context . contextId ! , validationResponse . status , message ) ; } } } const metadata = convertRawHeadersToMetadata ( blobCtx . request ! . getRawHeaders ( ) ) ; const res = await this . metadataStore . startCopyFromURL ( context , { account : sourceAccount , container : sourceContainer , blob : sourceBlob , snapshot } , { account , container , blob } , copySource , metadata , options . tier , options ) ; const response = { statusCode : 202 , eTag : res . etag , lastModified : res . lastModified , requestId : context . contextId , version : BLOB_API_VERSION , date : context . startTime , copyId : res . copyId , copyStatus : res . copyStatus , clientRequestId : options . requestId } ; return response ; } param { string } copyId param { Models . BlobAbortCopyFromURLOptionalParams } options param { Context } context returns { Promise < Models . BlobAbortCopyFromURLResponse > } memberof public async abortCopyFromURL ( copyId , options , context ) { const blobCtx = new BlobStorageContext ( context ) ; const accountName = blobCtx . account ! ; const containerName = blobCtx . container ! ; const blobName = blobCtx . blob ! ; const blob = await this . metadataStore . downloadBlob ( context , accountName , containerName , blobName , undefined , options . leaseAccessConditions ) ; if ( blob . properties . copyId !== copyId ) { throw StorageErrorFactory . getCopyIdMismatch ( context . contextId ! ) ; } if ( blob . properties . copyStatus === Models . CopyStatusType . Success ) { throw StorageErrorFactory . getNoPendingCopyOperation ( context . contextId ! ) ; } const response = { statusCode : 204 , requestId : context . contextId , version : BLOB_API_VERSION , date : context . startTime , clientRequestId : options . requestId } ; return response ; } param { string } copySource param { Models . BlobStartCopyFromURLOptionalParams } options param { Context } context returns { Promise < Models . BlobStartCopyFromURLResponse > } memberof public async copyFromURL ( copySource , options , context ) { const blobCtx = new BlobStorageContext ( context ) ; const account = blobCtx . account ! ; const container = blobCtx . container ! ; const blob = blobCtx . blob ! ; const url = new URL ( copySource ) ; const [ sourceAccount , sourceContainer , sourceBlob ] = extractStoragePartsFromPath ( url . hostname , url . pathname ) ; const snapshot = url . searchParams . get ( "snapshot" ) || "" ; if ( sourceAccount !== blobCtx . account || sourceAccount === undefined || sourceContainer === undefined || sourceBlob === undefined ) { throw StorageErrorFactory . getBlobNotFound ( context . contextId ! ) ; } const metadata = convertRawHeadersToMetadata ( blobCtx . request ! . getRawHeaders ( ) ) ; const res = await this . metadataStore . copyFromURL ( context , { account : sourceAccount , container : sourceContainer , blob : sourceBlob , snapshot } , { account , container , blob } , copySource , metadata , options . tier , options ) ; let copyStatus ; if ( res . copyStatus !== undefined ) { if ( res . copyStatus === Models . CopyStatusType . Success ) { copyStatus = Models . SyncCopyStatusType . Success ; } else { throw StorageErrorFactory . getUnexpectedSyncCopyStatus ( context . contextId ! , res . copyStatus ) ; } } const response = { statusCode : 202 , eTag : res . etag , lastModified : res . lastModified , requestId : context . contextId , version : BLOB_API_VERSION , date : context . startTime , copyId : res . copyId , copyStatus , clientRequestId : options . requestId } ; return response ; } param { Models . AccessTier } tier param { Models . BlobSetTierOptionalParams } options param { Context } context returns { Promise < Models . BlobSetTierResponse > } memberof public async setTier ( tier , options , context ) { const blobCtx = new BlobStorageContext ( context ) ; const account = blobCtx . account ! ; const container = blobCtx . container ! ; const blob = blobCtx . blob ! ; const res = await this . metadataStore . setTier ( context , account , container , blob , tier , options . leaseAccessConditions ) ; const response = { requestId : context . contextId , version : BLOB_API_VERSION , statusCode : res , clientRequestId : options . requestId } ; return response ; } param { Context } context returns { Promise < Models . BlobGetAccountInfoResponse > } memberof public async getAccountInfo ( context ) { const response = { statusCode : 200 , requestId : context . contextId , clientRequestId : context . request ! . getHeader ( "x-ms-client-request-id" ) , skuName : EMULATOR_ACCOUNT_SKUNAME , accountKind : EMULATOR_ACCOUNT_KIND , date : context . startTime ! , version : BLOB_API_VERSION } ; return response ; } param { Context } context returns { Promise < Models . BlobGetAccountInfoResponse > } memberof public async getAccountInfoWithHead ( context ) { return this . getAccountInfo ( context ) ; } private param { Models . BlobDownloadOptionalParams } options param { Context } context param { BlobModel } blob returns { Promise < Models . BlobDownloadResponse > } memberof private async downloadBlockBlobOrAppendBlob ( options , context , blob ) { if ( blob . isCommitted === false ) { throw StorageErrorFactory . getBlobNotFound ( context . contextId ! ) ; } const rangesParts = deserializeRangeHeader ( context . request ! . getHeader ( "range" ) , context . request ! . getHeader ( "x-ms-range" ) ) ; const rangeStart = rangesParts [ 0 ] ; let rangeEnd = rangesParts [ 1 ] ; if ( rangeStart > blob . properties . contentLength ! ) { throw StorageErrorFactory . getInvalidPageRange ( context . contextId ! ) ; } if ( rangeEnd + 1 >= blob . properties . contentLength ! ) { rangeEnd = blob . properties . contentLength ! - 1 ; } const contentLength = rangeEnd - rangeStart + 1 ; const partialRead = contentLength !== blob . properties . contentLength ! ; this . logger . info ( `BlobHandler:downloadBlockBlobOrAppendBlob() NormalizedDownloadRange=bytes=${ rangeStart }-${ rangeEnd } RequiredContentLength=${ contentLength }` , context . contextId ) ; let bodyGetter ; const blocks = blob . committedBlocksInOrder ; if ( blocks === undefined || blocks . length === 0 ) { bodyGetter = async ( ) => { if ( blob . persistency === undefined ) { return this . extentStore . readExtent ( undefined , context . contextId ) ; } return this . extentStore . readExtent ( { id : blob . persistency . id , offset : blob . persistency . offset + rangeStart , count : Math . min ( blob . persistency . count , contentLength ) } , context . contextId ) ; } ; } else { bodyGetter = async ( ) => { return this . extentStore . readExtents ( blocks . map ( ( block ) => block . persistency ) , rangeStart , rangeEnd + 1 - rangeStart , context . contextId ) ; } ; } let contentRange ; if ( context . request ! . getHeader ( "range" ) || context . request ! . getHeader ( "x-ms-range" ) ) { contentRange = `bytes ${ rangeStart }-${ rangeEnd }/${ blob . properties . contentLength ! }` ; } let body = await bodyGetter ( ) ; let contentMD5 ; if ( ! partialRead ) { contentMD5 = blob . properties . contentMD5 ; } if ( contentLength <= 4 * 1024 * 1024 && contentMD5 === undefined && body !== undefined ) { contentMD5 = await getMD5FromStream ( body ) ; body = await bodyGetter ( ) ; } const response = { statusCode : contentRange ? 206 : 200 , body , metadata : blob . metadata , eTag : blob . properties . etag , requestId : context . contextId , date : context . startTime ! , version : BLOB_API_VERSION , ... blob . properties , cacheControl : context . request ! . getQuery ( "rscc" ) ?? blob . properties . cacheControl , contentDisposition : context . request ! . getQuery ( "rscd" ) ?? blob . properties . contentDisposition , contentEncoding : context . request ! . getQuery ( "rsce" ) ?? blob . properties . contentEncoding , contentLanguage : context . request ! . getQuery ( "rscl" ) ?? blob . properties . contentLanguage , contentType : context . request ! . getQuery ( "rsct" ) ?? blob . properties . contentType , blobContentMD5 : blob . properties . contentMD5 , acceptRanges : "bytes" , contentLength , contentRange , contentMD5 , isServerEncrypted : true , clientRequestId : options . requestId , creationTime : blob . properties . creationTime , blobCommittedBlockCount : blob . properties . blobType === Models . BlobType . AppendBlob ? ( blob . committedBlocksInOrder || [ ] ) . length : undefined , } ; return response ; } private param { Models . BlobDownloadOptionalParams } options param { Context } context param { BlobModel } blob returns { Promise < Models . BlobDownloadResponse > } memberof private async downloadPageBlob ( options , context , blob ) { const rangesParts = deserializePageBlobRangeHeader ( context . request ! . getHeader ( "range" ) , context . request ! . getHeader ( "x-ms-range" ) , false ) ; const rangeStart = rangesParts [ 0 ] ; let rangeEnd = rangesParts [ 1 ] ; if ( rangeStart > blob . properties . contentLength ! ) { throw StorageErrorFactory . getInvalidPageRange ( context . contextId ! ) ; } if ( rangeEnd + 1 >= blob . properties . contentLength ! ) { rangeEnd = blob . properties . contentLength ! - 1 ; } const contentLength = rangeEnd - rangeStart + 1 ; const partialRead = contentLength !== blob . properties . contentLength ! ; this . logger . info ( `BlobHandler:downloadPageBlob() NormalizedDownloadRange=bytes=${ rangeStart }-${ rangeEnd } RequiredContentLength=${ contentLength }` , context . contextId ) ; blob . pageRangesInOrder = blob . pageRangesInOrder || [ ] ; const ranges = contentLength <= 0 ? [ ] : this . rangesManager . fillZeroRanges ( blob . pageRangesInOrder , { start : rangeStart , end : rangeEnd } ) ; const bodyGetter = async ( ) => { return this . extentStore . readExtents ( ranges . map ( ( value ) => value . persistency ) , 0 , contentLength , context . contextId ) ; } ; let body = await bodyGetter ( ) ; let contentMD5 ; if ( ! partialRead ) { contentMD5 = blob . properties . contentMD5 ; } if ( contentLength <= 4 * 1024 * 1024 && contentMD5 === undefined && body !== undefined ) { contentMD5 = await getMD5FromStream ( body ) ; body = await bodyGetter ( ) ; } let contentRange ; if ( context . request ! . getHeader ( "range" ) || context . request ! . getHeader ( "x-ms-range" ) ) { contentRange = `bytes ${ rangeStart }-${ rangeEnd }/${ blob . properties . contentLength ! }` ; } const response = { statusCode : rangesParts [ 1 ] === Infinity && rangesParts [ 0 ] === 0 ? 200 : 206 , body , metadata : blob . metadata , eTag : blob . properties . etag , requestId : context . contextId , date : context . startTime ! , version : BLOB_API_VERSION , ... blob . properties , cacheControl : context . request ! . getQuery ( "rscc" ) ?? blob . properties . cacheControl , contentDisposition : context . request ! . getQuery ( "rscd" ) ?? blob . properties . contentDisposition , contentEncoding : context . request ! . getQuery ( "rsce" ) ?? blob . properties . contentEncoding , contentLanguage : context . request ! . getQuery ( "rscl" ) ?? blob . properties . contentLanguage , contentType : context . request ! . getQuery ( "rsct" ) ?? blob . properties . contentType , contentLength , contentRange , contentMD5 , blobContentMD5 : blob . properties . contentMD5 , isServerEncrypted : true , creationTime : blob . properties . creationTime , clientRequestId : options . requestId } ; return response ; } }