1 | import { convertRawHeadersToMetadata } from "../../common/utils/utils";
|
2 | import BlobStorageContext from "../context/BlobStorageContext";
|
3 | import NotImplementedError from "../errors/NotImplementedError";
|
4 | import StorageErrorFactory from "../errors/StorageErrorFactory";
|
5 | import * as Models from "../generated/artifacts/models";
|
6 | import Context from "../generated/Context";
|
7 | import IBlockBlobHandler from "../generated/handlers/IBlockBlobHandler";
|
8 | import { parseXML } from "../generated/utils/xml";
|
9 | import { BlobModel, BlockModel } from "../persistence/IBlobMetadataStore";
|
10 | import { BLOB_API_VERSION } from "../utils/constants";
|
11 | import { getMD5FromStream, getMD5FromString, newEtag } from "../utils/utils";
|
12 | import BaseHandler from "./BaseHandler";
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 |
|
22 | export 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 |
|
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 |
|
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 |
|
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 |
|
132 |
|
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 |
|
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 |
|
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,
|
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 |
|
244 |
|
245 |
|
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 |
|
256 | const commitBlockList = [];
|
257 |
|
258 |
|
259 |
|
260 |
|
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 |
|
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 |
|
362 |
|
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 |
|
403 |
|
404 |
|
405 |
|
406 |
|
407 |
|
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 | }
|