@eggjs/multipart 4.0.0 → 5.0.0-beta.18

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 (70) hide show
  1. package/README.md +21 -32
  2. package/dist/app/extend/context.d.ts +101 -0
  3. package/dist/app/extend/context.js +199 -0
  4. package/dist/app/middleware/multipart.d.ts +7 -0
  5. package/dist/app/middleware/multipart.js +15 -0
  6. package/dist/app/schedule/clean_tmpdir.d.ts +29 -0
  7. package/dist/app/schedule/clean_tmpdir.js +57 -0
  8. package/dist/app.d.ts +10 -0
  9. package/dist/app.js +21 -0
  10. package/{src/config/config.default.ts → dist/config/config.default.d.ts} +11 -49
  11. package/dist/config/config.default.js +28 -0
  12. package/dist/index.d.ts +1 -0
  13. package/dist/index.js +3 -0
  14. package/dist/lib/LimitError.d.ts +8 -0
  15. package/dist/lib/LimitError.js +15 -0
  16. package/dist/lib/MultipartFileTooLargeError.d.ts +9 -0
  17. package/dist/lib/MultipartFileTooLargeError.js +17 -0
  18. package/dist/lib/utils.d.ts +8 -0
  19. package/dist/lib/utils.js +71 -0
  20. package/dist/types.d.ts +24 -0
  21. package/dist/types.js +1 -0
  22. package/package.json +55 -74
  23. package/dist/commonjs/app/extend/context.d.ts +0 -110
  24. package/dist/commonjs/app/extend/context.js +0 -279
  25. package/dist/commonjs/app/middleware/multipart.d.ts +0 -4
  26. package/dist/commonjs/app/middleware/multipart.js +0 -20
  27. package/dist/commonjs/app/schedule/clean_tmpdir.d.ts +0 -3
  28. package/dist/commonjs/app/schedule/clean_tmpdir.js +0 -58
  29. package/dist/commonjs/app.d.ts +0 -6
  30. package/dist/commonjs/app.js +0 -21
  31. package/dist/commonjs/config/config.default.d.ts +0 -98
  32. package/dist/commonjs/config/config.default.js +0 -31
  33. package/dist/commonjs/index.d.ts +0 -2
  34. package/dist/commonjs/index.js +0 -5
  35. package/dist/commonjs/lib/LimitError.d.ts +0 -5
  36. package/dist/commonjs/lib/LimitError.js +0 -16
  37. package/dist/commonjs/lib/MultipartFileTooLargeError.d.ts +0 -6
  38. package/dist/commonjs/lib/MultipartFileTooLargeError.js +0 -18
  39. package/dist/commonjs/lib/utils.d.ts +0 -4
  40. package/dist/commonjs/lib/utils.js +0 -93
  41. package/dist/commonjs/package.json +0 -3
  42. package/dist/esm/app/extend/context.d.ts +0 -110
  43. package/dist/esm/app/extend/context.js +0 -273
  44. package/dist/esm/app/middleware/multipart.d.ts +0 -4
  45. package/dist/esm/app/middleware/multipart.js +0 -18
  46. package/dist/esm/app/schedule/clean_tmpdir.d.ts +0 -3
  47. package/dist/esm/app/schedule/clean_tmpdir.js +0 -53
  48. package/dist/esm/app.d.ts +0 -6
  49. package/dist/esm/app.js +0 -18
  50. package/dist/esm/config/config.default.d.ts +0 -98
  51. package/dist/esm/config/config.default.js +0 -26
  52. package/dist/esm/index.d.ts +0 -2
  53. package/dist/esm/index.js +0 -3
  54. package/dist/esm/lib/LimitError.d.ts +0 -5
  55. package/dist/esm/lib/LimitError.js +0 -12
  56. package/dist/esm/lib/MultipartFileTooLargeError.d.ts +0 -6
  57. package/dist/esm/lib/MultipartFileTooLargeError.js +0 -14
  58. package/dist/esm/lib/utils.d.ts +0 -4
  59. package/dist/esm/lib/utils.js +0 -85
  60. package/dist/esm/package.json +0 -3
  61. package/dist/package.json +0 -4
  62. package/src/app/extend/context.ts +0 -368
  63. package/src/app/middleware/multipart.ts +0 -20
  64. package/src/app/schedule/clean_tmpdir.ts +0 -55
  65. package/src/app.ts +0 -20
  66. package/src/index.ts +0 -2
  67. package/src/lib/LimitError.ts +0 -12
  68. package/src/lib/MultipartFileTooLargeError.ts +0 -14
  69. package/src/lib/utils.ts +0 -92
  70. package/src/typings/index.d.ts +0 -4
package/README.md CHANGED
@@ -1,8 +1,6 @@
1
1
  # @eggjs/multipart
2
2
 
3
3
  [![NPM version][npm-image]][npm-url]
4
- [![Node.js CI](https://github.com/eggjs/multipart/actions/workflows/nodejs.yml/badge.svg)](https://github.com/eggjs/multipart/actions/workflows/nodejs.yml)
5
- [![Test coverage][codecov-image]][codecov-url]
6
4
  [![Known Vulnerabilities][snyk-image]][snyk-url]
7
5
  [![npm download][download-image]][download-url]
8
6
  [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://makeapullrequest.com)
@@ -10,8 +8,6 @@
10
8
 
11
9
  [npm-image]: https://img.shields.io/npm/v/@eggjs/multipart.svg?style=flat-square
12
10
  [npm-url]: https://npmjs.org/package/@eggjs/multipart
13
- [codecov-image]: https://codecov.io/github/eggjs/multipart/coverage.svg?branch=master
14
- [codecov-url]: https://codecov.io/github/eggjs/multipart?branch=master
15
11
  [snyk-image]: https://snyk.io/test/npm/@eggjs/multipart/badge.svg?style=flat-square
16
12
  [snyk-url]: https://snyk.io/test/npm/@eggjs/multipart
17
13
  [download-image]: https://img.shields.io/npm/dm/@eggjs/multipart.svg?style=flat-square
@@ -31,7 +27,8 @@ Default Whitelist:
31
27
  ```js
32
28
  const whitelist = [
33
29
  // images
34
- '.jpg', '.jpeg', // image/jpeg
30
+ '.jpg',
31
+ '.jpeg', // image/jpeg
35
32
  '.png', // image/png, image/x-png
36
33
  '.gif', // image/gif
37
34
  '.bmp', // image/bmp
@@ -41,14 +38,19 @@ const whitelist = [
41
38
  '.psd',
42
39
  // text
43
40
  '.svg',
44
- '.js', '.jsx',
41
+ '.js',
42
+ '.jsx',
45
43
  '.json',
46
- '.css', '.less',
47
- '.html', '.htm',
44
+ '.css',
45
+ '.less',
46
+ '.html',
47
+ '.htm',
48
48
  '.xml',
49
49
  // tar
50
50
  '.zip',
51
- '.gz', '.tgz', '.gzip',
51
+ '.gz',
52
+ '.tgz',
53
+ '.gzip',
52
54
  // video
53
55
  '.mp3',
54
56
  '.mp4',
@@ -75,10 +77,7 @@ Developer can custom additional file extensions:
75
77
  // config/config.default.js
76
78
  exports.multipart = {
77
79
  // will append to whilelist
78
- fileExtensions: [
79
- '.foo',
80
- '.apk',
81
- ],
80
+ fileExtensions: ['.foo', '.apk'],
82
81
  };
83
82
  ```
84
83
 
@@ -87,9 +86,7 @@ Can also **override** built-in whitelist, such as only allow png:
87
86
  ```js
88
87
  // config/config.default.js
89
88
  exports.multipart = {
90
- whitelist: [
91
- '.png',
92
- ],
89
+ whitelist: ['.png'],
93
90
  };
94
91
  ```
95
92
 
@@ -97,7 +94,7 @@ Or by function:
97
94
 
98
95
  ```js
99
96
  exports.multipart = {
100
- whitelist: (filename) => [ '.png' ].includes(path.extname(filename) || '')
97
+ whitelist: filename => ['.png'].includes(path.extname(filename) || ''),
101
98
  };
102
99
  ```
103
100
 
@@ -163,8 +160,7 @@ exports.multipart = {
163
160
 
164
161
  ```html
165
162
  <form method="POST" action="/upload?_csrf={{ ctx.csrf | safe }}" enctype="multipart/form-data">
166
- title: <input name="title" />
167
- file: <input name="file" type="file" />
163
+ title: <input name="title" /> file: <input name="file" type="file" />
168
164
  <button type="submit">Upload</button>
169
165
  </form>
170
166
  ```
@@ -206,9 +202,7 @@ module.exports = class extends Controller {
206
202
 
207
203
  ```html
208
204
  <form method="POST" action="/upload?_csrf={{ ctx.csrf | safe }}" enctype="multipart/form-data">
209
- title: <input name="title" />
210
- file1: <input name="file1" type="file" />
211
- file2: <input name="file2" type="file" />
205
+ title: <input name="title" /> file1: <input name="file1" type="file" /> file2: <input name="file2" type="file" />
212
206
  <button type="submit">Upload</button>
213
207
  </form>
214
208
  ```
@@ -238,7 +232,7 @@ module.exports = class extends Controller {
238
232
  } finally {
239
233
  // remove tmp files and don't block the request's response
240
234
  // cleanupRequestFiles won't throw error even remove file io error happen
241
- ctx.cleanupRequestFiles([ file ]);
235
+ ctx.cleanupRequestFiles([file]);
242
236
  }
243
237
  console.log(result);
244
238
  }
@@ -254,9 +248,7 @@ If you're well-known about know the Node.js Stream work, you should use the `str
254
248
 
255
249
  ```html
256
250
  <form method="POST" action="/upload?_csrf={{ ctx.csrf | safe }}" enctype="multipart/form-data">
257
- title: <input name="title" />
258
- file1: <input name="file1" type="file" />
259
- file2: <input name="file2" type="file" />
251
+ title: <input name="title" /> file1: <input name="file1" type="file" /> file2: <input name="file2" type="file" />
260
252
  <button type="submit">Upload</button>
261
253
  </form>
262
254
  ```
@@ -314,8 +306,7 @@ You can got upload stream by `ctx.getFileStream*()`.
314
306
 
315
307
  ```html
316
308
  <form method="POST" action="/upload?_csrf={{ ctx.csrf | safe }}" enctype="multipart/form-data">
317
- title: <input name="title" />
318
- file: <input name="file" type="file" />
309
+ title: <input name="title" /> file: <input name="file" type="file" />
319
310
  <button type="submit">Upload</button>
320
311
  </form>
321
312
  ```
@@ -371,9 +362,7 @@ module.exports = class extends Controller {
371
362
 
372
363
  ```html
373
364
  <form method="POST" action="/upload?_csrf={{ ctx.csrf | safe }}" enctype="multipart/form-data">
374
- title: <input name="title" />
375
- file1: <input name="file1" type="file" />
376
- file2: <input name="file2" type="file" />
365
+ title: <input name="title" /> file1: <input name="file1" type="file" /> file2: <input name="file2" type="file" />
377
366
  <button type="submit">Upload</button>
378
367
  </form>
379
368
  ```
@@ -439,6 +428,6 @@ NOTICE: `fileModeMatch` options only work on `stream` mode.
439
428
 
440
429
  ## Contributors
441
430
 
442
- [![Contributors](https://contrib.rocks/image?repo=eggjs/multipart)](https://github.com/eggjs/multipart/graphs/contributors)
431
+ [![Contributors](https://contrib.rocks/image?repo=eggjs/egg)](https://github.com/eggjs/egg/graphs/contributors)
443
432
 
444
433
  Made with [contributors-img](https://contrib.rocks).
@@ -0,0 +1,101 @@
1
+ import { Readable } from "node:stream";
2
+ import { Context } from "egg";
3
+
4
+ //#region src/app/extend/context.d.ts
5
+ interface EggFile {
6
+ field: string;
7
+ filename: string;
8
+ encoding: string;
9
+ mime: string;
10
+ filepath: string;
11
+ }
12
+ interface MultipartFileStream extends Readable {
13
+ fields: Record<string, any>;
14
+ filename: string;
15
+ fieldname: string;
16
+ mime: string;
17
+ mimeType: string;
18
+ transferEncoding: string;
19
+ encoding: string;
20
+ truncated: boolean;
21
+ }
22
+ interface MultipartOptions {
23
+ autoFields?: boolean;
24
+ /**
25
+ * required file submit, default is true
26
+ */
27
+ requireFile?: boolean;
28
+ /**
29
+ * default charset encoding
30
+ */
31
+ defaultCharset?: string;
32
+ /**
33
+ * compatible with defaultCharset
34
+ * @deprecated use `defaultCharset` instead
35
+ */
36
+ defCharset?: string;
37
+ defaultParamCharset?: string;
38
+ /**
39
+ * compatible with defaultParamCharset
40
+ * @deprecated use `defaultParamCharset` instead
41
+ */
42
+ defParamCharset?: string;
43
+ limits?: {
44
+ fieldNameSize?: number;
45
+ fieldSize?: number;
46
+ fields?: number;
47
+ fileSize?: number;
48
+ files?: number;
49
+ parts?: number;
50
+ headerPairs?: number;
51
+ };
52
+ checkFile?(fieldname: string, file: any, filename: string, encoding: string, mimetype: string): void | Error;
53
+ }
54
+ declare class MultipartContext extends Context {
55
+ /**
56
+ * create multipart.parts instance, to get separated files.
57
+ * @function Context#multipart
58
+ * @param {Object} [options] - override default multipart configurations
59
+ * - {Boolean} options.autoFields
60
+ * - {String} options.defaultCharset
61
+ * - {String} options.defaultParamCharset
62
+ * - {Object} options.limits
63
+ * - {Function} options.checkFile
64
+ * @return {Yieldable | AsyncIterable<Yieldable>} parts
65
+ */
66
+ multipart(options?: MultipartOptions): AsyncIterable<MultipartFileStream>;
67
+ /**
68
+ * save request multipart data and files to `ctx.request`
69
+ * @function Context#saveRequestFiles
70
+ * @param {Object} options - { limits, checkFile, ... }
71
+ */
72
+ saveRequestFiles(options?: MultipartOptions): Promise<void>;
73
+ /**
74
+ * get upload file stream
75
+ * @example
76
+ * ```js
77
+ * const stream = await ctx.getFileStream();
78
+ * // get other fields
79
+ * console.log(stream.fields);
80
+ * ```
81
+ * @function Context#getFileStream
82
+ * @param {Object} options
83
+ * - {Boolean} options.requireFile - required file submit, default is true
84
+ * - {String} options.defaultCharset
85
+ * - {String} options.defaultParamCharset
86
+ * - {Object} options.limits
87
+ * - {Function} options.checkFile
88
+ * @return {ReadStream} stream
89
+ * @since 1.0.0
90
+ * @deprecated Not safe enough, use `ctx.multipart()` instead
91
+ */
92
+ getFileStream(options?: MultipartOptions): Promise<MultipartFileStream>;
93
+ /**
94
+ * clean up request tmp files helper
95
+ * @function Context#cleanupRequestFiles
96
+ * @param {Array<String>} [files] - file paths need to cleanup, default is `ctx.request.files`.
97
+ */
98
+ cleanupRequestFiles(files?: EggFile[]): Promise<void>;
99
+ }
100
+ //#endregion
101
+ export { EggFile, MultipartFileStream, MultipartOptions, MultipartContext as default };
@@ -0,0 +1,199 @@
1
+ import { humanizeBytes } from "../../lib/utils.js";
2
+ import { LimitError } from "../../lib/LimitError.js";
3
+ import { MultipartFileTooLargeError } from "../../lib/MultipartFileTooLargeError.js";
4
+ import path from "node:path";
5
+ import assert from "node:assert";
6
+ import { randomUUID } from "node:crypto";
7
+ import fs from "node:fs/promises";
8
+ import { createWriteStream } from "node:fs";
9
+ import { PassThrough, Readable } from "node:stream";
10
+ import { pipeline } from "node:stream/promises";
11
+ import parse from "co-busboy";
12
+ import dayjs from "dayjs";
13
+ import { Context } from "egg";
14
+
15
+ //#region src/app/extend/context.ts
16
+ const HAS_CONSUMED = Symbol("Context#multipartHasConsumed");
17
+ var MultipartContext = class extends Context {
18
+ /**
19
+ * create multipart.parts instance, to get separated files.
20
+ * @function Context#multipart
21
+ * @param {Object} [options] - override default multipart configurations
22
+ * - {Boolean} options.autoFields
23
+ * - {String} options.defaultCharset
24
+ * - {String} options.defaultParamCharset
25
+ * - {Object} options.limits
26
+ * - {Function} options.checkFile
27
+ * @return {Yieldable | AsyncIterable<Yieldable>} parts
28
+ */
29
+ multipart(options = {}) {
30
+ const ctx = this;
31
+ if (!ctx.is("multipart")) ctx.throw(400, "Content-Type must be multipart/*");
32
+ assert(!ctx[HAS_CONSUMED], "the multipart request can't be consumed twice");
33
+ ctx[HAS_CONSUMED] = true;
34
+ const { autoFields, defaultCharset, defaultParamCharset, checkFile } = ctx.app.config.multipart;
35
+ const { fieldNameSize, fieldSize, fields, fileSize, files } = ctx.app.config.multipart;
36
+ options = extractOptions(options);
37
+ const parseOptions = Object.assign({
38
+ autoFields,
39
+ defCharset: defaultCharset,
40
+ defParamCharset: defaultParamCharset,
41
+ checkFile
42
+ }, options);
43
+ parseOptions.limits = Object.assign({
44
+ fieldNameSize,
45
+ fieldSize,
46
+ fields,
47
+ fileSize,
48
+ files
49
+ }, options.limits);
50
+ const parts = parse(this, parseOptions);
51
+ parts[Symbol.asyncIterator] = async function* () {
52
+ let part;
53
+ do {
54
+ part = await parts();
55
+ if (!part) continue;
56
+ if (Array.isArray(part)) {
57
+ if (part[3]) throw new LimitError("Request_fieldSize_limit", "Reach fieldSize limit");
58
+ } else {
59
+ if (!part.filename) {
60
+ ctx.coreLogger.debug("[egg-multipart] file field `%s` is upload without file stream, will drop it.", part.fieldname);
61
+ await pipeline(part, new PassThrough());
62
+ continue;
63
+ }
64
+ if (part.truncated) throw new LimitError("Request_fileSize_limit", "Reach fileSize limit");
65
+ else part.once("limit", function() {
66
+ this.emit("error", new LimitError("Request_fileSize_limit", "Reach fileSize limit"));
67
+ this.resume();
68
+ });
69
+ }
70
+ yield part;
71
+ } while (part !== void 0);
72
+ };
73
+ return parts;
74
+ }
75
+ /**
76
+ * save request multipart data and files to `ctx.request`
77
+ * @function Context#saveRequestFiles
78
+ * @param {Object} options - { limits, checkFile, ... }
79
+ */
80
+ async saveRequestFiles(options = {}) {
81
+ const ctx = this;
82
+ const allowArrayField = ctx.app.config.multipart.allowArrayField;
83
+ let storeDir;
84
+ const requestBody = {};
85
+ const requestFiles = [];
86
+ options.autoFields = false;
87
+ const parts = ctx.multipart(options);
88
+ try {
89
+ for await (const part of parts) if (Array.isArray(part)) {
90
+ const [fieldName, fieldValue] = part;
91
+ if (!allowArrayField) requestBody[fieldName] = fieldValue;
92
+ else if (!requestBody[fieldName]) requestBody[fieldName] = fieldValue;
93
+ else if (!Array.isArray(requestBody[fieldName])) requestBody[fieldName] = [requestBody[fieldName], fieldValue];
94
+ else requestBody[fieldName].push(fieldValue);
95
+ } else {
96
+ const { filename, fieldname, encoding, mime } = part;
97
+ if (!storeDir) {
98
+ storeDir = path.join(ctx.app.config.multipart.tmpdir, dayjs().format("YYYY/MM/DD/HH"));
99
+ await fs.mkdir(storeDir, { recursive: true });
100
+ }
101
+ const filepath = path.join(storeDir, randomUUID() + path.extname(filename));
102
+ const target = createWriteStream(filepath);
103
+ await pipeline(part, target);
104
+ const meta = {
105
+ filepath,
106
+ field: fieldname,
107
+ filename,
108
+ encoding,
109
+ mime,
110
+ fieldname,
111
+ transferEncoding: encoding,
112
+ mimeType: mime
113
+ };
114
+ requestFiles.push(meta);
115
+ }
116
+ } catch (err) {
117
+ await ctx.cleanupRequestFiles(requestFiles);
118
+ throw err;
119
+ }
120
+ ctx.request.body = requestBody;
121
+ ctx.request.files = requestFiles;
122
+ }
123
+ /**
124
+ * get upload file stream
125
+ * @example
126
+ * ```js
127
+ * const stream = await ctx.getFileStream();
128
+ * // get other fields
129
+ * console.log(stream.fields);
130
+ * ```
131
+ * @function Context#getFileStream
132
+ * @param {Object} options
133
+ * - {Boolean} options.requireFile - required file submit, default is true
134
+ * - {String} options.defaultCharset
135
+ * - {String} options.defaultParamCharset
136
+ * - {Object} options.limits
137
+ * - {Function} options.checkFile
138
+ * @return {ReadStream} stream
139
+ * @since 1.0.0
140
+ * @deprecated Not safe enough, use `ctx.multipart()` instead
141
+ */
142
+ async getFileStream(options = {}) {
143
+ options.autoFields = true;
144
+ const parts = this.multipart(options);
145
+ let stream = await parts();
146
+ if (options.requireFile !== false) {
147
+ if (!stream || !stream.filename) this.throw(400, "Can't found upload file");
148
+ }
149
+ if (!stream) stream = Readable.from([]);
150
+ if (stream.truncated) throw new LimitError("Request_fileSize_limit", "Request file too large, please check multipart config");
151
+ stream.fields = parts.field;
152
+ stream.once("limit", () => {
153
+ const err = new MultipartFileTooLargeError("Request file too large, please check multipart config", stream.fields, stream.filename);
154
+ if (stream.listenerCount("error") > 0) {
155
+ stream.emit("error", err);
156
+ this.coreLogger.warn(err);
157
+ } else {
158
+ this.coreLogger.error(err);
159
+ stream.on("error", () => {});
160
+ }
161
+ stream.resume();
162
+ });
163
+ return stream;
164
+ }
165
+ /**
166
+ * clean up request tmp files helper
167
+ * @function Context#cleanupRequestFiles
168
+ * @param {Array<String>} [files] - file paths need to cleanup, default is `ctx.request.files`.
169
+ */
170
+ async cleanupRequestFiles(files) {
171
+ if (!files || !files.length) files = this.request.files;
172
+ if (Array.isArray(files)) for (const file of files) try {
173
+ await fs.rm(file.filepath, {
174
+ force: true,
175
+ recursive: true
176
+ });
177
+ } catch (err) {
178
+ this.coreLogger.warn("[egg-multipart-cleanupRequestFiles-error] file: %j, error: %s", file, err);
179
+ }
180
+ }
181
+ };
182
+ function extractOptions(options = {}) {
183
+ const opts = {};
184
+ if (typeof options.autoFields === "boolean") opts.autoFields = options.autoFields;
185
+ if (options.limits) opts.limits = options.limits;
186
+ if (options.checkFile) opts.checkFile = options.checkFile;
187
+ if (options.defCharset) opts.defCharset = options.defCharset;
188
+ if (options.defParamCharset) opts.defParamCharset = options.defParamCharset;
189
+ if (options.defaultCharset) opts.defCharset = options.defaultCharset;
190
+ if (options.defaultParamCharset) opts.defParamCharset = options.defaultParamCharset;
191
+ if (options.limits) {
192
+ const limits = opts.limits = { ...options.limits };
193
+ for (const key in limits) if (key.endsWith("Size") && limits[key]) limits[key] = humanizeBytes(limits[key]);
194
+ }
195
+ return opts;
196
+ }
197
+
198
+ //#endregion
199
+ export { MultipartContext as default };
@@ -0,0 +1,7 @@
1
+ import { MultipartConfig } from "../../config/config.default.js";
2
+ import { Application, MiddlewareFunc } from "egg";
3
+
4
+ //#region src/app/middleware/multipart.d.ts
5
+ declare const _default: (options: MultipartConfig, _app: Application) => MiddlewareFunc;
6
+ //#endregion
7
+ export { _default as default };
@@ -0,0 +1,15 @@
1
+ import { pathMatching } from "egg-path-matching";
2
+
3
+ //#region src/app/middleware/multipart.ts
4
+ var multipart_default = (options, _app) => {
5
+ const matchFn = options.fileModeMatch && pathMatching({ match: options.fileModeMatch });
6
+ return async function multipart(ctx, next) {
7
+ if (!ctx.is("multipart")) return next();
8
+ if (matchFn && !matchFn(ctx)) return next();
9
+ await ctx.saveRequestFiles();
10
+ return next();
11
+ };
12
+ };
13
+
14
+ //#endregion
15
+ export { multipart_default as default };
@@ -0,0 +1,29 @@
1
+ import * as egg0 from "egg";
2
+ import { Application } from "egg";
3
+ import * as _eggjs_core0 from "@eggjs/core";
4
+ import * as egg_lib_core_base_context_logger0 from "egg/lib/core/base_context_logger";
5
+
6
+ //#region src/app/schedule/clean_tmpdir.d.ts
7
+ declare const _default: (app: Application) => {
8
+ new (ctx: _eggjs_core0.Context): {
9
+ [key: string]: any;
10
+ [key: symbol]: any;
11
+ _remove(dir: string): Promise<void>;
12
+ subscribe(): Promise<void>;
13
+ ctx: egg0.Context;
14
+ pathName?: string;
15
+ app: Application;
16
+ service: egg0.Controller;
17
+ "#logger"?: egg_lib_core_base_context_logger0.BaseContextLogger;
18
+ get logger(): egg_lib_core_base_context_logger0.BaseContextLogger;
19
+ config: Record<string, any>;
20
+ };
21
+ get schedule(): {
22
+ type: string;
23
+ cron: string;
24
+ disable: boolean;
25
+ immediate: boolean;
26
+ };
27
+ };
28
+ //#endregion
29
+ export { _default as default };
@@ -0,0 +1,57 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs/promises";
3
+ import dayjs from "dayjs";
4
+ import { Application } from "egg";
5
+
6
+ //#region src/app/schedule/clean_tmpdir.ts
7
+ var clean_tmpdir_default = (app) => {
8
+ return class CleanTmpdir extends app.Subscription {
9
+ static get schedule() {
10
+ return {
11
+ type: "worker",
12
+ cron: app.config.multipart.cleanSchedule.cron,
13
+ disable: app.config.multipart.cleanSchedule.disable,
14
+ immediate: false
15
+ };
16
+ }
17
+ async _remove(dir) {
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, {
23
+ force: true,
24
+ recursive: true
25
+ });
26
+ ctx.coreLogger.info("[@eggjs/multipart:CleanTmpdir:success] tmpdir: %j has been removed", dir);
27
+ } catch (err) {
28
+ /* c8 ignore next 3 */
29
+ ctx.coreLogger.error("[@eggjs/multipart:CleanTmpdir:error] remove tmpdir: %j error: %s", dir, err);
30
+ ctx.coreLogger.error(err);
31
+ }
32
+ }
33
+ }
34
+ async subscribe() {
35
+ const { ctx } = this;
36
+ const config = ctx.app.config;
37
+ ctx.coreLogger.info("[@eggjs/multipart:CleanTmpdir] start clean tmpdir: %j", config.multipart.tmpdir);
38
+ const lastYear = dayjs().subtract(1, "years");
39
+ const lastYearDir = path.join(config.multipart.tmpdir, lastYear.format("YYYY"));
40
+ await this._remove(lastYearDir);
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
+ for (let i = 1; i <= 7; i++) {
47
+ const date = dayjs().subtract(i, "days");
48
+ const dir = path.join(config.multipart.tmpdir, date.format("YYYY/MM/DD"));
49
+ await this._remove(dir);
50
+ }
51
+ ctx.coreLogger.info("[@eggjs/multipart:CleanTmpdir] end");
52
+ }
53
+ };
54
+ };
55
+
56
+ //#endregion
57
+ export { clean_tmpdir_default as default };
package/dist/app.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ import { Application, ILifecycleBoot } from "egg";
2
+
3
+ //#region src/app.d.ts
4
+ declare class AppBootHook implements ILifecycleBoot {
5
+ private readonly app;
6
+ constructor(app: Application);
7
+ configWillLoad(): void;
8
+ }
9
+ //#endregion
10
+ export { AppBootHook as default };
package/dist/app.js ADDED
@@ -0,0 +1,21 @@
1
+ import { normalizeOptions } from "./lib/utils.js";
2
+
3
+ //#region src/app.ts
4
+ var AppBootHook = class {
5
+ app;
6
+ constructor(app) {
7
+ this.app = app;
8
+ }
9
+ configWillLoad() {
10
+ this.app.config.multipart = normalizeOptions(this.app.config.multipart);
11
+ const options = this.app.config.multipart;
12
+ this.app.coreLogger.info("[@eggjs/multipart] %s mode enable", options.mode);
13
+ if (options.mode === "file" || options.fileModeMatch) {
14
+ this.app.coreLogger.info("[@eggjs/multipart] will save temporary files to %j, cleanup job cron: %j", options.tmpdir, options.cleanSchedule.cron);
15
+ this.app.config.coreMiddleware.push("multipart");
16
+ }
17
+ }
18
+ };
19
+
20
+ //#endregion
21
+ export { AppBootHook as default };
@@ -1,15 +1,9 @@
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';
1
+ import { Context, EggAppInfo } from "egg";
2
+ import { PathMatchingPattern } from "egg-path-matching";
5
3
 
6
- export type MatchItem = string | RegExp | ((ctx: Context) => boolean);
7
-
8
- /**
9
- * multipart parser options
10
- * @member Config#multipart
11
- */
12
- export interface MultipartConfig {
4
+ //#region src/config/config.default.d.ts
5
+ type MatchItem = string | RegExp | ((ctx: Context) => boolean);
6
+ interface MultipartConfig {
13
7
  /**
14
8
  * which mode to handle multipart request, default is `stream`, the hard way.
15
9
  * If set mode to `file`, it's the easy way to handle multipart request and save it to local files.
@@ -81,7 +75,7 @@ export interface MultipartConfig {
81
75
  /**
82
76
  * The cron expression for the schedule.
83
77
  * Default is `0 30 4 * * *`
84
- * @see https://github.com/eggjs/egg-schedule#cron-style-scheduling
78
+ * @see https://github.com/eggjs/egg/tree/next/plugins/schedule#cron-style-scheduling
85
79
  */
86
80
  cron: string;
87
81
  /**
@@ -89,42 +83,10 @@ export interface MultipartConfig {
89
83
  */
90
84
  disable: boolean;
91
85
  };
92
- checkFile?(
93
- fieldname: string,
94
- file: any,
95
- filename: string,
96
- encoding: string,
97
- mimetype: string
98
- ): void | Error;
86
+ checkFile?(fieldname: string, file: any, filename: string, encoding: string, mimetype: string): void | Error;
99
87
  }
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
- };
88
+ declare const _default: (appInfo: EggAppInfo) => {
89
+ multipart: MultipartConfig;
123
90
  };
124
-
125
- declare module '@eggjs/core' {
126
- // add EggAppConfig overrides types
127
- interface EggAppConfig {
128
- multipart: MultipartConfig;
129
- }
130
- }
91
+ //#endregion
92
+ export { MatchItem, MultipartConfig, _default as default };