@midwayjs/busboy 3.16.0

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 ADDED
@@ -0,0 +1,12 @@
1
+ # midway busboy module
2
+
3
+ [![Package Quality](http://npm.packagequality.com/shield/@midwayjs/validate.svg)](http://packagequality.com/#?package=@midwayjs/busboy)
4
+ [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/midwayjs/midway/pulls)
5
+
6
+ this is a sub package for midway.
7
+
8
+ Document: [https://midwayjs.org](https://midwayjs.org)
9
+
10
+ ## License
11
+
12
+ [MIT]((http://github.com/midwayjs/midway/blob/master/LICENSE))
@@ -0,0 +1,9 @@
1
+ import { ILogger } from '@midwayjs/core';
2
+ import { UploadOptions } from './interface';
3
+ export declare class BusboyConfiguration {
4
+ uploadConfig: UploadOptions;
5
+ logger: ILogger;
6
+ onReady(): Promise<void>;
7
+ onStop(): Promise<void>;
8
+ }
9
+ //# sourceMappingURL=configuration.d.ts.map
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.BusboyConfiguration = void 0;
13
+ const core_1 = require("@midwayjs/core");
14
+ const utils_1 = require("./utils");
15
+ const constants_1 = require("./constants");
16
+ const path_1 = require("path");
17
+ const os_1 = require("os");
18
+ let BusboyConfiguration = class BusboyConfiguration {
19
+ async onReady() {
20
+ const { tmpdir, cleanTimeout, mode } = this.uploadConfig;
21
+ if (mode === 'file' && tmpdir) {
22
+ await (0, utils_1.ensureDir)(tmpdir);
23
+ if (cleanTimeout) {
24
+ (0, utils_1.autoRemoveUploadTmpFile)(tmpdir, cleanTimeout).catch(err => {
25
+ this.logger.error(err);
26
+ });
27
+ }
28
+ }
29
+ }
30
+ async onStop() {
31
+ await (0, utils_1.stopAutoRemoveUploadTmpFile)();
32
+ }
33
+ };
34
+ __decorate([
35
+ (0, core_1.Config)('busboy'),
36
+ __metadata("design:type", Object)
37
+ ], BusboyConfiguration.prototype, "uploadConfig", void 0);
38
+ __decorate([
39
+ (0, core_1.Logger)('coreLogger'),
40
+ __metadata("design:type", Object)
41
+ ], BusboyConfiguration.prototype, "logger", void 0);
42
+ BusboyConfiguration = __decorate([
43
+ (0, core_1.Configuration)({
44
+ namespace: 'busboy',
45
+ importConfigs: [
46
+ {
47
+ default: {
48
+ busboy: {
49
+ mode: 'file',
50
+ whitelist: constants_1.uploadWhiteList,
51
+ tmpdir: (0, path_1.join)((0, os_1.tmpdir)(), 'midway-busboy-files'),
52
+ cleanTimeout: 5 * 60 * 1000,
53
+ base64: false,
54
+ },
55
+ },
56
+ },
57
+ ],
58
+ })
59
+ ], BusboyConfiguration);
60
+ exports.BusboyConfiguration = BusboyConfiguration;
61
+ //# sourceMappingURL=configuration.js.map
@@ -0,0 +1,24 @@
1
+ export declare const uploadWhiteList: string[];
2
+ export declare const DefaultUploadFileMimeType: {
3
+ '.jpg': string;
4
+ '.jpeg': string;
5
+ '.png': string;
6
+ '.gif': string;
7
+ '.bmp': string;
8
+ '.wbmp': string;
9
+ '.webp': string;
10
+ '.tif': string;
11
+ '.tiff': string;
12
+ '.psd': string;
13
+ '.svg': string;
14
+ '.xml': string;
15
+ '.pdf': string;
16
+ '.zip': string;
17
+ '.gz': string;
18
+ '.gzip': string;
19
+ '.mp3': string;
20
+ '.mp4': string;
21
+ '.avi': string;
22
+ };
23
+ export declare const EXT_KEY: unique symbol;
24
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EXT_KEY = exports.DefaultUploadFileMimeType = exports.uploadWhiteList = void 0;
4
+ exports.uploadWhiteList = [
5
+ // images
6
+ '.jpg',
7
+ '.jpeg',
8
+ '.png',
9
+ '.gif',
10
+ '.bmp',
11
+ '.wbmp',
12
+ '.webp',
13
+ '.tif',
14
+ '.tiff',
15
+ '.psd',
16
+ // text
17
+ '.svg',
18
+ '.js',
19
+ '.jsx',
20
+ '.json',
21
+ '.css',
22
+ '.less',
23
+ '.html',
24
+ '.htm',
25
+ '.xml',
26
+ '.pdf',
27
+ // tar
28
+ '.zip',
29
+ '.gz',
30
+ '.tgz',
31
+ '.gzip',
32
+ // video
33
+ '.mp3',
34
+ '.mp4',
35
+ '.avi',
36
+ ];
37
+ // https://mimetype.io/
38
+ exports.DefaultUploadFileMimeType = {
39
+ '.jpg': 'image/jpeg',
40
+ '.jpeg': 'image/jpeg',
41
+ '.png': 'image/png',
42
+ '.gif': 'image/gif',
43
+ '.bmp': 'image/bmp',
44
+ '.wbmp': 'image/vnd.wap.wbmp',
45
+ '.webp': 'image/webp',
46
+ '.tif': 'image/tiff',
47
+ '.tiff': 'image/tiff',
48
+ '.psd': 'image/vnd.adobe.photoshop',
49
+ '.svg': 'image/svg+xml',
50
+ '.xml': 'application/xml',
51
+ '.pdf': 'application/pdf',
52
+ '.zip': 'application/zip',
53
+ '.gz': 'application/gzip',
54
+ '.gzip': 'application/gzip',
55
+ '.mp3': 'audio/mpeg',
56
+ '.mp4': 'video/mp4',
57
+ '.avi': 'video/x-msvideo',
58
+ };
59
+ exports.EXT_KEY = Symbol('_ext');
60
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1,23 @@
1
+ import { httpError } from '@midwayjs/core';
2
+ export declare class MultipartInvalidFilenameError extends httpError.BadRequestError {
3
+ constructor(filename: string);
4
+ }
5
+ export declare class MultipartInvalidFileTypeError extends httpError.BadRequestError {
6
+ constructor(filename: string, currentType: string, type: string);
7
+ }
8
+ export declare class MultipartFileSizeLimitError extends httpError.BadRequestError {
9
+ constructor(filename: string);
10
+ }
11
+ export declare class MultipartError extends httpError.BadRequestError {
12
+ constructor(err: Error);
13
+ }
14
+ export declare class MultipartFileLimitError extends httpError.BadRequestError {
15
+ constructor();
16
+ }
17
+ export declare class MultipartPartsLimitError extends httpError.BadRequestError {
18
+ constructor();
19
+ }
20
+ export declare class MultipartFieldsLimitError extends httpError.BadRequestError {
21
+ constructor();
22
+ }
23
+ //# sourceMappingURL=error.d.ts.map
package/dist/error.js ADDED
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MultipartFieldsLimitError = exports.MultipartPartsLimitError = exports.MultipartFileLimitError = exports.MultipartError = exports.MultipartFileSizeLimitError = exports.MultipartInvalidFileTypeError = exports.MultipartInvalidFilenameError = void 0;
4
+ const core_1 = require("@midwayjs/core");
5
+ class MultipartInvalidFilenameError extends core_1.httpError.BadRequestError {
6
+ constructor(filename) {
7
+ super(`Invalid upload file name "${filename}", please check it`);
8
+ }
9
+ }
10
+ exports.MultipartInvalidFilenameError = MultipartInvalidFilenameError;
11
+ class MultipartInvalidFileTypeError extends core_1.httpError.BadRequestError {
12
+ constructor(filename, currentType, type) {
13
+ super(`Invalid upload file type, "${filename}" type(${currentType || 'unknown'}) is not ${type} , please check it`);
14
+ }
15
+ }
16
+ exports.MultipartInvalidFileTypeError = MultipartInvalidFileTypeError;
17
+ class MultipartFileSizeLimitError extends core_1.httpError.BadRequestError {
18
+ constructor(filename) {
19
+ super(`Upload file "${filename}" size exceeds the limit`);
20
+ }
21
+ }
22
+ exports.MultipartFileSizeLimitError = MultipartFileSizeLimitError;
23
+ class MultipartError extends core_1.httpError.BadRequestError {
24
+ constructor(err) {
25
+ super(err.message);
26
+ }
27
+ }
28
+ exports.MultipartError = MultipartError;
29
+ class MultipartFileLimitError extends core_1.httpError.BadRequestError {
30
+ constructor() {
31
+ super('Upload file count exceeds the limit');
32
+ }
33
+ }
34
+ exports.MultipartFileLimitError = MultipartFileLimitError;
35
+ // partsLimit
36
+ class MultipartPartsLimitError extends core_1.httpError.BadRequestError {
37
+ constructor() {
38
+ super('Upload parts count exceeds the limit');
39
+ }
40
+ }
41
+ exports.MultipartPartsLimitError = MultipartPartsLimitError;
42
+ // fieldsLimit
43
+ class MultipartFieldsLimitError extends core_1.httpError.BadRequestError {
44
+ constructor() {
45
+ super('Upload fields count exceeds the limit');
46
+ }
47
+ }
48
+ exports.MultipartFieldsLimitError = MultipartFieldsLimitError;
49
+ //# sourceMappingURL=error.js.map
@@ -0,0 +1,6 @@
1
+ export { BusboyConfiguration as Configuration } from './configuration';
2
+ export * from './interface';
3
+ export * from './middleware';
4
+ export * from './error';
5
+ export * from './constants';
6
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.Configuration = void 0;
18
+ var configuration_1 = require("./configuration");
19
+ Object.defineProperty(exports, "Configuration", { enumerable: true, get: function () { return configuration_1.BusboyConfiguration; } });
20
+ __exportStar(require("./interface"), exports);
21
+ __exportStar(require("./middleware"), exports);
22
+ __exportStar(require("./error"), exports);
23
+ __exportStar(require("./constants"), exports);
24
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,68 @@
1
+ /// <reference types="node" />
2
+ import { Readable } from 'stream';
3
+ import { IgnoreMatcher, IMidwayContext } from '@midwayjs/core';
4
+ import { BusboyConfig } from 'busboy';
5
+ export type UploadMode = 'stream' | 'file';
6
+ export interface UploadOptions extends BusboyConfig {
7
+ /**
8
+ * Upload mode, default is `file`
9
+ */
10
+ mode?: UploadMode;
11
+ /**
12
+ * The white ext file names
13
+ */
14
+ whitelist?: string[] | null | ((ctx: IMidwayContext<any>) => string[]);
15
+ /**
16
+ * Temporary file directory
17
+ */
18
+ tmpdir?: string;
19
+ /**
20
+ * Temporary file automatic cleanup time, default is 5 minutes
21
+ */
22
+ cleanTimeout?: number;
23
+ /**
24
+ * Whether the uploaded body is base64, for example, apigw of Tencent Cloud
25
+ */
26
+ base64?: boolean;
27
+ /**
28
+ * Which paths to ignore
29
+ */
30
+ ignore?: IgnoreMatcher<any> | IgnoreMatcher<any>[];
31
+ /**
32
+ * Match those paths with higher priority than ignore
33
+ */
34
+ match?: IgnoreMatcher<any> | IgnoreMatcher<any>[];
35
+ /**
36
+ * Mime type white list
37
+ */
38
+ mimeTypeWhiteList?: Record<string, string | string[]> | ((ctx: IMidwayContext<any>) => string | string[]);
39
+ }
40
+ export interface UploadFileInfo {
41
+ /**
42
+ * File name
43
+ */
44
+ filename: string;
45
+ /**
46
+ * file mime type
47
+ */
48
+ mimeType: string;
49
+ /**
50
+ * file data, a string of path
51
+ */
52
+ data: string;
53
+ }
54
+ export interface UploadStreamFileInfo {
55
+ /**
56
+ * File name
57
+ */
58
+ filename: string;
59
+ /**
60
+ * file mime type
61
+ */
62
+ mimeType: string;
63
+ /**
64
+ * file data, Readable stream
65
+ */
66
+ data: Readable;
67
+ }
68
+ //# sourceMappingURL=interface.d.ts.map
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=interface.js.map
@@ -0,0 +1,34 @@
1
+ /// <reference types="node" />
2
+ import { IMiddleware, ILogger, IgnoreMatcher, IMidwayApplication } from '@midwayjs/core';
3
+ import { UploadOptions } from '.';
4
+ import { BusboyConfig } from 'busboy';
5
+ export declare class UploadMiddleware implements IMiddleware<any, any> {
6
+ uploadConfig: UploadOptions;
7
+ logger: ILogger;
8
+ /**
9
+ * cache global upload white list when uploadConfig.whitelist is set
10
+ * @private
11
+ */
12
+ private uploadWhiteListMap;
13
+ /**
14
+ * cache global upload mime type white list when uploadConfig.mimeTypeWhiteList is set
15
+ * @private
16
+ */
17
+ private uploadFileMimeTypeMap;
18
+ match: IgnoreMatcher<any>[];
19
+ ignore: IgnoreMatcher<any>[];
20
+ init(): Promise<void>;
21
+ resolve(app: IMidwayApplication, options?: {
22
+ mode?: 'file' | 'stream';
23
+ } & BusboyConfig): (ctxOrReq: any, resOrNext: any, next: any) => Promise<any>;
24
+ static getName(): string;
25
+ checkAndGetExt(filename: any, whiteListMap?: Map<string, string>): string | boolean;
26
+ checkFileType(ext: string, data: Buffer, uploadFileMimeTypeMap?: Map<string, string[]>): Promise<{
27
+ passed: boolean;
28
+ mime?: string;
29
+ current?: string;
30
+ }>;
31
+ isReadableStream(req: any, isExpress: any): boolean;
32
+ getUploadBoundary(request: any): false | string;
33
+ }
34
+ //# sourceMappingURL=middleware.d.ts.map
@@ -0,0 +1,366 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.UploadMiddleware = void 0;
13
+ const core_1 = require("@midwayjs/core");
14
+ const path_1 = require("path");
15
+ const fs_1 = require("fs");
16
+ const stream_1 = require("stream");
17
+ const _1 = require(".");
18
+ const parse_1 = require("./parse");
19
+ const file_type_1 = require("file-type");
20
+ const utils_1 = require("./utils");
21
+ const busboy = require("busboy");
22
+ const { unlink, writeFile } = fs_1.promises;
23
+ let UploadMiddleware = class UploadMiddleware {
24
+ constructor() {
25
+ /**
26
+ * cache global upload white list when uploadConfig.whitelist is set
27
+ * @private
28
+ */
29
+ this.uploadWhiteListMap = new Map();
30
+ /**
31
+ * cache global upload mime type white list when uploadConfig.mimeTypeWhiteList is set
32
+ * @private
33
+ */
34
+ this.uploadFileMimeTypeMap = new Map();
35
+ }
36
+ async init() {
37
+ if (this.uploadConfig.match) {
38
+ this.match = [].concat(this.uploadConfig.match || []);
39
+ }
40
+ else {
41
+ this.ignore = [].concat(this.uploadConfig.ignore || []);
42
+ }
43
+ if (this.uploadConfig.whitelist &&
44
+ Array.isArray(this.uploadConfig.whitelist)) {
45
+ for (const whiteExt of this.uploadConfig.whitelist) {
46
+ this.uploadWhiteListMap.set(whiteExt, whiteExt);
47
+ }
48
+ }
49
+ if (this.uploadConfig.mimeTypeWhiteList) {
50
+ for (const ext in this.uploadConfig.mimeTypeWhiteList) {
51
+ const mime = [].concat(this.uploadConfig.mimeTypeWhiteList[ext]);
52
+ this.uploadFileMimeTypeMap.set(ext, mime);
53
+ }
54
+ }
55
+ }
56
+ resolve(app, options) {
57
+ const isExpress = app.getNamespace() === 'express';
58
+ const uploadConfig = options
59
+ ? (0, core_1.extend)({}, this.uploadConfig, options || {})
60
+ : this.uploadConfig;
61
+ return async (ctxOrReq, resOrNext, next) => {
62
+ var _a, _b;
63
+ const req = ((_a = ctxOrReq.request) === null || _a === void 0 ? void 0 : _a.req) || ctxOrReq.request || ctxOrReq;
64
+ next = isExpress ? next : resOrNext;
65
+ const boundary = this.getUploadBoundary(req);
66
+ if (!boundary) {
67
+ return next();
68
+ }
69
+ const { mode, tmpdir } = uploadConfig;
70
+ // create new map include custom white list
71
+ const currentContextWhiteListMap = new Map([
72
+ ...this.uploadWhiteListMap.entries(),
73
+ ]);
74
+ if (typeof uploadConfig.whitelist === 'function') {
75
+ const whiteListArray = uploadConfig.whitelist.call(this, ctxOrReq);
76
+ whiteListArray.forEach(ext => currentContextWhiteListMap.set(ext, ext));
77
+ }
78
+ // create new map include custom mime type white list
79
+ const currentContextMimeTypeWhiteListMap = new Map([
80
+ ...this.uploadFileMimeTypeMap.entries(),
81
+ ]);
82
+ if (typeof uploadConfig.mimeTypeWhiteList === 'function') {
83
+ const mimeTypeWhiteList = uploadConfig.mimeTypeWhiteList.call(this, ctxOrReq);
84
+ for (const ext in mimeTypeWhiteList) {
85
+ const mime = [].concat(mimeTypeWhiteList[ext]);
86
+ currentContextMimeTypeWhiteListMap.set(ext, mime);
87
+ }
88
+ }
89
+ ctxOrReq.cleanupRequestFiles = async () => {
90
+ var _a;
91
+ if (!((_a = ctxOrReq.files) === null || _a === void 0 ? void 0 : _a.length)) {
92
+ return [];
93
+ }
94
+ return Promise.all(ctxOrReq.files.map(async (fileInfo) => {
95
+ if (typeof fileInfo.data !== 'string') {
96
+ return false;
97
+ }
98
+ try {
99
+ await unlink(fileInfo.data);
100
+ return true;
101
+ }
102
+ catch (_a) {
103
+ return false;
104
+ }
105
+ }));
106
+ };
107
+ 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));
125
+ });
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
+ });
181
+ }
182
+ });
183
+ bb.on('field', (name, value, info) => {
184
+ fields.push(new Promise(resolve => {
185
+ resolve({
186
+ name,
187
+ value,
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());
202
+ });
203
+ req.pipe(bb);
204
+ });
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
+ },
217
+ });
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);
242
+ if (!passed) {
243
+ throw new _1.MultipartInvalidFileTypeError(fileInfo.filename, current, mime);
244
+ }
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
+ }));
265
+ }
266
+ await next();
267
+ };
268
+ }
269
+ static getName() {
270
+ return 'upload';
271
+ }
272
+ // check extensions
273
+ checkAndGetExt(filename, whiteListMap = this.uploadWhiteListMap) {
274
+ const lowerCaseFileNameList = filename.toLowerCase().split('.');
275
+ while (lowerCaseFileNameList.length) {
276
+ lowerCaseFileNameList.shift();
277
+ const curExt = `.${lowerCaseFileNameList.join('.')}`;
278
+ if (this.uploadConfig.whitelist === null) {
279
+ return (0, utils_1.formatExt)(curExt);
280
+ }
281
+ if (whiteListMap.has(curExt)) {
282
+ // Avoid the presence of hidden characters and return extensions in the white list.
283
+ return whiteListMap.get(curExt);
284
+ }
285
+ }
286
+ return false;
287
+ }
288
+ // check file-type
289
+ async checkFileType(ext, data, uploadFileMimeTypeMap = this.uploadFileMimeTypeMap) {
290
+ // fileType == null, pass check
291
+ if (!this.uploadConfig.mimeTypeWhiteList) {
292
+ return { passed: true };
293
+ }
294
+ const mime = uploadFileMimeTypeMap.get(ext);
295
+ if (!mime) {
296
+ return { passed: false, mime: ext };
297
+ }
298
+ if (!mime.length) {
299
+ return { passed: true };
300
+ }
301
+ const typeInfo = await (0, file_type_1.fromBuffer)(data);
302
+ if (!typeInfo) {
303
+ return { passed: false, mime: mime.join('、') };
304
+ }
305
+ const findMime = mime.find(mimeItem => mimeItem === typeInfo.mime);
306
+ return {
307
+ passed: !!findMime,
308
+ mime: mime.join('、'),
309
+ current: typeInfo.mime,
310
+ };
311
+ }
312
+ isReadableStream(req, isExpress) {
313
+ // ref: https://github.com/rvagg/isstream/blob/master/isstream.js#L10
314
+ if (req instanceof stream_1.Stream &&
315
+ typeof req._read === 'function' &&
316
+ typeof req._readableState === 'object' &&
317
+ (!req.body || isExpress)) {
318
+ return true;
319
+ }
320
+ if (req.pipe && req.on && !req.body) {
321
+ return true;
322
+ }
323
+ return false;
324
+ }
325
+ getUploadBoundary(request) {
326
+ var _a;
327
+ const method = (request.method || request.httpMethod || '').toUpperCase();
328
+ if (method !== 'POST' &&
329
+ method !== 'PUT' &&
330
+ method !== 'DELETE' &&
331
+ method !== 'PATCH') {
332
+ return false;
333
+ }
334
+ if (!((_a = request.headers) === null || _a === void 0 ? void 0 : _a['content-type'])) {
335
+ return false;
336
+ }
337
+ const contentType = request.headers['content-type'];
338
+ if (!contentType.startsWith('multipart/form-data;')) {
339
+ return false;
340
+ }
341
+ const boundaryMatch = /boundary=(.*)(;|\s|$)/.exec(contentType);
342
+ if (!(boundaryMatch === null || boundaryMatch === void 0 ? void 0 : boundaryMatch[1])) {
343
+ return false;
344
+ }
345
+ return boundaryMatch[1];
346
+ }
347
+ };
348
+ __decorate([
349
+ (0, core_1.Config)('busboy'),
350
+ __metadata("design:type", Object)
351
+ ], UploadMiddleware.prototype, "uploadConfig", void 0);
352
+ __decorate([
353
+ (0, core_1.Logger)(),
354
+ __metadata("design:type", Object)
355
+ ], UploadMiddleware.prototype, "logger", void 0);
356
+ __decorate([
357
+ (0, core_1.Init)(),
358
+ __metadata("design:type", Function),
359
+ __metadata("design:paramtypes", []),
360
+ __metadata("design:returntype", Promise)
361
+ ], UploadMiddleware.prototype, "init", null);
362
+ UploadMiddleware = __decorate([
363
+ (0, core_1.Middleware)()
364
+ ], UploadMiddleware);
365
+ exports.UploadMiddleware = UploadMiddleware;
366
+ //# sourceMappingURL=middleware.js.map
@@ -0,0 +1,10 @@
1
+ /// <reference types="node" />
2
+ import { UploadOptions } from './interface';
3
+ export declare const parseMultipart: (body: any, boundary: string, uploadConfig: UploadOptions) => Promise<{
4
+ files: any[];
5
+ fields: {};
6
+ }>;
7
+ export declare const bufferIndexOf: (buffer: Buffer, search: Buffer, offset?: number) => number;
8
+ export declare const bufferSplit: (buffer: Buffer, separator: Buffer, limit?: number) => Buffer[];
9
+ export declare const parseHead: (headBuf: Buffer) => {};
10
+ //# sourceMappingURL=parse.d.ts.map
package/dist/parse.js ADDED
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseHead = exports.bufferSplit = exports.bufferIndexOf = exports.parseMultipart = void 0;
4
+ const headSeparator = Buffer.from('\r\n\r\n');
5
+ const parseMultipart = async (body, boundary, uploadConfig) => {
6
+ if (typeof body === 'string') {
7
+ if (uploadConfig.base64) {
8
+ body = Buffer.from(body, 'base64');
9
+ }
10
+ else {
11
+ body = Buffer.from(body);
12
+ }
13
+ }
14
+ const bufferSeparator = Buffer.from('\r\n--' + boundary);
15
+ const fields = {};
16
+ const files = [];
17
+ (0, exports.bufferSplit)(body, bufferSeparator).forEach(buf => {
18
+ const [headerBuf, data] = (0, exports.bufferSplit)(buf, headSeparator, 2);
19
+ const head = (0, exports.parseHead)(headerBuf);
20
+ if (!head['content-disposition']) {
21
+ return;
22
+ }
23
+ if (!head['content-disposition'].filename) {
24
+ if (head['content-disposition'].name) {
25
+ fields[head['content-disposition'].name] = data.toString();
26
+ }
27
+ return;
28
+ }
29
+ files.push({
30
+ filename: head['content-disposition'].filename,
31
+ data,
32
+ fieldName: head['content-disposition'].name,
33
+ mimeType: head['content-type'],
34
+ });
35
+ });
36
+ return {
37
+ files,
38
+ fields,
39
+ };
40
+ };
41
+ exports.parseMultipart = parseMultipart;
42
+ // search buffer index
43
+ const bufferIndexOf = (buffer, search, offset) => {
44
+ return buffer.indexOf(search, offset);
45
+ };
46
+ exports.bufferIndexOf = bufferIndexOf;
47
+ // split buffer to buffer list
48
+ const bufferSplit = (buffer, separator, limit) => {
49
+ let index = 0;
50
+ const result = [];
51
+ let find = (0, exports.bufferIndexOf)(buffer, separator, index);
52
+ while (find !== -1) {
53
+ result.push(buffer.slice(index, find));
54
+ index = find + separator.length;
55
+ if (limit && result.length + 1 === limit) {
56
+ break;
57
+ }
58
+ find = (0, exports.bufferIndexOf)(buffer, separator, index);
59
+ }
60
+ result.push(buffer.slice(index));
61
+ return result;
62
+ };
63
+ exports.bufferSplit = bufferSplit;
64
+ const headReg = /^([^:]+):[ \t]?(.+)?$/;
65
+ const parseHead = (headBuf) => {
66
+ const head = {};
67
+ const headStrList = headBuf.toString().split('\r\n');
68
+ for (const headStr of headStrList) {
69
+ const matched = headReg.exec(headStr);
70
+ if (!matched) {
71
+ continue;
72
+ }
73
+ const name = matched[1].toLowerCase();
74
+ const value = matched[2]
75
+ ? matched[2].replace(/&#(\d+);/g, (origin, code) => {
76
+ try {
77
+ return String.fromCharCode(parseInt(code));
78
+ }
79
+ catch (_a) {
80
+ return origin;
81
+ }
82
+ })
83
+ : '';
84
+ if (name === 'content-disposition') {
85
+ const headCol = {};
86
+ value.split(/;\s+/).forEach((kv) => {
87
+ const [k, v] = kv.split('=');
88
+ headCol[k] = v ? v.replace(/^"/, '').replace(/"$/, '') : v !== null && v !== void 0 ? v : true;
89
+ });
90
+ head[name] = headCol;
91
+ }
92
+ else {
93
+ head[name] = value;
94
+ }
95
+ }
96
+ return head;
97
+ };
98
+ exports.parseHead = parseHead;
99
+ //# sourceMappingURL=parse.js.map
@@ -0,0 +1,6 @@
1
+ export declare const autoRemoveUploadTmpFile: (tmpDir: string, cleanTimeout: number) => Promise<void>;
2
+ export declare const stopAutoRemoveUploadTmpFile: () => Promise<void>;
3
+ export declare const checkExists: (path: string) => Promise<boolean>;
4
+ export declare const ensureDir: (dirPath: string) => Promise<boolean>;
5
+ export declare const formatExt: (ext: string) => string;
6
+ //# sourceMappingURL=utils.d.ts.map
package/dist/utils.js ADDED
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatExt = exports.ensureDir = exports.checkExists = exports.stopAutoRemoveUploadTmpFile = exports.autoRemoveUploadTmpFile = void 0;
4
+ const fs_1 = require("fs");
5
+ const path_1 = require("path");
6
+ const { readdir, access, stat, unlink, mkdir } = fs_1.promises;
7
+ let autoRemoveUploadTmpFileTimeoutHandler;
8
+ let autoRemoveUploadTmpFilePromise;
9
+ const autoRemoveUploadTmpFile = async (tmpDir, cleanTimeout) => {
10
+ clearTimeout(autoRemoveUploadTmpFileTimeoutHandler);
11
+ let waitTime = cleanTimeout / 3;
12
+ if (waitTime < 1000) {
13
+ waitTime = 1000;
14
+ }
15
+ if (autoRemoveUploadTmpFilePromise) {
16
+ const exists = await (0, exports.checkExists)(tmpDir);
17
+ if (exists) {
18
+ const paths = await readdir(tmpDir);
19
+ const now = Date.now();
20
+ await Promise.all(paths.map(async (path) => {
21
+ const filePath = (0, path_1.join)(tmpDir, path);
22
+ try {
23
+ const statInfo = await stat(filePath);
24
+ if (statInfo.isFile() && now - statInfo.ctimeMs > cleanTimeout) {
25
+ await unlink(filePath);
26
+ }
27
+ }
28
+ catch (_a) {
29
+ return false;
30
+ }
31
+ }));
32
+ }
33
+ }
34
+ autoRemoveUploadTmpFileTimeoutHandler = setTimeout(() => {
35
+ autoRemoveUploadTmpFilePromise = (0, exports.autoRemoveUploadTmpFile)(tmpDir, cleanTimeout);
36
+ }, waitTime);
37
+ };
38
+ exports.autoRemoveUploadTmpFile = autoRemoveUploadTmpFile;
39
+ const stopAutoRemoveUploadTmpFile = async () => {
40
+ if (autoRemoveUploadTmpFilePromise) {
41
+ await autoRemoveUploadTmpFilePromise;
42
+ autoRemoveUploadTmpFilePromise = null;
43
+ }
44
+ clearTimeout(autoRemoveUploadTmpFileTimeoutHandler);
45
+ };
46
+ exports.stopAutoRemoveUploadTmpFile = stopAutoRemoveUploadTmpFile;
47
+ const checkExists = async (path) => {
48
+ try {
49
+ await access(path, fs_1.constants.W_OK | fs_1.constants.R_OK);
50
+ return true;
51
+ }
52
+ catch (_a) {
53
+ return false;
54
+ }
55
+ };
56
+ exports.checkExists = checkExists;
57
+ const ensureDir = async (dirPath) => {
58
+ const isExists = await (0, exports.checkExists)(dirPath);
59
+ if (isExists) {
60
+ return true;
61
+ }
62
+ try {
63
+ await mkdir(dirPath, { recursive: true });
64
+ return true;
65
+ }
66
+ catch (_a) {
67
+ return false;
68
+ }
69
+ };
70
+ exports.ensureDir = ensureDir;
71
+ const formatExt = (ext) => {
72
+ return Buffer.from(ext.toLowerCase())
73
+ .filter(ext => {
74
+ // .
75
+ if (ext === 0x2e) {
76
+ return true;
77
+ }
78
+ // 0-9
79
+ if (ext >= 0x30 && ext <= 0x39) {
80
+ return true;
81
+ }
82
+ // a-z
83
+ if (ext >= 0x61 && ext <= 0x7a) {
84
+ return true;
85
+ }
86
+ return false;
87
+ })
88
+ .toString();
89
+ };
90
+ exports.formatExt = formatExt;
91
+ //# sourceMappingURL=utils.js.map
package/index.d.ts ADDED
@@ -0,0 +1,39 @@
1
+ import { UploadFileInfo, UploadOptions } from './dist/index';
2
+ export * from './dist/index';
3
+
4
+ declare module '@midwayjs/core/dist/interface' {
5
+ interface MidwayConfig {
6
+ busboy?: Partial<UploadOptions>;
7
+ }
8
+ }
9
+ declare module '@midwayjs/koa/dist/interface' {
10
+ interface Context {
11
+ files?: UploadFileInfo<any>[];
12
+ fields?: { [fieldName: string]: any };
13
+ cleanupRequestFiles?: () => Promise<Array<boolean>>;
14
+ }
15
+ }
16
+
17
+ declare module '@midwayjs/web/dist/interface' {
18
+ interface Context {
19
+ files?: UploadFileInfo<any>[];
20
+ fields?: { [fieldName: string]: any };
21
+ cleanupRequestFiles?: () => Promise<Array<boolean>>;
22
+ }
23
+ }
24
+
25
+ declare module '@midwayjs/faas/dist/interface' {
26
+ interface Context {
27
+ files?: UploadFileInfo<any>[];
28
+ fields?: { [fieldName: string]: any };
29
+ cleanupRequestFiles?: () => Promise<Array<boolean>>;
30
+ }
31
+ }
32
+
33
+ declare module '@midwayjs/express/dist/interface' {
34
+ interface Context {
35
+ files?: UploadFileInfo<any>[];
36
+ fields?: { [fieldName: string]: any };
37
+ cleanupRequestFiles?: () => Promise<Array<boolean>>;
38
+ }
39
+ }
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@midwayjs/busboy",
3
+ "version": "3.16.0",
4
+ "description": "Midway Component for upload",
5
+ "main": "dist/index.js",
6
+ "typings": "index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "test": "node --require=ts-node/register ../../node_modules/.bin/jest --runInBand",
10
+ "cov": "node --require=ts-node/register ../../node_modules/.bin/jest --runInBand --coverage --forceExit",
11
+ "ci": "npm run test"
12
+ },
13
+ "keywords": [],
14
+ "author": "",
15
+ "files": [
16
+ "dist/**/*.js",
17
+ "dist/**/*.d.ts",
18
+ "index.d.ts"
19
+ ],
20
+ "engines": {
21
+ "node": ">=12"
22
+ },
23
+ "license": "MIT",
24
+ "dependencies": {
25
+ "file-type": "16.5.4",
26
+ "busboy": "1.6.0",
27
+ "@types/busboy": "1.5.4"
28
+ },
29
+ "devDependencies": {
30
+ "@midwayjs/core": "^3.16.2",
31
+ "@midwayjs/express": "^3.16.2",
32
+ "@midwayjs/faas": "^3.16.2",
33
+ "@midwayjs/koa": "^3.16.2",
34
+ "@midwayjs/mock": "^3.16.2",
35
+ "@midwayjs/web": "^3.16.2"
36
+ }
37
+ }