UNPKG

29.9 kBPlain TextView Raw
1import { URL } from "url";
2
3import IExtentStore from "../../common/persistence/IExtentStore";
4import { convertRawHeadersToMetadata } from "../../common/utils/utils";
5import BlobStorageContext from "../context/BlobStorageContext";
6import NotImplementedError from "../errors/NotImplementedError";
7import StorageErrorFactory from "../errors/StorageErrorFactory";
8import * as Models from "../generated/artifacts/models";
9import Context from "../generated/Context";
10import IBlobHandler from "../generated/handlers/IBlobHandler";
11import ILogger from "../generated/utils/ILogger";
12import { extractStoragePartsFromPath } from "../middlewares/blobStorageContext.middleware";
13import IBlobMetadataStore, {
14 BlobModel
15} from "../persistence/IBlobMetadataStore";
16import {
17 BLOB_API_VERSION,
18 EMULATOR_ACCOUNT_KIND,
19 EMULATOR_ACCOUNT_SKUNAME,
20 HeaderConstants
21} from "../utils/constants";
22import {
23 deserializePageBlobRangeHeader,
24 deserializeRangeHeader,
25 getMD5FromStream
26} from "../utils/utils";
27import BaseHandler from "./BaseHandler";
28import IPageBlobRangesManager from "./IPageBlobRangesManager";
29
30/**
31 * BlobHandler handles Azure Storage Blob related requests.
32 *
33 * @export
34 * @class BlobHandler
35 * @extends {BaseHandler}
36 * @implements {IBlobHandler}
37 */
38export default class BlobHandler extends BaseHandler implements IBlobHandler {
39 constructor(
40 metadataStore: IBlobMetadataStore,
41 extentStore: IExtentStore,
42 logger: ILogger,
43 private readonly rangesManager: IPageBlobRangesManager
44 ) {
45 super(metadataStore, extentStore, logger);
46 }
47
48 public setAccessControl(
49 options: Models.BlobSetAccessControlOptionalParams,
50 context: Context
51 ): Promise<Models.BlobSetAccessControlResponse> {
52 throw new NotImplementedError(context.contextId);
53 }
54
55 public getAccessControl(
56 options: Models.BlobGetAccessControlOptionalParams,
57 context: Context
58 ): Promise<Models.BlobGetAccessControlResponse> {
59 throw new NotImplementedError(context.contextId);
60 }
61
62 public rename(
63 renameSource: string,
64 options: Models.BlobRenameOptionalParams,
65 context: Context
66 ): Promise<Models.BlobRenameResponse> {
67 throw new NotImplementedError(context.contextId);
68 }
69
70 public copyFromURL(
71 copySource: string,
72 options: Models.BlobCopyFromURLOptionalParams,
73 context: Context
74 ): Promise<Models.BlobCopyFromURLResponse> {
75 throw new NotImplementedError(context.contextId);
76 }
77
78 /**
79 * Download blob.
80 *
81 * @param {Models.BlobDownloadOptionalParams} options
82 * @param {Context} context
83 * @returns {Promise<Models.BlobDownloadResponse>}
84 * @memberof BlobHandler
85 */
86 public async download(
87 options: Models.BlobDownloadOptionalParams,
88 context: Context
89 ): Promise<Models.BlobDownloadResponse> {
90 const blobCtx = new BlobStorageContext(context);
91 const accountName = blobCtx.account!;
92 const containerName = blobCtx.container!;
93 const blobName = blobCtx.blob!;
94
95 const blob = await this.metadataStore.downloadBlob(
96 context,
97 accountName,
98 containerName,
99 blobName,
100 options.snapshot,
101 options.leaseAccessConditions,
102 options.modifiedAccessConditions
103 );
104
105 if (blob.properties.blobType === Models.BlobType.BlockBlob) {
106 return this.downloadBlockBlobOrAppendBlob(options, context, blob);
107 } else if (blob.properties.blobType === Models.BlobType.PageBlob) {
108 return this.downloadPageBlob(options, context, blob);
109 } else if (blob.properties.blobType === Models.BlobType.AppendBlob) {
110 return this.downloadBlockBlobOrAppendBlob(options, context, blob);
111 } else {
112 throw StorageErrorFactory.getInvalidOperation(context.contextId!);
113 }
114 }
115
116 /**
117 * Get blob properties.
118 *
119 * @param {Models.BlobGetPropertiesOptionalParams} options
120 * @param {Context} context
121 * @returns {Promise<Models.BlobGetPropertiesResponse>}
122 * @memberof BlobHandler
123 */
124 public async getProperties(
125 options: Models.BlobGetPropertiesOptionalParams,
126 context: Context
127 ): Promise<Models.BlobGetPropertiesResponse> {
128 const blobCtx = new BlobStorageContext(context);
129 const account = blobCtx.account!;
130 const container = blobCtx.container!;
131 const blob = blobCtx.blob!;
132 const res = await this.metadataStore.getBlobProperties(
133 context,
134 account,
135 container,
136 blob,
137 options.snapshot,
138 options.leaseAccessConditions,
139 options.modifiedAccessConditions
140 );
141
142 // TODO: Create get metadata specific request in swagger
143 const againstMetadata = context.request!.getQuery("comp") === "metadata";
144
145 const response: Models.BlobGetPropertiesResponse = againstMetadata
146 ? {
147 statusCode: 200,
148 metadata: res.metadata,
149 eTag: res.properties.etag,
150 requestId: context.contextId,
151 version: BLOB_API_VERSION,
152 date: context.startTime,
153 clientRequestId: options.requestId,
154 contentLength: res.properties.contentLength,
155 lastModified: res.properties.lastModified
156 }
157 : {
158 statusCode: 200,
159 metadata: res.metadata,
160 isIncrementalCopy: res.properties.incrementalCopy,
161 eTag: res.properties.etag,
162 requestId: context.contextId,
163 version: BLOB_API_VERSION,
164 date: context.startTime,
165 acceptRanges: "bytes",
166 blobCommittedBlockCount:
167 res.properties.blobType === Models.BlobType.AppendBlob
168 ? res.blobCommittedBlockCount
169 : undefined,
170 isServerEncrypted: true,
171 clientRequestId: options.requestId,
172 ...res.properties
173 };
174
175 return response;
176 }
177
178 /**
179 * Delete blob or snapshots.
180 *
181 * @param {Models.BlobDeleteMethodOptionalParams} options
182 * @param {Context} context
183 * @returns {Promise<Models.BlobDeleteResponse>}
184 * @memberof BlobHandler
185 */
186 public async delete(
187 options: Models.BlobDeleteMethodOptionalParams,
188 context: Context
189 ): Promise<Models.BlobDeleteResponse> {
190 const blobCtx = new BlobStorageContext(context);
191 const account = blobCtx.account!;
192 const container = blobCtx.container!;
193 const blob = blobCtx.blob!;
194 await this.metadataStore.deleteBlob(
195 context,
196 account,
197 container,
198 blob,
199 options
200 );
201
202 const response: Models.BlobDeleteResponse = {
203 statusCode: 202,
204 requestId: context.contextId,
205 date: context.startTime,
206 version: BLOB_API_VERSION,
207 clientRequestId: options.requestId
208 };
209
210 return response;
211 }
212
213 /**
214 * Undelete blob.
215 *
216 * @param {Models.BlobUndeleteOptionalParams} options
217 * @param {Context} context
218 * @returns {Promise<Models.BlobUndeleteResponse>}
219 * @memberof BlobHandler
220 */
221 public async undelete(
222 options: Models.BlobUndeleteOptionalParams,
223 context: Context
224 ): Promise<Models.BlobUndeleteResponse> {
225 throw new NotImplementedError(context.contextId);
226 }
227
228 /**
229 * Set HTTP Headers.
230 * see also https://docs.microsoft.com/en-us/rest/api/storageservices/set-blob-properties
231 *
232 * @param {Models.BlobSetHTTPHeadersOptionalParams} options
233 * @param {Context} context
234 * @returns {Promise<Models.BlobSetHTTPHeadersResponse>}
235 * @memberof BlobHandler
236 */
237 public async setHTTPHeaders(
238 options: Models.BlobSetHTTPHeadersOptionalParams,
239 context: Context
240 ): Promise<Models.BlobSetHTTPHeadersResponse> {
241 const blobCtx = new BlobStorageContext(context);
242 const account = blobCtx.account!;
243 const container = blobCtx.container!;
244 const blob = blobCtx.blob!;
245
246 let res;
247
248 // Workaround for https://github.com/Azure/Azurite/issues/332
249 const sequenceNumberAction = context.request!.getHeader(
250 HeaderConstants.X_MS_SEQUENCE_NUMBER_ACTION
251 );
252 const sequenceNumber = context.request!.getHeader(
253 HeaderConstants.X_MS_BLOB_SEQUENCE_NUMBER
254 );
255 if (sequenceNumberAction !== undefined) {
256 this.logger.verbose(
257 "BlobHandler:setHTTPHeaders() Redirect to updateSequenceNumber...",
258 context.contextId
259 );
260 res = await this.metadataStore.updateSequenceNumber(
261 context,
262 account,
263 container,
264 blob,
265 sequenceNumberAction.toLowerCase() as Models.SequenceNumberActionType,
266 sequenceNumber === undefined ? undefined : parseInt(sequenceNumber, 10),
267 options.leaseAccessConditions,
268 options.modifiedAccessConditions
269 );
270 } else {
271 res = await this.metadataStore.setBlobHTTPHeaders(
272 context,
273 account,
274 container,
275 blob,
276 options.leaseAccessConditions,
277 options.blobHTTPHeaders,
278 options.modifiedAccessConditions
279 );
280 }
281
282 const response: Models.BlobSetHTTPHeadersResponse = {
283 statusCode: 200,
284 eTag: res.etag,
285 lastModified: res.lastModified,
286 blobSequenceNumber: res.blobSequenceNumber,
287 requestId: context.contextId,
288 version: BLOB_API_VERSION,
289 date: context.startTime,
290 clientRequestId: options.requestId
291 };
292
293 return response;
294 }
295
296 /**
297 * Set Metadata.
298 *
299 * @param {Models.BlobSetMetadataOptionalParams} options
300 * @param {Context} context
301 * @returns {Promise<Models.BlobSetMetadataResponse>}
302 * @memberof BlobHandler
303 */
304 public async setMetadata(
305 options: Models.BlobSetMetadataOptionalParams,
306 context: Context
307 ): Promise<Models.BlobSetMetadataResponse> {
308 const blobCtx = new BlobStorageContext(context);
309 const account = blobCtx.account!;
310 const container = blobCtx.container!;
311 const blob = blobCtx.blob!;
312
313 // Preserve metadata key case
314 const metadata = convertRawHeadersToMetadata(
315 blobCtx.request!.getRawHeaders()
316 );
317
318 const res = await this.metadataStore.setBlobMetadata(
319 context,
320 account,
321 container,
322 blob,
323 options.leaseAccessConditions,
324 metadata,
325 options.modifiedAccessConditions
326 );
327
328 // ToDo: return correct headers and test for these.
329 const response: Models.BlobSetMetadataResponse = {
330 statusCode: 200,
331 eTag: res.etag,
332 lastModified: res.lastModified,
333 isServerEncrypted: true,
334 requestId: context.contextId,
335 date: context.startTime,
336 version: BLOB_API_VERSION,
337 clientRequestId: options.requestId
338 };
339
340 return response;
341 }
342
343 /**
344 * Acquire Blob Lease.
345 *
346 * @param {Models.BlobAcquireLeaseOptionalParams} options
347 * @param {Context} context
348 * @returns {Promise<Models.BlobAcquireLeaseResponse>}
349 * @memberof BlobHandler
350 */
351 public async acquireLease(
352 options: Models.BlobAcquireLeaseOptionalParams,
353 context: Context
354 ): Promise<Models.BlobAcquireLeaseResponse> {
355 const blobCtx = new BlobStorageContext(context);
356 const account = blobCtx.account!;
357 const container = blobCtx.container!;
358 const blob = blobCtx.blob!;
359 const snapshot = blobCtx.request!.getQuery("snapshot");
360
361 if (snapshot !== undefined && snapshot !== "") {
362 throw StorageErrorFactory.getInvalidOperation(
363 context.contextId,
364 "A lease cannot be granted for a blob snapshot"
365 );
366 }
367
368 const res = await this.metadataStore.acquireBlobLease(
369 context,
370 account,
371 container,
372 blob,
373 options.duration!,
374 options.proposedLeaseId,
375 options
376 );
377
378 const response: Models.BlobAcquireLeaseResponse = {
379 date: blobCtx.startTime!,
380 eTag: res.properties.etag,
381 lastModified: res.properties.lastModified,
382 leaseId: res.leaseId,
383 requestId: context.contextId,
384 version: BLOB_API_VERSION,
385 statusCode: 201,
386 clientRequestId: options.requestId
387 };
388
389 return response;
390 }
391
392 /**
393 * release blob lease
394 *
395 * @param {string} leaseId
396 * @param {Models.BlobReleaseLeaseOptionalParams} options
397 * @param {Context} context
398 * @returns {Promise<Models.BlobReleaseLeaseResponse>}
399 * @memberof BlobHandler
400 */
401 public async releaseLease(
402 leaseId: string,
403 options: Models.BlobReleaseLeaseOptionalParams,
404 context: Context
405 ): Promise<Models.BlobReleaseLeaseResponse> {
406 const blobCtx = new BlobStorageContext(context);
407 const account = blobCtx.account!;
408 const container = blobCtx.container!;
409 const blob = blobCtx.blob!;
410 const res = await this.metadataStore.releaseBlobLease(
411 context,
412 account,
413 container,
414 blob,
415 leaseId,
416 options
417 );
418
419 const response: Models.BlobReleaseLeaseResponse = {
420 date: blobCtx.startTime!,
421 eTag: res.etag,
422 lastModified: res.lastModified,
423 requestId: context.contextId,
424 version: BLOB_API_VERSION,
425 statusCode: 200,
426 clientRequestId: options.requestId
427 };
428
429 return response;
430 }
431
432 /**
433 * Renew blob lease
434 *
435 * @param {string} leaseId
436 * @param {Models.BlobRenewLeaseOptionalParams} options
437 * @param {Context} context
438 * @returns {Promise<Models.BlobRenewLeaseResponse>}
439 * @memberof BlobHandler
440 */
441 public async renewLease(
442 leaseId: string,
443 options: Models.BlobRenewLeaseOptionalParams,
444 context: Context
445 ): Promise<Models.BlobRenewLeaseResponse> {
446 const blobCtx = new BlobStorageContext(context);
447 const account = blobCtx.account!;
448 const container = blobCtx.container!;
449 const blob = blobCtx.blob!;
450 const res = await this.metadataStore.renewBlobLease(
451 context,
452 account,
453 container,
454 blob,
455 leaseId,
456 options
457 );
458
459 const response: Models.BlobRenewLeaseResponse = {
460 date: blobCtx.startTime!,
461 eTag: res.properties.etag,
462 lastModified: res.properties.lastModified,
463 leaseId: res.leaseId,
464 requestId: context.contextId,
465 version: BLOB_API_VERSION,
466 statusCode: 200,
467 clientRequestId: options.requestId
468 };
469
470 return response;
471 }
472
473 /**
474 * Change lease.
475 *
476 * @param {string} leaseId
477 * @param {string} proposedLeaseId
478 * @param {Models.BlobChangeLeaseOptionalParams} options
479 * @param {Context} context
480 * @returns {Promise<Models.BlobChangeLeaseResponse>}
481 * @memberof BlobHandler
482 */
483 public async changeLease(
484 leaseId: string,
485 proposedLeaseId: string,
486 options: Models.BlobChangeLeaseOptionalParams,
487 context: Context
488 ): Promise<Models.BlobChangeLeaseResponse> {
489 const blobCtx = new BlobStorageContext(context);
490 const account = blobCtx.account!;
491 const container = blobCtx.container!;
492 const blob = blobCtx.blob!;
493 const res = await this.metadataStore.changeBlobLease(
494 context,
495 account,
496 container,
497 blob,
498 leaseId,
499 proposedLeaseId,
500 options
501 );
502
503 const response: Models.BlobChangeLeaseResponse = {
504 date: blobCtx.startTime!,
505 eTag: res.properties.etag,
506 lastModified: res.properties.lastModified,
507 leaseId: res.leaseId,
508 requestId: context.contextId,
509 version: BLOB_API_VERSION,
510 statusCode: 200,
511 clientRequestId: options.requestId
512 };
513
514 return response;
515 }
516
517 /**
518 * Break lease.
519 *
520 * @param {Models.BlobBreakLeaseOptionalParams} options
521 * @param {Context} context
522 * @returns {Promise<Models.BlobBreakLeaseResponse>}
523 * @memberof BlobHandler
524 */
525 public async breakLease(
526 options: Models.BlobBreakLeaseOptionalParams,
527 context: Context
528 ): Promise<Models.BlobBreakLeaseResponse> {
529 const blobCtx = new BlobStorageContext(context);
530 const account = blobCtx.account!;
531 const container = blobCtx.container!;
532 const blob = blobCtx.blob!;
533 const res = await this.metadataStore.breakBlobLease(
534 context,
535 account,
536 container,
537 blob,
538 options.breakPeriod,
539 options
540 );
541
542 const response: Models.BlobBreakLeaseResponse = {
543 date: blobCtx.startTime!,
544 eTag: res.properties.etag,
545 lastModified: res.properties.lastModified,
546 leaseTime: res.leaseTime,
547 requestId: context.contextId,
548 version: BLOB_API_VERSION,
549 statusCode: 202,
550 clientRequestId: options.requestId
551 };
552
553 return response;
554 }
555
556 /**
557 * Create snapshot.
558 *
559 * @see https://docs.microsoft.com/en-us/rest/api/storageservices/snapshot-blob
560 *
561 * @param {Models.BlobCreateSnapshotOptionalParams} options
562 * @param {Context} context
563 * @returns {Promise<Models.BlobCreateSnapshotResponse>}
564 * @memberof BlobHandler
565 */
566 public async createSnapshot(
567 options: Models.BlobCreateSnapshotOptionalParams,
568 context: Context
569 ): Promise<Models.BlobCreateSnapshotResponse> {
570 const blobCtx = new BlobStorageContext(context);
571 const account = blobCtx.account!;
572 const container = blobCtx.container!;
573 const blob = blobCtx.blob!;
574
575 // Preserve metadata key case
576 const metadata = convertRawHeadersToMetadata(
577 blobCtx.request!.getRawHeaders()
578 );
579
580 const res = await this.metadataStore.createSnapshot(
581 context,
582 account,
583 container,
584 blob,
585 options.leaseAccessConditions,
586 !options.metadata || JSON.stringify(options.metadata) === "{}"
587 ? undefined
588 : metadata,
589 options.modifiedAccessConditions
590 );
591
592 const response: Models.BlobCreateSnapshotResponse = {
593 statusCode: 201,
594 eTag: res.properties.etag,
595 lastModified: res.properties.lastModified,
596 requestId: context.contextId,
597 date: context.startTime!,
598 version: BLOB_API_VERSION,
599 snapshot: res.snapshot,
600 clientRequestId: options.requestId
601 };
602
603 return response;
604 }
605
606 /**
607 * Start copy from Url.
608 *
609 * @param {string} copySource
610 * @param {Models.BlobStartCopyFromURLOptionalParams} options
611 * @param {Context} context
612 * @returns {Promise<Models.BlobStartCopyFromURLResponse>}
613 * @memberof BlobHandler
614 */
615 public async startCopyFromURL(
616 copySource: string,
617 options: Models.BlobStartCopyFromURLOptionalParams,
618 context: Context
619 ): Promise<Models.BlobStartCopyFromURLResponse> {
620 const blobCtx = new BlobStorageContext(context);
621 const account = blobCtx.account!;
622 const container = blobCtx.container!;
623 const blob = blobCtx.blob!;
624
625 // TODO: Check dest Lease status, and set to available if it's expired, see sample in BlobHandler.setMetadata()
626 const url = new URL(copySource);
627 const [
628 sourceAccount,
629 sourceContainer,
630 sourceBlob
631 ] = extractStoragePartsFromPath(url.hostname, url.pathname);
632 const snapshot = url.searchParams.get("snapshot") || "";
633
634 if (
635 sourceAccount !== blobCtx.account ||
636 sourceAccount === undefined ||
637 sourceContainer === undefined ||
638 sourceBlob === undefined
639 ) {
640 throw StorageErrorFactory.getBlobNotFound(context.contextId!);
641 }
642
643 // Preserve metadata key case
644 const metadata = convertRawHeadersToMetadata(
645 blobCtx.request!.getRawHeaders()
646 );
647
648 const res = await this.metadataStore.startCopyFromURL(
649 context,
650 {
651 account: sourceAccount,
652 container: sourceContainer,
653 blob: sourceBlob,
654 snapshot
655 },
656 { account, container, blob },
657 copySource,
658 metadata,
659 options.tier,
660 options
661 );
662
663 const response: Models.BlobStartCopyFromURLResponse = {
664 statusCode: 202,
665 eTag: res.etag,
666 lastModified: res.lastModified,
667 requestId: context.contextId,
668 version: BLOB_API_VERSION,
669 date: context.startTime,
670 copyId: res.copyId,
671 copyStatus: res.copyStatus,
672 clientRequestId: options.requestId
673 };
674
675 return response;
676 }
677
678 /**
679 * Abort copy from Url.
680 *
681 * @param {string} copyId
682 * @param {Models.BlobAbortCopyFromURLOptionalParams} options
683 * @param {Context} context
684 * @returns {Promise<Models.BlobAbortCopyFromURLResponse>}
685 * @memberof BlobHandler
686 */
687 public async abortCopyFromURL(
688 copyId: string,
689 options: Models.BlobAbortCopyFromURLOptionalParams,
690 context: Context
691 ): Promise<Models.BlobAbortCopyFromURLResponse> {
692 const blobCtx = new BlobStorageContext(context);
693 const accountName = blobCtx.account!;
694 const containerName = blobCtx.container!;
695 const blobName = blobCtx.blob!;
696 const blob = await this.metadataStore.downloadBlob(
697 context,
698 accountName,
699 containerName,
700 blobName,
701 undefined,
702 options.leaseAccessConditions
703 );
704
705 if (blob.properties.copyId !== copyId) {
706 throw StorageErrorFactory.getCopyIdMismatch(context.contextId!);
707 }
708
709 if (blob.properties.copyStatus === Models.CopyStatusType.Success) {
710 throw StorageErrorFactory.getNoPendingCopyOperation(context.contextId!);
711 }
712
713 const response: Models.BlobAbortCopyFromURLResponse = {
714 statusCode: 204,
715 requestId: context.contextId,
716 version: BLOB_API_VERSION,
717 date: context.startTime,
718 clientRequestId: options.requestId
719 };
720
721 return response;
722 }
723
724 /**
725 * Set blob tier.
726 *
727 * @param {Models.AccessTier} tier
728 * @param {Models.BlobSetTierOptionalParams} options
729 * @param {Context} context
730 * @returns {Promise<Models.BlobSetTierResponse>}
731 * @memberof BlobHandler
732 */
733 public async setTier(
734 tier: Models.AccessTier,
735 options: Models.BlobSetTierOptionalParams,
736 context: Context
737 ): Promise<Models.BlobSetTierResponse> {
738 const blobCtx = new BlobStorageContext(context);
739 const account = blobCtx.account!;
740 const container = blobCtx.container!;
741 const blob = blobCtx.blob!;
742 const res = await this.metadataStore.setTier(
743 context,
744 account,
745 container,
746 blob,
747 tier,
748 undefined
749 );
750
751 const response: Models.BlobSetTierResponse = {
752 requestId: context.contextId,
753 version: BLOB_API_VERSION,
754 statusCode: res,
755 clientRequestId: options.requestId
756 };
757
758 return response;
759 }
760
761 /**
762 * Get account information.
763 *
764 * @param {Context} context
765 * @returns {Promise<Models.BlobGetAccountInfoResponse>}
766 * @memberof BlobHandler
767 */
768 public async getAccountInfo(
769 context: Context
770 ): Promise<Models.BlobGetAccountInfoResponse> {
771 const response: Models.BlobGetAccountInfoResponse = {
772 statusCode: 200,
773 requestId: context.contextId,
774 clientRequestId: context.request!.getHeader("x-ms-client-request-id"),
775 skuName: EMULATOR_ACCOUNT_SKUNAME,
776 accountKind: EMULATOR_ACCOUNT_KIND,
777 date: context.startTime!,
778 version: BLOB_API_VERSION
779 };
780 return response;
781 }
782
783 /**
784 * Get account information with headers.
785 *
786 * @param {Context} context
787 * @returns {Promise<Models.BlobGetAccountInfoResponse>}
788 * @memberof BlobHandler
789 */
790 public async getAccountInfoWithHead(
791 context: Context
792 ): Promise<Models.BlobGetAccountInfoResponse> {
793 return this.getAccountInfo(context);
794 }
795
796 /**
797 * Download block blob or append blob.
798 *
799 * @private
800 * @param {Models.BlobDownloadOptionalParams} options
801 * @param {Context} context
802 * @param {BlobModel} blob
803 * @returns {Promise<Models.BlobDownloadResponse>}
804 * @memberof BlobHandler
805 */
806 private async downloadBlockBlobOrAppendBlob(
807 options: Models.BlobDownloadOptionalParams,
808 context: Context,
809 blob: BlobModel
810 ): Promise<Models.BlobDownloadResponse> {
811 if (blob.isCommitted === false) {
812 throw StorageErrorFactory.getBlobNotFound(context.contextId!);
813 }
814
815 // Deserializer doesn't handle range header currently, manually parse range headers here
816 const rangesParts = deserializeRangeHeader(
817 context.request!.getHeader("range"),
818 context.request!.getHeader("x-ms-range")
819 );
820 const rangeStart = rangesParts[0];
821 let rangeEnd = rangesParts[1];
822
823 // Will automatically shift request with longer data end than blob size to blob size
824 if (rangeEnd + 1 >= blob.properties.contentLength!) {
825 rangeEnd = blob.properties.contentLength! - 1;
826 }
827
828 const contentLength = rangeEnd - rangeStart + 1;
829 const partialRead = contentLength !== blob.properties.contentLength!;
830
831 this.logger.info(
832 // tslint:disable-next-line:max-line-length
833 `BlobHandler:downloadBlockBlobOrAppendBlob() NormalizedDownloadRange=bytes=${rangeStart}-${rangeEnd} RequiredContentLength=${contentLength}`,
834 context.contextId
835 );
836
837 let bodyGetter: () => Promise<NodeJS.ReadableStream | undefined>;
838 const blocks = blob.committedBlocksInOrder;
839 if (blocks === undefined || blocks.length === 0) {
840 bodyGetter = async () => {
841 if (blob.persistency === undefined) {
842 return this.extentStore.readExtent(undefined, context.contextId);
843 }
844 return this.extentStore.readExtent(
845 {
846 id: blob.persistency.id,
847 offset: blob.persistency.offset + rangeStart,
848 count: Math.min(blob.persistency.count, contentLength)
849 },
850 context.contextId
851 );
852 };
853 } else {
854 bodyGetter = async () => {
855 return this.extentStore.readExtents(
856 blocks.map(block => block.persistency),
857 rangeStart,
858 rangeEnd + 1 - rangeStart,
859 context.contextId
860 );
861 };
862 }
863
864 let contentRange: string | undefined;
865 if (
866 context.request!.getHeader("range") ||
867 context.request!.getHeader("x-ms-range")
868 ) {
869 contentRange = `bytes ${rangeStart}-${rangeEnd}/${blob.properties
870 .contentLength!}`;
871 }
872
873 let body: NodeJS.ReadableStream | undefined = await bodyGetter();
874 let contentMD5: Uint8Array | undefined;
875 if (!partialRead) {
876 contentMD5 = blob.properties.contentMD5;
877 }
878 if (
879 contentLength <= 4 * 1024 * 1024 &&
880 contentMD5 === undefined &&
881 body !== undefined
882 ) {
883 contentMD5 = await getMD5FromStream(body);
884 body = await bodyGetter();
885 }
886
887 const response: Models.BlobDownloadResponse = {
888 statusCode: contentRange ? 206 : 200,
889 body,
890 metadata: blob.metadata,
891 eTag: blob.properties.etag,
892 requestId: context.contextId,
893 date: context.startTime!,
894 version: BLOB_API_VERSION,
895 ...blob.properties,
896 blobContentMD5: blob.properties.contentMD5,
897 acceptRanges: "bytes",
898 contentLength,
899 contentRange,
900 contentMD5,
901 isServerEncrypted: true,
902 clientRequestId: options.requestId,
903 blobCommittedBlockCount:
904 blob.properties.blobType === Models.BlobType.AppendBlob
905 ? (blob.committedBlocksInOrder || []).length
906 : undefined
907 };
908
909 return response;
910 }
911
912 /**
913 * Download page blob.
914 *
915 * @private
916 * @param {Models.BlobDownloadOptionalParams} options
917 * @param {Context} context
918 * @param {BlobModel} blob
919 * @returns {Promise<Models.BlobDownloadResponse>}
920 * @memberof BlobHandler
921 */
922 private async downloadPageBlob(
923 options: Models.BlobDownloadOptionalParams,
924 context: Context,
925 blob: BlobModel
926 ): Promise<Models.BlobDownloadResponse> {
927 // Deserializer doesn't handle range header currently, manually parse range headers here
928 const rangesParts = deserializePageBlobRangeHeader(
929 context.request!.getHeader("range"),
930 context.request!.getHeader("x-ms-range"),
931 false
932 );
933 const rangeStart = rangesParts[0];
934 let rangeEnd = rangesParts[1];
935
936 // Will automatically shift request with longer data end than blob size to blob size
937 if (rangeEnd + 1 >= blob.properties.contentLength!) {
938 rangeEnd = blob.properties.contentLength! - 1;
939 }
940
941 const contentLength = rangeEnd - rangeStart + 1;
942 const partialRead = contentLength !== blob.properties.contentLength!;
943
944 this.logger.info(
945 // tslint:disable-next-line:max-line-length
946 `BlobHandler:downloadPageBlob() NormalizedDownloadRange=bytes=${rangeStart}-${rangeEnd} RequiredContentLength=${contentLength}`,
947 context.contextId
948 );
949
950 // if (contentLength <= 0) {
951 // return {
952 // statusCode: 200,
953 // body: undefined,
954 // metadata: blob.metadata,
955 // eTag: blob.properties.etag,
956 // requestId: context.contextID,
957 // date: context.startTime!,
958 // version: BLOB_API_VERSION,
959 // ...blob.properties,
960 // contentLength,
961 // contentMD5: undefined
962 // };
963 // }
964
965 blob.pageRangesInOrder = blob.pageRangesInOrder || [];
966 const ranges =
967 contentLength <= 0
968 ? []
969 : this.rangesManager.fillZeroRanges(blob.pageRangesInOrder, {
970 start: rangeStart,
971 end: rangeEnd
972 });
973
974 const bodyGetter = async () => {
975 return this.extentStore.readExtents(
976 ranges.map(value => value.persistency),
977 0,
978 contentLength,
979 context.contextId
980 );
981 };
982
983 let body: NodeJS.ReadableStream | undefined = await bodyGetter();
984 let contentMD5: Uint8Array | undefined;
985 if (!partialRead) {
986 contentMD5 = blob.properties.contentMD5;
987 }
988 if (
989 contentLength <= 4 * 1024 * 1024 &&
990 contentMD5 === undefined &&
991 body !== undefined
992 ) {
993 contentMD5 = await getMD5FromStream(body);
994 body = await bodyGetter();
995 }
996
997 let contentRange: string | undefined;
998 if (
999 context.request!.getHeader("range") ||
1000 context.request!.getHeader("x-ms-range")
1001 ) {
1002 contentRange = `bytes ${rangeStart}-${rangeEnd}/${blob.properties
1003 .contentLength!}`;
1004 }
1005
1006 const response: Models.BlobDownloadResponse = {
1007 statusCode:
1008 rangesParts[1] === Infinity && rangesParts[0] === 0 ? 200 : 206,
1009 body,
1010 metadata: blob.metadata,
1011 eTag: blob.properties.etag,
1012 requestId: context.contextId,
1013 date: context.startTime!,
1014 version: BLOB_API_VERSION,
1015 ...blob.properties,
1016 contentLength,
1017 contentRange,
1018 contentMD5,
1019 blobContentMD5: blob.properties.contentMD5,
1020 isServerEncrypted: true,
1021 clientRequestId: options.requestId
1022 };
1023
1024 return response;
1025 }
1026}