source: frontend/node_modules/webpack/lib/serialization/FileMiddleware.js

Last change on this file was 9af201e, checked in by MBK <marija.karapandzova@…>, 12 days ago

Fix frontend appearance

  • Property mode set to 100644
File size: 22.4 KB
RevLine 
[9af201e]1/*
2 MIT License http://www.opensource.org/licenses/mit-license.php
3*/
4
5"use strict";
6
7const { constants } = require("buffer");
8const { pipeline } = require("stream");
9const {
10 constants: zConstants,
11 // eslint-disable-next-line n/no-unsupported-features/node-builtins
12 createBrotliCompress,
13 // eslint-disable-next-line n/no-unsupported-features/node-builtins
14 createBrotliDecompress,
15 createGunzip,
16 createGzip
17} = require("zlib");
18const { DEFAULTS } = require("../config/defaults");
19const createHash = require("../util/createHash");
20const { dirname, join, mkdirp } = require("../util/fs");
21const memoize = require("../util/memoize");
22const SerializerMiddleware = require("./SerializerMiddleware");
23
24/** @typedef {import("../util/Hash").HashFunction} HashFunction */
25/** @typedef {import("../util/fs").IStats} IStats */
26/** @typedef {import("../util/fs").IntermediateFileSystem} IntermediateFileSystem */
27/** @typedef {import("./types").BufferSerializableType} BufferSerializableType */
28
29/*
30Format:
31
32File -> Header Section*
33
34Version -> u32
35AmountOfSections -> u32
36SectionSize -> i32 (if less than zero represents lazy value)
37
38Header -> Version AmountOfSections SectionSize*
39
40Buffer -> n bytes
41Section -> Buffer
42
43*/
44
45// "wpc" + 1 in little-endian
46const VERSION = 0x01637077;
47const WRITE_LIMIT_TOTAL = 0x7fff0000;
48const WRITE_LIMIT_CHUNK = 511 * 1024 * 1024;
49
50/**
51 * Returns hash.
52 * @param {Buffer[]} buffers buffers
53 * @param {HashFunction} hashFunction hash function to use
54 * @returns {string} hash
55 */
56const hashForName = (buffers, hashFunction) => {
57 const hash = createHash(hashFunction);
58 for (const buf of buffers) hash.update(buf);
59 return hash.digest("hex");
60};
61
62const COMPRESSION_CHUNK_SIZE = 100 * 1024 * 1024;
63const DECOMPRESSION_CHUNK_SIZE = 100 * 1024 * 1024;
64
65/** @type {(buffer: Buffer, value: number, offset: number) => void} */
66const writeUInt64LE = Buffer.prototype.writeBigUInt64LE
67 ? (buf, value, offset) => {
68 buf.writeBigUInt64LE(BigInt(value), offset);
69 }
70 : (buf, value, offset) => {
71 const low = value % 0x100000000;
72 const high = (value - low) / 0x100000000;
73 buf.writeUInt32LE(low, offset);
74 buf.writeUInt32LE(high, offset + 4);
75 };
76
77/** @type {(buffer: Buffer, offset: number) => void} */
78const readUInt64LE = Buffer.prototype.readBigUInt64LE
79 ? (buf, offset) => Number(buf.readBigUInt64LE(offset))
80 : (buf, offset) => {
81 const low = buf.readUInt32LE(offset);
82 const high = buf.readUInt32LE(offset + 4);
83 return high * 0x100000000 + low;
84 };
85
86/** @typedef {Promise<void | void[]>} BackgroundJob */
87
88/**
89 * Defines the serialize result type used by this module.
90 * @typedef {object} SerializeResult
91 * @property {string | false} name
92 * @property {number} size
93 * @property {BackgroundJob=} backgroundJob
94 */
95
96/** @typedef {{ name: string, size: number }} LazyOptions */
97/**
98 * Defines the lazy function type used by this module.
99 * @typedef {import("./SerializerMiddleware").LazyFunction<BufferSerializableType[], Buffer, FileMiddleware, LazyOptions>} LazyFunction
100 */
101
102/**
103 * Serializes this instance into the provided serializer context.
104 * @param {FileMiddleware} middleware this
105 * @param {(BufferSerializableType | LazyFunction)[]} data data to be serialized
106 * @param {string | boolean} name file base name
107 * @param {(name: string | false, buffers: Buffer[], size: number) => Promise<void>} writeFile writes a file
108 * @param {HashFunction=} hashFunction hash function to use
109 * @returns {Promise<SerializeResult>} resulting file pointer and promise
110 */
111const serialize = async (
112 middleware,
113 data,
114 name,
115 writeFile,
116 hashFunction = DEFAULTS.HASH_FUNCTION
117) => {
118 /** @type {(Buffer[] | Buffer | Promise<SerializeResult>)[]} */
119 const processedData = [];
120 /** @type {WeakMap<SerializeResult, LazyFunction>} */
121 const resultToLazy = new WeakMap();
122 /** @type {Buffer[] | undefined} */
123 let lastBuffers;
124 for (const item of await data) {
125 if (typeof item === "function") {
126 if (!SerializerMiddleware.isLazy(item)) {
127 throw new Error("Unexpected function");
128 }
129 if (!SerializerMiddleware.isLazy(item, middleware)) {
130 throw new Error(
131 "Unexpected lazy value with non-this target (can't pass through lazy values)"
132 );
133 }
134 lastBuffers = undefined;
135 const serializedInfo = SerializerMiddleware.getLazySerializedValue(item);
136 if (serializedInfo) {
137 if (typeof serializedInfo === "function") {
138 throw new Error(
139 "Unexpected lazy value with non-this target (can't pass through lazy values)"
140 );
141 } else {
142 processedData.push(serializedInfo);
143 }
144 } else {
145 const content = item();
146 if (content) {
147 const options = SerializerMiddleware.getLazyOptions(item);
148 processedData.push(
149 serialize(
150 middleware,
151 /** @type {BufferSerializableType[]} */
152 (content),
153 (options && options.name) || true,
154 writeFile,
155 hashFunction
156 ).then((result) => {
157 /** @type {LazyOptions} */
158 (item.options).size = result.size;
159 resultToLazy.set(result, item);
160 return result;
161 })
162 );
163 } else {
164 throw new Error(
165 "Unexpected falsy value returned by lazy value function"
166 );
167 }
168 }
169 } else if (item) {
170 if (lastBuffers) {
171 lastBuffers.push(item);
172 } else {
173 lastBuffers = [item];
174 processedData.push(lastBuffers);
175 }
176 } else {
177 throw new Error("Unexpected falsy value in items array");
178 }
179 }
180 /** @type {BackgroundJob[]} */
181 const backgroundJobs = [];
182 const resolvedData = (await Promise.all(processedData)).map((item) => {
183 if (Array.isArray(item) || Buffer.isBuffer(item)) return item;
184
185 backgroundJobs.push(
186 /** @type {BackgroundJob} */
187 (item.backgroundJob)
188 );
189 // create pointer buffer from size and name
190 const name = /** @type {string} */ (item.name);
191 const nameBuffer = Buffer.from(name);
192 const buf = Buffer.allocUnsafe(8 + nameBuffer.length);
193 writeUInt64LE(buf, item.size, 0);
194 nameBuffer.copy(buf, 8, 0);
195 const lazy =
196 /** @type {LazyFunction} */
197 (resultToLazy.get(item));
198 SerializerMiddleware.setLazySerializedValue(lazy, buf);
199 return buf;
200 });
201 /** @type {number[]} */
202 const lengths = [];
203 for (const item of resolvedData) {
204 if (Array.isArray(item)) {
205 let l = 0;
206 for (const b of item) l += b.length;
207 while (l > 0x7fffffff) {
208 lengths.push(0x7fffffff);
209 l -= 0x7fffffff;
210 }
211 lengths.push(l);
212 } else if (item) {
213 lengths.push(-item.length);
214 } else {
215 throw new Error(`Unexpected falsy value in resolved data ${item}`);
216 }
217 }
218 const header = Buffer.allocUnsafe(8 + lengths.length * 4);
219 header.writeUInt32LE(VERSION, 0);
220 header.writeUInt32LE(lengths.length, 4);
221 for (let i = 0; i < lengths.length; i++) {
222 header.writeInt32LE(lengths[i], 8 + i * 4);
223 }
224 /** @type {Buffer[]} */
225 const buf = [header];
226 for (const item of resolvedData) {
227 if (Array.isArray(item)) {
228 for (const b of item) buf.push(b);
229 } else if (item) {
230 buf.push(item);
231 }
232 }
233 if (name === true) {
234 name = hashForName(buf, hashFunction);
235 }
236 let size = 0;
237 for (const b of buf) size += b.length;
238 backgroundJobs.push(writeFile(name, buf, size));
239 return {
240 size,
241 name,
242 backgroundJob:
243 backgroundJobs.length === 1
244 ? backgroundJobs[0]
245 : /** @type {BackgroundJob} */ (Promise.all(backgroundJobs))
246 };
247};
248
249/**
250 * Restores this instance from the provided deserializer context.
251 * @param {FileMiddleware} middleware this
252 * @param {string | false} name filename
253 * @param {(name: string | false) => Promise<Buffer[]>} readFile read content of a file
254 * @returns {Promise<BufferSerializableType[]>} deserialized data
255 */
256const deserialize = async (middleware, name, readFile) => {
257 const contents = await readFile(name);
258 if (contents.length === 0) throw new Error(`Empty file ${name}`);
259 let contentsIndex = 0;
260 let contentItem = contents[0];
261 let contentItemLength = contentItem.length;
262 let contentPosition = 0;
263 if (contentItemLength === 0) throw new Error(`Empty file ${name}`);
264 const nextContent = () => {
265 contentsIndex++;
266 contentItem = contents[contentsIndex];
267 contentItemLength = contentItem.length;
268 contentPosition = 0;
269 };
270 /**
271 * Processes the provided n.
272 * @param {number} n number of bytes to ensure
273 */
274 const ensureData = (n) => {
275 if (contentPosition === contentItemLength) {
276 nextContent();
277 }
278 while (contentItemLength - contentPosition < n) {
279 const remaining = contentItem.subarray(contentPosition);
280 let lengthFromNext = n - remaining.length;
281 /** @type {Buffer[]} */
282 const buffers = [remaining];
283 for (let i = contentsIndex + 1; i < contents.length; i++) {
284 const l = contents[i].length;
285 if (l > lengthFromNext) {
286 buffers.push(contents[i].subarray(0, lengthFromNext));
287 contents[i] = contents[i].subarray(lengthFromNext);
288 lengthFromNext = 0;
289 break;
290 } else {
291 buffers.push(contents[i]);
292 contentsIndex = i;
293 lengthFromNext -= l;
294 }
295 }
296 if (lengthFromNext > 0) throw new Error("Unexpected end of data");
297 contentItem = Buffer.concat(buffers, n);
298 contentItemLength = n;
299 contentPosition = 0;
300 }
301 };
302 /**
303 * Returns value value.
304 * @returns {number} value value
305 */
306 const readUInt32LE = () => {
307 ensureData(4);
308 const value = contentItem.readUInt32LE(contentPosition);
309 contentPosition += 4;
310 return value;
311 };
312 /**
313 * Returns value value.
314 * @returns {number} value value
315 */
316 const readInt32LE = () => {
317 ensureData(4);
318 const value = contentItem.readInt32LE(contentPosition);
319 contentPosition += 4;
320 return value;
321 };
322 /**
323 * Returns buffer.
324 * @param {number} l length
325 * @returns {Buffer} buffer
326 */
327 const readSlice = (l) => {
328 ensureData(l);
329 if (contentPosition === 0 && contentItemLength === l) {
330 const result = contentItem;
331 if (contentsIndex + 1 < contents.length) {
332 nextContent();
333 } else {
334 contentPosition = l;
335 }
336 return result;
337 }
338 const result = contentItem.subarray(contentPosition, contentPosition + l);
339 contentPosition += l;
340 // we clone the buffer here to allow the original content to be garbage collected
341 return l * 2 < contentItem.buffer.byteLength ? Buffer.from(result) : result;
342 };
343 const version = readUInt32LE();
344 if (version !== VERSION) {
345 throw new Error("Invalid file version");
346 }
347 const sectionCount = readUInt32LE();
348 /** @type {number[]} */
349 const lengths = [];
350 let lastLengthPositive = false;
351 for (let i = 0; i < sectionCount; i++) {
352 const value = readInt32LE();
353 const valuePositive = value >= 0;
354 if (lastLengthPositive && valuePositive) {
355 lengths[lengths.length - 1] += value;
356 } else {
357 lengths.push(value);
358 lastLengthPositive = valuePositive;
359 }
360 }
361 /** @type {BufferSerializableType[]} */
362 const result = [];
363 for (let length of lengths) {
364 if (length < 0) {
365 const slice = readSlice(-length);
366 const size = Number(readUInt64LE(slice, 0));
367 const nameBuffer = slice.subarray(8);
368 const name = nameBuffer.toString();
369 const lazy =
370 /** @type {LazyFunction} */
371 (
372 SerializerMiddleware.createLazy(
373 memoize(() => deserialize(middleware, name, readFile)),
374 middleware,
375 { name, size },
376 slice
377 )
378 );
379 result.push(lazy);
380 } else {
381 if (contentPosition === contentItemLength) {
382 nextContent();
383 } else if (contentPosition !== 0) {
384 if (length <= contentItemLength - contentPosition) {
385 result.push(
386 Buffer.from(
387 contentItem.buffer,
388 contentItem.byteOffset + contentPosition,
389 length
390 )
391 );
392 contentPosition += length;
393 length = 0;
394 } else {
395 const l = contentItemLength - contentPosition;
396 result.push(
397 Buffer.from(
398 contentItem.buffer,
399 contentItem.byteOffset + contentPosition,
400 l
401 )
402 );
403 length -= l;
404 contentPosition = contentItemLength;
405 }
406 } else if (length >= contentItemLength) {
407 result.push(contentItem);
408 length -= contentItemLength;
409 contentPosition = contentItemLength;
410 } else {
411 result.push(
412 Buffer.from(contentItem.buffer, contentItem.byteOffset, length)
413 );
414 contentPosition += length;
415 length = 0;
416 }
417 while (length > 0) {
418 nextContent();
419 if (length >= contentItemLength) {
420 result.push(contentItem);
421 length -= contentItemLength;
422 contentPosition = contentItemLength;
423 } else {
424 result.push(
425 Buffer.from(contentItem.buffer, contentItem.byteOffset, length)
426 );
427 contentPosition += length;
428 length = 0;
429 }
430 }
431 }
432 }
433 return result;
434};
435
436/** @typedef {BufferSerializableType[]} DeserializedType */
437/** @typedef {true} SerializedType */
438/** @typedef {{ filename: string, extension?: string }} Context */
439
440/**
441 * Represents FileMiddleware.
442 * @extends {SerializerMiddleware<DeserializedType, SerializedType, Context>}
443 */
444class FileMiddleware extends SerializerMiddleware {
445 /**
446 * Creates an instance of FileMiddleware.
447 * @param {IntermediateFileSystem} fs filesystem
448 * @param {HashFunction} hashFunction hash function to use
449 */
450 constructor(fs, hashFunction = DEFAULTS.HASH_FUNCTION) {
451 super();
452 /** @type {IntermediateFileSystem} */
453 this.fs = fs;
454 /** @type {HashFunction} */
455 this._hashFunction = hashFunction;
456 }
457
458 /**
459 * Serializes this instance into the provided serializer context.
460 * @param {DeserializedType} data data
461 * @param {Context} context context object
462 * @returns {SerializedType | Promise<SerializedType> | null} serialized data
463 */
464 serialize(data, context) {
465 const { filename, extension = "" } = context;
466 return new Promise((resolve, reject) => {
467 mkdirp(this.fs, dirname(this.fs, filename), (err) => {
468 if (err) return reject(err);
469
470 // It's important that we don't touch existing files during serialization
471 // because serialize may read existing files (when deserializing)
472 /** @type {Set<string>} */
473 const allWrittenFiles = new Set();
474 /**
475 * Processes the provided name.
476 * @param {string | false} name name
477 * @param {Buffer[]} content content
478 * @param {number} size size
479 * @returns {Promise<void>}
480 */
481 const writeFile = async (name, content, size) => {
482 const file = name
483 ? join(this.fs, filename, `../${name}${extension}`)
484 : filename;
485 await new Promise(
486 /**
487 * Handles the callback logic for this hook.
488 * @param {(value?: undefined) => void} resolve resolve
489 * @param {(reason?: Error | null) => void} reject reject
490 */
491 (resolve, reject) => {
492 let stream = this.fs.createWriteStream(`${file}_`);
493 /** @type {undefined | import("zlib").Gzip | import("zlib").BrotliCompress} */
494 let compression;
495 if (file.endsWith(".gz")) {
496 compression = createGzip({
497 chunkSize: COMPRESSION_CHUNK_SIZE,
498 level: zConstants.Z_BEST_SPEED
499 });
500 } else if (file.endsWith(".br")) {
501 compression = createBrotliCompress({
502 chunkSize: COMPRESSION_CHUNK_SIZE,
503 params: {
504 [zConstants.BROTLI_PARAM_MODE]: zConstants.BROTLI_MODE_TEXT,
505 [zConstants.BROTLI_PARAM_QUALITY]: 2,
506 [zConstants.BROTLI_PARAM_DISABLE_LITERAL_CONTEXT_MODELING]: true,
507 [zConstants.BROTLI_PARAM_SIZE_HINT]: size
508 }
509 });
510 }
511 if (compression) {
512 pipeline(compression, stream, reject);
513 stream = compression;
514 stream.on("finish", () => resolve());
515 } else {
516 stream.on("error", (err) => reject(err));
517 stream.on("finish", () => resolve());
518 }
519 // split into chunks for WRITE_LIMIT_CHUNK size
520 /** @type {Buffer[]} */
521 const chunks = [];
522 for (const b of content) {
523 if (b.length < WRITE_LIMIT_CHUNK) {
524 chunks.push(b);
525 } else {
526 for (let i = 0; i < b.length; i += WRITE_LIMIT_CHUNK) {
527 chunks.push(b.subarray(i, i + WRITE_LIMIT_CHUNK));
528 }
529 }
530 }
531
532 const len = chunks.length;
533 let i = 0;
534 /**
535 * Processes the provided err.
536 * @param {(Error | null)=} err err
537 */
538 const batchWrite = (err) => {
539 // will be handled in "on" error handler
540 if (err) return;
541
542 if (i === len) {
543 stream.end();
544 return;
545 }
546
547 // queue up a batch of chunks up to the write limit
548 // end is exclusive
549 let end = i;
550 let sum = chunks[end++].length;
551 while (end < len) {
552 sum += chunks[end].length;
553 if (sum > WRITE_LIMIT_TOTAL) break;
554 end++;
555 }
556 while (i < end - 1) {
557 stream.write(chunks[i++]);
558 }
559 stream.write(chunks[i++], batchWrite);
560 };
561 batchWrite();
562 }
563 );
564 if (name) allWrittenFiles.add(file);
565 };
566
567 resolve(
568 serialize(this, data, false, writeFile, this._hashFunction).then(
569 async ({ backgroundJob }) => {
570 await backgroundJob;
571
572 // Rename the index file to disallow access during inconsistent file state
573 await new Promise(
574 /**
575 * Handles the callback logic for this hook.
576 * @param {(value?: undefined) => void} resolve resolve
577 */
578 (resolve) => {
579 this.fs.rename(filename, `${filename}.old`, (_err) => {
580 resolve();
581 });
582 }
583 );
584
585 // update all written files
586 await Promise.all(
587 Array.from(
588 allWrittenFiles,
589 (file) =>
590 new Promise(
591 /**
592 * Handles the callback logic for this hook.
593 * @param {(value?: undefined) => void} resolve resolve
594 * @param {(reason?: Error | null) => void} reject reject
595 * @returns {void}
596 */
597 (resolve, reject) => {
598 this.fs.rename(`${file}_`, file, (err) => {
599 if (err) return reject(err);
600 resolve();
601 });
602 }
603 )
604 )
605 );
606
607 // As final step automatically update the index file to have a consistent pack again
608 await new Promise(
609 /**
610 * Handles the callback logic for this hook.
611 * @param {(value?: undefined) => void} resolve resolve
612 * @returns {void}
613 */
614 (resolve) => {
615 this.fs.rename(`${filename}_`, filename, (err) => {
616 if (err) return reject(err);
617 resolve();
618 });
619 }
620 );
621 return /** @type {true} */ (true);
622 }
623 )
624 );
625 });
626 });
627 }
628
629 /**
630 * Restores this instance from the provided deserializer context.
631 * @param {SerializedType} data data
632 * @param {Context} context context object
633 * @returns {DeserializedType | Promise<DeserializedType>} deserialized data
634 */
635 deserialize(data, context) {
636 const { filename, extension = "" } = context;
637 /**
638 * Returns result.
639 * @param {string | boolean} name name
640 * @returns {Promise<Buffer[]>} result
641 */
642 const readFile = (name) =>
643 new Promise((resolve, reject) => {
644 const file = name
645 ? join(this.fs, filename, `../${name}${extension}`)
646 : filename;
647 this.fs.stat(file, (err, stats) => {
648 if (err) {
649 reject(err);
650 return;
651 }
652 let remaining = /** @type {IStats} */ (stats).size;
653 /** @type {Buffer | undefined} */
654 let currentBuffer;
655 /** @type {number | undefined} */
656 let currentBufferUsed;
657 /** @type {Buffer[]} */
658 const buf = [];
659 /** @type {import("zlib").Zlib & import("stream").Transform | undefined} */
660 let decompression;
661 if (file.endsWith(".gz")) {
662 decompression = createGunzip({
663 chunkSize: DECOMPRESSION_CHUNK_SIZE
664 });
665 } else if (file.endsWith(".br")) {
666 decompression = createBrotliDecompress({
667 chunkSize: DECOMPRESSION_CHUNK_SIZE
668 });
669 }
670 if (decompression) {
671 /** @typedef {(value: Buffer[] | PromiseLike<Buffer[]>) => void} NewResolve */
672 /** @typedef {(reason?: Error) => void} NewReject */
673
674 /** @type {NewResolve | undefined} */
675 let newResolve;
676 /** @type {NewReject | undefined} */
677 let newReject;
678 resolve(
679 Promise.all([
680 new Promise((rs, rj) => {
681 newResolve = rs;
682 newReject = rj;
683 }),
684 new Promise(
685 /**
686 * Handles the chunk size callback for this hook.
687 * @param {(value?: undefined) => void} resolve resolve
688 * @param {(reason?: Error) => void} reject reject
689 */
690 (resolve, reject) => {
691 decompression.on("data", (chunk) => buf.push(chunk));
692 decompression.on("end", () => resolve());
693 decompression.on("error", (err) => reject(err));
694 }
695 )
696 ]).then(() => buf)
697 );
698 resolve = /** @type {NewResolve} */ (newResolve);
699 reject = /** @type {NewReject} */ (newReject);
700 }
701 this.fs.open(file, "r", (err, _fd) => {
702 if (err) {
703 reject(err);
704 return;
705 }
706 const fd = /** @type {number} */ (_fd);
707 const read = () => {
708 if (currentBuffer === undefined) {
709 currentBuffer = Buffer.allocUnsafeSlow(
710 Math.min(
711 constants.MAX_LENGTH,
712 remaining,
713 decompression ? DECOMPRESSION_CHUNK_SIZE : Infinity
714 )
715 );
716 currentBufferUsed = 0;
717 }
718 let readBuffer = currentBuffer;
719 let readOffset = /** @type {number} */ (currentBufferUsed);
720 let readLength =
721 currentBuffer.length -
722 /** @type {number} */ (currentBufferUsed);
723 // values passed to fs.read must be valid int32 values
724 if (readOffset > 0x7fffffff) {
725 readBuffer = currentBuffer.subarray(readOffset);
726 readOffset = 0;
727 }
728 if (readLength > 0x7fffffff) {
729 readLength = 0x7fffffff;
730 }
731 this.fs.read(
732 fd,
733 readBuffer,
734 readOffset,
735 readLength,
736 null,
737 (err, bytesRead) => {
738 if (err) {
739 this.fs.close(fd, () => {
740 reject(err);
741 });
742 return;
743 }
744 /** @type {number} */
745 (currentBufferUsed) += bytesRead;
746 remaining -= bytesRead;
747 if (
748 currentBufferUsed ===
749 /** @type {Buffer} */
750 (currentBuffer).length
751 ) {
752 if (decompression) {
753 decompression.write(currentBuffer);
754 } else {
755 buf.push(
756 /** @type {Buffer} */
757 (currentBuffer)
758 );
759 }
760 currentBuffer = undefined;
761 if (remaining === 0) {
762 if (decompression) {
763 decompression.end();
764 }
765 this.fs.close(fd, (err) => {
766 if (err) {
767 reject(err);
768 return;
769 }
770 resolve(buf);
771 });
772 return;
773 }
774 }
775 read();
776 }
777 );
778 };
779 read();
780 });
781 });
782 });
783 return deserialize(this, false, readFile);
784 }
785}
786
787module.exports = FileMiddleware;
Note: See TracBrowser for help on using the repository browser.