1 | import { URL } from "url";
|
2 |
|
3 | import IExtentStore from "../../common/persistence/IExtentStore";
|
4 | import { convertRawHeadersToMetadata } from "../../common/utils/utils";
|
5 | import BlobStorageContext from "../context/BlobStorageContext";
|
6 | import NotImplementedError from "../errors/NotImplementedError";
|
7 | import StorageErrorFactory from "../errors/StorageErrorFactory";
|
8 | import * as Models from "../generated/artifacts/models";
|
9 | import Context from "../generated/Context";
|
10 | import IBlobHandler from "../generated/handlers/IBlobHandler";
|
11 | import ILogger from "../generated/utils/ILogger";
|
12 | import { extractStoragePartsFromPath } from "../middlewares/blobStorageContext.middleware";
|
13 | import IBlobMetadataStore, {
|
14 | BlobModel
|
15 | } from "../persistence/IBlobMetadataStore";
|
16 | import {
|
17 | BLOB_API_VERSION,
|
18 | EMULATOR_ACCOUNT_KIND,
|
19 | EMULATOR_ACCOUNT_SKUNAME,
|
20 | HeaderConstants
|
21 | } from "../utils/constants";
|
22 | import {
|
23 | deserializePageBlobRangeHeader,
|
24 | deserializeRangeHeader,
|
25 | getMD5FromStream
|
26 | } from "../utils/utils";
|
27 | import BaseHandler from "./BaseHandler";
|
28 | import IPageBlobRangesManager from "./IPageBlobRangesManager";
|
29 |
|
30 |
|
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 | export 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 |
|
80 |
|
81 |
|
82 |
|
83 |
|
84 |
|
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 |
|
118 |
|
119 |
|
120 |
|
121 |
|
122 |
|
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 |
|
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 |
|
180 |
|
181 |
|
182 |
|
183 |
|
184 |
|
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 |
|
215 |
|
216 |
|
217 |
|
218 |
|
219 |
|
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 |
|
230 |
|
231 |
|
232 |
|
233 |
|
234 |
|
235 |
|
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 |
|
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 |
|
298 |
|
299 |
|
300 |
|
301 |
|
302 |
|
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 |
|
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 |
|
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 |
|
345 |
|
346 |
|
347 |
|
348 |
|
349 |
|
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 |
|
394 |
|
395 |
|
396 |
|
397 |
|
398 |
|
399 |
|
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 |
|
434 |
|
435 |
|
436 |
|
437 |
|
438 |
|
439 |
|
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 |
|
475 |
|
476 |
|
477 |
|
478 |
|
479 |
|
480 |
|
481 |
|
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 |
|
519 |
|
520 |
|
521 |
|
522 |
|
523 |
|
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 |
|
558 |
|
559 |
|
560 |
|
561 |
|
562 |
|
563 |
|
564 |
|
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 |
|
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 |
|
608 |
|
609 |
|
610 |
|
611 |
|
612 |
|
613 |
|
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 |
|
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 |
|
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 |
|
680 |
|
681 |
|
682 |
|
683 |
|
684 |
|
685 |
|
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 |
|
726 |
|
727 |
|
728 |
|
729 |
|
730 |
|
731 |
|
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 |
|
763 |
|
764 |
|
765 |
|
766 |
|
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 |
|
785 |
|
786 |
|
787 |
|
788 |
|
789 |
|
790 | public async getAccountInfoWithHead(
|
791 | context: Context
|
792 | ): Promise<Models.BlobGetAccountInfoResponse> {
|
793 | return this.getAccountInfo(context);
|
794 | }
|
795 |
|
796 | |
797 |
|
798 |
|
799 |
|
800 |
|
801 |
|
802 |
|
803 |
|
804 |
|
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 |
|
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 |
|
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 |
|
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 |
|
914 |
|
915 |
|
916 |
|
917 |
|
918 |
|
919 |
|
920 |
|
921 |
|
922 | private async downloadPageBlob(
|
923 | options: Models.BlobDownloadOptionalParams,
|
924 | context: Context,
|
925 | blob: BlobModel
|
926 | ): Promise<Models.BlobDownloadResponse> {
|
927 |
|
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 |
|
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 |
|
946 | `BlobHandler:downloadPageBlob() NormalizedDownloadRange=bytes=${rangeStart}-${rangeEnd} RequiredContentLength=${contentLength}`,
|
947 | context.contextId
|
948 | );
|
949 |
|
950 |
|
951 |
|
952 |
|
953 |
|
954 |
|
955 |
|
956 |
|
957 |
|
958 |
|
959 |
|
960 |
|
961 |
|
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 | }
|