@eggjs/multipart 5.0.0-beta.18 → 5.0.0-beta.20

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.
@@ -0,0 +1,15 @@
1
+ //#region src/lib/LimitError.ts
2
+ var LimitError = class extends Error {
3
+ code;
4
+ status;
5
+ constructor(code, message) {
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
+ };
13
+
14
+ //#endregion
15
+ export { LimitError };
@@ -0,0 +1,17 @@
1
+ //#region src/lib/MultipartFileTooLargeError.ts
2
+ var MultipartFileTooLargeError = class extends Error {
3
+ status;
4
+ fields;
5
+ filename;
6
+ constructor(message, fields, filename) {
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
+ };
15
+
16
+ //#endregion
17
+ export { MultipartFileTooLargeError };
@@ -1,101 +1,2 @@
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
1
+ import { EggFile, MultipartContext, MultipartFileStream, MultipartOptions } from "../../context-COfddVcC.js";
101
2
  export { EggFile, MultipartFileStream, MultipartOptions, MultipartContext as default };
@@ -1,199 +1,6 @@
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";
1
+ import "../../utils-BwL2JNe7.js";
2
+ import "../../LimitError-BZqeZJ5w.js";
3
+ import "../../MultipartFileTooLargeError-CFG577Bz.js";
4
+ import { MultipartContext } from "../../context-CLeGGj9o.js";
14
5
 
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
6
  export { MultipartContext as default };
@@ -1,4 +1,4 @@
1
- import { MultipartConfig } from "../../config/config.default.js";
1
+ import { MultipartConfig } from "../../config.default-CPo4ogXL.js";
2
2
  import { Application, MiddlewareFunc } from "egg";
3
3
 
4
4
  //#region src/app/middleware/multipart.d.ts
@@ -14,7 +14,7 @@ declare const _default: (app: Application) => {
14
14
  pathName?: string;
15
15
  app: Application;
16
16
  service: egg0.Controller;
17
- "#logger"?: egg_lib_core_base_context_logger0.BaseContextLogger;
17
+ "__#private@#logger"?: egg_lib_core_base_context_logger0.BaseContextLogger;
18
18
  get logger(): egg_lib_core_base_context_logger0.BaseContextLogger;
19
19
  config: Record<string, any>;
20
20
  };
package/dist/app.js CHANGED
@@ -1,4 +1,4 @@
1
- import { normalizeOptions } from "./lib/utils.js";
1
+ import { normalizeOptions } from "./utils-BwL2JNe7.js";
2
2
 
3
3
  //#region src/app.ts
4
4
  var AppBootHook = class {
@@ -1,92 +1,2 @@
1
- import { Context, EggAppInfo } from "egg";
2
- import { PathMatchingPattern } from "egg-path-matching";
3
-
4
- //#region src/config/config.default.d.ts
5
- type MatchItem = string | RegExp | ((ctx: Context) => boolean);
6
- interface MultipartConfig {
7
- /**
8
- * which mode to handle multipart request, default is `stream`, the hard way.
9
- * If set mode to `file`, it's the easy way to handle multipart request and save it to local files.
10
- * If you don't know the Node.js Stream work, maybe you should use the `file` mode to get started.
11
- */
12
- mode: 'stream' | 'file';
13
- /**
14
- * special url to use file mode when global `mode` is `stream`.
15
- */
16
- fileModeMatch?: PathMatchingPattern;
17
- /**
18
- * Auto set fields to parts, default is `false`.
19
- * Only work on `stream` mode.
20
- * If set true,all fields will be auto handle and can access by `parts.fields`
21
- */
22
- autoFields: boolean;
23
- /**
24
- * default charset encoding, don't change it before you real know about it
25
- * Default is `utf8`
26
- */
27
- defaultCharset: string;
28
- /**
29
- * For multipart forms, the default character set to use for values of part header parameters (e.g. filename)
30
- * that are not extended parameters (that contain an explicit charset), don't change it before you real know about it
31
- * Default is `utf8`
32
- */
33
- defaultParamCharset: string;
34
- /**
35
- * Max field name size (in bytes), default is `100`
36
- */
37
- fieldNameSize: number;
38
- /**
39
- * Max field value size (in bytes), default is `100kb`
40
- */
41
- fieldSize: string | number;
42
- /**
43
- * Max number of non-file fields, default is `10`
44
- */
45
- fields: number;
46
- /**
47
- * Max file size (in bytes), default is `10mb`
48
- */
49
- fileSize: string | number;
50
- /**
51
- * Max number of file fields, default is `10`
52
- */
53
- files: number;
54
- /**
55
- * Add more ext file names to the `whitelist`, default is `[]`, only valid when `whitelist` is `null`
56
- */
57
- fileExtensions: string[];
58
- /**
59
- * The white ext file names, default is `null`
60
- */
61
- whitelist: string[] | ((filename: string) => boolean) | null;
62
- /**
63
- * Allow array field, default is `false`
64
- */
65
- allowArrayField: boolean;
66
- /**
67
- * The directory for temporary files. Only work on `file` mode.
68
- * Default is `os.tmpdir()/egg-multipart-tmp/${appInfo.name}`
69
- */
70
- tmpdir: string;
71
- /**
72
- * The schedule for cleaning temporary files. Only work on `file` mode.
73
- */
74
- cleanSchedule: {
75
- /**
76
- * The cron expression for the schedule.
77
- * Default is `0 30 4 * * *`
78
- * @see https://github.com/eggjs/egg/tree/next/plugins/schedule#cron-style-scheduling
79
- */
80
- cron: string;
81
- /**
82
- * Default is `false`
83
- */
84
- disable: boolean;
85
- };
86
- checkFile?(fieldname: string, file: any, filename: string, encoding: string, mimetype: string): void | Error;
87
- }
88
- declare const _default: (appInfo: EggAppInfo) => {
89
- multipart: MultipartConfig;
90
- };
91
- //#endregion
1
+ import { MatchItem, MultipartConfig, _default } from "../config.default-CPo4ogXL.js";
92
2
  export { MatchItem, MultipartConfig, _default as default };
@@ -1,28 +1,3 @@
1
- import path from "node:path";
2
- import os from "node:os";
1
+ import { config_default_default } from "../config.default-AMmYIt7n.js";
3
2
 
4
- //#region src/config/config.default.ts
5
- var config_default_default = (appInfo) => {
6
- return { multipart: {
7
- mode: "stream",
8
- autoFields: false,
9
- defaultCharset: "utf8",
10
- defaultParamCharset: "utf8",
11
- fieldNameSize: 100,
12
- fieldSize: "100kb",
13
- fields: 10,
14
- fileSize: "10mb",
15
- files: 10,
16
- fileExtensions: [],
17
- whitelist: null,
18
- allowArrayField: false,
19
- tmpdir: path.join(os.tmpdir(), "egg-multipart-tmp", appInfo.name),
20
- cleanSchedule: {
21
- cron: "0 30 4 * * *",
22
- disable: false
23
- }
24
- } };
25
- };
26
-
27
- //#endregion
28
3
  export { config_default_default as default };
@@ -0,0 +1,28 @@
1
+ import path from "node:path";
2
+ import os from "node:os";
3
+
4
+ //#region src/config/config.default.ts
5
+ var config_default_default = (appInfo) => {
6
+ return { multipart: {
7
+ mode: "stream",
8
+ autoFields: false,
9
+ defaultCharset: "utf8",
10
+ defaultParamCharset: "utf8",
11
+ fieldNameSize: 100,
12
+ fieldSize: "100kb",
13
+ fields: 10,
14
+ fileSize: "10mb",
15
+ files: 10,
16
+ fileExtensions: [],
17
+ whitelist: null,
18
+ allowArrayField: false,
19
+ tmpdir: path.join(os.tmpdir(), "egg-multipart-tmp", appInfo.name),
20
+ cleanSchedule: {
21
+ cron: "0 30 4 * * *",
22
+ disable: false
23
+ }
24
+ } };
25
+ };
26
+
27
+ //#endregion
28
+ export { config_default_default };
@@ -0,0 +1,92 @@
1
+ import { Context, EggAppInfo } from "egg";
2
+ import { PathMatchingPattern } from "egg-path-matching";
3
+
4
+ //#region src/config/config.default.d.ts
5
+ type MatchItem = string | RegExp | ((ctx: Context) => boolean);
6
+ interface MultipartConfig {
7
+ /**
8
+ * which mode to handle multipart request, default is `stream`, the hard way.
9
+ * If set mode to `file`, it's the easy way to handle multipart request and save it to local files.
10
+ * If you don't know the Node.js Stream work, maybe you should use the `file` mode to get started.
11
+ */
12
+ mode: 'stream' | 'file';
13
+ /**
14
+ * special url to use file mode when global `mode` is `stream`.
15
+ */
16
+ fileModeMatch?: PathMatchingPattern;
17
+ /**
18
+ * Auto set fields to parts, default is `false`.
19
+ * Only work on `stream` mode.
20
+ * If set true,all fields will be auto handle and can access by `parts.fields`
21
+ */
22
+ autoFields: boolean;
23
+ /**
24
+ * default charset encoding, don't change it before you real know about it
25
+ * Default is `utf8`
26
+ */
27
+ defaultCharset: string;
28
+ /**
29
+ * For multipart forms, the default character set to use for values of part header parameters (e.g. filename)
30
+ * that are not extended parameters (that contain an explicit charset), don't change it before you real know about it
31
+ * Default is `utf8`
32
+ */
33
+ defaultParamCharset: string;
34
+ /**
35
+ * Max field name size (in bytes), default is `100`
36
+ */
37
+ fieldNameSize: number;
38
+ /**
39
+ * Max field value size (in bytes), default is `100kb`
40
+ */
41
+ fieldSize: string | number;
42
+ /**
43
+ * Max number of non-file fields, default is `10`
44
+ */
45
+ fields: number;
46
+ /**
47
+ * Max file size (in bytes), default is `10mb`
48
+ */
49
+ fileSize: string | number;
50
+ /**
51
+ * Max number of file fields, default is `10`
52
+ */
53
+ files: number;
54
+ /**
55
+ * Add more ext file names to the `whitelist`, default is `[]`, only valid when `whitelist` is `null`
56
+ */
57
+ fileExtensions: string[];
58
+ /**
59
+ * The white ext file names, default is `null`
60
+ */
61
+ whitelist: string[] | ((filename: string) => boolean) | null;
62
+ /**
63
+ * Allow array field, default is `false`
64
+ */
65
+ allowArrayField: boolean;
66
+ /**
67
+ * The directory for temporary files. Only work on `file` mode.
68
+ * Default is `os.tmpdir()/egg-multipart-tmp/${appInfo.name}`
69
+ */
70
+ tmpdir: string;
71
+ /**
72
+ * The schedule for cleaning temporary files. Only work on `file` mode.
73
+ */
74
+ cleanSchedule: {
75
+ /**
76
+ * The cron expression for the schedule.
77
+ * Default is `0 30 4 * * *`
78
+ * @see https://github.com/eggjs/egg/tree/next/plugins/schedule#cron-style-scheduling
79
+ */
80
+ cron: string;
81
+ /**
82
+ * Default is `false`
83
+ */
84
+ disable: boolean;
85
+ };
86
+ checkFile?(fieldname: string, file: any, filename: string, encoding: string, mimetype: string): void | Error;
87
+ }
88
+ declare const _default: (appInfo: EggAppInfo) => {
89
+ multipart: MultipartConfig;
90
+ };
91
+ //#endregion
92
+ export { MatchItem, MultipartConfig, _default };
@@ -0,0 +1,199 @@
1
+ import { humanizeBytes } from "./utils-BwL2JNe7.js";
2
+ import { LimitError } from "./LimitError-BZqeZJ5w.js";
3
+ import { MultipartFileTooLargeError } from "./MultipartFileTooLargeError-CFG577Bz.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 };
@@ -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, MultipartContext, MultipartFileStream, MultipartOptions };
package/dist/index.d.ts CHANGED
@@ -1 +1,3 @@
1
- export { };
1
+ import "./context-COfddVcC.js";
2
+ import "./config.default-CPo4ogXL.js";
3
+ import "./types-DNLuS6JZ.js";
package/dist/index.js CHANGED
@@ -1,3 +1,8 @@
1
- import "./app/extend/context.js";
1
+ import "./utils-BwL2JNe7.js";
2
+ import "./config.default-AMmYIt7n.js";
3
+ import "./LimitError-BZqeZJ5w.js";
4
+ import "./MultipartFileTooLargeError-CFG577Bz.js";
5
+ import "./context-CLeGGj9o.js";
6
+ import "./types-BsuPrIpu.js";
2
7
 
3
8
  export { };
@@ -1,15 +1,3 @@
1
- //#region src/lib/LimitError.ts
2
- var LimitError = class extends Error {
3
- code;
4
- status;
5
- constructor(code, message) {
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
- };
1
+ import { LimitError } from "../LimitError-BZqeZJ5w.js";
13
2
 
14
- //#endregion
15
3
  export { LimitError };
@@ -1,17 +1,3 @@
1
- //#region src/lib/MultipartFileTooLargeError.ts
2
- var MultipartFileTooLargeError = class extends Error {
3
- status;
4
- fields;
5
- filename;
6
- constructor(message, fields, filename) {
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
- };
1
+ import { MultipartFileTooLargeError } from "../MultipartFileTooLargeError-CFG577Bz.js";
15
2
 
16
- //#endregion
17
3
  export { MultipartFileTooLargeError };
@@ -1,4 +1,4 @@
1
- import { MultipartConfig } from "../config/config.default.js";
1
+ import { MultipartConfig } from "../config.default-CPo4ogXL.js";
2
2
 
3
3
  //#region src/lib/utils.d.ts
4
4
  declare const whitelist: string[];
package/dist/lib/utils.js CHANGED
@@ -1,71 +1,3 @@
1
- import path from "node:path";
2
- import assert from "node:assert";
3
- import bytes from "bytes";
1
+ import { humanizeBytes, normalizeOptions, whitelist } from "../utils-BwL2JNe7.js";
4
2
 
5
- //#region src/lib/utils.ts
6
- const whitelist = [
7
- ".jpg",
8
- ".jpeg",
9
- ".png",
10
- ".gif",
11
- ".bmp",
12
- ".wbmp",
13
- ".webp",
14
- ".tif",
15
- ".psd",
16
- ".svg",
17
- ".js",
18
- ".jsx",
19
- ".json",
20
- ".css",
21
- ".less",
22
- ".html",
23
- ".htm",
24
- ".xml",
25
- ".zip",
26
- ".gz",
27
- ".tgz",
28
- ".gzip",
29
- ".mp3",
30
- ".mp4",
31
- ".avi"
32
- ];
33
- function humanizeBytes(size) {
34
- if (typeof size === "number") return size;
35
- return bytes(size);
36
- }
37
- function normalizeOptions(options) {
38
- options.fileSize = humanizeBytes(options.fileSize);
39
- options.fieldSize = humanizeBytes(options.fieldSize);
40
- options.fieldNameSize = humanizeBytes(options.fieldNameSize);
41
- options.mode = options.mode || "stream";
42
- assert(["stream", "file"].includes(options.mode), `Expect mode to be 'stream' or 'file', but got '${options.mode}'`);
43
- if (options.mode === "file") assert(!options.fileModeMatch, "`fileModeMatch` options only work on stream mode, please remove it");
44
- if (Array.isArray(options.whitelist)) options.whitelist = options.whitelist.map((extname) => extname.toLowerCase());
45
- if (Array.isArray(options.fileExtensions)) options.fileExtensions = options.fileExtensions.map((extname) => {
46
- return extname.startsWith(".") || extname === "" ? extname.toLowerCase() : `.${extname.toLowerCase()}`;
47
- });
48
- function checkExt(fileName) {
49
- if (typeof options.whitelist === "function") return options.whitelist(fileName);
50
- const extname = path.extname(fileName).toLowerCase();
51
- if (Array.isArray(options.whitelist)) return options.whitelist.includes(extname);
52
- return whitelist.includes(extname) || options.fileExtensions.includes(extname);
53
- }
54
- options.checkFile = (_fieldName, fileStream, fileName) => {
55
- if (!fileStream || !fileName) return;
56
- try {
57
- if (!checkExt(fileName)) {
58
- const err = /* @__PURE__ */ new Error("Invalid filename: " + fileName);
59
- Reflect.set(err, "status", 400);
60
- return err;
61
- }
62
- } catch (err) {
63
- err.status = 400;
64
- return err;
65
- }
66
- };
67
- return options;
68
- }
69
-
70
- //#endregion
71
3
  export { humanizeBytes, normalizeOptions, whitelist };
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,24 @@
1
+ import { EggFile, MultipartFileStream, MultipartOptions } from "./context-COfddVcC.js";
2
+ import { MultipartConfig } from "./config.default-CPo4ogXL.js";
3
+
4
+ //#region src/types.d.ts
5
+ declare module 'egg' {
6
+ interface EggAppConfig {
7
+ /**
8
+ * multipart parser options
9
+ * @member Config#multipart
10
+ */
11
+ multipart: MultipartConfig;
12
+ }
13
+ interface Request {
14
+ /**
15
+ * Files Object Array
16
+ */
17
+ files?: EggFile[];
18
+ }
19
+ interface Context {
20
+ saveRequestFiles(options?: MultipartOptions): Promise<void>;
21
+ getFileStream(options?: MultipartOptions): Promise<MultipartFileStream>;
22
+ cleanupRequestFiles(files?: EggFile[]): Promise<void>;
23
+ }
24
+ }
package/dist/types.d.ts CHANGED
@@ -1,24 +1,3 @@
1
- import { EggFile, MultipartFileStream, MultipartOptions } from "./app/extend/context.js";
2
- import { MultipartConfig } from "./config/config.default.js";
3
-
4
- //#region src/types.d.ts
5
- declare module 'egg' {
6
- interface EggAppConfig {
7
- /**
8
- * multipart parser options
9
- * @member Config#multipart
10
- */
11
- multipart: MultipartConfig;
12
- }
13
- interface Request {
14
- /**
15
- * Files Object Array
16
- */
17
- files?: EggFile[];
18
- }
19
- interface Context {
20
- saveRequestFiles(options?: MultipartOptions): Promise<void>;
21
- getFileStream(options?: MultipartOptions): Promise<MultipartFileStream>;
22
- cleanupRequestFiles(files?: EggFile[]): Promise<void>;
23
- }
24
- }
1
+ import "./context-COfddVcC.js";
2
+ import "./config.default-CPo4ogXL.js";
3
+ import "./types-DNLuS6JZ.js";
package/dist/types.js CHANGED
@@ -1 +1,3 @@
1
+ import "./types-BsuPrIpu.js";
2
+
1
3
  export { };
@@ -0,0 +1,71 @@
1
+ import path from "node:path";
2
+ import assert from "node:assert";
3
+ import bytes from "bytes";
4
+
5
+ //#region src/lib/utils.ts
6
+ const whitelist = [
7
+ ".jpg",
8
+ ".jpeg",
9
+ ".png",
10
+ ".gif",
11
+ ".bmp",
12
+ ".wbmp",
13
+ ".webp",
14
+ ".tif",
15
+ ".psd",
16
+ ".svg",
17
+ ".js",
18
+ ".jsx",
19
+ ".json",
20
+ ".css",
21
+ ".less",
22
+ ".html",
23
+ ".htm",
24
+ ".xml",
25
+ ".zip",
26
+ ".gz",
27
+ ".tgz",
28
+ ".gzip",
29
+ ".mp3",
30
+ ".mp4",
31
+ ".avi"
32
+ ];
33
+ function humanizeBytes(size) {
34
+ if (typeof size === "number") return size;
35
+ return bytes(size);
36
+ }
37
+ function normalizeOptions(options) {
38
+ options.fileSize = humanizeBytes(options.fileSize);
39
+ options.fieldSize = humanizeBytes(options.fieldSize);
40
+ options.fieldNameSize = humanizeBytes(options.fieldNameSize);
41
+ options.mode = options.mode || "stream";
42
+ assert(["stream", "file"].includes(options.mode), `Expect mode to be 'stream' or 'file', but got '${options.mode}'`);
43
+ if (options.mode === "file") assert(!options.fileModeMatch, "`fileModeMatch` options only work on stream mode, please remove it");
44
+ if (Array.isArray(options.whitelist)) options.whitelist = options.whitelist.map((extname) => extname.toLowerCase());
45
+ if (Array.isArray(options.fileExtensions)) options.fileExtensions = options.fileExtensions.map((extname) => {
46
+ return extname.startsWith(".") || extname === "" ? extname.toLowerCase() : `.${extname.toLowerCase()}`;
47
+ });
48
+ function checkExt(fileName) {
49
+ if (typeof options.whitelist === "function") return options.whitelist(fileName);
50
+ const extname = path.extname(fileName).toLowerCase();
51
+ if (Array.isArray(options.whitelist)) return options.whitelist.includes(extname);
52
+ return whitelist.includes(extname) || options.fileExtensions.includes(extname);
53
+ }
54
+ options.checkFile = (_fieldName, fileStream, fileName) => {
55
+ if (!fileStream || !fileName) return;
56
+ try {
57
+ if (!checkExt(fileName)) {
58
+ const err = /* @__PURE__ */ new Error("Invalid filename: " + fileName);
59
+ Reflect.set(err, "status", 400);
60
+ return err;
61
+ }
62
+ } catch (err) {
63
+ err.status = 400;
64
+ return err;
65
+ }
66
+ };
67
+ return options;
68
+ }
69
+
70
+ //#endregion
71
+ export { humanizeBytes, normalizeOptions, whitelist };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eggjs/multipart",
3
- "version": "5.0.0-beta.18",
3
+ "version": "5.0.0-beta.20",
4
4
  "description": "multipart plugin for egg",
5
5
  "eggPlugin": {
6
6
  "name": "multipart",
@@ -52,21 +52,21 @@
52
52
  "egg-path-matching": "^2.0.0"
53
53
  },
54
54
  "peerDependencies": {
55
- "egg": "4.1.0-beta.18"
55
+ "egg": "4.1.0-beta.20"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@types/bytes": "^3.1.5",
59
- "@types/node": "24.5.2",
59
+ "@types/node": "^24.6.2",
60
60
  "formstream": "^1.5.1",
61
61
  "is-type-of": "^2.2.0",
62
62
  "stream-wormhole": "^2.0.1",
63
63
  "tsdown": "^0.15.4",
64
- "typescript": "5.9.2",
64
+ "typescript": "^5.9.3",
65
65
  "urllib": "^4.8.2",
66
- "vitest": "4.0.0-beta.13",
67
- "@eggjs/mock": "7.0.0-beta.18",
68
- "@eggjs/tsconfig": "3.1.0-beta.18",
69
- "@eggjs/schedule": "6.0.0-beta.18"
66
+ "vitest": "4.0.0-beta.16",
67
+ "@eggjs/mock": "7.0.0-beta.20",
68
+ "@eggjs/schedule": "6.0.0-beta.20",
69
+ "@eggjs/tsconfig": "3.1.0-beta.20"
70
70
  },
71
71
  "files": [
72
72
  "dist"