@eggjs/multipart 4.0.0 → 5.0.0-beta.17
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.
- package/README.md +21 -32
- package/dist/app/extend/context.d.ts +101 -0
- package/dist/app/extend/context.js +199 -0
- package/dist/app/middleware/multipart.d.ts +7 -0
- package/dist/app/middleware/multipart.js +15 -0
- package/dist/app/schedule/clean_tmpdir.d.ts +29 -0
- package/dist/app/schedule/clean_tmpdir.js +57 -0
- package/dist/app.d.ts +10 -0
- package/dist/app.js +21 -0
- package/{src/config/config.default.ts → dist/config/config.default.d.ts} +11 -49
- package/dist/config/config.default.js +28 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -0
- package/dist/lib/LimitError.d.ts +8 -0
- package/dist/lib/LimitError.js +15 -0
- package/dist/lib/MultipartFileTooLargeError.d.ts +9 -0
- package/dist/lib/MultipartFileTooLargeError.js +17 -0
- package/dist/lib/utils.d.ts +8 -0
- package/dist/lib/utils.js +71 -0
- package/dist/types.d.ts +24 -0
- package/dist/types.js +1 -0
- package/package.json +55 -74
- package/dist/commonjs/app/extend/context.d.ts +0 -110
- package/dist/commonjs/app/extend/context.js +0 -279
- package/dist/commonjs/app/middleware/multipart.d.ts +0 -4
- package/dist/commonjs/app/middleware/multipart.js +0 -20
- package/dist/commonjs/app/schedule/clean_tmpdir.d.ts +0 -3
- package/dist/commonjs/app/schedule/clean_tmpdir.js +0 -58
- package/dist/commonjs/app.d.ts +0 -6
- package/dist/commonjs/app.js +0 -21
- package/dist/commonjs/config/config.default.d.ts +0 -98
- package/dist/commonjs/config/config.default.js +0 -31
- package/dist/commonjs/index.d.ts +0 -2
- package/dist/commonjs/index.js +0 -5
- package/dist/commonjs/lib/LimitError.d.ts +0 -5
- package/dist/commonjs/lib/LimitError.js +0 -16
- package/dist/commonjs/lib/MultipartFileTooLargeError.d.ts +0 -6
- package/dist/commonjs/lib/MultipartFileTooLargeError.js +0 -18
- package/dist/commonjs/lib/utils.d.ts +0 -4
- package/dist/commonjs/lib/utils.js +0 -93
- package/dist/commonjs/package.json +0 -3
- package/dist/esm/app/extend/context.d.ts +0 -110
- package/dist/esm/app/extend/context.js +0 -273
- package/dist/esm/app/middleware/multipart.d.ts +0 -4
- package/dist/esm/app/middleware/multipart.js +0 -18
- package/dist/esm/app/schedule/clean_tmpdir.d.ts +0 -3
- package/dist/esm/app/schedule/clean_tmpdir.js +0 -53
- package/dist/esm/app.d.ts +0 -6
- package/dist/esm/app.js +0 -18
- package/dist/esm/config/config.default.d.ts +0 -98
- package/dist/esm/config/config.default.js +0 -26
- package/dist/esm/index.d.ts +0 -2
- package/dist/esm/index.js +0 -3
- package/dist/esm/lib/LimitError.d.ts +0 -5
- package/dist/esm/lib/LimitError.js +0 -12
- package/dist/esm/lib/MultipartFileTooLargeError.d.ts +0 -6
- package/dist/esm/lib/MultipartFileTooLargeError.js +0 -14
- package/dist/esm/lib/utils.d.ts +0 -4
- package/dist/esm/lib/utils.js +0 -85
- package/dist/esm/package.json +0 -3
- package/dist/package.json +0 -4
- package/src/app/extend/context.ts +0 -368
- package/src/app/middleware/multipart.ts +0 -20
- package/src/app/schedule/clean_tmpdir.ts +0 -55
- package/src/app.ts +0 -20
- package/src/index.ts +0 -2
- package/src/lib/LimitError.ts +0 -12
- package/src/lib/MultipartFileTooLargeError.ts +0 -14
- package/src/lib/utils.ts +0 -92
- 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
|
-
[](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
|
[](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',
|
|
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',
|
|
41
|
+
'.js',
|
|
42
|
+
'.jsx',
|
|
45
43
|
'.json',
|
|
46
|
-
'.css',
|
|
47
|
-
'.
|
|
44
|
+
'.css',
|
|
45
|
+
'.less',
|
|
46
|
+
'.html',
|
|
47
|
+
'.htm',
|
|
48
48
|
'.xml',
|
|
49
49
|
// tar
|
|
50
50
|
'.zip',
|
|
51
|
-
'.gz',
|
|
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:
|
|
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([
|
|
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
|
-
[](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: egg0.EggApplicationCore;
|
|
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
|
|
2
|
-
import
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
126
|
-
// add EggAppConfig overrides types
|
|
127
|
-
interface EggAppConfig {
|
|
128
|
-
multipart: MultipartConfig;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
91
|
+
//#endregion
|
|
92
|
+
export { MatchItem, MultipartConfig, _default as default };
|