azurite
Version:
An open source Azure Storage API compatible server
877 lines • 40 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const ms_rest_js_1 = require("@azure/ms-rest-js");
const axios_1 = tslib_1.__importDefault(require("axios"));
const url_1 = require("url");
const utils_1 = require("../../common/utils/utils");
const BlobStorageContext_1 = tslib_1.__importDefault(require("../context/BlobStorageContext"));
const NotImplementedError_1 = tslib_1.__importDefault(require("../errors/NotImplementedError"));
const StorageErrorFactory_1 = tslib_1.__importDefault(require("../errors/StorageErrorFactory"));
const Models = tslib_1.__importStar(require("../generated/artifacts/models"));
const xml_1 = require("../generated/utils/xml");
const blobStorageContext_middleware_1 = require("../middlewares/blobStorageContext.middleware");
const constants_1 = require("../utils/constants");
const utils_2 = require("../utils/utils");
const BaseHandler_1 = tslib_1.__importDefault(require("./BaseHandler"));
/**
* BlobHandler handles Azure Storage Blob related requests.
*
* @export
* @class BlobHandler
* @extends {BaseHandler}
* @implements {IBlobHandler}
*/
class BlobHandler extends BaseHandler_1.default {
constructor(metadataStore, extentStore, logger, loose, rangesManager) {
super(metadataStore, extentStore, logger, loose);
this.rangesManager = rangesManager;
}
/**
* Download blob.
*
* @param {Models.BlobDownloadOptionalParams} options
* @param {Context} context
* @returns {Promise<Models.BlobDownloadResponse>}
* @memberof BlobHandler
*/
async download(options, context) {
const blobCtx = new BlobStorageContext_1.default(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_1.default.getInvalidOperation(context.contextId);
}
}
/**
* Get blob properties.
*
* @param {Models.BlobGetPropertiesOptionalParams} options
* @param {Context} context
* @returns {Promise<Models.BlobGetPropertiesResponse>}
* @memberof BlobHandler
*/
async getProperties(options, context) {
const blobCtx = new BlobStorageContext_1.default(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);
// TODO: Create get metadata specific request in swagger
const againstMetadata = context.request.getQuery("comp") === "metadata";
const response = againstMetadata
? {
statusCode: 200,
metadata: res.metadata,
eTag: res.properties.etag,
requestId: context.contextId,
version: constants_1.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: constants_1.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,
tagCount: res.properties.tagCount,
};
return response;
}
/**
* Delete blob or snapshots.
*
* @param {Models.BlobDeleteMethodOptionalParams} options
* @param {Context} context
* @returns {Promise<Models.BlobDeleteResponse>}
* @memberof BlobHandler
*/
async delete(options, context) {
const blobCtx = new BlobStorageContext_1.default(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: constants_1.BLOB_API_VERSION,
clientRequestId: options.requestId,
deleteTypePermanent: true
};
return response;
}
/**
* Undelete blob.
*
* @param {Models.BlobUndeleteOptionalParams} options
* @param {Context} context
* @returns {Promise<Models.BlobUndeleteResponse>}
* @memberof BlobHandler
*/
async undelete(options, context) {
throw new NotImplementedError_1.default(context.contextId);
}
async setExpiry(expiryOptions, options, context) {
throw new NotImplementedError_1.default(context.contextId);
}
/**
* Set HTTP Headers.
* see also https://docs.microsoft.com/en-us/rest/api/storageservices/set-blob-properties
*
* @param {Models.BlobSetHTTPHeadersOptionalParams} options
* @param {Context} context
* @returns {Promise<Models.BlobSetHTTPHeadersResponse>}
* @memberof BlobHandler
*/
async setHTTPHeaders(options, context) {
const blobCtx = new BlobStorageContext_1.default(context);
const account = blobCtx.account;
const container = blobCtx.container;
const blob = blobCtx.blob;
let res;
// Workaround for https://github.com/Azure/Azurite/issues/332
const sequenceNumberAction = context.request.getHeader(constants_1.HeaderConstants.X_MS_SEQUENCE_NUMBER_ACTION);
const sequenceNumber = context.request.getHeader(constants_1.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(), 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: constants_1.BLOB_API_VERSION,
date: context.startTime,
clientRequestId: options.requestId
};
return response;
}
async setImmutabilityPolicy(options, context) {
throw new NotImplementedError_1.default(context.contextId);
}
async deleteImmutabilityPolicy(options, context) {
throw new NotImplementedError_1.default(context.contextId);
}
async setLegalHold(legalHold, options, context) {
throw new NotImplementedError_1.default(context.contextId);
}
/**
* Set Metadata.
*
* @param {Models.BlobSetMetadataOptionalParams} options
* @param {Context} context
* @returns {Promise<Models.BlobSetMetadataResponse>}
* @memberof BlobHandler
*/
async setMetadata(options, context) {
const blobCtx = new BlobStorageContext_1.default(context);
const account = blobCtx.account;
const container = blobCtx.container;
const blob = blobCtx.blob;
// Preserve metadata key case
const metadata = (0, utils_1.convertRawHeadersToMetadata)(blobCtx.request.getRawHeaders(), context.contextId);
const res = await this.metadataStore.setBlobMetadata(context, account, container, blob, options.leaseAccessConditions, metadata, options.modifiedAccessConditions);
// ToDo: return correct headers and test for these.
const response = {
statusCode: 200,
eTag: res.etag,
lastModified: res.lastModified,
isServerEncrypted: true,
requestId: context.contextId,
date: context.startTime,
version: constants_1.BLOB_API_VERSION,
clientRequestId: options.requestId
};
return response;
}
/**
* Acquire Blob Lease.
*
* @param {Models.BlobAcquireLeaseOptionalParams} options
* @param {Context} context
* @returns {Promise<Models.BlobAcquireLeaseResponse>}
* @memberof BlobHandler
*/
async acquireLease(options, context) {
const blobCtx = new BlobStorageContext_1.default(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_1.default.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: constants_1.BLOB_API_VERSION,
statusCode: 201,
clientRequestId: options.requestId
};
return response;
}
/**
* release blob lease
*
* @param {string} leaseId
* @param {Models.BlobReleaseLeaseOptionalParams} options
* @param {Context} context
* @returns {Promise<Models.BlobReleaseLeaseResponse>}
* @memberof BlobHandler
*/
async releaseLease(leaseId, options, context) {
const blobCtx = new BlobStorageContext_1.default(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: constants_1.BLOB_API_VERSION,
statusCode: 200,
clientRequestId: options.requestId
};
return response;
}
/**
* Renew blob lease
*
* @param {string} leaseId
* @param {Models.BlobRenewLeaseOptionalParams} options
* @param {Context} context
* @returns {Promise<Models.BlobRenewLeaseResponse>}
* @memberof BlobHandler
*/
async renewLease(leaseId, options, context) {
const blobCtx = new BlobStorageContext_1.default(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: constants_1.BLOB_API_VERSION,
statusCode: 200,
clientRequestId: options.requestId
};
return response;
}
/**
* Change lease.
*
* @param {string} leaseId
* @param {string} proposedLeaseId
* @param {Models.BlobChangeLeaseOptionalParams} options
* @param {Context} context
* @returns {Promise<Models.BlobChangeLeaseResponse>}
* @memberof BlobHandler
*/
async changeLease(leaseId, proposedLeaseId, options, context) {
const blobCtx = new BlobStorageContext_1.default(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: constants_1.BLOB_API_VERSION,
statusCode: 200,
clientRequestId: options.requestId
};
return response;
}
/**
* Break lease.
*
* @param {Models.BlobBreakLeaseOptionalParams} options
* @param {Context} context
* @returns {Promise<Models.BlobBreakLeaseResponse>}
* @memberof BlobHandler
*/
async breakLease(options, context) {
const blobCtx = new BlobStorageContext_1.default(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: constants_1.BLOB_API_VERSION,
statusCode: 202,
clientRequestId: options.requestId
};
return response;
}
/**
* Create snapshot.
*
* @see https://docs.microsoft.com/en-us/rest/api/storageservices/snapshot-blob
*
* @param {Models.BlobCreateSnapshotOptionalParams} options
* @param {Context} context
* @returns {Promise<Models.BlobCreateSnapshotResponse>}
* @memberof BlobHandler
*/
async createSnapshot(options, context) {
const blobCtx = new BlobStorageContext_1.default(context);
const account = blobCtx.account;
const container = blobCtx.container;
const blob = blobCtx.blob;
// Preserve metadata key case
const metadata = (0, utils_1.convertRawHeadersToMetadata)(blobCtx.request.getRawHeaders(), context.contextId);
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: constants_1.BLOB_API_VERSION,
snapshot: res.snapshot,
clientRequestId: options.requestId
};
return response;
}
/**
* Start copy from Url.
*
* @param {string} copySource
* @param {Models.BlobStartCopyFromURLOptionalParams} options
* @param {Context} context
* @returns {Promise<Models.BlobStartCopyFromURLResponse>}
* @memberof BlobHandler
*/
async startCopyFromURL(copySource, options, context) {
const blobCtx = new BlobStorageContext_1.default(context);
const account = blobCtx.account;
const container = blobCtx.container;
const blob = blobCtx.blob;
// TODO: Check dest Lease status, and set to available if it's expired, see sample in BlobHandler.setMetadata()
const url = this.NewUriFromCopySource(copySource, context);
const [sourceAccount, sourceContainer, sourceBlob] = (0, blobStorageContext_middleware_1.extractStoragePartsFromPath)(url.hostname, url.pathname, blobCtx.disableProductStyleUrl);
const snapshot = url.searchParams.get("snapshot") || "";
if (sourceAccount === undefined ||
sourceContainer === undefined ||
sourceBlob === undefined) {
throw StorageErrorFactory_1.default.getBlobNotFound(context.contextId);
}
const sig = url.searchParams.get("sig");
if ((sourceAccount !== blobCtx.account) || (sig !== null)) {
await this.validateCopySource(copySource, sourceAccount, context);
}
// Preserve metadata key case
const metadata = (0, utils_1.convertRawHeadersToMetadata)(blobCtx.request.getRawHeaders(), context.contextId);
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: constants_1.BLOB_API_VERSION,
date: context.startTime,
copyId: res.copyId,
copyStatus: res.copyStatus,
clientRequestId: options.requestId
};
return response;
}
async validateCopySource(copySource, sourceAccount, context) {
// Currently the only cross-account copy support is from/to the same Azurite instance. In either case access
// is determined by performing a request to the copy source to see if the authentication is valid.
const blobCtx = new BlobStorageContext_1.default(context);
const currentServer = blobCtx.request.getHeader("Host") || "";
const url = this.NewUriFromCopySource(copySource, context);
if (currentServer !== url.host) {
this.logger.error(`BlobHandler:startCopyFromURL() Source account ${url} is not on the same Azurite instance as target account ${blobCtx.account}`, context.contextId);
throw StorageErrorFactory_1.default.getCannotVerifyCopySource(context.contextId, 404, "The specified resource does not exist");
}
this.logger.debug(`BlobHandler:startCopyFromURL() Validating access to the source account ${sourceAccount}`, context.contextId);
// In order to retrieve proper error details we make a metadata request to the copy source. If we instead issue
// a HEAD request then the error details are not returned and reporting authentication failures to the caller
// becomes a black box.
const metadataUrl = ms_rest_js_1.URLBuilder.parse(copySource);
metadataUrl.setQueryParameter("comp", "metadata");
const validationResponse = await axios_1.default.get(metadataUrl.toString(), {
// Instructs axios to not throw an error for non-2xx responses
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_1.default.getCannotVerifyCopySource(context.contextId, validationResponse.status, "The specified resource does not exist");
}
else {
// For non-successful responses attempt to unwrap the error message from the metadata call.
let message = "Could not verify the copy source within the specified time.";
if (validationResponse.headers[constants_1.HeaderConstants.CONTENT_TYPE] ===
"application/xml") {
const authenticationError = await (0, xml_1.parseXML)(validationResponse.data);
if (authenticationError.Message !== undefined) {
message = authenticationError.Message.replace(/\n+/gm, "");
}
}
throw StorageErrorFactory_1.default.getCannotVerifyCopySource(context.contextId, validationResponse.status, message);
}
}
}
/**
* Abort copy from Url.
*
* @param {string} copyId
* @param {Models.BlobAbortCopyFromURLOptionalParams} options
* @param {Context} context
* @returns {Promise<Models.BlobAbortCopyFromURLResponse>}
* @memberof BlobHandler
*/
async abortCopyFromURL(copyId, options, context) {
const blobCtx = new BlobStorageContext_1.default(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_1.default.getCopyIdMismatch(context.contextId);
}
if (blob.properties.copyStatus === Models.CopyStatusType.Success) {
throw StorageErrorFactory_1.default.getNoPendingCopyOperation(context.contextId);
}
const response = {
statusCode: 204,
requestId: context.contextId,
version: constants_1.BLOB_API_VERSION,
date: context.startTime,
clientRequestId: options.requestId
};
return response;
}
/**
* Copy from Url.
*
* @param {string} copySource
* @param {Models.BlobStartCopyFromURLOptionalParams} options
* @param {Context} context
* @returns {Promise<Models.BlobStartCopyFromURLResponse>}
* @memberof BlobHandler
*/
async copyFromURL(copySource, options, context) {
const blobCtx = new BlobStorageContext_1.default(context);
const account = blobCtx.account;
const container = blobCtx.container;
const blob = blobCtx.blob;
// TODO: Check dest Lease status, and set to available if it's expired, see sample in BlobHandler.setMetadata()
const url = this.NewUriFromCopySource(copySource, context);
const [sourceAccount, sourceContainer, sourceBlob] = (0, blobStorageContext_middleware_1.extractStoragePartsFromPath)(url.hostname, url.pathname, blobCtx.disableProductStyleUrl);
const snapshot = url.searchParams.get("snapshot") || "";
if (sourceAccount === undefined ||
sourceContainer === undefined ||
sourceBlob === undefined) {
throw StorageErrorFactory_1.default.getBlobNotFound(context.contextId);
}
if (sourceAccount !== blobCtx.account) {
await this.validateCopySource(copySource, sourceAccount, context);
}
// Specifying x-ms-copy-source-tag-option as COPY and x-ms-tags will result in error
if (options.copySourceTags === Models.BlobCopySourceTags.COPY && options.blobTagsString !== undefined) {
throw StorageErrorFactory_1.default.getBothUserTagsAndSourceTagsCopyPresentException(context.contextId);
}
// Preserve metadata key case
const metadata = (0, utils_1.convertRawHeadersToMetadata)(blobCtx.request.getRawHeaders(), context.contextId);
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_1.default.getUnexpectedSyncCopyStatus(context.contextId, res.copyStatus);
}
}
const response = {
statusCode: 202,
eTag: res.etag,
lastModified: res.lastModified,
requestId: context.contextId,
version: constants_1.BLOB_API_VERSION,
date: context.startTime,
copyId: res.copyId,
copyStatus,
clientRequestId: options.requestId
};
return response;
}
/**
* Set blob tier.
*
* @param {Models.AccessTier} tier
* @param {Models.BlobSetTierOptionalParams} options
* @param {Context} context
* @returns {Promise<Models.BlobSetTierResponse>}
* @memberof BlobHandler
*/
async setTier(tier, options, context) {
const blobCtx = new BlobStorageContext_1.default(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: constants_1.BLOB_API_VERSION,
statusCode: res,
clientRequestId: options.requestId
};
return response;
}
/**
* Get account information.
*
* @param {Context} context
* @returns {Promise<Models.BlobGetAccountInfoResponse>}
* @memberof BlobHandler
*/
async getAccountInfo(context) {
const response = {
statusCode: 200,
requestId: context.contextId,
clientRequestId: context.request.getHeader("x-ms-client-request-id"),
skuName: constants_1.EMULATOR_ACCOUNT_SKUNAME,
accountKind: constants_1.EMULATOR_ACCOUNT_KIND,
date: context.startTime,
version: constants_1.BLOB_API_VERSION
};
return response;
}
/**
* Get account information with headers.
*
* @param {Context} context
* @returns {Promise<Models.BlobGetAccountInfoResponse>}
* @memberof BlobHandler
*/
async getAccountInfoWithHead(context) {
return this.getAccountInfo(context);
}
/**
* Download block blob or append blob.
*
* @private
* @param {Models.BlobDownloadOptionalParams} options
* @param {Context} context
* @param {BlobModel} blob
* @returns {Promise<Models.BlobDownloadResponse>}
* @memberof BlobHandler
*/
async downloadBlockBlobOrAppendBlob(options, context, blob) {
if (blob.isCommitted === false) {
throw StorageErrorFactory_1.default.getBlobNotFound(context.contextId);
}
// Deserializer doesn't handle range header currently, manually parse range headers here
const rangesParts = (0, utils_2.deserializeRangeHeader)(context.request.getHeader("range"), context.request.getHeader("x-ms-range"));
const rangeStart = rangesParts[0];
let rangeEnd = rangesParts[1];
// Start Range is bigger than blob length
if (rangeStart > blob.properties.contentLength) {
throw StorageErrorFactory_1.default.getInvalidPageRange(context.contextId);
}
// Will automatically shift request with longer data end than blob size to blob size
if (rangeEnd + 1 >= blob.properties.contentLength) {
// report error is blob size is 0, and rangeEnd is specified but not 0
if (blob.properties.contentLength == 0 && rangeEnd !== 0 && rangeEnd !== Infinity) {
throw StorageErrorFactory_1.default.getInvalidPageRange2(context.contextId);
}
else {
rangeEnd = blob.properties.contentLength - 1;
}
}
const contentLength = rangeEnd - rangeStart + 1;
const partialRead = contentLength !== blob.properties.contentLength;
this.logger.info(
// tslint:disable-next-line:max-line-length
`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 (0, utils_1.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: constants_1.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: contentRange ? (context.request.getHeader("x-ms-range-get-content-md5") ? contentMD5 : undefined) : contentMD5,
tagCount: (0, utils_2.getBlobTagsCount)(blob.blobTags),
isServerEncrypted: true,
clientRequestId: options.requestId,
creationTime: blob.properties.creationTime,
blobCommittedBlockCount: blob.properties.blobType === Models.BlobType.AppendBlob
? (blob.committedBlocksInOrder || []).length
: undefined,
};
return response;
}
/**
* Download page blob.
*
* @private
* @param {Models.BlobDownloadOptionalParams} options
* @param {Context} context
* @param {BlobModel} blob
* @returns {Promise<Models.BlobDownloadResponse>}
* @memberof BlobHandler
*/
async downloadPageBlob(options, context, blob) {
// Deserializer doesn't handle range header currently, manually parse range headers here
const rangesParts = (0, utils_2.deserializePageBlobRangeHeader)(context.request.getHeader("range"), context.request.getHeader("x-ms-range"), false);
const rangeStart = rangesParts[0];
let rangeEnd = rangesParts[1];
// Start Range is bigger than blob length
if (rangeStart > blob.properties.contentLength) {
throw StorageErrorFactory_1.default.getInvalidPageRange(context.contextId);
}
// Will automatically shift request with longer data end than blob size to blob size
if (rangeEnd + 1 >= blob.properties.contentLength) {
// report error is blob size is 0, and rangeEnd is specified but not 0
if (blob.properties.contentLength == 0 && rangeEnd !== 0 && rangeEnd !== Infinity) {
throw StorageErrorFactory_1.default.getInvalidPageRange2(context.contextId);
}
else {
rangeEnd = blob.properties.contentLength - 1;
}
}
const contentLength = rangeEnd - rangeStart + 1;
const partialRead = contentLength !== blob.properties.contentLength;
this.logger.info(
// tslint:disable-next-line:max-line-length
`BlobHandler:downloadPageBlob() NormalizedDownloadRange=bytes=${rangeStart}-${rangeEnd} RequiredContentLength=${contentLength}`, context.contextId);
// if (contentLength <= 0) {
// return {
// statusCode: 200,
// body: undefined,
// metadata: blob.metadata,
// eTag: blob.properties.etag,
// requestId: context.contextID,
// date: context.startTime!,
// version: BLOB_API_VERSION,
// ...blob.properties,
// contentLength,
// contentMD5: undefined
// };
// }
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 (0, utils_1.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: constants_1.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: contentRange ? (context.request.getHeader("x-ms-range-get-content-md5") ? contentMD5 : undefined) : contentMD5,
blobContentMD5: blob.properties.contentMD5,
tagCount: (0, utils_2.getBlobTagsCount)(blob.blobTags),
isServerEncrypted: true,
creationTime: blob.properties.creationTime,
clientRequestId: options.requestId
};
return response;
}
async query(options, context) {
throw new NotImplementedError_1.default(context.contextId);
}
async getTags(options, context) {
const blobCtx = new BlobStorageContext_1.default(context);
const account = blobCtx.account;
const container = blobCtx.container;
const blob = blobCtx.blob;
const tags = await this.metadataStore.getBlobTag(context, account, container, blob, options.snapshot, options.leaseAccessConditions, options.modifiedAccessConditions);
const response = {
statusCode: 200,
blobTagSet: tags === undefined ? [] : tags.blobTagSet,
requestId: context.contextId,
version: constants_1.BLOB_API_VERSION,
date: context.startTime,
clientRequestId: options.requestId,
};
return response;
}
async setTags(options, context) {
const blobCtx = new BlobStorageContext_1.default(context);
const account = blobCtx.account;
const container = blobCtx.container;
const blob = blobCtx.blob;
// Blob Tags need to set
const tags = options.tags;
(0, utils_2.validateBlobTag)(tags, context.contextId);
// Get snapshot (swagger not defined snapshot as parameter, but server support set tag on blob snapshot)
let snapshot = context.request.getQuery("snapshot");
await this.metadataStore.setBlobTag(context, account, container, blob, snapshot, options.leaseAccessConditions, tags, options.modifiedAccessConditions);
const response = {
statusCode: 204,
requestId: context.contextId,
date: context.startTime,
version: constants_1.BLOB_API_VERSION,
clientRequestId: options.requestId
};
return response;
}
NewUriFromCopySource(copySource, context) {
try {
return new url_1.URL(copySource);
}
catch {
throw StorageErrorFactory_1.default.getInvalidHeaderValue(context.contextId, {
HeaderName: "x-ms-copy-source",
HeaderValue: copySource
});
}
}
}
exports.default = BlobHandler;
//# sourceMappingURL=BlobHandler.js.map