这不是第一篇文章,第一篇文章主要实现原生 javascript 进行文件分片及 axios 的合并请求操作,主要是前端的实现。

express实现文件分片上传(一)JS原生文件分片及axios合并请求

express实现文件分片上传(二)后端API的实现

一、需求

实现了前端文件分片并且充分利用了 axios.all() 处理完所有的分片上传后,需要开发后端文件上传和合并功能。

使用的框架为 express4.x,文件上传使用 multer,需要使用 body-parser 进行请求的主体的解析。

关于 multer 处理文件上传我写过两篇实例文章:

二、分片上传的处理

1、分片上传中间件

分片上传通过一个中间件 uploadMiddleware 进行处理,分片不需要存储在真正的 uploads 文件夹中,因此使用了一个 ~uploads 的文件夹,这个中间件是通过 multer 实现的。

const multer = require('multer');
const chunksBasePath = '~uploads/';
const storage = multer.diskStorage({
    destination:chunksBasePath,
});
const baseUpload = multer({storage});
const upload = baseUpload.single('file');
/**
 * @name uploadMiddleware
 * @description 文件上传中间件,upload 方法调用的时候 会有 err 进行错误判断
 * @param {*} req
 * @param {*} res
 * @param {*} next
 */
const uploadMiddleware = (req,res,next)=>{
    upload(req,res,(err)=>{
        if(err){
            // 进行错误捕获
            res.json({code:-1,msg:err.toString()});
        }else{
            next();
        }
    });
};
module.exports = uploadMiddleware;

2、分片上传的 route 处理

主要的处理流程如下:

  • 通过前端传递过来的 hash 构建临时存放 chunk 的文件夹(第一次需要创建)
  • 将上传后的 chunk 文件重命名(也就是移动文件)到 名为 hash 的文件夹下
const chunkBasePath = '~uploads/';
// 上传chunks
app.post('/upload_chunks',uploadChunksMiddleware,(req,res)=>{
    // 创建chunk的目录
    // hash 是前端传递过来的
    const chunkTmpDir = chunkBasePath + req.body.hash + '/';
    // 判断目录是否存在
    if(!fs.existsSync(chunkTmpDir)) fs.mkdirSync(chunkTmpDir);
    // 移动切片文件
    fs.renameSync(req.file.path,chunkTmpDir + req.body.hash + '-' + req.body.index);
    res.send(req.file);
});

3、分片合并 route

当前端所有的文件上传均成功之后,会请求合并分片,主要的处理步骤是:

  • 创建保存的文件夹(如果不存在)按照时间创建文件夹:如 uploads/20180506/
  • 创建一个空的文件,文件名唯一即可,后缀使用传递过来的后缀
  • 从分片的文件夹(hash命名)中获取所有的分片文件
  • 检查分片数量是否正确
  • 按照顺序依次处理每个分片,读取分片数据并 appendFile 追加写入到创建的图片文件中
  • 清空所有的 chunk ,删除 chunk hash 文件夹
// 文件分片
app.post('/merge_chunks',(req,res)=>{
    const total = req.body.total;
    const hash = req.body.hash;
    const saveDir = fileBasePath + new Date().getFullYear()+ (new Date().getMonth() + 1 )+new Date().getDate() + '/';
    const savePath = saveDir + Date.now() + hash + '.' + req.body.ext;
    const chunkDir = chunkBasePath + '/' + hash + '/';
    try{
        // 创建保存的文件夹(如果不存在)
        if(!fs.existsSync(saveDir)) fs.mkdirSync(saveDir);
        // 创建文件
        fs.writeFileSync(savePath,'');
        // 读取所有的chunks 文件名存放在数组中
        const chunks = fs.readdirSync(chunkBasePath + '/' + hash);
        // 检查切片数量是否正确
        if(chunks.length !== total || chunks.length === 0) return res.send({code:-1,msg:'切片文件数量不符合'});
        for (let i = 0; i < total; i++) {
            // 追加写入到文件中
            fs.appendFileSync(savePath, fs.readFileSync(chunkDir + hash + '-' +i));
            // 删除本次使用的chunk
            fs.unlinkSync(chunkDir + hash + '-' +i);
        }
        // 删除chunk的文件夹
        fs.rmdirSync(chunkDir);
        // 返回uploads下的路径,不返回uploads
        res.json({code:0,msg:'文件上传成功',data:{path:savePath.split(fileBasePath)[savePath.split(fileBasePath).length-1]}});
   }catch (err){
       res.json({code:-1,msg:'出现异常,上传失败'});
   }
});

3、提供一个图片访问路由

提供图片访问路由,能够访问上传的图片:

因此需要提供 dir/path201859/15258566299381525856629806306603821800519151525779366427.jpg

// 返回文件
app.get('/uploads/:dir/:path',(req,res)=>{
    const url = path.resolve(__dirname,`${fileBasePath}/${req.params.dir}/${req.params.path}`);
    res.type('png').sendFile(url);
});

三、效果

demo.jpg

四、完整项目代码

可以在仓库下载代码,并且本地运行。

github:

gitosc: