UNPKG

azurite

Version:

An open source Azure Storage API compatible server

352 lines 16.7 kB
"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