UNPKG

14.5 kBPlain TextView Raw
1import IExtentStore from "../../common/persistence/IExtentStore";
2import { convertRawHeadersToMetadata } from "../../common/utils/utils";
3import BlobStorageContext from "../context/BlobStorageContext";
4import NotImplementedError from "../errors/NotImplementedError";
5import StorageErrorFactory from "../errors/StorageErrorFactory";
6import * as Models from "../generated/artifacts/models";
7import Context from "../generated/Context";
8import IPageBlobHandler from "../generated/handlers/IPageBlobHandler";
9import ILogger from "../generated/utils/ILogger";
10import BlobLeaseAdapter from "../lease/BlobLeaseAdapter";
11import BlobWriteLeaseValidator from "../lease/BlobWriteLeaseValidator";
12import IBlobMetadataStore, {
13 BlobModel
14} from "../persistence/IBlobMetadataStore";
15import { BLOB_API_VERSION } from "../utils/constants";
16import { deserializePageBlobRangeHeader, newEtag } from "../utils/utils";
17import BaseHandler from "./BaseHandler";
18import IPageBlobRangesManager from "./IPageBlobRangesManager";
19
20/**
21 * PageBlobHandler handles Azure Storage PageBlob related requests.
22 *
23 * @export
24 * @class PageBlobHandler
25 * @extends {BaseHandler}
26 * @implements {IPageBlobHandler}
27 */
28export default class PageBlobHandler extends BaseHandler
29 implements IPageBlobHandler {
30 constructor(
31 metadataStore: IBlobMetadataStore,
32 extentStore: IExtentStore,
33 logger: ILogger,
34 private readonly rangesManager: IPageBlobRangesManager
35 ) {
36 super(metadataStore, extentStore, logger);
37 }
38
39 public async uploadPagesFromURL(
40 sourceUrl: string,
41 sourceRange: string,
42 contentLength: number,
43 range: string,
44 options: Models.PageBlobUploadPagesFromURLOptionalParams,
45 context: Context
46 ): Promise<Models.PageBlobUploadPagesFromURLResponse> {
47 throw new NotImplementedError(context.contextId);
48 }
49
50 public async create(
51 contentLength: number,
52 blobContentLength: number,
53 options: Models.PageBlobCreateOptionalParams,
54 context: Context
55 ): Promise<Models.PageBlobCreateResponse> {
56 const blobCtx = new BlobStorageContext(context);
57 const accountName = blobCtx.account!;
58 const containerName = blobCtx.container!;
59 const blobName = blobCtx.blob!;
60 const date = blobCtx.startTime!;
61
62 if (options.pageBlobAccessTier !== undefined) {
63 throw StorageErrorFactory.getAccessTierNotSupportedForBlobType(
64 context.contextId!
65 );
66 }
67
68 if (contentLength !== 0) {
69 throw StorageErrorFactory.getInvalidOperation(
70 blobCtx.contextId!,
71 "Content-Length must be 0 for Create Page Blob request."
72 );
73 }
74
75 if (blobContentLength % 512 !== 0) {
76 throw StorageErrorFactory.getInvalidOperation(
77 blobCtx.contextId!,
78 "x-ms-content-length must be aligned to a 512-byte boundary."
79 );
80 }
81
82 options.blobHTTPHeaders = options.blobHTTPHeaders || {};
83 const contentType =
84 options.blobHTTPHeaders.blobContentType ||
85 context.request!.getHeader("content-type") ||
86 "application/octet-stream";
87
88 // const accessTierInferred = options.pageBlobAccessTier === undefined;
89
90 // Check Blob size match tier
91 // if (
92 // !accessTierInferred &&
93 // blobContentLength > PageBlobAccessTierThreshold.get(tier)!
94 // ) {
95 // throw StorageErrorFactory.getBlobBlobTierInadequateForContentLength(
96 // blobCtx.contextID!
97 // );
98 // }
99
100 // Preserve metadata key case
101 const metadata = convertRawHeadersToMetadata(
102 blobCtx.request!.getRawHeaders()
103 );
104
105 const etag = newEtag();
106 const blob: BlobModel = {
107 deleted: false,
108 metadata,
109 accountName,
110 containerName,
111 name: blobName,
112 properties: {
113 creationTime: date,
114 lastModified: date,
115 etag,
116 contentLength: blobContentLength,
117 contentType,
118 contentEncoding: options.blobHTTPHeaders.blobContentEncoding,
119 contentLanguage: options.blobHTTPHeaders.blobContentLanguage,
120 contentMD5: options.blobHTTPHeaders.blobContentMD5,
121 contentDisposition: options.blobHTTPHeaders.blobContentDisposition,
122 cacheControl: options.blobHTTPHeaders.blobCacheControl,
123 blobSequenceNumber: options.blobSequenceNumber
124 ? options.blobSequenceNumber
125 : 0,
126 blobType: Models.BlobType.PageBlob,
127 leaseStatus: Models.LeaseStatusType.Unlocked,
128 leaseState: Models.LeaseStateType.Available,
129 serverEncrypted: true
130 // TODO: May support setting this part for a premium storage account.
131 // accessTier: accessTierInferred
132 // ? ((options.pageBlobAccessTier as any) as Models.AccessTier)
133 // : Models.AccessTier.P4, // TODO: Infer tier from size
134 // accessTierInferred
135 },
136 snapshot: "",
137 isCommitted: true,
138 pageRangesInOrder: []
139 };
140
141 // TODO: What's happens when create page blob right before commit block list? Or should we lock
142 // Should we check if there is an uncommitted blob?
143 await this.metadataStore.createBlob(
144 context,
145 blob,
146 options.leaseAccessConditions,
147 options.modifiedAccessConditions
148 );
149
150 const response: Models.PageBlobCreateResponse = {
151 statusCode: 201,
152 eTag: etag,
153 lastModified: blob.properties.lastModified,
154 contentMD5: blob.properties.contentMD5,
155 requestId: context.contextId,
156 version: BLOB_API_VERSION,
157 date,
158 isServerEncrypted: true,
159 clientRequestId: options.requestId
160 };
161
162 return response;
163 }
164
165 public async uploadPages(
166 body: NodeJS.ReadableStream,
167 contentLength: number,
168 options: Models.PageBlobUploadPagesOptionalParams,
169 context: Context
170 ): Promise<Models.PageBlobUploadPagesResponse> {
171 const blobCtx = new BlobStorageContext(context);
172 const accountName = blobCtx.account!;
173 const containerName = blobCtx.container!;
174 const blobName = blobCtx.blob!;
175 const date = blobCtx.startTime!;
176
177 if (contentLength % 512 !== 0) {
178 throw StorageErrorFactory.getInvalidOperation(
179 blobCtx.contextId!,
180 "content-length or x-ms-content-length must be aligned to a 512-byte boundary."
181 );
182 }
183
184 const blob = await this.metadataStore.downloadBlob(
185 context,
186 accountName,
187 containerName,
188 blobName,
189 undefined,
190 options.leaseAccessConditions
191 );
192
193 if (blob.properties.blobType !== Models.BlobType.PageBlob) {
194 throw StorageErrorFactory.getBlobInvalidBlobType(blobCtx.contextId!);
195 }
196
197 // Check Lease status
198 new BlobWriteLeaseValidator(options.leaseAccessConditions).validate(
199 new BlobLeaseAdapter(blob),
200 context
201 );
202
203 let ranges;
204 try {
205 ranges = deserializePageBlobRangeHeader(
206 blobCtx.request!.getHeader("range"),
207 blobCtx.request!.getHeader("x-ms-range"),
208 true
209 );
210 } catch (err) {
211 throw StorageErrorFactory.getInvalidPageRange(blobCtx.contextId!);
212 }
213
214 const start = ranges[0];
215 const end = ranges[1]; // Inclusive
216 if (end - start + 1 !== contentLength) {
217 throw StorageErrorFactory.getInvalidPageRange(blobCtx.contextId!);
218 }
219
220 const persistency = await this.extentStore.appendExtent(
221 body,
222 context.contextId
223 );
224 if (persistency.count !== contentLength) {
225 // TODO: Confirm status code
226 throw StorageErrorFactory.getInvalidOperation(
227 blobCtx.contextId!,
228 `The size of the request body ${persistency.count} mismatches the content-length ${contentLength}.`
229 );
230 }
231
232 const res = await this.metadataStore.uploadPages(
233 context,
234 blob,
235 start,
236 end,
237 persistency,
238 options.leaseAccessConditions,
239 options.modifiedAccessConditions,
240 options.sequenceNumberAccessConditions
241 );
242
243 const response: Models.PageBlobUploadPagesResponse = {
244 statusCode: 201,
245 eTag: res.etag,
246 lastModified: date,
247 contentMD5: undefined, // TODO
248 blobSequenceNumber: res.blobSequenceNumber,
249 requestId: blobCtx.contextId,
250 version: BLOB_API_VERSION,
251 date,
252 isServerEncrypted: true,
253 clientRequestId: options.requestId
254 };
255
256 return response;
257 }
258
259 public async clearPages(
260 contentLength: number,
261 options: Models.PageBlobClearPagesOptionalParams,
262 context: Context
263 ): Promise<Models.PageBlobClearPagesResponse> {
264 const blobCtx = new BlobStorageContext(context);
265 const accountName = blobCtx.account!;
266 const containerName = blobCtx.container!;
267 const blobName = blobCtx.blob!;
268 const date = blobCtx.startTime!;
269
270 if (contentLength !== 0) {
271 throw StorageErrorFactory.getInvalidOperation(
272 blobCtx.contextId!,
273 "content-length or x-ms-content-length must be 0 for clear pages operation."
274 );
275 }
276
277 const blob = await this.metadataStore.downloadBlob(
278 context,
279 accountName,
280 containerName,
281 blobName,
282 undefined,
283 options.leaseAccessConditions
284 );
285
286 if (blob.properties.blobType !== Models.BlobType.PageBlob) {
287 throw StorageErrorFactory.getBlobInvalidBlobType(blobCtx.contextId!);
288 }
289
290 let ranges;
291 try {
292 ranges = deserializePageBlobRangeHeader(
293 blobCtx.request!.getHeader("range"),
294 blobCtx.request!.getHeader("x-ms-range"),
295 true
296 );
297 } catch (err) {
298 throw StorageErrorFactory.getInvalidPageRange(blobCtx.contextId!);
299 }
300
301 const start = ranges[0];
302 const end = ranges[1];
303
304 const res = await this.metadataStore.clearRange(
305 context,
306 blob,
307 start,
308 end,
309 options.leaseAccessConditions,
310 options.modifiedAccessConditions,
311 options.sequenceNumberAccessConditions
312 );
313
314 const response: Models.PageBlobClearPagesResponse = {
315 statusCode: 201,
316 eTag: res.etag,
317 lastModified: date,
318 contentMD5: undefined, // TODO
319 blobSequenceNumber: res.blobSequenceNumber,
320 requestId: blobCtx.contextId,
321 version: BLOB_API_VERSION,
322 clientRequestId: options.requestId,
323 date
324 };
325
326 return response;
327 }
328
329 public async getPageRanges(
330 options: Models.PageBlobGetPageRangesOptionalParams,
331 context: Context
332 ): Promise<Models.PageBlobGetPageRangesResponse> {
333 const blobCtx = new BlobStorageContext(context);
334 const accountName = blobCtx.account!;
335 const containerName = blobCtx.container!;
336 const blobName = blobCtx.blob!;
337 const date = blobCtx.startTime!;
338
339 const blob = await this.metadataStore.getPageRanges(
340 context,
341 accountName,
342 containerName,
343 blobName,
344 options.snapshot,
345 options.leaseAccessConditions,
346 options.modifiedAccessConditions
347 );
348
349 if (blob.properties.blobType !== Models.BlobType.PageBlob) {
350 throw StorageErrorFactory.getBlobInvalidBlobType(blobCtx.contextId!);
351 }
352
353 let ranges = deserializePageBlobRangeHeader(
354 blobCtx.request!.getHeader("range"),
355 blobCtx.request!.getHeader("x-ms-range"),
356 false
357 );
358 if (!ranges) {
359 ranges = [0, blob.properties.contentLength! - 1];
360 }
361
362 blob.pageRangesInOrder = blob.pageRangesInOrder || [];
363 const impactedRanges = this.rangesManager.cutRanges(
364 blob.pageRangesInOrder,
365 {
366 start: ranges[0],
367 end: ranges[1]
368 }
369 );
370
371 const response: Models.PageBlobGetPageRangesResponse = {
372 statusCode: 200,
373 pageRange: impactedRanges,
374 eTag: blob.properties.etag,
375 blobContentLength: blob.properties.contentLength,
376 lastModified: date,
377 requestId: blobCtx.contextId,
378 version: BLOB_API_VERSION,
379 clientRequestId: options.requestId,
380 date
381 };
382
383 return response;
384 }
385
386 public async getPageRangesDiff(
387 prevsnapshot: string,
388 options: Models.PageBlobGetPageRangesDiffOptionalParams,
389 context: Context
390 ): Promise<Models.PageBlobGetPageRangesDiffResponse> {
391 throw new NotImplementedError(context.contextId);
392 }
393
394 public async resize(
395 blobContentLength: number,
396 options: Models.PageBlobResizeOptionalParams,
397 context: Context
398 ): Promise<Models.PageBlobResizeResponse> {
399 const blobCtx = new BlobStorageContext(context);
400 const accountName = blobCtx.account!;
401 const containerName = blobCtx.container!;
402 const blobName = blobCtx.blob!;
403 const date = blobCtx.startTime!;
404
405 if (blobContentLength % 512 !== 0) {
406 throw StorageErrorFactory.getInvalidOperation(
407 blobCtx.contextId!,
408 "x-ms-blob-content-length must be aligned to a 512-byte boundary for Page Blob Resize request."
409 );
410 }
411
412 const res = await this.metadataStore.resizePageBlob(
413 context,
414 accountName,
415 containerName,
416 blobName,
417 blobContentLength,
418 options.leaseAccessConditions,
419 options.modifiedAccessConditions
420 );
421
422 const response: Models.PageBlobResizeResponse = {
423 statusCode: 200,
424 eTag: res.etag,
425 lastModified: res.lastModified,
426 blobSequenceNumber: res.blobSequenceNumber,
427 requestId: blobCtx.contextId,
428 version: BLOB_API_VERSION,
429 clientRequestId: options.requestId,
430 date
431 };
432
433 return response;
434 }
435
436 public async updateSequenceNumber(
437 sequenceNumberAction: Models.SequenceNumberActionType,
438 options: Models.PageBlobUpdateSequenceNumberOptionalParams,
439 context: Context
440 ): Promise<Models.PageBlobUpdateSequenceNumberResponse> {
441 const blobCtx = new BlobStorageContext(context);
442 const accountName = blobCtx.account!;
443 const containerName = blobCtx.container!;
444 const blobName = blobCtx.blob!;
445 const date = blobCtx.startTime!;
446
447 const res = await this.metadataStore.updateSequenceNumber(
448 context,
449 accountName,
450 containerName,
451 blobName,
452 sequenceNumberAction,
453 options.blobSequenceNumber,
454 options.leaseAccessConditions,
455 options.modifiedAccessConditions
456 );
457
458 const response: Models.PageBlobUpdateSequenceNumberResponse = {
459 statusCode: 200,
460 eTag: res.etag,
461 lastModified: res.lastModified,
462 blobSequenceNumber: res.blobSequenceNumber,
463 requestId: blobCtx.contextId,
464 version: BLOB_API_VERSION,
465 clientRequestId: options.requestId,
466 date
467 };
468
469 return response;
470 }
471
472 public async copyIncremental(
473 copySource: string,
474 options: Models.PageBlobCopyIncrementalOptionalParams,
475 context: Context
476 ): Promise<Models.PageBlobCopyIncrementalResponse> {
477 throw new NotImplementedError(context.contextId);
478 }
479}