azurite
Version:
An open source Azure Storage API compatible server
352 lines • 16.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const utils_1 = require("../../common/utils/utils");
const utils_2 = 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 constants_1 = require("../utils/constants");
const BaseHandler_1 = tslib_1.__importDefault(require("./BaseHandler"));
const utils_3 = require("../utils/utils");
/**
* BlobHandler handles Azure Storage BlockBlob related requests.
*
* @export
* @class BlockBlobHandler
* @extends {BaseHandler}
* @implements {IBlockBlobHandler}
*/
class BlockBlobHandler extends BaseHandler_1.default {
async upload(body, contentLength, options, context) {
// TODO: Check Lease status, and set to available if it's expired, see sample in BlobHandler.setMetadata()
const blobCtx = new BlobStorageContext_1.default(context);
const accountName = blobCtx.account;
const containerName = blobCtx.container;
const blobName = blobCtx.blob;
const date = context.startTime;
const etag = (0, utils_2.newEtag)();
options.blobHTTPHeaders = options.blobHTTPHeaders || {};
const contentType = options.blobHTTPHeaders.blobContentType ||
context.request.getHeader("content-type") ||
"application/octet-stream";
const contentMD5 = context.request.getHeader("content-md5")
? options.blobHTTPHeaders.blobContentMD5 ||
context.request.getHeader("content-md5")
: undefined;
await this.metadataStore.checkContainerExist(context, accountName, containerName);
const persistency = await this.extentStore.appendExtent(body, context.contextId);
if (persistency.count !== contentLength) {
throw StorageErrorFactory_1.default.getInvalidOperation(blobCtx.contextId, `The size of the request body ${persistency.count} mismatches the content-length ${contentLength}.`);
}
// Calculate MD5 for validation
const stream = await this.extentStore.readExtent(persistency, context.contextId);
const calculatedContentMD5 = await (0, utils_2.getMD5FromStream)(stream);
if (contentMD5 !== undefined) {
if (typeof contentMD5 === "string") {
const calculatedContentMD5String = Buffer.from(calculatedContentMD5).toString("base64");
if (contentMD5 !== calculatedContentMD5String) {
throw StorageErrorFactory_1.default.getInvalidOperation(context.contextId, "Provided contentMD5 doesn't match.");
}
}
else {
if (!Buffer.from(contentMD5).equals(calculatedContentMD5)) {
throw StorageErrorFactory_1.default.getInvalidOperation(context.contextId, "Provided contentMD5 doesn't match.");
}
}
}
const blob = {
deleted: false,
// Preserve metadata key case
metadata: (0, utils_1.convertRawHeadersToMetadata)(blobCtx.request.getRawHeaders(), context.contextId),
accountName,
containerName,
name: blobName,
properties: {
creationTime: date,
lastModified: date,
etag,
contentLength,
contentType,
contentEncoding: options.blobHTTPHeaders.blobContentEncoding,
contentLanguage: options.blobHTTPHeaders.blobContentLanguage,
contentMD5: calculatedContentMD5,
contentDisposition: options.blobHTTPHeaders.blobContentDisposition,
cacheControl: options.blobHTTPHeaders.blobCacheControl,
blobType: Models.BlobType.BlockBlob,
leaseStatus: Models.LeaseStatusType.Unlocked,
leaseState: Models.LeaseStateType.Available,
serverEncrypted: true,
accessTier: Models.AccessTier.Hot,
accessTierInferred: true,
accessTierChangeTime: date
},
snapshot: "",
isCommitted: true,
persistency,
blobTags: options.blobTagsString === undefined ? undefined : (0, utils_3.getTagsFromString)(options.blobTagsString, context.contextId),
};
if (options.tier !== undefined) {
blob.properties.accessTier = this.parseTier(options.tier);
if (blob.properties.accessTier === undefined) {
throw StorageErrorFactory_1.default.getInvalidHeaderValue(context.contextId, {
HeaderName: "x-ms-access-tier",
HeaderValue: `${options.tier}`
});
}
blob.properties.accessTierInferred = false;
}
// TODO: Need a lock for multi keys including containerName and blobName
// TODO: Provide a specified function.
await this.metadataStore.createBlob(context, blob, options.leaseAccessConditions, options.modifiedAccessConditions);
const response = {
statusCode: 201,
eTag: etag,
lastModified: date,
contentMD5: blob.properties.contentMD5,
requestId: blobCtx.contextId,
version: constants_1.BLOB_API_VERSION,
date,
isServerEncrypted: true,
clientRequestId: options.requestId
};
return response;
}
async putBlobFromUrl(contentLength, copySource, options, context) {
throw new NotImplementedError_1.default(context.contextId);
}
async stageBlock(blockId, contentLength, body, options, context) {
const blobCtx = new BlobStorageContext_1.default(context);
const accountName = blobCtx.account;
const containerName = blobCtx.container;
const blobName = blobCtx.blob;
const date = blobCtx.startTime;
// stageBlock operation doesn't have blobHTTPHeaders
// https://learn.microsoft.com/en-us/rest/api/storageservices/put-block
// options.blobHTTPHeaders = options.blobHTTPHeaders || {};
const contentMD5 = context.request.getHeader("content-md5")
? options.transactionalContentMD5 ||
context.request.getHeader("content-md5")
: undefined;
this.validateBlockId(blockId, blobCtx);
await this.metadataStore.checkContainerExist(context, accountName, containerName);
const persistency = await this.extentStore.appendExtent(body, context.contextId);
if (persistency.count !== contentLength) {
// TODO: Confirm error code
throw StorageErrorFactory_1.default.getInvalidOperation(blobCtx.contextId, `The size of the request body ${persistency.count} mismatches the content-length ${contentLength}.`);
}
// Calculate MD5 for validation
const stream = await this.extentStore.readExtent(persistency, context.contextId);
const calculatedContentMD5 = await (0, utils_2.getMD5FromStream)(stream);
if (contentMD5 !== undefined) {
if (typeof contentMD5 === "string") {
const calculatedContentMD5String = Buffer.from(calculatedContentMD5).toString("base64");
if (contentMD5 !== calculatedContentMD5String) {
throw StorageErrorFactory_1.default.getInvalidOperation(context.contextId, "Provided contentMD5 doesn't match.");
}
}
else {
if (!Buffer.from(contentMD5).equals(calculatedContentMD5)) {
throw StorageErrorFactory_1.default.getInvalidOperation(context.contextId, "Provided contentMD5 doesn't match.");
}
}
}
const block = {
accountName,
containerName,
blobName,
isCommitted: false,
name: blockId,
size: contentLength,
persistency
};
// TODO: Verify it.
await this.metadataStore.stageBlock(context, block, options.leaseAccessConditions);
const response = {
statusCode: 201,
contentMD5: undefined, // TODO: Block content MD5
requestId: blobCtx.contextId,
version: constants_1.BLOB_API_VERSION,
date,
isServerEncrypted: true,
clientRequestId: options.requestId
};
return response;
}
async stageBlockFromURL(blockId, contentLength, sourceUrl, options, context) {
throw new NotImplementedError_1.default(context.contextId);
}
async commitBlockList(blocks, options, context) {
const blobCtx = new BlobStorageContext_1.default(context);
const accountName = blobCtx.account;
const containerName = blobCtx.container;
const blobName = blobCtx.blob;
const request = blobCtx.request;
options.blobHTTPHeaders = options.blobHTTPHeaders || {};
const contentType = options.blobHTTPHeaders.blobContentType || "application/octet-stream";
// Here we leveraged generated code utils to parser xml
// Re-parsing request body to get destination blocks
// We don't leverage serialized blocks parameter because it doesn't include sequence
const rawBody = request.getBody();
const badRequestError = StorageErrorFactory_1.default.getInvalidOperation(blobCtx.contextId);
if (rawBody === undefined) {
throw badRequestError;
}
let parsed;
try {
parsed = await (0, xml_1.parseXML)(rawBody, true);
}
catch (err) {
// return the 400(InvalidXmlDocument) error for issue 1955
throw StorageErrorFactory_1.default.getInvalidXmlDocument(context.contextId);
}
// Validate selected block list
const commitBlockList = [];
// $$ is the built-in field of xml2js parsing results when enabling explicitChildrenWithOrder
// TODO: Should make these fields explicit for parseXML method
// TODO: What happens when committedBlocks and uncommittedBlocks contains same block ID?
if (parsed !== undefined && parsed.$$ instanceof Array) {
for (const block of parsed.$$) {
const blockID = block._;
const blockCommitType = block["#name"];
if (blockID === undefined || blockCommitType === undefined) {
throw badRequestError;
}
commitBlockList.push({
blockName: blockID,
blockCommitType
});
}
}
const blob = {
accountName,
containerName,
name: blobName,
snapshot: "",
blobTags: options.blobTagsString === undefined ? undefined : (0, utils_3.getTagsFromString)(options.blobTagsString, context.contextId),
properties: {
lastModified: context.startTime,
creationTime: context.startTime,
etag: (0, utils_2.newEtag)()
},
isCommitted: true
};
blob.properties.blobType = Models.BlobType.BlockBlob;
blob.metadata = (0, utils_1.convertRawHeadersToMetadata)(
// Preserve metadata key case
blobCtx.request.getRawHeaders(), context.contextId);
blob.properties.accessTier = Models.AccessTier.Hot;
blob.properties.cacheControl = options.blobHTTPHeaders.blobCacheControl;
blob.properties.contentType = contentType;
blob.properties.contentMD5 = options.blobHTTPHeaders.blobContentMD5;
blob.properties.contentEncoding =
options.blobHTTPHeaders.blobContentEncoding;
blob.properties.contentLanguage =
options.blobHTTPHeaders.blobContentLanguage;
blob.properties.contentDisposition =
options.blobHTTPHeaders.blobContentDisposition;
if (options.tier !== undefined) {
blob.properties.accessTier = this.parseTier(options.tier);
if (blob.properties.accessTier === undefined) {
throw StorageErrorFactory_1.default.getInvalidHeaderValue(context.contextId, {
HeaderName: "x-ms-access-tier",
HeaderValue: `${options.tier}`
});
}
}
else {
blob.properties.accessTier = Models.AccessTier.Hot;
blob.properties.accessTierInferred = true;
}
await this.metadataStore.commitBlockList(context, blob, commitBlockList, options.leaseAccessConditions, options.modifiedAccessConditions);
const contentMD5 = await (0, utils_2.getMD5FromString)(rawBody);
const response = {
statusCode: 201,
eTag: blob.properties.etag,
lastModified: blobCtx.startTime,
contentMD5,
requestId: blobCtx.contextId,
version: constants_1.BLOB_API_VERSION,
date: blobCtx.startTime,
isServerEncrypted: true,
clientRequestId: options.requestId
};
return response;
}
async getBlockList(options, context) {
const blobCtx = new BlobStorageContext_1.default(context);
const accountName = blobCtx.account;
const containerName = blobCtx.container;
const blobName = blobCtx.blob;
const date = blobCtx.startTime;
const res = await this.metadataStore.getBlockList(context, accountName, containerName, blobName, options.snapshot, undefined, options.leaseAccessConditions, options.modifiedAccessConditions);
// TODO: Create uncommitted blockblob when stage block
// TODO: Conditional headers support?
res.properties = res.properties || {};
const response = {
statusCode: 200,
lastModified: res.properties.lastModified,
eTag: res.properties.etag,
contentType: res.properties.contentType,
blobContentLength: res.properties.contentLength,
requestId: blobCtx.contextId,
version: constants_1.BLOB_API_VERSION,
date,
committedBlocks: [],
uncommittedBlocks: []
};
if (options.listType !== undefined &&
(options.listType.toLowerCase() ===
Models.BlockListType.All.toLowerCase() ||
options.listType.toLowerCase() ===
Models.BlockListType.Uncommitted.toLowerCase())) {
response.uncommittedBlocks = res.uncommittedBlocks;
}
if (options.listType === undefined ||
options.listType.toLowerCase() ===
Models.BlockListType.All.toLowerCase() ||
options.listType.toLowerCase() ===
Models.BlockListType.Committed.toLowerCase()) {
response.committedBlocks = res.committedBlocks;
}
response.clientRequestId = options.requestId;
return response;
}
/**
* Get the tier setting from request headers.
*
* @private
* @param {string} tier
* @returns {(Models.AccessTier | undefined)}
* @memberof BlobHandler
*/
parseTier(tier) {
tier = tier.toLowerCase();
if (tier === Models.AccessTier.Hot.toLowerCase()) {
return Models.AccessTier.Hot;
}
if (tier === Models.AccessTier.Cool.toLowerCase()) {
return Models.AccessTier.Cool;
}
if (tier === Models.AccessTier.Archive.toLowerCase()) {
return Models.AccessTier.Archive;
}
if (tier === Models.AccessTier.Cold.toLowerCase()) {
return Models.AccessTier.Cold;
}
return undefined;
}
validateBlockId(blockId, context) {
const rawBlockId = Buffer.from(blockId, "base64");
if (blockId !== rawBlockId.toString("base64")) {
throw StorageErrorFactory_1.default.getInvalidQueryParameterValue(context.contextId, "blockid", blockId, "Not a valid base64 string.");
}
if (rawBlockId.length > 64) {
throw StorageErrorFactory_1.default.getOutOfRangeInput(context.contextId, "blockid", blockId, "Block ID length cannot exceed 64.");
}
}
}
exports.default = BlockBlobHandler;
//# sourceMappingURL=BlockBlobHandler.js.map