formidable处理文件上传的细节
koa在请求体的处理方面依赖于通用插件koa-bodyparser或者koa-body,前者比较小巧,内部使用了co-body库,可以处理一般的x-www-form-urlencoded格式的请求,但不能处理文件上传;但后者则内置了formidable库,在应对文件上传方面得心应手,本文就formidable文件上传的细节进行了一些分析。
koa-body的一般使用
const Koa = require('koa2')
const koaBody = require('koa-body');
const app = new Koa()
app.use(koaBody({
multipart: true,
formidable: {
// multiples: 接受多文件上传,默认为true
// uploadDir: os.tmpDir() 文件上传路径,默认为系统临时文件夹
keepExtensions: true // 保留文件原本的扩展名,否则没有扩展名,默认为false
}
}))
app.use(ctx => {
ctx.body = `Request Body: ${JSON.stringify(ctx.request.body)}, ${JSON.stringify(ctx.request.files)}`;
})
app.listen(3003, () => console.log('server running on port 3003'))
在koaBody的选项中开启multipart即可,非常简便,其中的formidable可以指定设置,具体配置见文档
文件form和普通form的区别
普通form
下面是一个普通form的例子:
<form action="http://localhost:3003" method="POST">
<p>
姓名:<input type="text" name="name">
</p>
<p>
年龄:<input type="number" name="age">
</p>
<p>
<input type="submit" value="提交">
</p>
</form>
在填写表单点击提交按钮时,它的请求报文格式是:
// 省略了一些头字段
POST http://localhost:3003/ HTTP/1.1
...
Content-Type: application/x-www-form-urlencoded
...
name=MickFone&age=58
Content-Type是application/x-www-form-urlencoded,请求体是标准的form请求体格式,键值之间使用=
连接,field之间则使用&
。
文件form
<form action="http://localhost:3003" method="POST" enctype="multipart/form-data">
<p>
姓名:<input type="text" name="name">
</p>
<p>
年龄:<input type="number" name="age">
</p>
<p>
资料:<input type="file" multiple name="materials">
</p>
<p>
<input type="submit" value="提交">
</p>
</form>
它的请求报文格式是:
// 省略了一些头字段,所有的换行均为\\r\\n
POST http://localhost:3003/ HTTP/1.1
...
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryI58Bh9EERAVK3lE7
...
------WebKitFormBoundaryI58Bh9EERAVK3lE7
Content-Disposition: form-data; name="name"
MickFone
------WebKitFormBoundaryI58Bh9EERAVK3lE7
Content-Disposition: form-data; name="age"
58
------WebKitFormBoundaryI58Bh9EERAVK3lE7
Content-Disposition: form-data; name="materials"; filename="sign.png"
Content-Type: image/png
PNG信息(乱码)
------WebKitFormBoundaryI58Bh9EERAVK3lE7
Content-Disposition: form-data; name="materials"; filename="文本.txt"
Content-Type: text/plain
两个黄鹂鸣翠柳,
一行白鹭上青天。
------WebKitFormBoundaryI58Bh9EERAVK3lE7--
上传文件时的Conent-Type是multipart/form-data,这种情况下就不能再简单地使用=, &
连接项,因为可能上传了二进制文件,而且文件上传需要携带它们的描述信息,比如mime类型、文件名、编码方式这些,所以multipart/form-data类型的表单格式会变得比较复杂。
来看看具体的格式,首先在请求头中Content-Type不仅指定了mime类型,还指定了一个boundary,以这个作为请求体中每个field之间的界线,每两个boundary之间的部分都是一个表单项,或者是一个文件。每个部分都有Content-Disposition头,其中的name属性指定了文件在form表单中的name;如果是文件,则有额外的filename属性表示上传文件名(带扩展名)和Content-Type头描述其mime类型。描述信息结束后是一个空行,紧接着的是表单项的值或者文件的内容,最后以一个boundary加上--
作为请求体的结尾。
请求体格式可以大概表示为:
--${boundary}
${描述头1 Content-Disposition: form-data; ...}
${描述头2}
..
// 空白行
${内容}
if (end) --${boundary}--
那么koa-body是怎么处理这种上传格式的呢?koa-body内部使用了formidable,这是个专门解决这类问题的库,我们来分析一下它的细节处理。
这里需要注意的一点是,Content-Disposition这个头字段如果出现在响应头中,则表明浏览器应该如何去呈现这个文件,
如:Content-Disposition: inline表明浏览器默认应当在页面中打开它,而 Content-Disposition: attachment 则会使
浏览器打开一个“另存为”的对话框保存文件。由此可见其与请求体中这个字段的含义有较大的区别,不能混淆这个头字段在请求
和响应两种场景的含义。
formidable对文件上传的处理
- 首先从请求头的Content-Type检测出multipart/form-data类型,并且读取boundary
- 创建了一个Multiple parser(一个对象模式的Transform流)来用来解析ctx.req,使用boundary初始化它
- Multiple parser开始处理请求体,它内部的实现是一个自动机,通过它提取出文件头信息(mime、编码、文件名等)
- 头信息提取完后,根据文件信息新建File可读流和一个part工具流,随着parser的解析触发part的data事件,将ctx.req暂停,然后往File可读流中写入内容,接着恢复ctx.req的流动性
下面是一个简单版本的实现,只能处理单文件上传,省略了解析字符串的自动机部分
// middleware.js
const { Transform, Stream } = require('stream')
const fs = require('fs')
const path = require('path')
const dir = path.dirname(process.argv[1])
const LF = 10 // '\n'
const CR = 13 // '\r'
const HYPHEN = 45 // '-'
module.exports = async function middleWare(ctx, next) {
return new Promise((resolve, reject) => {
let part
let File, filepath
// 1. 从Content-Type拿到boundary
const contentType = ctx.headers['content-type']
const boundaryReg = contentType.match(/boundary=(.*)/)
const boundary = Buffer.from('--' + boundaryReg[1])
// 用来收集普通的表单元素
let fields = {}
// 是否碰到文件的标记
let fileFlag = false
// 2. 创建转换流
const transformer = new Transform({
// 运行在对象模式
objectMode: true,
transform(buffer, encoding, callback) {
let prevIndex = 0
let fieldBegin = 0
let fieldEnd = 0
for (let i = 0, l = buffer.length; i < l; i++) {
const c = buffer[i]
// 检测到空行
if (!fieldBegin && c === LF && buffer[i-1] == CR && buffer[i-2] === c) {
// 第一部分头信息,可以使用utf8编码提取文件信息
let fileInfoBuffer = buffer.slice(prevIndex, i+1)
let fileInfoString = fileInfoBuffer.toString('utf8')
// 这里只简单地用正则去匹配了文件名
const filenameReg = /name="([^"]+)"(; filename="([^"]+)")?/
if (fileInfoString.match(filenameReg)) {
let name = RegExp.$1
let filename = RegExp.$3
// 3. 获取文件名
if (filename) {
this.push({ name: 'filename', buffer: Buffer.from(filename) })
fieldBegin = i + 1
fileFlag = true
}
// 获取表单name属性
else if (name) {
fieldBegin = i + 1
this.push({ name: 'fieldname', buffer: Buffer.from(name) })
}
}
}
// 简单地用校验开头和结尾匹配的方式判断表单值或文件的结尾
else if (fieldBegin && c === LF && buffer[i+1] === HYPHEN && buffer[i+2] === HYPHEN) {
let j = i + boundary.length
if (buffer[j] === boundary[boundary.length - 1]) {
fieldEnd = i - 1
let fileBuffer = buffer.slice(fieldBegin, fieldEnd)
this.push({ name: 'fielddata', buffer: fileBuffer })
fieldBegin = fieldEnd = 0
prevIndex = i + 1
}
}
}
callback()
}
})
// 让转换流开始流动
let currentFieldName = ''
transformer.on('data', ({ name, buffer }) => {
if (name === 'fieldname') {
currentFieldName = buffer.toString()
}
else if (name === 'filename') {
// 这是个无关紧要的工具流
part = new Stream()
part.readable = true
// 4. 创建待写入的文件流
filepath = path.resolve(dir, buffer.toString())
File = fs.createWriteStream(filepath)
// 这里只简单使用了工具流的EventEmitter特征
part.on('data', (chunk) => {
ctx.req.pause()
File.write(chunk, () => {
ctx.req.resume()
})
})
// 写入后再执行后续操作
part.on('end', () => {
File.end('', async () => {
ctx.fields = fields
ctx.file = filepath
console.log(filepath)
fileFlag = false
resolve()
})
})
}
else if (name === 'fielddata') {
if (fileFlag) {
// 如果是文件,向工具流发送数据
part.emit('data', buffer)
}
// 否则是普通的表单值
else if (currentFieldName) {
let field = fields[currentFieldName]
if (typeof field === 'string') {
fields[currentFieldName] = [field, buffer.toString()]
} else if (Array.isArray(field)) {
field.push(buffer.toString())
} else {
fields[currentFieldName] = buffer.toString()
}
currentFieldName = ''
}
}
})
transformer.on('end', () => {
if (part instanceof Stream) {
part.emit('end')
}
})
ctx.req.on('end', () => {
transformer.end()
})
ctx.req.pipe(transformer)
}).then(next)
}
// app.js
const Koa = require('koa2')
const middleWare = require('./middleware')
const app = new Koa()
app.use(middleWare)
app.use(ctx => {
ctx.body = `Request Fields: ${JSON.stringify(ctx.fields)}, Request Body: ${ctx.file}`;
})
if (!module.parent) {
app.listen(3003, () => console.log('server running on port 3003...'))
}
如果接收这样的表单:
<form action="http://localhost:3003" method="POST" enctype="multipart/form-data">
<p>
姓名:<input type="text" name="name">
</p>
<p>
年龄:<input type="number" name="age">
</p>
<p>
爱好1:<input type="text" name="hobit">
</p>
<p>
爱好2:<input type="text" name="hobit">
</p>
<p>
资料:<input type="file" multiple name="materials">
</p>
<p>
<input type="submit" value="提交">
</p>
</form>
响应结果为:
Request Fields: {"name":"MickFone","age":"58","hobit":["pingpong","video games"]}, Request Body: ...\upload\instance.png
总结
- multipart/form-data的表单格式与一般的x-www-form-urlencoded有很大区别,在读取数据上要麻烦一些
- formidable大致使用了四个步骤读取了multipart/form-data中的普通表单字段和文件,并把文件保存到本地
- 通过了解formidable的基本实现,需要掌握Node.js自定义流的用法
formidable处理文件上传的细节的更多相关文章
- NodeJS+formidable实现文件上传加自动重命名
前述 本人node初学者,此前使用原生node实现文件上传时遇到了一些困难,只做到了.txt 和.png两中格式的文件可以正常上传,如果上传其他格式文件服务端保存的文件会无法正常打开,原因是对form ...
- NodeJS使用formidable实现文件上传
最近自学了一下NodeJS,然后做了一个小demo,实现歌曲的添加.修改.播放和删除的功能,其中自然要实现音乐和图片的上传功能.于是上网查找资料,找到了一个formidable插件,该插件可以很好的实 ...
- multipart/form-data请求与文件上传的细节
<!DOCTYPE html><html><head lang="en"> <meta charset="UTF-8" ...
- 在 Node 中使用 formidable 处理文件上传
具体使用方式参照官方文档:https://www.npmjs.com/package/formidable 第一:安装: # npm install --save formidable yarn ad ...
- Nodejs进阶:基于express+multer的文件上传
关于作者 程序猿小卡,前腾讯IMWEB团队成员,阿里云栖社区专家博主.欢迎加入 Express前端交流群(197339705). 正在填坑:<Nodejs学习笔记> / <Expre ...
- NodeJS学习笔记 进阶 (4)基于express+muter的文件上传(ok)
个人总结:这篇文章主要讲了multer插件的使用,类似于formidable,可以用来处理post表单中的文件上传,读完这篇文章需要10分钟. 摘选自网络 概览 图片上传是web开发中经常用到的功能, ...
- java web学习总结(二十四) -------------------Servlet文件上传和下载的实现
在Web应用系统开发中,文件上传和下载功能是非常常用的功能,今天来讲一下JavaWeb中的文件上传和下载功能的实现. 对于文件上传,浏览器在上传的过程中是将文件以流的形式提交到服务器端的,如果直接使用 ...
- (转载)JavaWeb学习总结(五十)——文件上传和下载
源地址:http://www.cnblogs.com/xdp-gacl/p/4200090.html 在Web应用系统开发中,文件上传和下载功能是非常常用的功能,今天来讲一下JavaWeb中的文件上传 ...
- JavaWeb学习总结,文件上传和下载
在Web应用系统开发中,文件上传和下载功能是非常常用的功能,今天来讲一下JavaWeb中的文件上传和下载功能的实现. 对于文件上传,浏览器在上传的过程中是将文件以流的形式提交到服务器端的,如果直接使用 ...
- java文件上传和下载
简介 文件上传和下载是java web中常见的操作,文件上传主要是将文件通过IO流传放到服务器的某一个特定的文件夹下,而文件下载则是与文件上传相反,将文件从服务器的特定的文件夹下的文件通过IO流下载到 ...
随机推荐
- Java枚举类的学习
package java1; /** * @author 高槐玉 * #Description: * 枚举类的使用 * 1,枚举类的理解:类的对象只有有限个,确定的.我们称此类为枚举类 * 2.当需要 ...
- 【Java学习Day05】LDEA的安装和使用
LDEA安装 进入LDEA所有版本下载地址,建议下载LDEA2018 3.6版本 安装好LDEA后双击打开LDEA点击Nest,选择合适的文件路径,个人不建议放在C盘 选择好合适的文件路径后点击Nex ...
- CH32V307/CH32V203 IO翻转速度测试
CH32V307/CH32V203 IO极限翻转测试 记录RISC-V MCU CH32V307/CH32V203 在144MHz主频.-Os优化下,IO极限翻转频率. GPIO初始化代码如下: /* ...
- os-内核通知链notifier.c
8. linux内核通知链 8.1. 概述 在Linux内核中,各个子系统之间有很强的相互关系,某些子系统可能对其它子系统产生的事件感兴趣.为了让某个子系统在发生某个事件时通知感兴趣的子系统,Linu ...
- C++ 11 数字转字符串新功能
// 头文件 <string>string to_string (int val);string to_string (long val);string to_string (long l ...
- 学习-Vue3-条件渲染
v-if支持在 <template> 元素上使用,能和 v-else 搭配使用. v-show 不支持在 <template> 元素上使用, 也不能和 v-else 搭配使用. ...
- office2016word打开总是提示安全模式
突然打开word和Excel提示是否使用安全模式,如果选择否就自动退出office,选择是进入后,编辑一下也会自己退出,非常郁闷. 之后上网查看,尝试了许多: 1.win+R 运行%appdata%\ ...
- 动态规划-3-RNA的二级结构
/*状态转移方程: OPT(i , j)= max(OPT(i , j − 1) , max( 1+OPT(i , t − 1)+OPT(t + 1, j − 1))), where the max ...
- 回溯-1-N皇后(Backtracking-1-N Queens)
#include <stdio.h> #define N 4 enum bool {TRUE, FALSE}; void print_Q(int *Q) { int i; for (i = ...
- COM 对象的利用与挖掘4
作者:Joey@天玄安全实验室 前言 本文在FireEye的研究Hunting COM Objects[1]的基础上,讲述COM对象在IE漏洞.shellcode和Office宏中的利用方式以及如何挖 ...