文件上传
适用于 @midwayjs/faas 、@midwayjs/web 、@midwayjs/koa 和 @midwayjs/express 多种框架的通用上传组件,支持 file (服务器临时文件) 和 stream (流)两种模式。
相关信息:
| web 支持情况 | |
|---|---|
| @midwayjs/koa | ✅ |
| @midwayjs/faas | 💬 |
| @midwayjs/web | ✅ |
| @midwayjs/express | ✅ |
💬 部分函数计算平台不支持流式请求响应,请参考对应平台能力。
本模块自 3.17.0 起替换 upload 组件。
和 upload 组件的差异为:
- 1、配置的 key 从
upload调整为busboy - 2、中间件不再默认加载,手动可配置到全局或者路由
- 3、入参定义类型调整为
UploadStreamFileInfo - 4、
fileSize的配置有调整
安装依赖
$ npm i @midwayjs/busboy@3 --save
或者在 package.json 中增加如下依赖后,重新安装。
{
"dependencies": {
"@midwayjs/busboy": "^3.0.0",
// ...
},
"devDependencies": {
// ...
}
}
启用组件
// src/configuratin.ts
import { Configuration } from '@midwayjs/core';
import * as busboy from '@midwayjs/busboy';
@Configuration({
imports: [
// ...other components
busboy
],
// ...
})
export class MainConfiguration {}
配置中间件
组件中提供了 UploadMiddleware 这个中间件,可以将其配置到全局或者特定路由,推荐配置到特定路由,提升性能。
路由中间件
import { Controller, Post } from '@midwayjs/core';
import { UploadMiddleware } from '@midwayjs/busboy';
@Controller('/')
export class HomeController {
@Post('/upload', { middleware: [UploadMiddleware] })
async upload(/*...*/) {
// ...
}
}
全局中间件
- @midwayjs/koa
- @midwayjs/web
- @midwayjs/express
- @midwayjs/faas
// src/configuratin.ts
import { Configuration } from '@midwayjs/core';
import * as busboy from '@midwayjs/busboy';
import { Application } from '@midwayjs/koa';
@Configuration({
// ...
})
export class MainConfiguration {
@App('koa')
app: Application;
async onReady() {
this.app.useMiddleware(busboy.UploadMiddleware);
}
}
// src/configuratin.ts
import { Configuration } from '@midwayjs/core';
import * as busboy from '@midwayjs/busboy';
import { Application } from '@midwayjs/web';
@Configuration({
// ...
})
export class MainConfiguration {
@App('egg')
app: Application;
async onReady() {
this.app.useMiddleware(busboy.UploadMiddleware);
}
}
// src/configuratin.ts
import { Configuration } from '@midwayjs/core';
import * as busboy from '@midwayjs/busboy';
import { Application } from '@midwayjs/express';
@Configuration({
// ...
})
export class MainConfiguration {
@App('express')
app: Application;
async onReady() {
this.app.useMiddleware(busboy.UploadMiddleware);
}
}
// src/configuratin.ts
import { Configuration } from '@midwayjs/core';
import * as busboy from '@midwayjs/busboy';
import { Application } from '@midwayjs/faas';
@Configuration({
// ...
})
export class MainConfiguration {
@App('faas')
app: Application;
async onReady() {
this.app.useMiddleware(busboy.UploadMiddleware);
}
}
配置
组件使用 busboy 作为配置的 key。
上传模式
上传分为三种模式,文件模式,流式模式以及新增的异步迭代器模式。
代码中使用 @Files() 装饰器获取上传的文件, @Fields 装饰器获取其他上传表单字段。
- 文件模式
- 异步迭代器模式
- 流模式
file 为默认值,配置 mode 为 file 字符串。
// src/config/config.default.ts
export default {
// ...
busboy: {
mode: 'file',
},
}
在代码中获取上传的文件,支持同时上传多个文件。
import { Controller, Post, Files, Fields } from '@midwayjs/core';
import { UploadFileInfo } from '@midwayjs/busboy';
@Controller('/')
export class HomeController {
@Post('/upload', /*...*/)
async upload(@Files() files: Array<UploadFileInfo>, @Fields() fields: Record<string, string>) {
/*
files = [
{
filename: 'test.pdf', // 文件原名
data: '/var/tmp/xxx.pdf', // 服务器临时文件地址
mimeType: 'application/pdf', // mime
fieldName: 'file' // field name
},
]
*/
}
}
使用 file 模式时, 获取的 data 为上传的文件在服务器的 临时文件地址,后续可以再通过 fs.createReadStream 等方式来处理此文件内容,支持同时上传多个文件,多个文件会以数组的形式存放。
每个数组内的对象包含以下几个字段
export interface UploadFileInfo {
/**
* 上传的文件名
*/
filename: string;
/**
* 上传文件 mime 类型
*/
mimeType: string;
/**
* 上传服务端保存的路径
*/
data: string;
/**
* 上传的表单字段名
*/
fieldName: string;
}
从 v3.18.0 提供,替代原有的 stream 模式,该模式支持多个文件流式上传。
配置 mode 为 asyncIterator 字符串。
// src/config/config.default.ts
export default {
// ...
busboy: {
mode: 'asyncIterator',
},
}
在代码中获取上传的文件。
import { Controller, Post, Files, Fields } from '@midwayjs/core';
import { UploadStreamFileInfo, UploadStreamFieldInfo } from '@midwayjs/busboy';
@Controller('/')
export class HomeController {
@Post('/upload', /*...*/)
async upload(
@Files() fileIterator: AsyncGenerator<UploadStreamFileInfo>,
@Fields() fieldIterator: AsyncGenerator<UploadStreamFieldInfo>
) {
// ...
}
}
在该模式下,@Files 和 @File 装饰器会提供同一个 AsyncGenerator ,而 @Fields 会也同样会提供一个 AsyncGenerator。
通过循环 AsyncGenerator ,可以针对每个上传文件的 ReadStream 做处理。
import { Controller, Post, Files, Fields } from '@midwayjs/core';
import { UploadStreamFileInfo, UploadStreamFieldInfo } from '@midwayjs/busboy';
import { tmpdir } from 'os';
import { createWriteStream } from 'fs';
@Controller('/')
export class HomeController {
@Post('/upload', /*...*/)
async upload(
@Files() fileIterator: AsyncGenerator<UploadStreamFileInfo>,
@Fields() fieldIterator: AsyncGenerator<UploadStreamFieldInfo>
) {
for await (const file of fileIterator) {
const { filename, data } = file;
const p = join(tmpdir, filename);
const stream = createWriteStream(p);
data.pipe(stream);
}
for await (const { name, value } of fieldIterator) {
// ...
}
// ...
}
}
注意,如果一次上传中任意一个文件抛出了错误,本次上传流会直接关闭,所有未传输完成的文件都会异常。
异步迭代器中的上传对象包含以下几个字段。
export interface UploadStreamFieldInfo {
/**
* 上传的文件名
*/
filename: string;
/**
* 上传文件 mime 类型
*/
mimeType: string;
/**
* 上传文 件的文件流
*/
data: Readable;
/**
* 上传的表单字段名
*/
fieldName: string;
}
异步迭代器中的 @Fields 的对象略有不同,返回的数据会包含 name 和 value 字段。
export interface UploadStreamFieldInfo {
/**
* 表单名
*/
name: string;
/**
* 表单值
*/
value: any;
}
不再推荐使用。
配置 mode 为 stream 字符串。
使用 stream 模式时,通过 @Files 中获取的 data 为 ReadStream,后续可以再通过 pipe 等方式继续将数据流转至其他 WriteStream 或 TransformStream。
使用 stream 模式时,仅同时上传一个文件,即 @Files 数组中只有一个文件数据对象。
另外,stream 模式 不会 在服务器上产生临时文件,所以获取到上传的内容后无需手动清理临时文件缓存。
faas 场景实现方式视平台而定,如果平台不支持流式请求/响应但是业务开启了 mode: 'stream',将采用先读取到内存,再模拟流式传输来降级处理。
在代码中获取上传的文件,流式模式下仅支持单个文件。
import { Controller, Post, Files, Fields } from '@midwayjs/core';
import { UploadStreamFileInfo } from '@midwayjs/busboy';
@Controller('/')
export class HomeController {
@Post('/upload', /*...*/)
async upload(@Files() files: Array<UploadStreamFileInfo>, @Fields() fields: Record<string, string) {
/*
files = [
{
filename: 'test.pdf', // 文件原名
data: ReadStream, // 文件流
mimeType: 'application/pdf', // mime
fieldName: 'file' // field name
},
]
*/
}
}
流式模式中的对象包含以下几个字段。
export interface UploadStreamFieldInfo {
/**
* 上传的文件名
*/
filename: string;
/**
* 上传文件 mime 类型
*/
mimeType: string;
/**
* 上传文件的文件流
*/
data: Readable;
/**
* 上传的表单字段名
*/
fieldName: string;
}
上传文件后缀检查
通过 whitelist 属性,配置允许上传的文件后缀名,配置 null 则不校验后缀名。
如果配置为 null,则不对上传文件后缀名进行校验,如果采取文件上传模式 (mode=file),则会有可能被攻击者所利用,上传 .php、.asp 等后缀的 WebShell 实现攻击行为。
当然,由于组件会对上传后的临时文件采取 重新随机生成 文件名写入,只要开发者 不将 上传后的临时文件地址返回给用户,那么即使用户上传了一些不被预期的文件,那也无需过多担心会被利用。
如果上传的文件后缀不匹配,会响应 400 error,默认值如下:
'.jpg',
'.jpeg',
'.png',
'.gif',
'.bmp',
'.wbmp',
'.webp',
'.tif',
'.psd',
'.svg',
'.js',
'.jsx',
'.json',
'.css',
'.less',
'.html',
'.htm',
'.xml',
'.pdf',
'.zip',
'.gz',
'.tgz',
'.gzip',
'.mp3',
'.mp4',
'.avi',
可以通过组件中导出的 uploadWhiteList 获取到默认的后缀名白名单。
另外,midway 上传组件,为了避免部分 恶意用户,通过某些技术手段来伪造一些可以被截断的扩展名,所以会对获取到的扩展名的二进制数据进行过滤,仅支持 0x2e(即英文点 .)、0x30-0x39(即数字 0-9)、0x61-0x7a(即小写字母 a-z) 范围内的字符作为扩展名,其他字符将会被自动忽略。
你可以传递一个函数,可以根据不同的条件动态返回白名单。
// src/config/config.default.ts
import { uploadWhiteList } from '@midwayjs/busboy';
import { tmpdir } from 'os';
import { join } from 'path';
export default {
// ...
busboy: {
whitelist: (ctx) => {
if (ctx.path === '/') {
return [
'.jpg',
'.jpeg',
];
} else {
return [
'.jpg',
]
};
},
// ...
},
}