UNPKG

22.8 kBPlain TextView Raw
1import { convertRawHeadersToMetadata } from "../../common/utils/utils";
2import BlobStorageContext from "../context/BlobStorageContext";
3import * as Models from "../generated/artifacts/models";
4import Context from "../generated/Context";
5import IContainerHandler from "../generated/handlers/IContainerHandler";
6import {
7 BLOB_API_VERSION,
8 EMULATOR_ACCOUNT_KIND,
9 EMULATOR_ACCOUNT_SKUNAME
10} from "../utils/constants";
11import { DEFAULT_LIST_BLOBS_MAX_RESULTS } from "../utils/constants";
12import { newEtag, removeQuotationFromListBlobEtag } from "../utils/utils";
13import BaseHandler from "./BaseHandler";
14
15/**
16 * ContainerHandler handles Azure Storage Blob container related requests.
17 *
18 * @export
19 * @class ContainerHandler
20 * @implements {IHandler}
21 */
22export default class ContainerHandler extends BaseHandler
23 implements IContainerHandler {
24 /**
25 * Create container.
26 *
27 * @param {Models.ContainerCreateOptionalParams} options
28 * @param {Context} context
29 * @returns {Promise<Models.ContainerCreateResponse>}
30 * @memberof ContainerHandler
31 */
32 public async create(
33 options: Models.ContainerCreateOptionalParams,
34 context: Context
35 ): Promise<Models.ContainerCreateResponse> {
36 const blobCtx = new BlobStorageContext(context);
37 const accountName = blobCtx.account!;
38 const containerName = blobCtx.container!;
39 const lastModified = blobCtx.startTime!;
40 const etag = newEtag();
41
42 // Preserve metadata key case
43 const metadata = convertRawHeadersToMetadata(
44 blobCtx.request!.getRawHeaders()
45 );
46
47 await this.metadataStore.createContainer(context, {
48 accountName,
49 name: containerName,
50 metadata,
51 properties: {
52 etag,
53 lastModified,
54 leaseStatus: Models.LeaseStatusType.Unlocked,
55 leaseState: Models.LeaseStateType.Available,
56 publicAccess: options.access,
57 hasImmutabilityPolicy: false,
58 hasLegalHold: false
59 }
60 });
61
62 const response: Models.ContainerCreateResponse = {
63 statusCode: 201,
64 requestId: blobCtx.contextId,
65 clientRequestId: options.requestId,
66 eTag: etag,
67 lastModified,
68 version: BLOB_API_VERSION
69 };
70
71 return response;
72 }
73
74 /**
75 * get container properties
76 *
77 * @param {Models.ContainerGetPropertiesOptionalParams} options
78 * @param {Context} context
79 * @returns {Promise<Models.ContainerGetPropertiesResponse>}
80 * @memberof ContainerHandler
81 */
82 public async getProperties(
83 options: Models.ContainerGetPropertiesOptionalParams,
84 context: Context
85 ): Promise<Models.ContainerGetPropertiesResponse> {
86 const blobCtx = new BlobStorageContext(context);
87 const accountName = blobCtx.account!;
88 const containerName = blobCtx.container!;
89
90 const containerProperties = await this.metadataStore.getContainerProperties(
91 context,
92 accountName,
93 containerName,
94 options.leaseAccessConditions
95 );
96
97 const response: Models.ContainerGetPropertiesResponse = {
98 statusCode: 200,
99 requestId: context.contextId,
100 clientRequestId: options.requestId,
101 eTag: containerProperties.properties.etag,
102 ...containerProperties.properties,
103 blobPublicAccess: containerProperties.properties.publicAccess,
104 metadata: containerProperties.metadata,
105 version: BLOB_API_VERSION
106 };
107
108 return response;
109 }
110
111 /**
112 * get container properties with headers
113 *
114 * @param {Models.ContainerGetPropertiesOptionalParams} options
115 * @param {Context} context
116 * @returns {Promise<Models.ContainerGetPropertiesResponse>}
117 * @memberof ContainerHandler
118 */
119 public async getPropertiesWithHead(
120 options: Models.ContainerGetPropertiesOptionalParams,
121 context: Context
122 ): Promise<Models.ContainerGetPropertiesResponse> {
123 return this.getProperties(options, context);
124 }
125
126 /**
127 * Delete container.
128 *
129 * @param {Models.ContainerDeleteMethodOptionalParams} options
130 * @param {Context} context
131 * @returns {Promise<Models.ContainerDeleteResponse>}
132 * @memberof ContainerHandler
133 */
134 public async delete(
135 options: Models.ContainerDeleteMethodOptionalParams,
136 context: Context
137 ): Promise<Models.ContainerDeleteResponse> {
138 const blobCtx = new BlobStorageContext(context);
139 const accountName = blobCtx.account!;
140 const containerName = blobCtx.container!;
141
142 // TODO: Mark container as being deleted status, then (mark) delete all blobs async
143 // When above finishes, execute following delete container operation
144 // Because following delete container operation will only delete DB metadata for container and
145 // blobs under the container, but will not clean up blob data in disk
146 // The current design will directly remove the container and all the blobs belong to it.
147 await this.metadataStore.deleteContainer(
148 context,
149 accountName,
150 containerName,
151 options
152 );
153
154 const response: Models.ContainerDeleteResponse = {
155 statusCode: 202,
156 requestId: context.contextId,
157 clientRequestId: options.requestId,
158 date: context.startTime,
159 version: BLOB_API_VERSION
160 };
161
162 return response;
163 }
164
165 /**
166 * Set container metadata.
167 *
168 * @param {Models.ContainerSetMetadataOptionalParams} options
169 * @param {Context} context
170 * @returns {Promise<Models.ContainerSetMetadataResponse>}
171 * @memberof ContainerHandler
172 */
173 public async setMetadata(
174 options: Models.ContainerSetMetadataOptionalParams,
175 context: Context
176 ): Promise<Models.ContainerSetMetadataResponse> {
177 const blobCtx = new BlobStorageContext(context);
178 const accountName = blobCtx.account!;
179 const containerName = blobCtx.container!;
180 const date = blobCtx.startTime!;
181 const eTag = newEtag();
182
183 // Preserve metadata key case
184 const metadata = convertRawHeadersToMetadata(
185 blobCtx.request!.getRawHeaders()
186 );
187
188 await this.metadataStore.setContainerMetadata(
189 context,
190 accountName,
191 containerName,
192 date,
193 eTag,
194 metadata,
195 options.leaseAccessConditions,
196 options.modifiedAccessConditions
197 );
198
199 const response: Models.ContainerSetMetadataResponse = {
200 statusCode: 200,
201 requestId: context.contextId,
202 clientRequestId: options.requestId,
203 date,
204 eTag,
205 lastModified: date
206 };
207
208 return response;
209 }
210
211 /**
212 * Get container access policy.
213 *
214 * @param {Models.ContainerGetAccessPolicyOptionalParams} options
215 * @param {Context} context
216 * @returns {Promise<Models.ContainerGetAccessPolicyResponse>}
217 * @memberof ContainerHandler
218 */
219 public async getAccessPolicy(
220 options: Models.ContainerGetAccessPolicyOptionalParams,
221 context: Context
222 ): Promise<Models.ContainerGetAccessPolicyResponse> {
223 const blobCtx = new BlobStorageContext(context);
224 const accountName = blobCtx.account!;
225 const containerName = blobCtx.container!;
226
227 const containerAcl = await this.metadataStore.getContainerACL(
228 context,
229 accountName,
230 containerName,
231 options.leaseAccessConditions
232 );
233
234 const response: any = [];
235 const responseArray = response as Models.SignedIdentifier[];
236 const responseObject = response as Models.ContainerGetAccessPolicyHeaders & {
237 statusCode: 200;
238 };
239 if (containerAcl.containerAcl !== undefined) {
240 responseArray.push(...containerAcl.containerAcl);
241 }
242 responseObject.date = containerAcl.properties.lastModified;
243 responseObject.blobPublicAccess = containerAcl.properties.publicAccess;
244 responseObject.eTag = containerAcl.properties.etag;
245 responseObject.lastModified = containerAcl.properties.lastModified;
246 responseObject.requestId = context.contextId;
247 responseObject.version = BLOB_API_VERSION;
248 responseObject.statusCode = 200;
249 responseObject.clientRequestId = options.requestId;
250
251 // TODO: Need fix generator code since the output containerAcl can't be serialized correctly
252 // tslint:disable-next-line:max-line-length
253 // Correct responds: <?xml version="1.0" encoding="utf-8"?><SignedIdentifiers><SignedIdentifier><Id>123</Id><AccessPolicy><Start>2019-04-30T16:00:00.0000000Z</Start><Expiry>2019-12-31T16:00:00.0000000Z</Expiry><Permission>r</Permission></AccessPolicy></SignedIdentifier></SignedIdentifiers>
254 // tslint:disable-next-line:max-line-length
255 // Current responds: <?xml version="1.0" encoding="UTF-8" standalone="yes"?><parsedResponse><Id>123</Id><AccessPolicy><Start>2019-04-30T16:00:00Z</Start><Expiry>2019-12-31T16:00:00Z</Expiry><Permission>r</Permission></AccessPolicy></parsedResponse>"
256 return response;
257 }
258
259 /**
260 * set container access policy
261 *
262 * @param {Models.ContainerSetAccessPolicyOptionalParams} options
263 * @param {Context} context
264 * @returns {Promise<Models.ContainerSetAccessPolicyResponse>}
265 * @memberof ContainerHandler
266 */
267 public async setAccessPolicy(
268 options: Models.ContainerSetAccessPolicyOptionalParams,
269 context: Context
270 ): Promise<Models.ContainerSetAccessPolicyResponse> {
271 const blobCtx = new BlobStorageContext(context);
272 const accountName = blobCtx.account!;
273 const containerName = blobCtx.container!;
274
275 const date = blobCtx.startTime!;
276 const eTag = newEtag();
277 await this.metadataStore.setContainerACL(
278 context,
279 accountName,
280 containerName,
281 {
282 lastModified: date,
283 etag: eTag,
284 publicAccess: options.access,
285 containerAcl: options.containerAcl,
286 leaseAccessConditions: options.leaseAccessConditions,
287 modifiedAccessConditions: options.modifiedAccessConditions
288 }
289 );
290
291 const response: Models.ContainerSetAccessPolicyResponse = {
292 date,
293 eTag,
294 lastModified: date,
295 requestId: context.contextId,
296 version: BLOB_API_VERSION,
297 statusCode: 200,
298 clientRequestId: options.requestId
299 };
300
301 return response;
302 }
303
304 /**
305 * Acquire container lease.
306 *
307 * @see https://docs.microsoft.com/en-us/rest/api/storageservices/lease-container
308 *
309 * @param {Models.ContainerAcquireLeaseOptionalParams} options
310 * @param {Context} context
311 * @returns {Promise<Models.ContainerAcquireLeaseResponse>}
312 * @memberof ContainerHandler
313 */
314 public async acquireLease(
315 options: Models.ContainerAcquireLeaseOptionalParams,
316 context: Context
317 ): Promise<Models.ContainerAcquireLeaseResponse> {
318 const blobCtx = new BlobStorageContext(context);
319 const accountName = blobCtx.account!;
320 const containerName = blobCtx.container!;
321
322 const res = await this.metadataStore.acquireContainerLease(
323 context,
324 accountName,
325 containerName,
326 options
327 );
328
329 const response: Models.ContainerAcquireLeaseResponse = {
330 statusCode: 201,
331 requestId: context.contextId,
332 clientRequestId: options.requestId,
333 date: blobCtx.startTime!,
334 eTag: res.properties.etag,
335 lastModified: res.properties.lastModified,
336 leaseId: res.leaseId,
337 version: BLOB_API_VERSION
338 };
339
340 return response;
341 }
342
343 /**
344 * Release container lease.
345 *
346 * @see https://docs.microsoft.com/en-us/rest/api/storageservices/lease-container
347 *
348 * @param {string} leaseId
349 * @param {Models.ContainerReleaseLeaseOptionalParams} options
350 * @param {Context} context
351 * @returns {Promise<Models.ContainerReleaseLeaseResponse>}
352 * @memberof ContainerHandler
353 */
354 public async releaseLease(
355 leaseId: string,
356 options: Models.ContainerReleaseLeaseOptionalParams,
357 context: Context
358 ): Promise<Models.ContainerReleaseLeaseResponse> {
359 const blobCtx = new BlobStorageContext(context);
360 const accountName = blobCtx.account!;
361 const containerName = blobCtx.container!;
362
363 const res = await this.metadataStore.releaseContainerLease(
364 context,
365 accountName,
366 containerName,
367 leaseId,
368 options
369 );
370
371 const response: Models.ContainerReleaseLeaseResponse = {
372 statusCode: 200,
373 requestId: context.contextId,
374 clientRequestId: options.requestId,
375 date: blobCtx.startTime!,
376 eTag: res.etag,
377 lastModified: res.lastModified,
378 version: BLOB_API_VERSION
379 };
380
381 return response;
382 }
383
384 /**
385 * Renew container lease.
386 *
387 * @see https://docs.microsoft.com/en-us/rest/api/storageservices/lease-container
388 *
389 * @param {string} leaseId
390 * @param {Models.ContainerRenewLeaseOptionalParams} options
391 * @param {Context} context
392 * @returns {Promise<Models.ContainerRenewLeaseResponse>}
393 * @memberof ContainerHandler
394 */
395 public async renewLease(
396 leaseId: string,
397 options: Models.ContainerRenewLeaseOptionalParams,
398 context: Context
399 ): Promise<Models.ContainerRenewLeaseResponse> {
400 const blobCtx = new BlobStorageContext(context);
401 const accountName = blobCtx.account!;
402 const containerName = blobCtx.container!;
403
404 const res = await this.metadataStore.renewContainerLease(
405 context,
406 accountName,
407 containerName,
408 leaseId,
409 options
410 );
411
412 const response: Models.ContainerRenewLeaseResponse = {
413 statusCode: 200,
414 requestId: context.contextId,
415 clientRequestId: options.requestId,
416 date: blobCtx.startTime!,
417 leaseId: res.leaseId,
418 eTag: res.properties.etag,
419 lastModified: res.properties.lastModified,
420 version: BLOB_API_VERSION
421 };
422
423 return response;
424 }
425
426 /**
427 * Break container lease.
428 *
429 * @see https://docs.microsoft.com/en-us/rest/api/storageservices/lease-container
430 *
431 * @param {Models.ContainerBreakLeaseOptionalParams} options
432 * @param {Context} context
433 * @returns {Promise<Models.ContainerBreakLeaseResponse>}
434 * @memberof ContainerHandler
435 */
436 public async breakLease(
437 options: Models.ContainerBreakLeaseOptionalParams,
438 context: Context
439 ): Promise<Models.ContainerBreakLeaseResponse> {
440 const blobCtx = new BlobStorageContext(context);
441 const accountName = blobCtx.account!;
442 const containerName = blobCtx.container!;
443
444 const res = await this.metadataStore.breakContainerLease(
445 context,
446 accountName,
447 containerName,
448 options.breakPeriod,
449 options
450 );
451
452 const response: Models.ContainerBreakLeaseResponse = {
453 statusCode: 202,
454 requestId: context.contextId,
455 clientRequestId: options.requestId,
456 date: blobCtx.startTime!,
457 eTag: res.properties.etag,
458 lastModified: res.properties.lastModified,
459 leaseTime: res.leaseTime,
460 version: BLOB_API_VERSION
461 };
462
463 return response;
464 }
465
466 /**
467 * Change container lease.
468 *
469 * @see https://docs.microsoft.com/en-us/rest/api/storageservices/lease-container
470 *
471 * @param {string} leaseId
472 * @param {string} proposedLeaseId
473 * @param {Models.ContainerChangeLeaseOptionalParams} options
474 * @param {Context} context
475 * @returns {Promise<Models.ContainerChangeLeaseResponse>}
476 * @memberof ContainerHandler
477 */
478 public async changeLease(
479 leaseId: string,
480 proposedLeaseId: string,
481 options: Models.ContainerChangeLeaseOptionalParams,
482 context: Context
483 ): Promise<Models.ContainerChangeLeaseResponse> {
484 const blobCtx = new BlobStorageContext(context);
485 const accountName = blobCtx.account!;
486 const containerName = blobCtx.container!;
487
488 const res = await this.metadataStore.changeContainerLease(
489 context,
490 accountName,
491 containerName,
492 leaseId,
493 proposedLeaseId,
494 options
495 );
496
497 const response: Models.ContainerChangeLeaseResponse = {
498 statusCode: 200,
499 requestId: context.contextId,
500 clientRequestId: options.requestId,
501 date: blobCtx.startTime!,
502 eTag: res.properties.etag,
503 lastModified: res.properties.lastModified,
504 leaseId: res.leaseId,
505 version: BLOB_API_VERSION
506 };
507
508 return response;
509 }
510
511 /**
512 * list blobs flat segments
513 *
514 * @param {Models.ContainerListBlobFlatSegmentOptionalParams} options
515 * @param {Context} context
516 * @returns {Promise<Models.ContainerListBlobFlatSegmentResponse>}
517 * @memberof ContainerHandler
518 */
519 public async listBlobFlatSegment(
520 options: Models.ContainerListBlobFlatSegmentOptionalParams,
521 context: Context
522 ): Promise<Models.ContainerListBlobFlatSegmentResponse> {
523 const blobCtx = new BlobStorageContext(context);
524 const accountName = blobCtx.account!;
525 const containerName = blobCtx.container!;
526 await this.metadataStore.checkContainerExist(
527 context,
528 accountName,
529 containerName
530 );
531
532 const request = context.request!;
533 const marker = options.marker;
534 const delimiter = "";
535 options.marker = options.marker || "";
536 let includeSnapshots: boolean = false;
537 if (options.include !== undefined) {
538 if (options.include.includes(Models.ListBlobsIncludeItem.Snapshots)) {
539 includeSnapshots = true;
540 }
541 }
542 if (
543 options.maxresults === undefined ||
544 options.maxresults > DEFAULT_LIST_BLOBS_MAX_RESULTS
545 ) {
546 options.maxresults = DEFAULT_LIST_BLOBS_MAX_RESULTS;
547 }
548
549 const [blobs, nextMarker] = await this.metadataStore.listBlobs(
550 context,
551 accountName,
552 containerName,
553 undefined,
554 options.prefix,
555 options.maxresults,
556 marker,
557 includeSnapshots
558 );
559
560 const serviceEndpoint = `${request.getEndpoint()}/${accountName}`;
561 const response: Models.ContainerListBlobFlatSegmentResponse = {
562 statusCode: 200,
563 contentType: "application/xml",
564 requestId: context.contextId,
565 version: BLOB_API_VERSION,
566 date: context.startTime,
567 serviceEndpoint,
568 containerName,
569 prefix: options.prefix || "",
570 marker: options.marker,
571 maxResults: options.maxresults,
572 delimiter,
573 segment: {
574 blobItems: blobs.map(item => {
575 return {
576 ...item,
577 deleted: item.deleted !== true ? undefined : true,
578 properties: {
579 ...item.properties,
580 etag: removeQuotationFromListBlobEtag(item.properties.etag),
581 accessTierInferred:
582 item.properties.accessTierInferred === true ? true : undefined
583 }
584 };
585 })
586 },
587 clientRequestId: options.requestId,
588 nextMarker: `${nextMarker || ""}`
589 };
590
591 return response;
592 }
593
594 /**
595 * List blobs hierarchy.
596 *
597 * @param {string} delimiter
598 * @param {Models.ContainerListBlobHierarchySegmentOptionalParams} options
599 * @param {Context} context
600 * @returns {Promise<Models.ContainerListBlobHierarchySegmentResponse>}
601 * @memberof ContainerHandler
602 */
603 public async listBlobHierarchySegment(
604 delimiter: string,
605 options: Models.ContainerListBlobHierarchySegmentOptionalParams,
606 context: Context
607 ): Promise<Models.ContainerListBlobHierarchySegmentResponse> {
608 // TODO: Need update list out blobs lease properties with BlobHandler.updateLeaseAttributes()
609 const blobCtx = new BlobStorageContext(context);
610 const accountName = blobCtx.account!;
611 const containerName = blobCtx.container!;
612 await this.metadataStore.checkContainerExist(
613 context,
614 accountName,
615 containerName
616 );
617
618 const request = context.request!;
619 const marker = options.marker;
620 delimiter = delimiter === "" ? "/" : delimiter;
621 options.prefix = options.prefix || "";
622 options.marker = options.marker || "";
623 let includeSnapshots: boolean = false;
624 if (options.include !== undefined) {
625 if (options.include.includes(Models.ListBlobsIncludeItem.Snapshots)) {
626 includeSnapshots = true;
627 }
628 }
629 if (
630 options.maxresults === undefined ||
631 options.maxresults > DEFAULT_LIST_BLOBS_MAX_RESULTS
632 ) {
633 options.maxresults = DEFAULT_LIST_BLOBS_MAX_RESULTS;
634 }
635
636 const [blobs, nextMarker] = await this.metadataStore.listBlobs(
637 context,
638 accountName,
639 containerName,
640 undefined,
641 options.prefix,
642 options.maxresults,
643 marker,
644 includeSnapshots
645 );
646
647 const blobItems: Models.BlobItem[] = [];
648 const blobPrefixes: Models.BlobPrefix[] = [];
649 const blobPrefixesSet = new Set<string>();
650
651 const prefixLength = options.prefix.length;
652 for (const blob of blobs) {
653 const delimiterPosAfterPrefix = blob.name.indexOf(
654 delimiter,
655 prefixLength
656 );
657
658 // This is a blob
659 if (delimiterPosAfterPrefix < 0) {
660 blob.deleted = blob.deleted !== true ? undefined : true;
661 blobItems.push(blob);
662 } else {
663 // This is a prefix
664 const prefix = blob.name.substr(0, delimiterPosAfterPrefix + 1);
665 blobPrefixesSet.add(prefix);
666 }
667 }
668
669 const iter = blobPrefixesSet.values();
670 let val;
671 while (!(val = iter.next()).done) {
672 blobPrefixes.push({ name: val.value });
673 }
674
675 const serviceEndpoint = `${request.getEndpoint()}/${accountName}`;
676 const response: Models.ContainerListBlobHierarchySegmentResponse = {
677 statusCode: 200,
678 contentType: "application/xml",
679 requestId: context.contextId,
680 version: BLOB_API_VERSION,
681 date: context.startTime,
682 serviceEndpoint,
683 containerName,
684 prefix: options.prefix,
685 marker: options.marker,
686 maxResults: options.maxresults,
687 delimiter,
688 segment: {
689 blobPrefixes,
690 blobItems: blobItems.map(item => {
691 return {
692 ...item,
693 properties: {
694 ...item.properties,
695 etag: removeQuotationFromListBlobEtag(item.properties.etag),
696 accessTierInferred:
697 item.properties.accessTierInferred === true ? true : undefined
698 }
699 };
700 })
701 },
702 clientRequestId: options.requestId,
703 nextMarker: `${nextMarker || ""}`
704 };
705
706 return response;
707 }
708
709 /**
710 * get account info
711 *
712 * @param {Context} context
713 * @returns {Promise<Models.ContainerGetAccountInfoResponse>}
714 * @memberof ContainerHandler
715 */
716 public async getAccountInfo(
717 context: Context
718 ): Promise<Models.ContainerGetAccountInfoResponse> {
719 const response: Models.ContainerGetAccountInfoResponse = {
720 statusCode: 200,
721 requestId: context.contextId,
722 clientRequestId: context.request!.getHeader("x-ms-client-request-id"),
723 skuName: EMULATOR_ACCOUNT_SKUNAME,
724 accountKind: EMULATOR_ACCOUNT_KIND,
725 date: context.startTime!,
726 version: BLOB_API_VERSION
727 };
728 return response;
729 }
730
731 /**
732 * get account info with headers
733 *
734 * @param {Context} context
735 * @returns {Promise<Models.ContainerGetAccountInfoResponse>}
736 * @memberof ContainerHandler
737 */
738 public async getAccountInfoWithHead(
739 context: Context
740 ): Promise<Models.ContainerGetAccountInfoResponse> {
741 return this.getAccountInfo(context);
742 }
743}