@eggjs/multipart 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +444 -0
  3. package/dist/commonjs/app/extend/context.d.ts +110 -0
  4. package/dist/commonjs/app/extend/context.js +279 -0
  5. package/dist/commonjs/app/middleware/multipart.d.ts +4 -0
  6. package/dist/commonjs/app/middleware/multipart.js +20 -0
  7. package/dist/commonjs/app/schedule/clean_tmpdir.d.ts +3 -0
  8. package/dist/commonjs/app/schedule/clean_tmpdir.js +58 -0
  9. package/dist/commonjs/app.d.ts +6 -0
  10. package/dist/commonjs/app.js +21 -0
  11. package/dist/commonjs/config/config.default.d.ts +98 -0
  12. package/dist/commonjs/config/config.default.js +31 -0
  13. package/dist/commonjs/index.d.ts +2 -0
  14. package/dist/commonjs/index.js +5 -0
  15. package/dist/commonjs/lib/LimitError.d.ts +5 -0
  16. package/dist/commonjs/lib/LimitError.js +16 -0
  17. package/dist/commonjs/lib/MultipartFileTooLargeError.d.ts +6 -0
  18. package/dist/commonjs/lib/MultipartFileTooLargeError.js +18 -0
  19. package/dist/commonjs/lib/utils.d.ts +4 -0
  20. package/dist/commonjs/lib/utils.js +93 -0
  21. package/dist/commonjs/package.json +3 -0
  22. package/dist/esm/app/extend/context.d.ts +110 -0
  23. package/dist/esm/app/extend/context.js +273 -0
  24. package/dist/esm/app/middleware/multipart.d.ts +4 -0
  25. package/dist/esm/app/middleware/multipart.js +18 -0
  26. package/dist/esm/app/schedule/clean_tmpdir.d.ts +3 -0
  27. package/dist/esm/app/schedule/clean_tmpdir.js +53 -0
  28. package/dist/esm/app.d.ts +6 -0
  29. package/dist/esm/app.js +18 -0
  30. package/dist/esm/config/config.default.d.ts +98 -0
  31. package/dist/esm/config/config.default.js +26 -0
  32. package/dist/esm/index.d.ts +2 -0
  33. package/dist/esm/index.js +3 -0
  34. package/dist/esm/lib/LimitError.d.ts +5 -0
  35. package/dist/esm/lib/LimitError.js +12 -0
  36. package/dist/esm/lib/MultipartFileTooLargeError.d.ts +6 -0
  37. package/dist/esm/lib/MultipartFileTooLargeError.js +14 -0
  38. package/dist/esm/lib/utils.d.ts +4 -0
  39. package/dist/esm/lib/utils.js +85 -0
  40. package/dist/esm/package.json +3 -0
  41. package/dist/package.json +4 -0
  42. package/package.json +103 -0
  43. package/src/app/extend/context.ts +368 -0
  44. package/src/app/middleware/multipart.ts +20 -0
  45. package/src/app/schedule/clean_tmpdir.ts +55 -0
  46. package/src/app.ts +20 -0
  47. package/src/config/config.default.ts +130 -0
  48. package/src/index.ts +2 -0
  49. package/src/lib/LimitError.ts +12 -0
  50. package/src/lib/MultipartFileTooLargeError.ts +14 -0
  51. package/src/lib/utils.ts +92 -0
  52. package/src/typings/index.d.ts +4 -0
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.whitelist = void 0;
7
+ exports.humanizeBytes = humanizeBytes;
8
+ exports.normalizeOptions = normalizeOptions;
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const node_assert_1 = __importDefault(require("node:assert"));
11
+ const bytes_1 = __importDefault(require("bytes"));
12
+ exports.whitelist = [
13
+ // images
14
+ '.jpg', '.jpeg', // image/jpeg
15
+ '.png', // image/png, image/x-png
16
+ '.gif', // image/gif
17
+ '.bmp', // image/bmp
18
+ '.wbmp', // image/vnd.wap.wbmp
19
+ '.webp',
20
+ '.tif',
21
+ '.psd',
22
+ // text
23
+ '.svg',
24
+ '.js', '.jsx',
25
+ '.json',
26
+ '.css', '.less',
27
+ '.html', '.htm',
28
+ '.xml',
29
+ // tar
30
+ '.zip',
31
+ '.gz', '.tgz', '.gzip',
32
+ // video
33
+ '.mp3',
34
+ '.mp4',
35
+ '.avi',
36
+ ];
37
+ function humanizeBytes(size) {
38
+ if (typeof size === 'number') {
39
+ return size;
40
+ }
41
+ return (0, bytes_1.default)(size);
42
+ }
43
+ function normalizeOptions(options) {
44
+ // make sure to cast the value of config **Size to number
45
+ options.fileSize = humanizeBytes(options.fileSize);
46
+ options.fieldSize = humanizeBytes(options.fieldSize);
47
+ options.fieldNameSize = humanizeBytes(options.fieldNameSize);
48
+ // validate mode
49
+ options.mode = options.mode || 'stream';
50
+ (0, node_assert_1.default)(['stream', 'file'].includes(options.mode), `Expect mode to be 'stream' or 'file', but got '${options.mode}'`);
51
+ if (options.mode === 'file') {
52
+ (0, node_assert_1.default)(!options.fileModeMatch, '`fileModeMatch` options only work on stream mode, please remove it');
53
+ }
54
+ // normalize whitelist
55
+ if (Array.isArray(options.whitelist)) {
56
+ options.whitelist = options.whitelist.map(extname => extname.toLowerCase());
57
+ }
58
+ // normalize fileExtensions
59
+ if (Array.isArray(options.fileExtensions)) {
60
+ options.fileExtensions = options.fileExtensions.map(extname => {
61
+ return (extname.startsWith('.') || extname === '') ? extname.toLowerCase() : `.${extname.toLowerCase()}`;
62
+ });
63
+ }
64
+ function checkExt(fileName) {
65
+ if (typeof options.whitelist === 'function') {
66
+ return options.whitelist(fileName);
67
+ }
68
+ const extname = node_path_1.default.extname(fileName).toLowerCase();
69
+ if (Array.isArray(options.whitelist)) {
70
+ return options.whitelist.includes(extname);
71
+ }
72
+ // only if user don't provide whitelist, we will use default whitelist + fileExtensions
73
+ return exports.whitelist.includes(extname) || options.fileExtensions.includes(extname);
74
+ }
75
+ options.checkFile = (_fieldName, fileStream, fileName) => {
76
+ // just ignore, if no file
77
+ if (!fileStream || !fileName)
78
+ return;
79
+ try {
80
+ if (!checkExt(fileName)) {
81
+ const err = new Error('Invalid filename: ' + fileName);
82
+ Reflect.set(err, 'status', 400);
83
+ return err;
84
+ }
85
+ }
86
+ catch (err) {
87
+ err.status = 400;
88
+ return err;
89
+ }
90
+ };
91
+ return options;
92
+ }
93
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidXRpbHMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvbGliL3V0aWxzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7Ozs7OztBQStCQSxzQ0FLQztBQUVELDRDQXFEQztBQTNGRCwwREFBNkI7QUFDN0IsOERBQWlDO0FBQ2pDLGtEQUEwQjtBQUdiLFFBQUEsU0FBUyxHQUFHO0lBQ3ZCLFNBQVM7SUFDVCxNQUFNLEVBQUUsT0FBTyxFQUFFLGFBQWE7SUFDOUIsTUFBTSxFQUFFLHlCQUF5QjtJQUNqQyxNQUFNLEVBQUUsWUFBWTtJQUNwQixNQUFNLEVBQUUsWUFBWTtJQUNwQixPQUFPLEVBQUUscUJBQXFCO0lBQzlCLE9BQU87SUFDUCxNQUFNO0lBQ04sTUFBTTtJQUNOLE9BQU87SUFDUCxNQUFNO0lBQ04sS0FBSyxFQUFFLE1BQU07SUFDYixPQUFPO0lBQ1AsTUFBTSxFQUFFLE9BQU87SUFDZixPQUFPLEVBQUUsTUFBTTtJQUNmLE1BQU07SUFDTixNQUFNO0lBQ04sTUFBTTtJQUNOLEtBQUssRUFBRSxNQUFNLEVBQUUsT0FBTztJQUN0QixRQUFRO0lBQ1IsTUFBTTtJQUNOLE1BQU07SUFDTixNQUFNO0NBQ1AsQ0FBQztBQUVGLFNBQWdCLGFBQWEsQ0FBQyxJQUFxQjtJQUNqRCxJQUFJLE9BQU8sSUFBSSxLQUFLLFFBQVEsRUFBRSxDQUFDO1FBQzdCLE9BQU8sSUFBSSxDQUFDO0lBQ2QsQ0FBQztJQUNELE9BQU8sSUFBQSxlQUFLLEVBQUMsSUFBSSxDQUFXLENBQUM7QUFDL0IsQ0FBQztBQUVELFNBQWdCLGdCQUFnQixDQUFDLE9BQXdCO0lBQ3ZELHlEQUF5RDtJQUN6RCxPQUFPLENBQUMsUUFBUSxHQUFHLGFBQWEsQ0FBQyxPQUFPLENBQUMsUUFBUSxDQUFDLENBQUM7SUFDbkQsT0FBTyxDQUFDLFNBQVMsR0FBRyxhQUFhLENBQUMsT0FBTyxDQUFDLFNBQVMsQ0FBQyxDQUFDO0lBQ3JELE9BQU8sQ0FBQyxhQUFhLEdBQUcsYUFBYSxDQUFDLE9BQU8sQ0FBQyxhQUFhLENBQUMsQ0FBQztJQUU3RCxnQkFBZ0I7SUFDaEIsT0FBTyxDQUFDLElBQUksR0FBRyxPQUFPLENBQUMsSUFBSSxJQUFJLFFBQVEsQ0FBQztJQUN4QyxJQUFBLHFCQUFNLEVBQUMsQ0FBRSxRQUFRLEVBQUUsTUFBTSxDQUFFLENBQUMsUUFBUSxDQUFDLE9BQU8sQ0FBQyxJQUFJLENBQUMsRUFBRSxrREFBa0QsT0FBTyxDQUFDLElBQUksR0FBRyxDQUFDLENBQUM7SUFDdkgsSUFBSSxPQUFPLENBQUMsSUFBSSxLQUFLLE1BQU0sRUFBRSxDQUFDO1FBQzVCLElBQUEscUJBQU0sRUFBQyxDQUFDLE9BQU8sQ0FBQyxhQUFhLEVBQUUsb0VBQW9FLENBQUMsQ0FBQztJQUN2RyxDQUFDO0lBRUQsc0JBQXNCO0lBQ3RCLElBQUksS0FBSyxDQUFDLE9BQU8sQ0FBQyxPQUFPLENBQUMsU0FBUyxDQUFDLEVBQUUsQ0FBQztRQUNyQyxPQUFPLENBQUMsU0FBUyxHQUFHLE9BQU8sQ0FBQyxTQUFTLENBQUMsR0FBRyxDQUFDLE9BQU8sQ0FBQyxFQUFFLENBQUMsT0FBTyxDQUFDLFdBQVcsRUFBRSxDQUFDLENBQUM7SUFDOUUsQ0FBQztJQUVELDJCQUEyQjtJQUMzQixJQUFJLEtBQUssQ0FBQyxPQUFPLENBQUMsT0FBTyxDQUFDLGNBQWMsQ0FBQyxFQUFFLENBQUM7UUFDMUMsT0FBTyxDQUFDLGNBQWMsR0FBRyxPQUFPLENBQUMsY0FBYyxDQUFDLEdBQUcsQ0FBQyxPQUFPLENBQUMsRUFBRTtZQUM1RCxPQUFPLENBQUMsT0FBTyxDQUFDLFVBQVUsQ0FBQyxHQUFHLENBQUMsSUFBSSxPQUFPLEtBQUssRUFBRSxDQUFDLENBQUMsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxXQUFXLEVBQUUsQ0FBQyxDQUFDLENBQUMsSUFBSSxPQUFPLENBQUMsV0FBVyxFQUFFLEVBQUUsQ0FBQztRQUMzRyxDQUFDLENBQUMsQ0FBQztJQUNMLENBQUM7SUFFRCxTQUFTLFFBQVEsQ0FBQyxRQUFnQjtRQUNoQyxJQUFJLE9BQU8sT0FBTyxDQUFDLFNBQVMsS0FBSyxVQUFVLEVBQUUsQ0FBQztZQUM1QyxPQUFPLE9BQU8sQ0FBQyxTQUFTLENBQUMsUUFBUSxDQUFDLENBQUM7UUFDckMsQ0FBQztRQUNELE1BQU0sT0FBTyxHQUFHLG1CQUFJLENBQUMsT0FBTyxDQUFDLFFBQVEsQ0FBQyxDQUFDLFdBQVcsRUFBRSxDQUFDO1FBQ3JELElBQUksS0FBSyxDQUFDLE9BQU8sQ0FBQyxPQUFPLENBQUMsU0FBUyxDQUFDLEVBQUUsQ0FBQztZQUNyQyxPQUFPLE9BQU8sQ0FBQyxTQUFTLENBQUMsUUFBUSxDQUFDLE9BQU8sQ0FBQyxDQUFDO1FBQzdDLENBQUM7UUFDRCx1RkFBdUY7UUFDdkYsT0FBTyxpQkFBUyxDQUFDLFFBQVEsQ0FBQyxPQUFPLENBQUMsSUFBSSxPQUFPLENBQUMsY0FBYyxDQUFDLFFBQVEsQ0FBQyxPQUFPLENBQUMsQ0FBQztJQUNqRixDQUFDO0lBRUQsT0FBTyxDQUFDLFNBQVMsR0FBRyxDQUFDLFVBQWtCLEVBQUUsVUFBZSxFQUFFLFFBQWdCLEVBQWdCLEVBQUU7UUFDMUYsMEJBQTBCO1FBQzFCLElBQUksQ0FBQyxVQUFVLElBQUksQ0FBQyxRQUFRO1lBQUUsT0FBTztRQUNyQyxJQUFJLENBQUM7WUFDSCxJQUFJLENBQUMsUUFBUSxDQUFDLFFBQVEsQ0FBQyxFQUFFLENBQUM7Z0JBQ3hCLE1BQU0sR0FBRyxHQUFHLElBQUksS0FBSyxDQUFDLG9CQUFvQixHQUFHLFFBQVEsQ0FBQyxDQUFDO2dCQUN2RCxPQUFPLENBQUMsR0FBRyxDQUFDLEdBQUcsRUFBRSxRQUFRLEVBQUUsR0FBRyxDQUFDLENBQUM7Z0JBQ2hDLE9BQU8sR0FBRyxDQUFDO1lBQ2IsQ0FBQztRQUNILENBQUM7UUFBQyxPQUFPLEdBQVEsRUFBRSxDQUFDO1lBQ2xCLEdBQUcsQ0FBQyxNQUFNLEdBQUcsR0FBRyxDQUFDO1lBQ2pCLE9BQU8sR0FBRyxDQUFDO1FBQ2IsQ0FBQztJQUNILENBQUMsQ0FBQztJQUVGLE9BQU8sT0FBTyxDQUFDO0FBQ2pCLENBQUMifQ==
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "commonjs"
3
+ }
@@ -0,0 +1,110 @@
1
+ import { Readable } from 'node:stream';
2
+ import { Context } from '@eggjs/core';
3
+ export interface EggFile {
4
+ field: string;
5
+ filename: string;
6
+ encoding: string;
7
+ mime: string;
8
+ filepath: string;
9
+ }
10
+ export interface MultipartFileStream extends Readable {
11
+ fields: Record<string, any>;
12
+ filename: string;
13
+ fieldname: string;
14
+ mime: string;
15
+ mimeType: string;
16
+ transferEncoding: string;
17
+ encoding: string;
18
+ truncated: boolean;
19
+ }
20
+ export interface MultipartOptions {
21
+ autoFields?: boolean;
22
+ /**
23
+ * required file submit, default is true
24
+ */
25
+ requireFile?: boolean;
26
+ /**
27
+ * default charset encoding
28
+ */
29
+ defaultCharset?: string;
30
+ /**
31
+ * compatible with defaultCharset
32
+ * @deprecated use `defaultCharset` instead
33
+ */
34
+ defCharset?: string;
35
+ defaultParamCharset?: string;
36
+ /**
37
+ * compatible with defaultParamCharset
38
+ * @deprecated use `defaultParamCharset` instead
39
+ */
40
+ defParamCharset?: string;
41
+ limits?: {
42
+ fieldNameSize?: number;
43
+ fieldSize?: number;
44
+ fields?: number;
45
+ fileSize?: number;
46
+ files?: number;
47
+ parts?: number;
48
+ headerPairs?: number;
49
+ };
50
+ checkFile?(fieldname: string, file: any, filename: string, encoding: string, mimetype: string): void | Error;
51
+ }
52
+ export default class MultipartContext extends Context {
53
+ /**
54
+ * create multipart.parts instance, to get separated files.
55
+ * @function Context#multipart
56
+ * @param {Object} [options] - override default multipart configurations
57
+ * - {Boolean} options.autoFields
58
+ * - {String} options.defaultCharset
59
+ * - {String} options.defaultParamCharset
60
+ * - {Object} options.limits
61
+ * - {Function} options.checkFile
62
+ * @return {Yieldable | AsyncIterable<Yieldable>} parts
63
+ */
64
+ multipart(options?: MultipartOptions): AsyncIterable<MultipartFileStream>;
65
+ /**
66
+ * save request multipart data and files to `ctx.request`
67
+ * @function Context#saveRequestFiles
68
+ * @param {Object} options - { limits, checkFile, ... }
69
+ */
70
+ saveRequestFiles(options?: MultipartOptions): Promise<void>;
71
+ /**
72
+ * get upload file stream
73
+ * @example
74
+ * ```js
75
+ * const stream = await ctx.getFileStream();
76
+ * // get other fields
77
+ * console.log(stream.fields);
78
+ * ```
79
+ * @function Context#getFileStream
80
+ * @param {Object} options
81
+ * - {Boolean} options.requireFile - required file submit, default is true
82
+ * - {String} options.defaultCharset
83
+ * - {String} options.defaultParamCharset
84
+ * - {Object} options.limits
85
+ * - {Function} options.checkFile
86
+ * @return {ReadStream} stream
87
+ * @since 1.0.0
88
+ * @deprecated Not safe enough, use `ctx.multipart()` instead
89
+ */
90
+ getFileStream(options?: MultipartOptions): Promise<MultipartFileStream>;
91
+ /**
92
+ * clean up request tmp files helper
93
+ * @function Context#cleanupRequestFiles
94
+ * @param {Array<String>} [files] - file paths need to cleanup, default is `ctx.request.files`.
95
+ */
96
+ cleanupRequestFiles(files?: EggFile[]): Promise<void>;
97
+ }
98
+ declare module '@eggjs/core' {
99
+ interface Request {
100
+ /**
101
+ * Files Object Array
102
+ */
103
+ files?: EggFile[];
104
+ }
105
+ interface Context {
106
+ saveRequestFiles(options?: MultipartOptions): Promise<void>;
107
+ getFileStream(options?: MultipartOptions): Promise<MultipartFileStream>;
108
+ cleanupRequestFiles(files?: EggFile[]): Promise<void>;
109
+ }
110
+ }
@@ -0,0 +1,273 @@
1
+ import assert from 'node:assert';
2
+ import path from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ import fs from 'node:fs/promises';
5
+ import { createWriteStream } from 'node:fs';
6
+ import { Readable, PassThrough } from 'node:stream';
7
+ import { pipeline } from 'node:stream/promises';
8
+ // @ts-expect-error no types
9
+ import parse from 'co-busboy';
10
+ import dayjs from 'dayjs';
11
+ import { Context } from '@eggjs/core';
12
+ import { humanizeBytes } from '../../lib/utils.js';
13
+ import { LimitError } from '../../lib/LimitError.js';
14
+ import { MultipartFileTooLargeError } from '../../lib/MultipartFileTooLargeError.js';
15
+ const HAS_CONSUMED = Symbol('Context#multipartHasConsumed');
16
+ export default class MultipartContext extends Context {
17
+ /**
18
+ * create multipart.parts instance, to get separated files.
19
+ * @function Context#multipart
20
+ * @param {Object} [options] - override default multipart configurations
21
+ * - {Boolean} options.autoFields
22
+ * - {String} options.defaultCharset
23
+ * - {String} options.defaultParamCharset
24
+ * - {Object} options.limits
25
+ * - {Function} options.checkFile
26
+ * @return {Yieldable | AsyncIterable<Yieldable>} parts
27
+ */
28
+ multipart(options = {}) {
29
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
30
+ const ctx = this;
31
+ // multipart/form-data
32
+ if (!ctx.is('multipart'))
33
+ ctx.throw(400, 'Content-Type must be multipart/*');
34
+ assert(!ctx[HAS_CONSUMED], 'the multipart request can\'t be consumed twice');
35
+ ctx[HAS_CONSUMED] = true;
36
+ const { autoFields, defaultCharset, defaultParamCharset, checkFile } = ctx.app.config.multipart;
37
+ const { fieldNameSize, fieldSize, fields, fileSize, files } = ctx.app.config.multipart;
38
+ options = extractOptions(options);
39
+ const parseOptions = Object.assign({
40
+ autoFields,
41
+ defCharset: defaultCharset,
42
+ defParamCharset: defaultParamCharset,
43
+ checkFile,
44
+ }, options);
45
+ // https://github.com/mscdex/busboy#busboy-methods
46
+ // merge limits
47
+ parseOptions.limits = Object.assign({
48
+ fieldNameSize,
49
+ fieldSize,
50
+ fields,
51
+ fileSize,
52
+ files,
53
+ }, options.limits);
54
+ // mount asyncIterator, so we can use `for await` to get parts
55
+ const parts = parse(this, parseOptions);
56
+ parts[Symbol.asyncIterator] = async function* () {
57
+ let part;
58
+ do {
59
+ part = await parts();
60
+ if (!part)
61
+ continue;
62
+ if (Array.isArray(part)) {
63
+ if (part[3])
64
+ throw new LimitError('Request_fieldSize_limit', 'Reach fieldSize limit');
65
+ // TODO: still not support at busboy 1.x (only support at urlencoded)
66
+ // https://github.com/mscdex/busboy/blob/v0.3.1/lib/types/multipart.js#L5
67
+ // https://github.com/mscdex/busboy/blob/master/lib/types/multipart.js#L251
68
+ // if (part[2]) throw new LimitError('Request_fieldNameSize_limit', 'Reach fieldNameSize limit');
69
+ }
70
+ else {
71
+ // user click `upload` before choose a file, `part` will be file stream, but `part.filename` is empty must handler this, such as log error.
72
+ if (!part.filename) {
73
+ ctx.coreLogger.debug('[egg-multipart] file field `%s` is upload without file stream, will drop it.', part.fieldname);
74
+ await pipeline(part, new PassThrough());
75
+ continue;
76
+ }
77
+ // TODO: check whether filename is malicious input
78
+ // busboy only set truncated when consume the stream
79
+ if (part.truncated) {
80
+ // in case of emit 'limit' too fast
81
+ throw new LimitError('Request_fileSize_limit', 'Reach fileSize limit');
82
+ }
83
+ else {
84
+ part.once('limit', function () {
85
+ this.emit('error', new LimitError('Request_fileSize_limit', 'Reach fileSize limit'));
86
+ this.resume();
87
+ });
88
+ }
89
+ }
90
+ // dispatch part to outter logic such as for-await-of
91
+ yield part;
92
+ } while (part !== undefined);
93
+ };
94
+ return parts;
95
+ }
96
+ /**
97
+ * save request multipart data and files to `ctx.request`
98
+ * @function Context#saveRequestFiles
99
+ * @param {Object} options - { limits, checkFile, ... }
100
+ */
101
+ async saveRequestFiles(options = {}) {
102
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
103
+ const ctx = this;
104
+ const allowArrayField = ctx.app.config.multipart.allowArrayField;
105
+ let storeDir;
106
+ const requestBody = {};
107
+ const requestFiles = [];
108
+ options.autoFields = false;
109
+ const parts = ctx.multipart(options);
110
+ try {
111
+ for await (const part of parts) {
112
+ if (Array.isArray(part)) {
113
+ // fields
114
+ const [fieldName, fieldValue] = part;
115
+ if (!allowArrayField) {
116
+ requestBody[fieldName] = fieldValue;
117
+ }
118
+ else {
119
+ if (!requestBody[fieldName]) {
120
+ requestBody[fieldName] = fieldValue;
121
+ }
122
+ else if (!Array.isArray(requestBody[fieldName])) {
123
+ requestBody[fieldName] = [requestBody[fieldName], fieldValue];
124
+ }
125
+ else {
126
+ requestBody[fieldName].push(fieldValue);
127
+ }
128
+ }
129
+ }
130
+ else {
131
+ // stream
132
+ const { filename, fieldname, encoding, mime } = part;
133
+ if (!storeDir) {
134
+ // ${tmpdir}/YYYY/MM/DD/HH
135
+ storeDir = path.join(ctx.app.config.multipart.tmpdir, dayjs().format('YYYY/MM/DD/HH'));
136
+ await fs.mkdir(storeDir, { recursive: true });
137
+ }
138
+ // write to tmp file
139
+ const filepath = path.join(storeDir, randomUUID() + path.extname(filename));
140
+ const target = createWriteStream(filepath);
141
+ await pipeline(part, target);
142
+ const meta = {
143
+ filepath,
144
+ field: fieldname,
145
+ filename,
146
+ encoding,
147
+ mime,
148
+ // keep same property name as file stream, https://github.com/cojs/busboy/blob/master/index.js#L114
149
+ fieldname,
150
+ transferEncoding: encoding,
151
+ mimeType: mime,
152
+ };
153
+ requestFiles.push(meta);
154
+ }
155
+ }
156
+ }
157
+ catch (err) {
158
+ await ctx.cleanupRequestFiles(requestFiles);
159
+ throw err;
160
+ }
161
+ ctx.request.body = requestBody;
162
+ ctx.request.files = requestFiles;
163
+ }
164
+ /**
165
+ * get upload file stream
166
+ * @example
167
+ * ```js
168
+ * const stream = await ctx.getFileStream();
169
+ * // get other fields
170
+ * console.log(stream.fields);
171
+ * ```
172
+ * @function Context#getFileStream
173
+ * @param {Object} options
174
+ * - {Boolean} options.requireFile - required file submit, default is true
175
+ * - {String} options.defaultCharset
176
+ * - {String} options.defaultParamCharset
177
+ * - {Object} options.limits
178
+ * - {Function} options.checkFile
179
+ * @return {ReadStream} stream
180
+ * @since 1.0.0
181
+ * @deprecated Not safe enough, use `ctx.multipart()` instead
182
+ */
183
+ async getFileStream(options = {}) {
184
+ options.autoFields = true;
185
+ const parts = this.multipart(options);
186
+ let stream = await parts();
187
+ if (options.requireFile !== false) {
188
+ // stream not exists, treat as an exception
189
+ if (!stream || !stream.filename) {
190
+ this.throw(400, 'Can\'t found upload file');
191
+ }
192
+ }
193
+ if (!stream) {
194
+ stream = Readable.from([]);
195
+ }
196
+ if (stream.truncated) {
197
+ throw new LimitError('Request_fileSize_limit', 'Request file too large, please check multipart config');
198
+ }
199
+ stream.fields = parts.field;
200
+ stream.once('limit', () => {
201
+ const err = new MultipartFileTooLargeError('Request file too large, please check multipart config', stream.fields, stream.filename);
202
+ if (stream.listenerCount('error') > 0) {
203
+ stream.emit('error', err);
204
+ this.coreLogger.warn(err);
205
+ }
206
+ else {
207
+ this.coreLogger.error(err);
208
+ // ignore next error event
209
+ stream.on('error', () => { });
210
+ }
211
+ // ignore all data
212
+ stream.resume();
213
+ });
214
+ return stream;
215
+ }
216
+ /**
217
+ * clean up request tmp files helper
218
+ * @function Context#cleanupRequestFiles
219
+ * @param {Array<String>} [files] - file paths need to cleanup, default is `ctx.request.files`.
220
+ */
221
+ async cleanupRequestFiles(files) {
222
+ if (!files || !files.length) {
223
+ files = this.request.files;
224
+ }
225
+ if (Array.isArray(files)) {
226
+ for (const file of files) {
227
+ try {
228
+ await fs.rm(file.filepath, { force: true, recursive: true });
229
+ }
230
+ catch (err) {
231
+ // warning log
232
+ this.coreLogger.warn('[egg-multipart-cleanupRequestFiles-error] file: %j, error: %s', file, err);
233
+ }
234
+ }
235
+ }
236
+ }
237
+ }
238
+ function extractOptions(options = {}) {
239
+ const opts = {};
240
+ if (typeof options.autoFields === 'boolean') {
241
+ opts.autoFields = options.autoFields;
242
+ }
243
+ if (options.limits) {
244
+ opts.limits = options.limits;
245
+ }
246
+ if (options.checkFile) {
247
+ opts.checkFile = options.checkFile;
248
+ }
249
+ if (options.defCharset) {
250
+ opts.defCharset = options.defCharset;
251
+ }
252
+ if (options.defParamCharset) {
253
+ opts.defParamCharset = options.defParamCharset;
254
+ }
255
+ // compatible with config names
256
+ if (options.defaultCharset) {
257
+ opts.defCharset = options.defaultCharset;
258
+ }
259
+ if (options.defaultParamCharset) {
260
+ opts.defParamCharset = options.defaultParamCharset;
261
+ }
262
+ // limits
263
+ if (options.limits) {
264
+ const limits = opts.limits = { ...options.limits };
265
+ for (const key in limits) {
266
+ if (key.endsWith('Size') && limits[key]) {
267
+ limits[key] = humanizeBytes(limits[key]);
268
+ }
269
+ }
270
+ }
271
+ return opts;
272
+ }
273
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29udGV4dC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uL3NyYy9hcHAvZXh0ZW5kL2NvbnRleHQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxNQUFNLE1BQU0sYUFBYSxDQUFDO0FBQ2pDLE9BQU8sSUFBSSxNQUFNLFdBQVcsQ0FBQztBQUM3QixPQUFPLEVBQUUsVUFBVSxFQUFFLE1BQU0sYUFBYSxDQUFDO0FBQ3pDLE9BQU8sRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBQ2xDLE9BQU8sRUFBRSxpQkFBaUIsRUFBRSxNQUFNLFNBQVMsQ0FBQztBQUM1QyxPQUFPLEVBQUUsUUFBUSxFQUFFLFdBQVcsRUFBRSxNQUFNLGFBQWEsQ0FBQztBQUNwRCxPQUFPLEVBQUUsUUFBUSxFQUFFLE1BQU0sc0JBQXNCLENBQUM7QUFDaEQsNEJBQTRCO0FBQzVCLE9BQU8sS0FBSyxNQUFNLFdBQVcsQ0FBQztBQUM5QixPQUFPLEtBQUssTUFBTSxPQUFPLENBQUM7QUFDMUIsT0FBTyxFQUFFLE9BQU8sRUFBRSxNQUFNLGFBQWEsQ0FBQztBQUN0QyxPQUFPLEVBQUUsYUFBYSxFQUFFLE1BQU0sb0JBQW9CLENBQUM7QUFDbkQsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLHlCQUF5QixDQUFDO0FBQ3JELE9BQU8sRUFBRSwwQkFBMEIsRUFBRSxNQUFNLHlDQUF5QyxDQUFDO0FBRXJGLE1BQU0sWUFBWSxHQUFHLE1BQU0sQ0FBQyw4QkFBOEIsQ0FBQyxDQUFDO0FBNEQ1RCxNQUFNLENBQUMsT0FBTyxPQUFPLGdCQUFpQixTQUFRLE9BQU87SUFDbkQ7Ozs7Ozs7Ozs7T0FVRztJQUNILFNBQVMsQ0FBQyxVQUE0QixFQUFFO1FBQ3RDLDREQUE0RDtRQUM1RCxNQUFNLEdBQUcsR0FBRyxJQUFJLENBQUM7UUFDakIsc0JBQXNCO1FBQ3RCLElBQUksQ0FBQyxHQUFHLENBQUMsRUFBRSxDQUFDLFdBQVcsQ0FBQztZQUFFLEdBQUcsQ0FBQyxL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
@@ -0,0 +1,4 @@
1
+ import type { Context, Next, EggCore } from '@eggjs/core';
2
+ import type { MultipartConfig } from '../../config/config.default.js';
3
+ declare const _default: (options: MultipartConfig, _app: EggCore) => (ctx: Context, next: Next) => Promise<void>;
4
+ export default _default;
@@ -0,0 +1,18 @@
1
+ import { pathMatching } from 'egg-path-matching';
2
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
3
+ export default (options, _app) => {
4
+ // normalize options
5
+ const matchFn = options.fileModeMatch && pathMatching({
6
+ match: options.fileModeMatch,
7
+ // pathToRegexpModule: app.options.pathToRegexpModule,
8
+ });
9
+ return async function multipart(ctx, next) {
10
+ if (!ctx.is('multipart'))
11
+ return next();
12
+ if (matchFn && !matchFn(ctx))
13
+ return next();
14
+ await ctx.saveRequestFiles();
15
+ return next();
16
+ };
17
+ };
18
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibXVsdGlwYXJ0LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vc3JjL2FwcC9taWRkbGV3YXJlL211bHRpcGFydC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsWUFBWSxFQUFFLE1BQU0sbUJBQW1CLENBQUM7QUFJakQsNkRBQTZEO0FBQzdELGVBQWUsQ0FBQyxPQUF3QixFQUFFLElBQWEsRUFBRSxFQUFFO0lBQ3pELG9CQUFvQjtJQUNwQixNQUFNLE9BQU8sR0FBRyxPQUFPLENBQUMsYUFBYSxJQUFJLFlBQVksQ0FBQztRQUNwRCxLQUFLLEVBQUUsT0FBTyxDQUFDLGFBQWE7UUFDNUIsc0RBQXNEO0tBQ3ZELENBQUMsQ0FBQztJQUVILE9BQU8sS0FBSyxVQUFVLFNBQVMsQ0FBQyxHQUFZLEVBQUUsSUFBVTtRQUN0RCxJQUFJLENBQUMsR0FBRyxDQUFDLEVBQUUsQ0FBQyxXQUFXLENBQUM7WUFBRSxPQUFPLElBQUksRUFBRSxDQUFDO1FBQ3hDLElBQUksT0FBTyxJQUFJLENBQUMsT0FBTyxDQUFDLEdBQUcsQ0FBQztZQUFFLE9BQU8sSUFBSSxFQUFFLENBQUM7UUFFNUMsTUFBTSxHQUFHLENBQUMsZ0JBQWdCLEVBQUUsQ0FBQztRQUM3QixPQUFPLElBQUksRUFBRSxDQUFDO0lBQ2hCLENBQUMsQ0FBQztBQUNKLENBQUMsQ0FBQyJ9
@@ -0,0 +1,3 @@
1
+ import { EggCore } from '@eggjs/core';
2
+ declare const _default: (app: EggCore) => any;
3
+ export default _default;
@@ -0,0 +1,53 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs/promises';
3
+ import dayjs from 'dayjs';
4
+ export default (app) => {
5
+ return class CleanTmpdir extends app.Subscription {
6
+ static get schedule() {
7
+ return {
8
+ type: 'worker',
9
+ cron: app.config.multipart.cleanSchedule.cron,
10
+ disable: app.config.multipart.cleanSchedule.disable,
11
+ immediate: false,
12
+ };
13
+ }
14
+ async _remove(dir) {
15
+ const { ctx } = this;
16
+ if (await fs.access(dir).then(() => true, () => false)) {
17
+ ctx.coreLogger.info('[@eggjs/multipart:CleanTmpdir] removing tmpdir: %j', dir);
18
+ try {
19
+ await fs.rm(dir, { force: true, recursive: true });
20
+ ctx.coreLogger.info('[@eggjs/multipart:CleanTmpdir:success] tmpdir: %j has been removed', dir);
21
+ }
22
+ catch (err) {
23
+ /* c8 ignore next 3 */
24
+ ctx.coreLogger.error('[@eggjs/multipart:CleanTmpdir:error] remove tmpdir: %j error: %s', dir, err);
25
+ ctx.coreLogger.error(err);
26
+ }
27
+ }
28
+ }
29
+ async subscribe() {
30
+ const { ctx } = this;
31
+ const config = ctx.app.config;
32
+ ctx.coreLogger.info('[@eggjs/multipart:CleanTmpdir] start clean tmpdir: %j', config.multipart.tmpdir);
33
+ // last year
34
+ const lastYear = dayjs().subtract(1, 'years');
35
+ const lastYearDir = path.join(config.multipart.tmpdir, lastYear.format('YYYY'));
36
+ await this._remove(lastYearDir);
37
+ // 3 months
38
+ for (let i = 1; i <= 3; i++) {
39
+ const date = dayjs().subtract(i, 'months');
40
+ const dir = path.join(config.multipart.tmpdir, date.format('YYYY/MM'));
41
+ await this._remove(dir);
42
+ }
43
+ // 7 days
44
+ for (let i = 1; i <= 7; i++) {
45
+ const date = dayjs().subtract(i, 'days');
46
+ const dir = path.join(config.multipart.tmpdir, date.format('YYYY/MM/DD'));
47
+ await this._remove(dir);
48
+ }
49
+ ctx.coreLogger.info('[@eggjs/multipart:CleanTmpdir] end');
50
+ }
51
+ };
52
+ };
53
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xlYW5fdG1wZGlyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vc3JjL2FwcC9zY2hlZHVsZS9jbGVhbl90bXBkaXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxJQUFJLE1BQU0sV0FBVyxDQUFDO0FBQzdCLE9BQU8sRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBQ2xDLE9BQU8sS0FBSyxNQUFNLE9BQU8sQ0FBQztBQUcxQixlQUFlLENBQUMsR0FBWSxFQUFPLEVBQUU7SUFDbkMsT0FBTyxNQUFNLFdBQVksU0FBUSxHQUFHLENBQUMsWUFBWTtRQUMvQyxNQUFNLEtBQUssUUFBUTtZQUNqQixPQUFPO2dCQUNMLElBQUksRUFBRSxRQUFRO2dCQUNkLElBQUksRUFBRSxHQUFHLENBQUMsTUFBTSxDQUFDLFNBQVMsQ0FBQyxhQUFhLENBQUMsSUFBSTtnQkFDN0MsT0FBTyxFQUFFLEdBQUcsQ0FBQyxNQUFNLENBQUMsU0FBUyxDQUFDLGFBQWEsQ0FBQyxPQUFPO2dCQUNuRCxTQUFTLEVBQUUsS0FBSzthQUNqQixDQUFDO1FBQ0osQ0FBQztRQUVELEtBQUssQ0FBQyxPQUFPLENBQUMsR0FBVztZQUN2QixNQUFNLEVBQUUsR0FBRyxFQUFFLEdBQUcsSUFBSSxDQUFDO1lBQ3JCLElBQUksTUFBTSxFQUFFLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFDLElBQUksQ0FBQyxHQUFHLEVBQUUsQ0FBQyxJQUFJLEVBQUUsR0FBRyxFQUFFLENBQUMsS0FBSyxDQUFDLEVBQUUsQ0FBQztnQkFDdkQsR0FBRyxDQUFDLFVBQVUsQ0FBQyxJQUFJLENBQUMsb0RBQW9ELEVBQUUsR0FBRyxDQUFDLENBQUM7Z0JBQy9FLElBQUksQ0FBQztvQkFDSCxNQUFNLEVBQUUsQ0FBQyxFQUFFLENBQUMsR0FBRyxFQUFFLEVBQUUsS0FBSyxFQUFFLElBQUksRUFBRSxTQUFTLEVBQUUsSUFBSSxFQUFFLENBQUMsQ0FBQztvQkFDbkQsR0FBRyxDQUFDLFVBQVUsQ0FBQyxJQUFJLENBQUMsb0VBQW9FLEVBQUUsR0FBRyxDQUFDLENBQUM7Z0JBQ2pHLENBQUM7Z0JBQUMsT0FBTyxHQUFHLEVBQUUsQ0FBQztvQkFDYixzQkFBc0I7b0JBQ3RCLEdBQUcsQ0FBQyxVQUFVLENBQUMsS0FBSyxDQUFDLGtFQUFrRSxFQUFFLEdBQUcsRUFBRSxHQUFHLENBQUMsQ0FBQztvQkFDbkcsR0FBRyxDQUFDLFVBQVUsQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUM7Z0JBQzVCLENBQUM7WUFDSCxDQUFDO1FBQ0gsQ0FBQztRQUVELEtBQUssQ0FBQyxTQUFTO1lBQ2IsTUFBTSxFQUFFLEdBQUcsRUFBRSxHQUFHLElBQUksQ0FBQztZQUNyQixNQUFNLE1BQU0sR0FBRyxHQUFHLENBQUMsR0FBRyxDQUFDLE1BQU0sQ0FBQztZQUM5QixHQUFHLENBQUMsVUFBVSxDQUFDLElBQUksQ0FBQyx1REFBdUQsRUFBRSxNQUFNLENBQUMsU0FBUyxDQUFDLE1BQU0sQ0FBQyxDQUFDO1lBQ3RHLFlBQVk7WUFDWixNQUFNLFFBQVEsR0FBRyxLQUFLLEVBQUUsQ0FBQyxRQUFRLENBQUMsQ0FBQyxFQUFFLE9BQU8sQ0FBQyxDQUFDO1lBQzlDLE1BQU0sV0FBVyxHQUFHLElBQUksQ0FBQyxJQUFJLENBQUMsTUFBTSxDQUFDLFNBQVMsQ0FBQyxNQUFNLEVBQUUsUUFBUSxDQUFDLE1BQU0sQ0FBQyxNQUFNLENBQUMsQ0FBQyxDQUFDO1lBQ2hGLE1BQU0sSUFBSSxDQUFDLE9BQU8sQ0FBQyxXQUFXLENBQUMsQ0FBQztZQUNoQyxXQUFXO1lBQ1gsS0FBSyxJQUFJLENBQUMsR0FBRyxDQUFDLEVBQUUsQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDLEVBQUUsRUFBRSxDQUFDO2dCQUM1QixNQUFNLElBQUksR0FBRyxLQUFLLEVBQUUsQ0FBQyxRQUFRLENBQUMsQ0FBQyxFQUFFLFFBQVEsQ0FBQyxDQUFDO2dCQUMzQyxNQUFNLEdBQUcsR0FBRyxJQUFJLENBQUMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxTQUFTLENBQUMsTUFBTSxFQUFFLElBQUksQ0FBQyxNQUFNLENBQUMsU0FBUyxDQUFDLENBQUMsQ0FBQztnQkFDdkUsTUFBTSxJQUFJLENBQUMsT0FBTyxDQUFDLEdBQUcsQ0FBQyxDQUFDO1lBQzFCLENBQUM7WUFDRCxTQUFTO1lBQ1QsS0FBSyxJQUFJLENBQUMsR0FBRyxDQUFDLEVBQUUsQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDLEVBQUUsRUFBRSxDQUFDO2dCQUM1QixNQUFNLElBQUksR0FBRyxLQUFLLEVBQUUsQ0FBQyxRQUFRLENBQUMsQ0FBQyxFQUFFLE1BQU0sQ0FBQyxDQUFDO2dCQUN6QyxNQUFNLEdBQUcsR0FBRyxJQUFJLENBQUMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxTQUFTLENBQUMsTUFBTSxFQUFFLElBQUksQ0FBQyxNQUFNLENBQUMsWUFBWSxDQUFDLENBQUMsQ0FBQztnQkFDMUUsTUFBTSxJQUFJLENBQUMsT0FBTyxDQUFDLEdBQUcsQ0FBQyxDQUFDO1lBQzFCLENBQUM7WUFDRCxHQUFHLENBQUMsVUFBVSxDQUFDLElBQUksQ0FBQyxvQ0FBb0MsQ0FBQyxDQUFDO1FBQzVELENBQUM7S0FDRixDQUFDO0FBQ0osQ0FBQyxDQUFDIn0=
@@ -0,0 +1,6 @@
1
+ import type { EggCore, ILifecycleBoot } from '@eggjs/core';
2
+ export default class AppBootHook implements ILifecycleBoot {
3
+ private app;
4
+ constructor(app: EggCore);
5
+ configWillLoad(): void;
6
+ }
@@ -0,0 +1,18 @@
1
+ import { normalizeOptions } from './lib/utils.js';
2
+ export default class AppBootHook {
3
+ app;
4
+ constructor(app) {
5
+ this.app = app;
6
+ }
7
+ configWillLoad() {
8
+ this.app.config.multipart = normalizeOptions(this.app.config.multipart);
9
+ const options = this.app.config.multipart;
10
+ this.app.coreLogger.info('[@eggjs/multipart] %s mode enable', options.mode);
11
+ if (options.mode === 'file' || options.fileModeMatch) {
12
+ this.app.coreLogger.info('[@eggjs/multipart] will save temporary files to %j, cleanup job cron: %j', options.tmpdir, options.cleanSchedule.cron);
13
+ // enable multipart middleware
14
+ this.app.config.coreMiddleware.push('multipart');
15
+ }
16
+ }
17
+ }
18
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXBwLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2FwcC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFDQSxPQUFPLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQUVsRCxNQUFNLENBQUMsT0FBTyxPQUFPLFdBQVc7SUFDVjtJQUFwQixZQUFvQixHQUFZO1FBQVosUUFBRyxHQUFILEdBQUcsQ0FBUztJQUFHLENBQUM7SUFFcEMsY0FBYztRQUNaLElBQUksQ0FBQyxHQUFHLENBQUMsTUFBTSxDQUFDLFNBQVMsR0FBRyxnQkFBZ0IsQ0FBQyxJQUFJLENBQUMsR0FBRyxDQUFDLE1BQU0sQ0FBQyxTQUFTLENBQUMsQ0FBQztRQUN4RSxNQUFNLE9BQU8sR0FBRyxJQUFJLENBQUMsR0FBRyxDQUFDLE1BQU0sQ0FBQyxTQUFTLENBQUM7UUFFMUMsSUFBSSxDQUFDLEdBQUcsQ0FBQyxVQUFVLENBQUMsSUFBSSxDQUFDLG1DQUFtQyxFQUFFLE9BQU8sQ0FBQyxJQUFJLENBQUMsQ0FBQztRQUM1RSxJQUFJLE9BQU8sQ0FBQyxJQUFJLEtBQUssTUFBTSxJQUFJLE9BQU8sQ0FBQyxhQUFhLEVBQUUsQ0FBQztZQUNyRCxJQUFJLENBQUMsR0FBRyxDQUFDLFVBQVUsQ0FBQyxJQUFJLENBQUMsMEVBQTBFLEVBQ2pHLE9BQU8sQ0FBQyxNQUFNLEVBQUUsT0FBTyxDQUFDLGFBQWEsQ0FBQyxJQUFJLENBQUMsQ0FBQztZQUM5Qyw4QkFBOEI7WUFDOUIsSUFBSSxDQUFDLEdBQUcsQ0FBQyxNQUFNLENBQUMsY0FBYyxDQUFDLElBQUksQ0FBQyxXQUFXLENBQUMsQ0FBQztRQUNuRCxDQUFDO0lBQ0gsQ0FBQztDQUNGIn0=