Express の 文件上传
上一篇文章讲了文件下载。其实大家可以自己看 multer 项目文档。我就是照着这个自己写的。
multer
处理multipart/form-data
类型的POST
请求,即处理文件上传。当然没有文件的表单也可以的。
安装 multer
npm install --save multer
单文件上传
multer
在request
对象上增加了file
或者files
属性分别用于接收表单中的单文件或多文件。
<form action="http://localhost:3000/uploadSingle" method="post" enctype="multipart/form-data">
<input type="text" name="username" placeholder="请输入姓名"/><br>
<input type="password" name="password" placeholder="请输入密码"/><br>
<input type="file" name="resume" id="resume"><br>
<button type="submit">提交</button>
</form>
app.post('/uploadSingle', upload.single('resume'), (req, res) => {
let resume = req.file;
console.dir(resume);
let { username, password } = req.body;
console.log(username);
console.log(password);
res.setHeader('Content-Type', 'text/plain');
res.send(JSON.stringify(req.body));
});
首先,express
会在当前路径下创建temp
文件夹用来存储上传的文件。然后看一下打印的内容
{
fieldname: 'resume', // html <input type="file"> 标签的 name 属性值
originalname: 'if.pdf', // 上传的文件名
encoding: '7bit',
mimetype: 'application/pdf', // 文件的媒体类型
destination: 'temp/', // 文件保存位置
filename: '2f84eb8d718b7cb27ba9a38bd1d884c1', // 在 destination 中的文件名
path: 'temp\\2f84eb8d718b7cb27ba9a38bd1d884c1', // 文件全路径
size: 3020009 // 文件大小,单位:字节
}
很显然的问题是,上传的文件是一个随机名字。为什么?源码中在multer()
函数的注释中有这么一段话。大意就是如果调用multer()
函数是没有storage
参数而只有dest
参数,那文件就会以随机名字存储在dest
指定的地方。
The
StorageEngine
specified instorage
will be used to store files. Ifstorage
is not set anddest
is, files will be stored indest
on the local file system with random names. If neither are set, files will be stored in memory.
要解决这个问题,直觉很简单,改文件名不就好了。对✅,改文件名
app.post('/uploadSingle', upload.single('resume'), (req, res) => {
let resume = req.file;
console.dir(resume);
fs.renameSync('temp/' + resume.filename, 'temp/' + resume.originalname);
res.setHeader('Content-Type', 'text/plain');
res.send(JSON.stringify(req.body));
});
当当当当,大功告成。
等等🤔,这样做好像有些无奈,毕竟文件名本身就有,干嘛要多一行代码处理。再者,以后存储位置要改变的话,每个函数里都要改变,也是得不偿失。
磁盘存储
multer
对象提供diskStorage
方法允许配置在文件系统如何存储上传的文件。这个对象有两个属性
destination
: 存储上传文件的地方。可以是字符串或函数。如果是字符串类型并且位置不存在,multer
就会递归创建。如果既不传字符串也不传函数,就会将文件存储在操作系统的临时目录os.tmpdir()
下。windows 的 lenovo 用户临时目录就是👇filename
: 只能是函数。如果不传函数,multer
将使用没有扩展名的伪随机的 32 位十六进制字符串命名文件。- 上面两个属性,如果传值为函数类型,函数的参数是一样的,分别是
requst
: 一般的Request
对象。file
:上传文件相关信息的对象。callback
:回调函数。回调函数的第一个参数都是Error
,即如果发生异常的异常对象,第二个参数才是保存文件的路径(destination)
或文件名(filename)
。官方文档把这个形参写为cb
,搞得我一下子反应不过来啥意思😅
贴一个完整代码
const express = require('express');
const multer = require('multer');
let app = express();
let storage = multer.diskStorage({
destination: function (req, file, callback) {
// 注意这里支持相对路径哦
callback(null, '../resource');
},
filename: function (req, file, callback) {
callback(null, file.originalname);
}
});
// 这里使用了属性值的简写形式,不熟悉 js 的同学可能要补补课了。
const upload = multer({ storage });
app.post('/uploadSingle', upload.single('resume'), (req, res) => {
let resume = req.file;
console.dir(resume);
res.setHeader('Content-Type', 'text/plain');
res.send(JSON.stringify(req.body));
});
app.listen('3000', () => {
console.log('server started...');
});
再次执行代码,就会在resource
目录下出现我们上传的文件。亲测:上传多个同名文件会出现覆盖,所以在定义文件名时可以加个时间戳或者随机数之类的。
多文件上传
🆕新需求:投简历的表单不仅需要上传简历,还需要上传证件照。所以,一个表单应该有两个可以上传文件的地方。
<form action="http://localhost:3000/uploadMany" method="post" enctype="multipart/form-data">
<input type="text" name="username" placeholder="请输入姓名"/><br>
<input type="password" name="password" placeholder="请输入密码"/><br>
<input type="file" name="resume" id="resume">简历<br>
<input type="file" name="logo" id="logo2">头像<br>
<button type="submit">提交</button>
</form>
在接收文件时,我们需要分别处理不同的文件。
首先,定义要接收哪些文件,每种文件最多几个。使用multer
实例的fields
方法,该方法接收一个对象数组为参数,每个对象有下面两个属性
name
:对应表单input type="file"
的name
属性maxCount
:最多接收多少个文件。默认是Infinity
无限多个😮
const upload = multer({ storage });
let filesToReceive = upload.fields([
{ name: 'resume', maxCount: 1 },
{ name: 'logo', maxCount: 1 },
]);
然后,将filesToReceive
传给POST
路由处理方法并拿到文件信息。注意拿文件信息时使用的是files
属性,files
是一个对象,键是字符串,值是数组。
app.post('/uploadMany', filesToReceive, (req, res) => {
let resume = req.files['resume'][0];
let logo = req.files['logo'][0];
console.log(resume);
console.log(logo);
res.setHeader('Content-Type', 'text/plain');
res.send(JSON.stringify(req.body));
});
看一下打印的内容
{
fieldname: 'logo',
originalname: 'logo.txt',
encoding: '7bit',
mimetype: 'text/plain',
destination: '../resource',
filename: 'logo.txt',
path: '..\\resource\\logo.txt',
size: 0 // 一个空的txt
}
无文件上传
无文件上传很好理解嘛,只要使用none()
方法。下面是一个不上传任何文件的普普通通表单
<form action="http://localhost:3000/uploadNone" method="post" enctype="multipart/form-data">
<input type="text" name="username" placeholder="请输入姓名"/><br>
<input type="password" name="password" placeholder="请输入密码"/><br>
<button type="submit">提交</button>
</form>
app.post('/uploadNone', upload.none(), (req, res) => {
let { username, password } = req.body;
res.setHeader('Content-Type', 'text/plain');
res.send(JSON.stringify(req.body));
});
如果你强行上传文件,哎,人家给你报错😕MulterError: Unexpected field
文件限制 limits
看乐上面的内容,你就知道multer
很多默认值比较坑,竟然能上传最多Infinity
个文件🙄。所以,很有必要对上传的文件进行过滤,避免恶意的表单提交。
我们下面设置上传文件大小最大1024B,即 1KB
。在构造multer
实例时指定limits
对象用来限制文件上传。
fileSize
:文件最大长度。单位字节
。默认无限大files
:上传文件最大数量。默认无限大fields
:上传对象中非文件字段的最大数量。默认无限大- 等
const upload = multer({
storage,
limits: {
// 单位 字节
fileSize: 1024
}
});
如果上传超过最大数量,就会报错❌
文件过滤 FileFilter
那之前的例子好了,如果我们只想让用户上传简历和证件照,就表示我们只接受PDF
和JPG/JPEG/PNG
格式的文件,其他的、可能伤害服务器的文件,比如.sh/.bat/.exe
文件,统统拒绝🙅或忽略,哪儿来的回哪儿去。
使用fileFilter
定制我们的文件过滤。这个方法接收三个参数
requst
:一般的Request
对象。file
:包含上传文件相关信息的对象。callback
:回调函数,两个参数,第一个参数是Error
对象,无异常传null
。第二个参数是boolean
值,传true
表示允许上传,false
表示不允许。
const upload = multer({
storage,
limits: {
fileSize: 1024
},
// 文件过滤器
fileFilter: function (req, file, callback) {
if (file.originalname.indexOf('.sh') !== -1 || file.originalname.indexOf('.exe') !== -1) {
callback(new Error('不允许上传乱七八糟的文件'), false);
} else if (file.originalname.indexOf('.pdf') !== -1 || file.originalname.indexOf('.jpeg') !== -1) {
callback(null, true);
}
}
});
我测试上传个exe
文件,看看效果哈哈哈
异常处理
上面对文件做了限制和过滤,其实…不太友好😐毕竟直接把错误异常返回个用户看挺不专业的,所以我们有必要捕捉这样的异常并进行包装。
multer
官网中说不要将multer
作为全局中间件使用,所以我们上面的例子都没有这么作。现在我们按照官网,将单个文件上传的代码稍微改一改
修改前
const upload = multer({ ... });
app.post('/uploadSingle', upload.single('resume'), (req, res) => {
let resume = req.file;
console.dir(resume);
res.setHeader('Content-Type', 'text/plain');
res.send(JSON.stringify(req.body));
});
修改后。❗注意这里我修改了上传exe
文件时抛出异常的类型
const upload = multer({
storage,
// 文件限制
limits: {
// 最大文件 1024(1KB),单位 字节
fileSize: 1024
},
// 文件过滤器
fileFilter: function (req, file, callback) {
if (file.originalname.indexOf('.sh') !== -1 || file.originalname.indexOf('.exe') !== -1) {
callback(new multer.MulterError('没想到你会上传这个文件', '不允许上传乱七八糟的文件'), false);
} else if (file.originalname.indexOf('.pdf') !== -1 || file.originalname.indexOf('.jpeg') !== -1) {
callback(null, true);
}
}
});
let singleUpload = upload.single('resume');
app.post('/uploadSingle', (req, res) => {
singleUpload(req, res, (err) => {
// 如果是 Multer 的异常
if (err instanceof multer.MulterError) {
res.json(err);
return;
} else if (err) { // 其他异常
res.json(err);
return;
}
let resume = req.file;
res.setHeader('Content-Type', 'text/plain');
res.send(JSON.stringify(req.body));
});
});
再次上传exe
文件时,就会报错提示。可以再次封装同全局统一异常返回。
好了,上传就暂时写到这里