UNPKG

14.2 kBPlain TextView Raw
1import { convertRawHeadersToMetadata } from "../../common/utils/utils";
2import BlobStorageContext from "../context/BlobStorageContext";
3import NotImplementedError from "../errors/NotImplementedError";
4import StorageErrorFactory from "../errors/StorageErrorFactory";
5import * as Models from "../generated/artifacts/models";
6import Context from "../generated/Context";
7import IBlockBlobHandler from "../generated/handlers/IBlockBlobHandler";
8import { parseXML } from "../generated/utils/xml";
9import { BlobModel, BlockModel } from "../persistence/IBlobMetadataStore";
10import { BLOB_API_VERSION } from "../utils/constants";
11import { getMD5FromStream, getMD5FromString, newEtag } from "../utils/utils";
12import BaseHandler from "./BaseHandler";
13
14/**
15 * BlobHandler handles Azure Storage BlockBlob related requests.
16 *
17 * @export
18 * @class BlockBlobHandler
19 * @extends {BaseHandler}
20 * @implements {IBlockBlobHandler}
21 */
22export default class BlockBlobHandler extends BaseHandler
23 implements IBlockBlobHandler {
24 public async upload(
25 body: NodeJS.ReadableStream,
26 contentLength: number,
27 options: Models.BlockBlobUploadOptionalParams,
28 context: Context
29 ): Promise<Models.BlockBlobUploadResponse> {
30 // TODO: Check Lease status, and set to available if it's expired, see sample in BlobHandler.setMetadata()
31 const blobCtx = new BlobStorageContext(context);
32 const accountName = blobCtx.account!;
33 const containerName = blobCtx.container!;
34 const blobName = blobCtx.blob!;
35 const date = context.startTime!;
36 const etag = newEtag();
37 options.blobHTTPHeaders = options.blobHTTPHeaders || {};
38 const contentType =
39 options.blobHTTPHeaders.blobContentType ||
40 context.request!.getHeader("content-type") ||
41 "application/octet-stream";
42 const contentMD5 = context.request!.getHeader("content-md5")
43 ? options.blobHTTPHeaders.blobContentMD5 ||
44 context.request!.getHeader("content-md5")
45 : undefined;
46
47 await this.metadataStore.checkContainerExist(
48 context,
49 accountName,
50 containerName
51 );
52
53 const persistency = await this.extentStore.appendExtent(
54 body,
55 context.contextId
56 );
57 if (persistency.count !== contentLength) {
58 throw StorageErrorFactory.getInvalidOperation(
59 blobCtx.contextId!,
60 `The size of the request body ${persistency.count} mismatches the content-length ${contentLength}.`
61 );
62 }
63
64 // Calculate MD5 for validation
65 const stream = await this.extentStore.readExtent(
66 persistency,
67 context.contextId
68 );
69 const calculatedContentMD5 = await getMD5FromStream(stream);
70 if (contentMD5 !== undefined) {
71 if (typeof contentMD5 === "string") {
72 const calculatedContentMD5String = Buffer.from(
73 calculatedContentMD5
74 ).toString("base64");
75 if (contentMD5 !== calculatedContentMD5String) {
76 throw StorageErrorFactory.getInvalidOperation(
77 context.contextId!,
78 "Provided contentMD5 doesn't match."
79 );
80 }
81 } else {
82 if (!Buffer.from(contentMD5).equals(calculatedContentMD5)) {
83 throw StorageErrorFactory.getInvalidOperation(
84 context.contextId!,
85 "Provided contentMD5 doesn't match."
86 );
87 }
88 }
89 }
90
91 const blob: BlobModel = {
92 deleted: false,
93 // Preserve metadata key case
94 metadata: convertRawHeadersToMetadata(blobCtx.request!.getRawHeaders()),
95 accountName,
96 containerName,
97 name: blobName,
98 properties: {
99 creationTime: date,
100 lastModified: date,
101 etag,
102 contentLength,
103 contentType,
104 contentEncoding: options.blobHTTPHeaders.blobContentEncoding,
105 contentLanguage: options.blobHTTPHeaders.blobContentLanguage,
106 contentMD5: calculatedContentMD5,
107 contentDisposition: options.blobHTTPHeaders.blobContentDisposition,
108 cacheControl: options.blobHTTPHeaders.blobCacheControl,
109 blobType: Models.BlobType.BlockBlob,
110 leaseStatus: Models.LeaseStatusType.Unlocked,
111 leaseState: Models.LeaseStateType.Available,
112 serverEncrypted: true,
113 accessTier: Models.AccessTier.Hot,
114 accessTierInferred: true,
115 accessTierChangeTime: date
116 },
117 snapshot: "",
118 isCommitted: true,
119 persistency
120 };
121
122 if (options.tier !== undefined) {
123 blob.properties.accessTier = this.parseTier(options.tier);
124 if (blob.properties.accessTier === undefined) {
125 throw StorageErrorFactory.getInvalidHeaderValue(context.contextId, {
126 HeaderName: "x-ms-access-tier",
127 HeaderValue: `${options.tier}`
128 });
129 }
130 }
131 // TODO: Need a lock for multi keys including containerName and blobName
132 // TODO: Provide a specified function.
133 await this.metadataStore.createBlob(
134 context,
135 blob,
136 options.leaseAccessConditions,
137 options.modifiedAccessConditions
138 );
139
140 const response: Models.BlockBlobUploadResponse = {
141 statusCode: 201,
142 eTag: etag,
143 lastModified: date,
144 contentMD5: blob.properties.contentMD5,
145 requestId: blobCtx.contextId,
146 version: BLOB_API_VERSION,
147 date,
148 isServerEncrypted: true,
149 clientRequestId: options.requestId
150 };
151
152 return response;
153 }
154
155 public async stageBlock(
156 blockId: string,
157 contentLength: number,
158 body: NodeJS.ReadableStream,
159 options: Models.BlockBlobStageBlockOptionalParams,
160 context: Context
161 ): Promise<Models.BlockBlobStageBlockResponse> {
162 const blobCtx = new BlobStorageContext(context);
163 const accountName = blobCtx.account!;
164 const containerName = blobCtx.container!;
165 const blobName = blobCtx.blob!;
166 const date = blobCtx.startTime!;
167
168 this.validateBlockId(blockId, blobCtx);
169
170 await this.metadataStore.checkContainerExist(
171 context,
172 accountName,
173 containerName
174 );
175
176 const persistency = await this.extentStore.appendExtent(
177 body,
178 context.contextId
179 );
180 if (persistency.count !== contentLength) {
181 // TODO: Confirm error code
182 throw StorageErrorFactory.getInvalidOperation(
183 blobCtx.contextId!,
184 `The size of the request body ${persistency.count} mismatches the content-length ${contentLength}.`
185 );
186 }
187
188 const block: BlockModel = {
189 accountName,
190 containerName,
191 blobName,
192 isCommitted: false,
193 name: blockId,
194 size: contentLength,
195 persistency
196 };
197
198 // TODO: Verify it.
199 await this.metadataStore.stageBlock(
200 context,
201 block,
202 options.leaseAccessConditions
203 );
204
205 const response: Models.BlockBlobStageBlockResponse = {
206 statusCode: 201,
207 contentMD5: undefined, // TODO: Block content MD5
208 requestId: blobCtx.contextId,
209 version: BLOB_API_VERSION,
210 date,
211 isServerEncrypted: true,
212 clientRequestId: options.requestId
213 };
214
215 return response;
216 }
217
218 public async stageBlockFromURL(
219 blockId: string,
220 contentLength: number,
221 sourceUrl: string,
222 options: Models.BlockBlobStageBlockFromURLOptionalParams,
223 context: Context
224 ): Promise<Models.BlockBlobStageBlockFromURLResponse> {
225 throw new NotImplementedError(context.contextId);
226 }
227
228 public async commitBlockList(
229 blocks: Models.BlockLookupList,
230 options: Models.BlockBlobCommitBlockListOptionalParams,
231 context: Context
232 ): Promise<Models.BlockBlobCommitBlockListResponse> {
233 const blobCtx = new BlobStorageContext(context);
234 const accountName = blobCtx.account!;
235 const containerName = blobCtx.container!;
236 const blobName = blobCtx.blob!;
237 const request = blobCtx.request!;
238
239 options.blobHTTPHeaders = options.blobHTTPHeaders || {};
240 const contentType =
241 options.blobHTTPHeaders.blobContentType || "application/octet-stream";
242
243 // Here we leveraged generated code utils to parser xml
244 // Re-parsing request body to get destination blocks
245 // We don't leverage serialized blocks parameter because it doesn't include sequence
246 const rawBody = request.getBody();
247 const badRequestError = StorageErrorFactory.getInvalidOperation(
248 blobCtx.contextId!
249 );
250 if (rawBody === undefined) {
251 throw badRequestError;
252 }
253 const parsed = await parseXML(rawBody, true);
254
255 // Validate selected block list
256 const commitBlockList = [];
257
258 // $$ is the built-in field of xml2js parsing results when enabling explicitChildrenWithOrder
259 // TODO: Should make these fields explicit for parseXML method
260 // TODO: What happens when committedBlocks and uncommittedBlocks contains same block ID?
261 if (parsed !== undefined && parsed.$$ instanceof Array) {
262 for (const block of parsed.$$) {
263 const blockID: string | undefined = block._;
264 const blockCommitType: string | undefined = block["#name"];
265
266 if (blockID === undefined || blockCommitType === undefined) {
267 throw badRequestError;
268 }
269 commitBlockList.push({
270 blockName: blockID,
271 blockCommitType
272 });
273 }
274 }
275
276 const blob: BlobModel = {
277 accountName,
278 containerName,
279 name: blobName,
280 snapshot: "",
281 properties: {
282 lastModified: context.startTime!,
283 creationTime: context.startTime!,
284 etag: newEtag()
285 },
286 isCommitted: true
287 };
288
289 blob.properties.blobType = Models.BlobType.BlockBlob;
290 blob.metadata = convertRawHeadersToMetadata(
291 // Preserve metadata key case
292 blobCtx.request!.getRawHeaders()
293 );
294 blob.properties.accessTier = Models.AccessTier.Hot;
295 blob.properties.cacheControl = options.blobHTTPHeaders.blobCacheControl;
296 blob.properties.contentType = contentType;
297 blob.properties.contentMD5 = options.blobHTTPHeaders.blobContentMD5;
298 blob.properties.contentEncoding =
299 options.blobHTTPHeaders.blobContentEncoding;
300 blob.properties.contentLanguage =
301 options.blobHTTPHeaders.blobContentLanguage;
302 blob.properties.contentDisposition =
303 options.blobHTTPHeaders.blobContentDisposition;
304
305 if (options.tier !== undefined) {
306 blob.properties.accessTier = this.parseTier(options.tier);
307 if (blob.properties.accessTier === undefined) {
308 throw StorageErrorFactory.getInvalidHeaderValue(context.contextId, {
309 HeaderName: "x-ms-access-tier",
310 HeaderValue: `${options.tier}`
311 });
312 }
313 } else {
314 blob.properties.accessTier = Models.AccessTier.Hot;
315 blob.properties.accessTierInferred = true;
316 }
317
318 await this.metadataStore.commitBlockList(
319 context,
320 blob,
321 commitBlockList,
322 options.leaseAccessConditions,
323 options.modifiedAccessConditions
324 );
325
326 const contentMD5 = await getMD5FromString(rawBody);
327
328 const response: Models.BlockBlobCommitBlockListResponse = {
329 statusCode: 201,
330 eTag: blob.properties.etag,
331 lastModified: blobCtx.startTime,
332 contentMD5,
333 requestId: blobCtx.contextId,
334 version: BLOB_API_VERSION,
335 date: blobCtx.startTime,
336 isServerEncrypted: true,
337 clientRequestId: options.requestId
338 };
339 return response;
340 }
341
342 public async getBlockList(
343 options: Models.BlockBlobGetBlockListOptionalParams,
344 context: Context
345 ): Promise<Models.BlockBlobGetBlockListResponse> {
346 const blobCtx = new BlobStorageContext(context);
347 const accountName = blobCtx.account!;
348 const containerName = blobCtx.container!;
349 const blobName = blobCtx.blob!;
350 const date = blobCtx.startTime!;
351
352 const res = await this.metadataStore.getBlockList(
353 context,
354 accountName,
355 containerName,
356 blobName,
357 undefined,
358 options.leaseAccessConditions
359 );
360
361 // TODO: Create uncommitted blockblob when stage block
362 // TODO: Conditional headers support?
363
364 res.properties = res.properties || {};
365 const response: Models.BlockBlobGetBlockListResponse = {
366 statusCode: 200,
367 lastModified: res.properties.lastModified,
368 eTag: res.properties.etag,
369 contentType: res.properties.contentType,
370 blobContentLength: res.properties.contentLength,
371 requestId: blobCtx.contextId,
372 version: BLOB_API_VERSION,
373 date,
374 committedBlocks: [],
375 uncommittedBlocks: []
376 };
377
378 if (
379 options.listType !== undefined &&
380 (options.listType.toLowerCase() ===
381 Models.BlockListType.All.toLowerCase() ||
382 options.listType.toLowerCase() ===
383 Models.BlockListType.Uncommitted.toLowerCase())
384 ) {
385 response.uncommittedBlocks = res.uncommittedBlocks;
386 }
387 if (
388 options.listType === undefined ||
389 options.listType.toLowerCase() ===
390 Models.BlockListType.All.toLowerCase() ||
391 options.listType.toLowerCase() ===
392 Models.BlockListType.Committed.toLowerCase()
393 ) {
394 response.committedBlocks = res.committedBlocks;
395 }
396 response.clientRequestId = options.requestId;
397
398 return response;
399 }
400
401 /**
402 * Get the tier setting from request headers.
403 *
404 * @private
405 * @param {string} tier
406 * @returns {(Models.AccessTier | undefined)}
407 * @memberof BlobHandler
408 */
409 private parseTier(tier: string): Models.AccessTier | undefined {
410 tier = tier.toLowerCase();
411 if (tier === Models.AccessTier.Hot.toLowerCase()) {
412 return Models.AccessTier.Hot;
413 }
414 if (tier === Models.AccessTier.Cool.toLowerCase()) {
415 return Models.AccessTier.Cool;
416 }
417 if (tier === Models.AccessTier.Archive.toLowerCase()) {
418 return Models.AccessTier.Archive;
419 }
420 return undefined;
421 }
422
423 private validateBlockId(blockId: string, context: Context): void {
424 const rawBlockId = Buffer.from(blockId, "base64");
425
426 if (blockId !== rawBlockId.toString("base64")) {
427 throw StorageErrorFactory.getInvalidQueryParameterValue(
428 context.contextId,
429 "blockid",
430 blockId,
431 "Not a valid base64 string."
432 );
433 }
434
435 if (rawBlockId.length > 64) {
436 throw StorageErrorFactory.getOutOfRangeInput(
437 context.contextId!,
438 "blockid",
439 blockId,
440 "Block ID length cannot exceed 64."
441 );
442 }
443 }
444}