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

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.
@@ -1,199 +0,0 @@
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 };
@@ -1,101 +0,0 @@
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 };
@@ -1 +0,0 @@
1
- export { };
@@ -1,24 +0,0 @@
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
- }
@@ -1,71 +0,0 @@
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 };