@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,368 @@
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
+
16
+ const HAS_CONSUMED = Symbol('Context#multipartHasConsumed');
17
+
18
+ export interface EggFile {
19
+ field: string;
20
+ filename: string;
21
+ encoding: string;
22
+ mime: string;
23
+ filepath: string;
24
+ }
25
+
26
+ export interface MultipartFileStream extends Readable {
27
+ fields: Record<string, any>;
28
+ filename: string;
29
+ fieldname: string;
30
+ mime: string;
31
+ mimeType: string;
32
+ transferEncoding: string;
33
+ encoding: string;
34
+ truncated: boolean;
35
+ }
36
+
37
+ export interface MultipartOptions {
38
+ autoFields?: boolean;
39
+ /**
40
+ * required file submit, default is true
41
+ */
42
+ requireFile?: boolean;
43
+ /**
44
+ * default charset encoding
45
+ */
46
+ defaultCharset?: string;
47
+ /**
48
+ * compatible with defaultCharset
49
+ * @deprecated use `defaultCharset` instead
50
+ */
51
+ defCharset?: string;
52
+ defaultParamCharset?: string;
53
+ /**
54
+ * compatible with defaultParamCharset
55
+ * @deprecated use `defaultParamCharset` instead
56
+ */
57
+ defParamCharset?: string;
58
+ limits?: {
59
+ fieldNameSize?: number;
60
+ fieldSize?: number;
61
+ fields?: number;
62
+ fileSize?: number;
63
+ files?: number;
64
+ parts?: number;
65
+ headerPairs?: number;
66
+ };
67
+ checkFile?(
68
+ fieldname: string,
69
+ file: any,
70
+ filename: string,
71
+ encoding: string,
72
+ mimetype: string
73
+ ): void | Error;
74
+ }
75
+
76
+ export default class MultipartContext extends Context {
77
+ /**
78
+ * create multipart.parts instance, to get separated files.
79
+ * @function Context#multipart
80
+ * @param {Object} [options] - override default multipart configurations
81
+ * - {Boolean} options.autoFields
82
+ * - {String} options.defaultCharset
83
+ * - {String} options.defaultParamCharset
84
+ * - {Object} options.limits
85
+ * - {Function} options.checkFile
86
+ * @return {Yieldable | AsyncIterable<Yieldable>} parts
87
+ */
88
+ multipart(options: MultipartOptions = {}): AsyncIterable<MultipartFileStream> {
89
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
90
+ const ctx = this;
91
+ // multipart/form-data
92
+ if (!ctx.is('multipart')) ctx.throw(400, 'Content-Type must be multipart/*');
93
+
94
+ assert(!ctx[HAS_CONSUMED], 'the multipart request can\'t be consumed twice');
95
+ ctx[HAS_CONSUMED] = true;
96
+
97
+ const { autoFields, defaultCharset, defaultParamCharset, checkFile } = ctx.app.config.multipart;
98
+ const { fieldNameSize, fieldSize, fields, fileSize, files } = ctx.app.config.multipart;
99
+ options = extractOptions(options);
100
+
101
+ const parseOptions = Object.assign({
102
+ autoFields,
103
+ defCharset: defaultCharset,
104
+ defParamCharset: defaultParamCharset,
105
+ checkFile,
106
+ }, options);
107
+
108
+ // https://github.com/mscdex/busboy#busboy-methods
109
+ // merge limits
110
+ parseOptions.limits = Object.assign({
111
+ fieldNameSize,
112
+ fieldSize,
113
+ fields,
114
+ fileSize,
115
+ files,
116
+ }, options.limits);
117
+
118
+ // mount asyncIterator, so we can use `for await` to get parts
119
+ const parts = parse(this, parseOptions);
120
+ parts[Symbol.asyncIterator] = async function* () {
121
+ let part: MultipartFileStream | undefined;
122
+ do {
123
+ part = await parts();
124
+
125
+ if (!part) continue;
126
+
127
+ if (Array.isArray(part)) {
128
+ if (part[3]) throw new LimitError('Request_fieldSize_limit', 'Reach fieldSize limit');
129
+ // TODO: still not support at busboy 1.x (only support at urlencoded)
130
+ // https://github.com/mscdex/busboy/blob/v0.3.1/lib/types/multipart.js#L5
131
+ // https://github.com/mscdex/busboy/blob/master/lib/types/multipart.js#L251
132
+ // if (part[2]) throw new LimitError('Request_fieldNameSize_limit', 'Reach fieldNameSize limit');
133
+ } else {
134
+ // user click `upload` before choose a file, `part` will be file stream, but `part.filename` is empty must handler this, such as log error.
135
+ if (!part.filename) {
136
+ ctx.coreLogger.debug('[egg-multipart] file field `%s` is upload without file stream, will drop it.', part.fieldname);
137
+ await pipeline(part, new PassThrough());
138
+ continue;
139
+ }
140
+ // TODO: check whether filename is malicious input
141
+
142
+ // busboy only set truncated when consume the stream
143
+ if (part.truncated) {
144
+ // in case of emit 'limit' too fast
145
+ throw new LimitError('Request_fileSize_limit', 'Reach fileSize limit');
146
+ } else {
147
+ part.once('limit', function(this: MultipartFileStream) {
148
+ this.emit('error', new LimitError('Request_fileSize_limit', 'Reach fileSize limit'));
149
+ this.resume();
150
+ });
151
+ }
152
+ }
153
+
154
+ // dispatch part to outter logic such as for-await-of
155
+ yield part;
156
+
157
+ } while (part !== undefined);
158
+ };
159
+ return parts;
160
+ }
161
+
162
+ /**
163
+ * save request multipart data and files to `ctx.request`
164
+ * @function Context#saveRequestFiles
165
+ * @param {Object} options - { limits, checkFile, ... }
166
+ */
167
+ async saveRequestFiles(options: MultipartOptions = {}) {
168
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
169
+ const ctx = this;
170
+
171
+ const allowArrayField = ctx.app.config.multipart.allowArrayField;
172
+
173
+ let storeDir: string | undefined;
174
+
175
+ const requestBody: Record<string, any> = {};
176
+ const requestFiles: EggFile[] = [];
177
+
178
+ options.autoFields = false;
179
+ const parts = ctx.multipart(options);
180
+
181
+ try {
182
+ for await (const part of parts) {
183
+ if (Array.isArray(part)) {
184
+ // fields
185
+ const [ fieldName, fieldValue ] = part;
186
+ if (!allowArrayField) {
187
+ requestBody[fieldName] = fieldValue;
188
+ } else {
189
+ if (!requestBody[fieldName]) {
190
+ requestBody[fieldName] = fieldValue;
191
+ } else if (!Array.isArray(requestBody[fieldName])) {
192
+ requestBody[fieldName] = [ requestBody[fieldName], fieldValue ];
193
+ } else {
194
+ requestBody[fieldName].push(fieldValue);
195
+ }
196
+ }
197
+ } else {
198
+ // stream
199
+ const { filename, fieldname, encoding, mime } = part;
200
+
201
+ if (!storeDir) {
202
+ // ${tmpdir}/YYYY/MM/DD/HH
203
+ storeDir = path.join(ctx.app.config.multipart.tmpdir, dayjs().format('YYYY/MM/DD/HH'));
204
+ await fs.mkdir(storeDir, { recursive: true });
205
+ }
206
+
207
+ // write to tmp file
208
+ const filepath = path.join(storeDir, randomUUID() + path.extname(filename));
209
+ const target = createWriteStream(filepath);
210
+ await pipeline(part, target);
211
+
212
+ const meta = {
213
+ filepath,
214
+ field: fieldname,
215
+ filename,
216
+ encoding,
217
+ mime,
218
+ // keep same property name as file stream, https://github.com/cojs/busboy/blob/master/index.js#L114
219
+ fieldname,
220
+ transferEncoding: encoding,
221
+ mimeType: mime,
222
+ };
223
+
224
+ requestFiles.push(meta);
225
+ }
226
+ }
227
+ } catch (err) {
228
+ await ctx.cleanupRequestFiles(requestFiles);
229
+ throw err;
230
+ }
231
+
232
+ ctx.request.body = requestBody;
233
+ ctx.request.files = requestFiles;
234
+ }
235
+
236
+ /**
237
+ * get upload file stream
238
+ * @example
239
+ * ```js
240
+ * const stream = await ctx.getFileStream();
241
+ * // get other fields
242
+ * console.log(stream.fields);
243
+ * ```
244
+ * @function Context#getFileStream
245
+ * @param {Object} options
246
+ * - {Boolean} options.requireFile - required file submit, default is true
247
+ * - {String} options.defaultCharset
248
+ * - {String} options.defaultParamCharset
249
+ * - {Object} options.limits
250
+ * - {Function} options.checkFile
251
+ * @return {ReadStream} stream
252
+ * @since 1.0.0
253
+ * @deprecated Not safe enough, use `ctx.multipart()` instead
254
+ */
255
+ async getFileStream(options: MultipartOptions = {}): Promise<MultipartFileStream> {
256
+ options.autoFields = true;
257
+ const parts: any = this.multipart(options);
258
+ let stream: MultipartFileStream = await parts();
259
+
260
+ if (options.requireFile !== false) {
261
+ // stream not exists, treat as an exception
262
+ if (!stream || !stream.filename) {
263
+ this.throw(400, 'Can\'t found upload file');
264
+ }
265
+ }
266
+
267
+ if (!stream) {
268
+ stream = Readable.from([]) as MultipartFileStream;
269
+ }
270
+
271
+ if (stream.truncated) {
272
+ throw new LimitError('Request_fileSize_limit', 'Request file too large, please check multipart config');
273
+ }
274
+
275
+ stream.fields = parts.field;
276
+ stream.once('limit', () => {
277
+ const err = new MultipartFileTooLargeError(
278
+ 'Request file too large, please check multipart config', stream.fields, stream.filename);
279
+ if (stream.listenerCount('error') > 0) {
280
+ stream.emit('error', err);
281
+ this.coreLogger.warn(err);
282
+ } else {
283
+ this.coreLogger.error(err);
284
+ // ignore next error event
285
+ stream.on('error', () => {});
286
+ }
287
+ // ignore all data
288
+ stream.resume();
289
+ });
290
+ return stream;
291
+ }
292
+
293
+ /**
294
+ * clean up request tmp files helper
295
+ * @function Context#cleanupRequestFiles
296
+ * @param {Array<String>} [files] - file paths need to cleanup, default is `ctx.request.files`.
297
+ */
298
+ async cleanupRequestFiles(files?: EggFile[]) {
299
+ if (!files || !files.length) {
300
+ files = this.request.files;
301
+ }
302
+ if (Array.isArray(files)) {
303
+ for (const file of files) {
304
+ try {
305
+ await fs.rm(file.filepath, { force: true, recursive: true });
306
+ } catch (err) {
307
+ // warning log
308
+ this.coreLogger.warn('[egg-multipart-cleanupRequestFiles-error] file: %j, error: %s', file, err);
309
+ }
310
+ }
311
+ }
312
+ }
313
+ }
314
+
315
+ function extractOptions(options: MultipartOptions = {}) {
316
+ const opts: MultipartOptions = {};
317
+ if (typeof options.autoFields === 'boolean') {
318
+ opts.autoFields = options.autoFields;
319
+ }
320
+ if (options.limits) {
321
+ opts.limits = options.limits;
322
+ }
323
+ if (options.checkFile) {
324
+ opts.checkFile = options.checkFile;
325
+ }
326
+
327
+ if (options.defCharset) {
328
+ opts.defCharset = options.defCharset;
329
+ }
330
+ if (options.defParamCharset) {
331
+ opts.defParamCharset = options.defParamCharset;
332
+ }
333
+ // compatible with config names
334
+ if (options.defaultCharset) {
335
+ opts.defCharset = options.defaultCharset;
336
+ }
337
+ if (options.defaultParamCharset) {
338
+ opts.defParamCharset = options.defaultParamCharset;
339
+ }
340
+
341
+ // limits
342
+ if (options.limits) {
343
+ const limits: Record<string, number | undefined> = opts.limits = { ...options.limits };
344
+ for (const key in limits) {
345
+ if (key.endsWith('Size') && limits[key]) {
346
+ limits[key] = humanizeBytes(limits[key]);
347
+ }
348
+ }
349
+ }
350
+
351
+ return opts;
352
+ }
353
+
354
+ declare module '@eggjs/core' {
355
+ interface Request {
356
+ /**
357
+ * Files Object Array
358
+ */
359
+ files?: EggFile[];
360
+ }
361
+
362
+ interface Context {
363
+ saveRequestFiles(options?: MultipartOptions): Promise<void>;
364
+ getFileStream(options?: MultipartOptions): Promise<MultipartFileStream>;
365
+ cleanupRequestFiles(files?: EggFile[]): Promise<void>;
366
+ }
367
+ }
368
+
@@ -0,0 +1,20 @@
1
+ import { pathMatching } from 'egg-path-matching';
2
+ import type { Context, Next, EggCore } from '@eggjs/core';
3
+ import type { MultipartConfig } from '../../config/config.default.js';
4
+
5
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
6
+ export default (options: MultipartConfig, _app: EggCore) => {
7
+ // normalize options
8
+ const matchFn = options.fileModeMatch && pathMatching({
9
+ match: options.fileModeMatch,
10
+ // pathToRegexpModule: app.options.pathToRegexpModule,
11
+ });
12
+
13
+ return async function multipart(ctx: Context, next: Next) {
14
+ if (!ctx.is('multipart')) return next();
15
+ if (matchFn && !matchFn(ctx)) return next();
16
+
17
+ await ctx.saveRequestFiles();
18
+ return next();
19
+ };
20
+ };
@@ -0,0 +1,55 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs/promises';
3
+ import dayjs from 'dayjs';
4
+ import { EggCore } from '@eggjs/core';
5
+
6
+ export default (app: EggCore): any => {
7
+ return class CleanTmpdir extends app.Subscription {
8
+ static get schedule() {
9
+ return {
10
+ type: 'worker',
11
+ cron: app.config.multipart.cleanSchedule.cron,
12
+ disable: app.config.multipart.cleanSchedule.disable,
13
+ immediate: false,
14
+ };
15
+ }
16
+
17
+ async _remove(dir: string) {
18
+ const { ctx } = this;
19
+ if (await fs.access(dir).then(() => true, () => false)) {
20
+ ctx.coreLogger.info('[@eggjs/multipart:CleanTmpdir] removing tmpdir: %j', dir);
21
+ try {
22
+ await fs.rm(dir, { force: true, recursive: true });
23
+ ctx.coreLogger.info('[@eggjs/multipart:CleanTmpdir:success] tmpdir: %j has been removed', dir);
24
+ } catch (err) {
25
+ /* c8 ignore next 3 */
26
+ ctx.coreLogger.error('[@eggjs/multipart:CleanTmpdir:error] remove tmpdir: %j error: %s', dir, err);
27
+ ctx.coreLogger.error(err);
28
+ }
29
+ }
30
+ }
31
+
32
+ async subscribe() {
33
+ const { ctx } = this;
34
+ const config = ctx.app.config;
35
+ ctx.coreLogger.info('[@eggjs/multipart:CleanTmpdir] start clean tmpdir: %j', config.multipart.tmpdir);
36
+ // last year
37
+ const lastYear = dayjs().subtract(1, 'years');
38
+ const lastYearDir = path.join(config.multipart.tmpdir, lastYear.format('YYYY'));
39
+ await this._remove(lastYearDir);
40
+ // 3 months
41
+ for (let i = 1; i <= 3; i++) {
42
+ const date = dayjs().subtract(i, 'months');
43
+ const dir = path.join(config.multipart.tmpdir, date.format('YYYY/MM'));
44
+ await this._remove(dir);
45
+ }
46
+ // 7 days
47
+ for (let i = 1; i <= 7; i++) {
48
+ const date = dayjs().subtract(i, 'days');
49
+ const dir = path.join(config.multipart.tmpdir, date.format('YYYY/MM/DD'));
50
+ await this._remove(dir);
51
+ }
52
+ ctx.coreLogger.info('[@eggjs/multipart:CleanTmpdir] end');
53
+ }
54
+ };
55
+ };
package/src/app.ts ADDED
@@ -0,0 +1,20 @@
1
+ import type { EggCore, ILifecycleBoot } from '@eggjs/core';
2
+ import { normalizeOptions } from './lib/utils.js';
3
+
4
+ export default class AppBootHook implements ILifecycleBoot {
5
+ constructor(private app: EggCore) {}
6
+
7
+ configWillLoad() {
8
+ this.app.config.multipart = normalizeOptions(this.app.config.multipart);
9
+ const options = this.app.config.multipart;
10
+
11
+ this.app.coreLogger.info('[@eggjs/multipart] %s mode enable', options.mode);
12
+ if (options.mode === 'file' || options.fileModeMatch) {
13
+ this.app.coreLogger.info('[@eggjs/multipart] will save temporary files to %j, cleanup job cron: %j',
14
+ options.tmpdir, options.cleanSchedule.cron);
15
+ // enable multipart middleware
16
+ this.app.config.coreMiddleware.push('multipart');
17
+ }
18
+ }
19
+ }
20
+
@@ -0,0 +1,130 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ import type { Context, EggAppInfo } from '@eggjs/core';
4
+ import type { PathMatchingPattern } from 'egg-path-matching';
5
+
6
+ export type MatchItem = string | RegExp | ((ctx: Context) => boolean);
7
+
8
+ /**
9
+ * multipart parser options
10
+ * @member Config#multipart
11
+ */
12
+ export interface MultipartConfig {
13
+ /**
14
+ * which mode to handle multipart request, default is `stream`, the hard way.
15
+ * If set mode to `file`, it's the easy way to handle multipart request and save it to local files.
16
+ * If you don't know the Node.js Stream work, maybe you should use the `file` mode to get started.
17
+ */
18
+ mode: 'stream' | 'file';
19
+ /**
20
+ * special url to use file mode when global `mode` is `stream`.
21
+ */
22
+ fileModeMatch?: PathMatchingPattern;
23
+ /**
24
+ * Auto set fields to parts, default is `false`.
25
+ * Only work on `stream` mode.
26
+ * If set true,all fields will be auto handle and can access by `parts.fields`
27
+ */
28
+ autoFields: boolean;
29
+ /**
30
+ * default charset encoding, don't change it before you real know about it
31
+ * Default is `utf8`
32
+ */
33
+ defaultCharset: string;
34
+ /**
35
+ * For multipart forms, the default character set to use for values of part header parameters (e.g. filename)
36
+ * that are not extended parameters (that contain an explicit charset), don't change it before you real know about it
37
+ * Default is `utf8`
38
+ */
39
+ defaultParamCharset: string;
40
+ /**
41
+ * Max field name size (in bytes), default is `100`
42
+ */
43
+ fieldNameSize: number;
44
+ /**
45
+ * Max field value size (in bytes), default is `100kb`
46
+ */
47
+ fieldSize: string | number;
48
+ /**
49
+ * Max number of non-file fields, default is `10`
50
+ */
51
+ fields: number;
52
+ /**
53
+ * Max file size (in bytes), default is `10mb`
54
+ */
55
+ fileSize: string | number;
56
+ /**
57
+ * Max number of file fields, default is `10`
58
+ */
59
+ files: number;
60
+ /**
61
+ * Add more ext file names to the `whitelist`, default is `[]`, only valid when `whitelist` is `null`
62
+ */
63
+ fileExtensions: string[];
64
+ /**
65
+ * The white ext file names, default is `null`
66
+ */
67
+ whitelist: string[] | ((filename: string) => boolean) | null;
68
+ /**
69
+ * Allow array field, default is `false`
70
+ */
71
+ allowArrayField: boolean;
72
+ /**
73
+ * The directory for temporary files. Only work on `file` mode.
74
+ * Default is `os.tmpdir()/egg-multipart-tmp/${appInfo.name}`
75
+ */
76
+ tmpdir: string;
77
+ /**
78
+ * The schedule for cleaning temporary files. Only work on `file` mode.
79
+ */
80
+ cleanSchedule: {
81
+ /**
82
+ * The cron expression for the schedule.
83
+ * Default is `0 30 4 * * *`
84
+ * @see https://github.com/eggjs/egg-schedule#cron-style-scheduling
85
+ */
86
+ cron: string;
87
+ /**
88
+ * Default is `false`
89
+ */
90
+ disable: boolean;
91
+ };
92
+ checkFile?(
93
+ fieldname: string,
94
+ file: any,
95
+ filename: string,
96
+ encoding: string,
97
+ mimetype: string
98
+ ): void | Error;
99
+ }
100
+
101
+ export default (appInfo: EggAppInfo) => {
102
+ return {
103
+ multipart: {
104
+ mode: 'stream',
105
+ autoFields: false,
106
+ defaultCharset: 'utf8',
107
+ defaultParamCharset: 'utf8',
108
+ fieldNameSize: 100,
109
+ fieldSize: '100kb',
110
+ fields: 10,
111
+ fileSize: '10mb',
112
+ files: 10,
113
+ fileExtensions: [],
114
+ whitelist: null,
115
+ allowArrayField: false,
116
+ tmpdir: path.join(os.tmpdir(), 'egg-multipart-tmp', appInfo.name),
117
+ cleanSchedule: {
118
+ cron: '0 30 4 * * *',
119
+ disable: false,
120
+ },
121
+ } as MultipartConfig,
122
+ };
123
+ };
124
+
125
+ declare module '@eggjs/core' {
126
+ // add EggAppConfig overrides types
127
+ interface EggAppConfig {
128
+ multipart: MultipartConfig;
129
+ }
130
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ import './config/config.default.js';
2
+ import './app/extend/context.js';
@@ -0,0 +1,12 @@
1
+ export class LimitError extends Error {
2
+ code: string;
3
+ status: number;
4
+
5
+ constructor(code: string, message: string) {
6
+ super(message);
7
+ this.code = code;
8
+ this.status = 413;
9
+ this.name = this.constructor.name;
10
+ Error.captureStackTrace(this, this.constructor);
11
+ }
12
+ }
@@ -0,0 +1,14 @@
1
+ export class MultipartFileTooLargeError extends Error {
2
+ status: number;
3
+ fields: Record<string, any>;
4
+ filename: string;
5
+
6
+ constructor(message: string, fields: Record<string, any>, filename: string) {
7
+ super(message);
8
+ this.name = this.constructor.name;
9
+ this.status = 413;
10
+ this.fields = fields;
11
+ this.filename = filename;
12
+ Error.captureStackTrace(this, this.constructor);
13
+ }
14
+ }