1 | import IExtentStore from "../../common/persistence/IExtentStore";
|
2 | import { convertRawHeadersToMetadata } from "../../common/utils/utils";
|
3 | import BlobStorageContext from "../context/BlobStorageContext";
|
4 | import NotImplementedError from "../errors/NotImplementedError";
|
5 | import StorageErrorFactory from "../errors/StorageErrorFactory";
|
6 | import * as Models from "../generated/artifacts/models";
|
7 | import Context from "../generated/Context";
|
8 | import IPageBlobHandler from "../generated/handlers/IPageBlobHandler";
|
9 | import ILogger from "../generated/utils/ILogger";
|
10 | import BlobLeaseAdapter from "../lease/BlobLeaseAdapter";
|
11 | import BlobWriteLeaseValidator from "../lease/BlobWriteLeaseValidator";
|
12 | import IBlobMetadataStore, {
|
13 | BlobModel
|
14 | } from "../persistence/IBlobMetadataStore";
|
15 | import { BLOB_API_VERSION } from "../utils/constants";
|
16 | import { deserializePageBlobRangeHeader, newEtag } from "../utils/utils";
|
17 | import BaseHandler from "./BaseHandler";
|
18 | import IPageBlobRangesManager from "./IPageBlobRangesManager";
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 | export 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 |
|
89 |
|
90 |
|
91 |
|
92 |
|
93 |
|
94 |
|
95 |
|
96 |
|
97 |
|
98 |
|
99 |
|
100 |
|
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 |
|
131 |
|
132 |
|
133 |
|
134 |
|
135 | },
|
136 | snapshot: "",
|
137 | isCommitted: true,
|
138 | pageRangesInOrder: []
|
139 | };
|
140 |
|
141 |
|
142 |
|
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 |
|
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];
|
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 |
|
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,
|
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,
|
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 | }
|