koa使用multer上传实现以文件hash(md5)作为文件名附文件秒传原理

multer是express模块下的文件上传中间件,类似的中间件还有formidable等,这里以multer为例说明如何实现以文件的hash(如md5)命名。以及文件秒传原理。

文件秒传原理

在文件上传时,客户端会同步对待上传的文件进行hash计算,以MD5为例,配合客户端极速上传插件,可以很快完成。用计算出的hashCode去服务端查找,如果存在该记录的文件,则无需继续上传,直接标记完成,达到秒传的效果。

服务端需要做的就是记录存储的文件与hashCode的关系,提供API查询。

文件秒传中存在的问题依然是hash碰撞,按照MD5的存储信息量,在现实中,除非故意如此,其实可以忽略不计。

这就是为什么, 你好不容易下了片,上传到网盘几秒就成功了。

大文件的MD5计算

文件秒传存在的意义在于,对于大文件,一方面可以减少服务器存储压力,节省流量,另一方面,可以减少传输时间。

很多东西,只要一大,就有些问题了。大文件的hash计算也是如此,如果全部读入内存,内存就爆了。好在hash计算本就是分片的(MD5以512位分组来处理输入的信息),那么可以使用流的方式,分片计算,最后算出整个文件的md5。我们可以把这个过程放到第一次文件上传时进行:

1
2
3
4
5
6
7
8
9
var crypto = require('crypto')
// 分片计算
file.stream.on('data', function (chunk) {
hash.update(chunk)
})
file.stream.on('end', function() {
var md5 = hash.digest('hex');
// 计算完成
})

koa-multer 实现md5作为文件名

请见multer 把修改后的disk.js放到自己的目录。使用示例见下:

multerUtil.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const fs = require("fs");
const multer = require('koa-multer');
const diskStorage = require('./disk');
multer.diskStorage = diskStorage;
// 自动创建支持的存储路径
const floders = ['avatar', 'record', 'channel'];
for (const floder of floders) {
const path = `uploads/images/${floder}`;
if (!fs.existsSync(path)){
fs.mkdirSync(path);
}
}
const storage = multer.diskStorage({
destination: function (req, file, cb) {
const floder = req.body.floder; // 目标文件夹名称
if (/^[a-zA-Z\d]+$/.test(floder)) {
cb(null, `uploads/images/${floder}`);
}
},
});
const upload = multer({
storage: storage,
limits: {
fileSize: 1024 * 1024, // bytes
}
});
module.exports = upload;

index.js

1
2
3
4
5
6
7
8
const router = require('koa-router')();
const service = require('./service');
const authService = require('../../auth/service');
const upload = require('./multerUtil');
router.post('/', upload.single('file'), service.upload);
module.exports = router;

service.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const HttpStatus = require('http-status-codes');
import { Result } from '../Result';
const User = require('../../database').models.user;
const service = {
/**
* 通用的上传,上传完成返回文件路径
*
* @param {*} ctx
* @param {*} next
*/
async upload(ctx, next) {
const floder = ctx.req.body.floder; // 目标文件夹
const file = ctx.req.file;
const path = `/images/${floder}/${file.filename}`;
return ctx.body = Result.ok(path);
},
};
module.exports = service;

关于Hash

其实hash,本身就是个摘要,只要能概况这个文件或事物本身,那他也算个hash.比如你的名字。