@midwayjs/busboy 3.17.3 → 3.18.2

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/dist/error.js CHANGED
@@ -4,13 +4,13 @@ exports.MultipartFieldsLimitError = exports.MultipartPartsLimitError = exports.M
4
4
  const core_1 = require("@midwayjs/core");
5
5
  class MultipartInvalidFilenameError extends core_1.httpError.BadRequestError {
6
6
  constructor(filename) {
7
- super(`Invalid upload file name "${filename}", please check it`);
7
+ super(`Invalid file name or suffix "${filename}".`);
8
8
  }
9
9
  }
10
10
  exports.MultipartInvalidFilenameError = MultipartInvalidFilenameError;
11
11
  class MultipartInvalidFileTypeError extends core_1.httpError.BadRequestError {
12
12
  constructor(filename, currentType, type) {
13
- super(`Invalid upload file type, "${filename}" type(${currentType || 'unknown'}) is not ${type} , please check it`);
13
+ super(`Invalid file type, "${filename}" type(${currentType || 'unknown'}) is not ${type}.`);
14
14
  }
15
15
  }
16
16
  exports.MultipartInvalidFileTypeError = MultipartInvalidFileTypeError;
@@ -2,7 +2,7 @@
2
2
  import { Readable } from 'stream';
3
3
  import { IgnoreMatcher, IMidwayContext } from '@midwayjs/core';
4
4
  import { BusboyConfig } from 'busboy';
5
- export type UploadMode = 'stream' | 'file';
5
+ export type UploadMode = 'stream' | 'file' | 'asyncIterator';
6
6
  export interface UploadOptions extends BusboyConfig {
7
7
  /**
8
8
  * Upload mode, default is `file`
@@ -50,6 +50,10 @@ export interface UploadFileInfo {
50
50
  * file data, a string of path
51
51
  */
52
52
  data: string;
53
+ /**
54
+ * field name
55
+ */
56
+ fieldName: string;
53
57
  }
54
58
  export interface UploadStreamFileInfo {
55
59
  /**
@@ -64,5 +68,19 @@ export interface UploadStreamFileInfo {
64
68
  * file data, Readable stream
65
69
  */
66
70
  data: Readable;
71
+ /**
72
+ * field name
73
+ */
74
+ fieldName: string;
75
+ }
76
+ export interface UploadStreamFieldInfo {
77
+ /**
78
+ * field name
79
+ */
80
+ name: string;
81
+ /**
82
+ * field value
83
+ */
84
+ value: any;
67
85
  }
68
86
  //# sourceMappingURL=interface.d.ts.map
@@ -1,6 +1,6 @@
1
1
  /// <reference types="node" />
2
2
  import { IMiddleware, ILogger, IgnoreMatcher, IMidwayApplication } from '@midwayjs/core';
3
- import { UploadOptions } from '.';
3
+ import { UploadOptions, UploadMode } from './interface';
4
4
  import { BusboyConfig } from 'busboy';
5
5
  export declare class UploadMiddleware implements IMiddleware<any, any> {
6
6
  uploadConfig: UploadOptions;
@@ -17,10 +17,13 @@ export declare class UploadMiddleware implements IMiddleware<any, any> {
17
17
  private uploadFileMimeTypeMap;
18
18
  match: IgnoreMatcher<any>[];
19
19
  ignore: IgnoreMatcher<any>[];
20
- init(): Promise<void>;
20
+ protected init(): Promise<void>;
21
21
  resolve(app: IMidwayApplication, options?: {
22
- mode?: 'file' | 'stream';
22
+ mode?: UploadMode;
23
23
  } & BusboyConfig): (ctxOrReq: any, resOrNext: any, next: any) => Promise<any>;
24
+ private processFileOrStream;
25
+ private processAsyncIterator;
26
+ private processServerlessUpload;
24
27
  static getName(): string;
25
28
  checkAndGetExt(filename: any, whiteListMap?: Map<string, string>): string | boolean;
26
29
  checkFileType(ext: string, data: Buffer, uploadFileMimeTypeMap?: Map<string, string[]>): Promise<{
@@ -14,11 +14,12 @@ const core_1 = require("@midwayjs/core");
14
14
  const path_1 = require("path");
15
15
  const fs_1 = require("fs");
16
16
  const stream_1 = require("stream");
17
- const _1 = require(".");
17
+ const error_1 = require("./error");
18
18
  const parse_1 = require("./parse");
19
19
  const file_type_1 = require("file-type");
20
20
  const utils_1 = require("./utils");
21
21
  const busboy = require("busboy");
22
+ const constants_1 = require("./constants");
22
23
  const { unlink, writeFile } = fs_1.promises;
23
24
  let UploadMiddleware = class UploadMiddleware {
24
25
  constructor() {
@@ -59,14 +60,14 @@ let UploadMiddleware = class UploadMiddleware {
59
60
  ? (0, core_1.extend)({}, this.uploadConfig, options || {})
60
61
  : this.uploadConfig;
61
62
  return async (ctxOrReq, resOrNext, next) => {
62
- var _a, _b;
63
+ var _a;
63
64
  const req = ((_a = ctxOrReq.request) === null || _a === void 0 ? void 0 : _a.req) || ctxOrReq.request || ctxOrReq;
64
65
  next = isExpress ? next : resOrNext;
65
66
  const boundary = this.getUploadBoundary(req);
66
67
  if (!boundary) {
67
68
  return next();
68
69
  }
69
- const { mode, tmpdir } = uploadConfig;
70
+ const useAsyncIterator = uploadConfig.mode === 'asyncIterator';
70
71
  // create new map include custom white list
71
72
  const currentContextWhiteListMap = new Map([
72
73
  ...this.uploadWhiteListMap.entries(),
@@ -105,166 +106,250 @@ let UploadMiddleware = class UploadMiddleware {
105
106
  }));
106
107
  };
107
108
  if (this.isReadableStream(req, isExpress)) {
108
- let isStreamResolve = false;
109
- const { files = [], fields = [] } = await new Promise((resolveP, reject) => {
110
- const bb = busboy({
111
- headers: req.headers,
112
- ...uploadConfig,
113
- });
114
- const fields = [];
115
- const files = [];
116
- let fileModeCount = 0;
117
- bb.on('file', async (name, file, info) => {
118
- const { filename, encoding, mimeType } = info;
119
- const ext = this.checkAndGetExt(filename, currentContextWhiteListMap);
120
- if (!ext) {
121
- reject(new _1.MultipartInvalidFilenameError(filename));
122
- }
123
- file.on('limit', () => {
124
- reject(new _1.MultipartFileSizeLimitError(filename));
109
+ if (useAsyncIterator) {
110
+ await this.processAsyncIterator(req, uploadConfig, currentContextWhiteListMap, currentContextMimeTypeWhiteListMap, ctxOrReq);
111
+ }
112
+ else {
113
+ await this.processFileOrStream(req, uploadConfig, currentContextWhiteListMap, currentContextMimeTypeWhiteListMap, ctxOrReq);
114
+ }
115
+ }
116
+ else {
117
+ await this.processServerlessUpload(req, boundary, uploadConfig, next, currentContextWhiteListMap, currentContextMimeTypeWhiteListMap, ctxOrReq);
118
+ }
119
+ await next();
120
+ };
121
+ }
122
+ async processFileOrStream(req, uploadConfig, currentContextWhiteListMap, currentContextMimeTypeWhiteListMap, ctxOrReq) {
123
+ let isStreamResolve = false;
124
+ const { mode, tmpdir } = uploadConfig;
125
+ const { files = [], fields = [] } = await new Promise((resolveP, reject) => {
126
+ const bb = busboy({
127
+ headers: req.headers,
128
+ ...uploadConfig,
129
+ });
130
+ const fields = [];
131
+ const files = [];
132
+ bb.on('file', async (name, file, info) => {
133
+ this.logger.debug('[busboy]: busboy file event');
134
+ const { filename, encoding, mimeType } = info;
135
+ const ext = this.checkAndGetExt(filename, currentContextWhiteListMap);
136
+ if (!ext) {
137
+ reject(new error_1.MultipartInvalidFilenameError(filename));
138
+ }
139
+ file.once('limit', () => {
140
+ reject(new error_1.MultipartFileSizeLimitError(filename));
141
+ });
142
+ if (mode === 'stream') {
143
+ if (isStreamResolve) {
144
+ // will be skip
145
+ file.resume();
146
+ return;
147
+ }
148
+ files.push(new Promise(resolve => {
149
+ resolve({
150
+ fieldName: name,
151
+ filename,
152
+ encoding,
153
+ mimeType,
154
+ data: file,
125
155
  });
126
- if (mode === 'stream') {
127
- if (isStreamResolve) {
128
- // will be skip
129
- file.resume();
130
- return;
131
- }
132
- files.push(new Promise(resolve => {
133
- resolve({
134
- filename,
135
- mimeType,
136
- encoding,
137
- data: file,
138
- });
139
- }));
140
- isStreamResolve = true;
141
- return resolveP({
142
- fields: await Promise.all(fields),
143
- files: await Promise.all(files),
144
- });
145
- }
146
- else {
147
- fileModeCount++;
148
- // file mode
149
- const requireId = `upload_${Date.now()}.${Math.random()}`;
150
- // read stream pipe to temp file
151
- const tempFile = (0, path_1.resolve)(tmpdir, `${requireId}${ext}`);
152
- // get buffer from stream, and check file type
153
- file.once('data', async (chunk) => {
154
- const { passed, mime, current } = await this.checkFileType(ext, chunk, currentContextMimeTypeWhiteListMap);
155
- if (!passed) {
156
- file.pause();
157
- reject(new _1.MultipartInvalidFileTypeError(filename, current, mime));
158
- }
159
- });
160
- const writeStream = file.pipe((0, fs_1.createWriteStream)(tempFile));
161
- file.on('end', () => {
162
- fileModeCount--;
163
- });
164
- writeStream.on('error', reject);
165
- writeStream.on('finish', async () => {
166
- files.push(new Promise(resolve => {
167
- resolve({
168
- filename,
169
- mimeType,
170
- encoding,
171
- data: tempFile,
172
- });
173
- }));
174
- if (fileModeCount === 0) {
175
- return resolveP({
176
- fields: await Promise.all(fields),
177
- files: await Promise.all(files),
178
- });
179
- }
180
- });
156
+ }));
157
+ isStreamResolve = true;
158
+ // busboy 这里无法触发 close 事件,所以这里直接 resolve
159
+ return resolveP({
160
+ fields,
161
+ files: await Promise.all(files),
162
+ });
163
+ }
164
+ else {
165
+ this.logger.debug('[busboy]: file mode, file data event');
166
+ // file mode
167
+ const requireId = `upload_${Date.now()}.${Math.random()}`;
168
+ // read stream pipe to temp file
169
+ const tempFile = (0, path_1.resolve)(tmpdir, `${requireId}${ext}`);
170
+ // get buffer from stream, and check file type
171
+ file.once('data', async (chunk) => {
172
+ const { passed, mime, current } = await this.checkFileType(ext, chunk, currentContextMimeTypeWhiteListMap);
173
+ if (!passed) {
174
+ file.pause();
175
+ reject(new error_1.MultipartInvalidFileTypeError(filename, current, mime));
181
176
  }
182
177
  });
183
- bb.on('field', (name, value, info) => {
184
- fields.push(new Promise(resolve => {
178
+ const fp = new Promise(resolve => {
179
+ const writeStream = file.pipe((0, fs_1.createWriteStream)(tempFile));
180
+ writeStream.on('error', reject);
181
+ writeStream.on('finish', () => {
185
182
  resolve({
186
- name,
187
- value,
183
+ filename,
184
+ mimeType,
185
+ encoding,
186
+ fieldName: name,
187
+ data: tempFile,
188
188
  });
189
- }));
190
- });
191
- bb.on('error', (err) => {
192
- reject(new _1.MultipartError(err));
193
- });
194
- bb.on('partsLimit', () => {
195
- reject(new _1.MultipartPartsLimitError());
196
- });
197
- bb.on('filesLimit', () => {
198
- reject(new _1.MultipartFileLimitError());
199
- });
200
- bb.on('fieldsLimit', () => {
201
- reject(new _1.MultipartFieldsLimitError());
189
+ });
202
190
  });
203
- req.pipe(bb);
191
+ files.push(fp);
192
+ }
193
+ });
194
+ bb.on('close', async () => {
195
+ this.logger.debug('[busboy]: busboy close');
196
+ // close 事件会在 busboy 解析完所有数据后触发,但是这个时候有可能没有写完文件,所以使用 Promise.all 等待所有文件写入完成
197
+ resolveP({
198
+ fields,
199
+ files: await Promise.all(files),
204
200
  });
205
- ctxOrReq.files = files;
206
- ctxOrReq.fields = fields.reduce((accumulator, current) => {
207
- accumulator[current.name] = current.value;
208
- return accumulator;
209
- }, {});
210
- Object.defineProperty(ctxOrReq, 'file', {
211
- get() {
212
- return ctxOrReq.files[0];
213
- },
214
- set(v) {
215
- ctxOrReq.files = [v];
216
- },
201
+ });
202
+ bb.on('field', (name, value, info) => {
203
+ fields.push({
204
+ name,
205
+ value,
206
+ info,
217
207
  });
218
- }
219
- else {
220
- let body;
221
- if (((_b = req === null || req === void 0 ? void 0 : req.originEvent) === null || _b === void 0 ? void 0 : _b.body) &&
222
- (typeof req.originEvent.body === 'string' ||
223
- Buffer.isBuffer(req.originEvent.body))) {
224
- body = req.originEvent.body;
225
- }
226
- else {
227
- body = req.body;
228
- }
229
- const data = await (0, parse_1.parseMultipart)(body, boundary, uploadConfig);
230
- if (!data) {
231
- return next();
232
- }
233
- ctxOrReq.fields = data.fields;
234
- const requireId = `upload_${Date.now()}.${Math.random()}`;
235
- const files = data.files;
236
- for (const fileInfo of files) {
237
- const ext = this.checkAndGetExt(fileInfo.filename, currentContextWhiteListMap);
238
- if (!ext) {
239
- throw new _1.MultipartInvalidFilenameError(fileInfo.filename);
240
- }
241
- const { passed, mime, current } = await this.checkFileType(ext, fileInfo.data, currentContextMimeTypeWhiteListMap);
208
+ });
209
+ bb.on('error', (err) => {
210
+ reject(new error_1.MultipartError(err));
211
+ });
212
+ bb.on('partsLimit', () => {
213
+ reject(new error_1.MultipartPartsLimitError());
214
+ });
215
+ bb.on('filesLimit', () => {
216
+ reject(new error_1.MultipartFileLimitError());
217
+ });
218
+ bb.on('fieldsLimit', () => {
219
+ reject(new error_1.MultipartFieldsLimitError());
220
+ });
221
+ req.pipe(bb);
222
+ });
223
+ ctxOrReq.files = files;
224
+ ctxOrReq.fields = fields.reduce((accumulator, current) => {
225
+ accumulator[current.name] = current.value;
226
+ return accumulator;
227
+ }, {});
228
+ }
229
+ async processAsyncIterator(req, uploadConfig, currentContextWhiteListMap, currentContextMimeTypeWhiteListMap, ctxOrReq) {
230
+ const bb = busboy({
231
+ headers: req.headers,
232
+ ...uploadConfig,
233
+ });
234
+ const fileReadable = new stream_1.Readable({
235
+ objectMode: true,
236
+ read() { },
237
+ autoDestroy: true,
238
+ });
239
+ const fieldReadable = new stream_1.Readable({
240
+ objectMode: true,
241
+ read() { },
242
+ autoDestroy: true,
243
+ });
244
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
245
+ const self = this;
246
+ async function* streamToAsyncIteratorWithError(stream) {
247
+ for await (const chunk of stream) {
248
+ chunk.data.once('data', async (chunk) => {
249
+ const { passed, mime, current } = await self.checkFileType(chunk.ext, chunk, currentContextMimeTypeWhiteListMap);
242
250
  if (!passed) {
243
- throw new _1.MultipartInvalidFileTypeError(fileInfo.filename, current, mime);
251
+ throw new error_1.MultipartInvalidFileTypeError(chunk.filename, current, mime);
244
252
  }
245
- fileInfo[_1.EXT_KEY] = ext;
246
- }
247
- ctxOrReq.files = await Promise.all(files.map(async (file, index) => {
248
- const { data } = file;
249
- if (mode === 'file') {
250
- const ext = file[_1.EXT_KEY];
251
- const tmpFileName = (0, path_1.resolve)(tmpdir, `${requireId}.${index}${ext}`);
252
- await writeFile(tmpFileName, data, 'binary');
253
- file.data = tmpFileName;
254
- }
255
- else if (mode === 'stream') {
256
- file.data = new stream_1.Readable({
257
- read() {
258
- this.push(data);
259
- this.push(null);
260
- },
261
- });
262
- }
263
- return file;
264
- }));
253
+ });
254
+ yield chunk;
265
255
  }
266
- await next();
267
- };
256
+ }
257
+ const files = streamToAsyncIteratorWithError(fileReadable);
258
+ const fields = (0, utils_1.streamToAsyncIterator)(fieldReadable);
259
+ bb.on('file', (name, file, info) => {
260
+ const { filename, encoding, mimeType } = info;
261
+ const ext = this.checkAndGetExt(filename, currentContextWhiteListMap);
262
+ if (!ext) {
263
+ fileReadable.destroy(new error_1.MultipartInvalidFilenameError(filename));
264
+ return;
265
+ }
266
+ file.once('limit', () => {
267
+ fileReadable.destroy(new error_1.MultipartFileSizeLimitError(filename));
268
+ });
269
+ fileReadable.push({
270
+ fieldName: name,
271
+ filename,
272
+ mimeType,
273
+ encoding,
274
+ data: file,
275
+ ext,
276
+ });
277
+ });
278
+ bb.on('field', (name, value, info) => {
279
+ fieldReadable.push({
280
+ name,
281
+ value,
282
+ info,
283
+ });
284
+ });
285
+ bb.on('close', () => {
286
+ fileReadable.push(null);
287
+ fieldReadable.push(null);
288
+ });
289
+ bb.on('error', (err) => {
290
+ fileReadable.destroy(new error_1.MultipartError(err));
291
+ });
292
+ bb.on('partsLimit', () => {
293
+ fileReadable.destroy(new error_1.MultipartPartsLimitError());
294
+ });
295
+ bb.on('filesLimit', () => {
296
+ fileReadable.destroy(new error_1.MultipartFileLimitError());
297
+ });
298
+ bb.on('fieldsLimit', () => {
299
+ fieldReadable.destroy(new error_1.MultipartFieldsLimitError());
300
+ });
301
+ req.pipe(bb);
302
+ ctxOrReq.fields = fields;
303
+ ctxOrReq.files = files;
304
+ }
305
+ async processServerlessUpload(req, boundary, uploadConfig, next, currentContextWhiteListMap, currentContextMimeTypeWhiteListMap, ctxOrReq) {
306
+ var _a;
307
+ let body;
308
+ if (((_a = req === null || req === void 0 ? void 0 : req.originEvent) === null || _a === void 0 ? void 0 : _a.body) &&
309
+ (typeof req.originEvent.body === 'string' ||
310
+ Buffer.isBuffer(req.originEvent.body))) {
311
+ body = req.originEvent.body;
312
+ }
313
+ else {
314
+ body = req.body;
315
+ }
316
+ const { mode, tmpdir } = uploadConfig;
317
+ const data = await (0, parse_1.parseMultipart)(body, boundary, uploadConfig);
318
+ if (!data) {
319
+ return next();
320
+ }
321
+ const requireId = `upload_${Date.now()}.${Math.random()}`;
322
+ for (const fileInfo of data.files) {
323
+ const ext = this.checkAndGetExt(fileInfo.filename, currentContextWhiteListMap);
324
+ if (!ext) {
325
+ throw new error_1.MultipartInvalidFilenameError(fileInfo.filename);
326
+ }
327
+ const { passed, mime, current } = await this.checkFileType(ext, fileInfo.data, currentContextMimeTypeWhiteListMap);
328
+ if (!passed) {
329
+ throw new error_1.MultipartInvalidFileTypeError(fileInfo.filename, current, mime);
330
+ }
331
+ fileInfo[constants_1.EXT_KEY] = ext;
332
+ }
333
+ const files = await Promise.all(data.files.map(async (file, index) => {
334
+ const { data } = file;
335
+ if (mode === 'file') {
336
+ const ext = file[constants_1.EXT_KEY];
337
+ const tmpFileName = (0, path_1.resolve)(tmpdir, `${requireId}.${index}${ext}`);
338
+ await writeFile(tmpFileName, data, 'binary');
339
+ file.data = tmpFileName;
340
+ }
341
+ else if (mode === 'stream') {
342
+ file.data = new stream_1.Readable({
343
+ read() {
344
+ this.push(data);
345
+ this.push(null);
346
+ },
347
+ });
348
+ }
349
+ return file;
350
+ }));
351
+ ctxOrReq.fields = data.fields;
352
+ ctxOrReq.files = files;
268
353
  }
269
354
  static getName() {
270
355
  return 'upload';
package/dist/utils.d.ts CHANGED
@@ -1,6 +1,9 @@
1
+ /// <reference types="node" />
2
+ import { Readable } from 'stream';
1
3
  export declare const autoRemoveUploadTmpFile: (tmpDir: string, cleanTimeout: number) => Promise<void>;
2
4
  export declare const stopAutoRemoveUploadTmpFile: () => Promise<void>;
3
5
  export declare const checkExists: (path: string) => Promise<boolean>;
4
6
  export declare const ensureDir: (dirPath: string) => Promise<boolean>;
5
7
  export declare const formatExt: (ext: string) => string;
8
+ export declare function streamToAsyncIterator(stream: Readable): AsyncGenerator<any>;
6
9
  //# sourceMappingURL=utils.d.ts.map
package/dist/utils.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.formatExt = exports.ensureDir = exports.checkExists = exports.stopAutoRemoveUploadTmpFile = exports.autoRemoveUploadTmpFile = void 0;
3
+ exports.streamToAsyncIterator = exports.formatExt = exports.ensureDir = exports.checkExists = exports.stopAutoRemoveUploadTmpFile = exports.autoRemoveUploadTmpFile = void 0;
4
4
  const fs_1 = require("fs");
5
5
  const path_1 = require("path");
6
6
  const { readdir, access, stat, unlink, mkdir } = fs_1.promises;
@@ -88,4 +88,10 @@ const formatExt = (ext) => {
88
88
  .toString();
89
89
  };
90
90
  exports.formatExt = formatExt;
91
+ async function* streamToAsyncIterator(stream) {
92
+ for await (const chunk of stream) {
93
+ yield chunk;
94
+ }
95
+ }
96
+ exports.streamToAsyncIterator = streamToAsyncIterator;
91
97
  //# sourceMappingURL=utils.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@midwayjs/busboy",
3
- "version": "3.17.3",
3
+ "version": "3.18.2",
4
4
  "description": "Midway Component for upload",
5
5
  "main": "dist/index.js",
6
6
  "typings": "index.d.ts",
@@ -27,12 +27,12 @@
27
27
  "file-type": "16.5.4"
28
28
  },
29
29
  "devDependencies": {
30
- "@midwayjs/core": "^3.17.1",
31
- "@midwayjs/express": "^3.17.3",
32
- "@midwayjs/faas": "^3.17.1",
33
- "@midwayjs/koa": "^3.17.1",
34
- "@midwayjs/mock": "^3.17.1",
35
- "@midwayjs/web": "^3.17.1"
30
+ "@midwayjs/core": "^3.18.0",
31
+ "@midwayjs/express": "^3.18.2",
32
+ "@midwayjs/faas": "^3.18.2",
33
+ "@midwayjs/koa": "^3.18.2",
34
+ "@midwayjs/mock": "^3.18.2",
35
+ "@midwayjs/web": "^3.18.2"
36
36
  },
37
- "gitHead": "f4086c0e118673052202ffa92149aacd89e9ddc5"
37
+ "gitHead": "0cf1ac5a107aa1de73ff184f403cba2be0a408f4"
38
38
  }