一、需求

本篇文章基于之前的文件续写, 因此一些基的使用需要参照下面的文章内容,不在重复基本代码

之前写了一篇文章,关于使用 koa-body 进行文件上传的,基本的代码不再重复,文章地址如下:

使用 koa-body 无法向 multer 直接通过 diskStorage 进行存储路径和文件名的配置,需要借助 onFileBegin:(name,file)=>{}进行。

上传的基目录是 public/upload,但是需要根据日期构建文件夹,并且文件名使用Date.now()+Math.random()生成,其实文件名是可以直接使用默认的就挺好,自己生成主要是方便演示。

最终的结果是:

public/upload/20180621/123123123123.txt

onFileBegin 会传递两个参数,一个是 name,这个 name 是表单的 name 属性传递过来的,而 file 是一个 File 类型的对象,主要的属性如下:

属性名说明
size文件大小
path文件上传的路径(注意不是最终的路径)
name文件的原始名称
type文件类型
hash如果配置了 hash 之后会有值
lastModifiedDate最近修改时间

二、实现

为了更好的实现需求,封装了一些基本的工具方法。

1、生成文件夹名称

文件路径:utils/getUploadDirname.js

function getUploadDirName(){
  const date = new Date();
  let month = Number.parseInt(date.getMonth()) + 1;
  month = month.toString().length > 1 ? month : `0${month}`;
  const dir = `${date.getFullYear()}${month}${date.getDate()}`;
  return dir;
}

module.exports = getUploadDirName;

2、检查文件夹路径是否存在,如果不存在则创建文件夹

koa-body 的文件件上传目录必须保证文件夹的存在,否则无法写入,因此需要判断文件夹是否存在,如果不存在,则新建一个文件夹即可。

文件路径:utils/checkDirExist.js

/**
 * @description 判断文件夹是否存在 如果不存在则创建文件夹
 */
const path = require('path');
const fs = require('fs');

function checkDirExist(p) {
  if (!fs.existsSync(p)) {
    fs.mkdirSync(p);
  }
}

module.exports = checkDirExist;

3、获取文件的后缀

如果使用默认文件上传后的名字,则不需要这个方法,因为 koa-body 配置 formidable:{keepExtensions: true,} 之后会直接保留原始的文件后缀。

获取文件后缀依靠的是 file.name 属性,通过原始文件名获取到后缀。

文件路径:utils/getUploadFileExt.js

function getUploadFileExt(name) {
  let ext = name.split('.');
  return ext[ext.length - 1];
}

module.exports = getUploadFileExt;

4、在 onFileBegin 中进行属性重赋值

有了助手方法之后,直接引入并且在 koa-body 的方法中使用即可。

文件路径: app.js

以下代码中 koaBody() / getUploadDirName() / checkDirExist() / getUploadFileName() 均已经引入。

app.use(koaBody({
  multipart:true,
  encoding:'gzip',
  formidable:{
    uploadDir:path.join(__dirname,'public/upload'),
    keepExtensions: true,
    maxFieldsSize:2 * 1024 * 1024,
    onFileBegin:(name,file) => {
      // console.log(file);
      // 获取文件后缀
      const ext =getUploadFileExt(file.name);
      // 最终要保存到的文件夹目录
      const dir = path.join(__dirname,`public/upload/${getUploadDirName()}`);
      // 检查文件夹是否存在如果不存在则新建文件夹
      checkDirExist(dir);
      // 重新覆盖 file.path 属性
      file.path = `${dir}/${getUploadFileName(ext)}`;
    },
    onError:(err)=>{
      console.log(err);
    }
  }
}));

三、效果

除了上面的一些变动外,其他的代码均不需要变动,最终上传的文件如下:

1.jpg

文件上传后的信息如下:

 {
  "photo": {
    "size": 907,
    "path": "E:\\Aprojects\\node_projects\\koa2\\20180619\\demo1\\public\\upload\\20180621/15295654622244128.txt",
    "name": "新建文本文档.txt",
    "type": "text/plain",
    "mtime": "2018-06-21T07:17:42.226Z"
  }
}

四、只保存需要的文件路径

虽然文件上传之后会有 path 属性,但是实际上这个 path 属性对我们的帮助不是很大,最终我们需要 20180621/15295654622244128.txt 这个路径而已。

因此需要修改一下 onFileBegin 的部分代码(其中一些代码变动了,和上面并不一样,但只是方便最后两行代码而已,功能没有变化):

onFileBegin: (name, file) => {
      // console.log(file);
      // 获取文件后缀
      const ext = getUploadFileExt(file.name);
      // 最终要保存到的文件夹目录
      const dirName = getUploadDirName();
      const dir = path.join(__dirname, `public/upload/${dirName}`);
      // 检查文件夹是否存在如果不存在则新建文件夹
      checkDirExist(dir);
      // 获取文件名称
      const fileName = getUploadFileName(ext);
      // 重新覆盖 file.path 属性
      file.path = `${dir}/${fileName}`;
      app.context.uploadpath = app.context.uploadpath ? app.context.uploadpath : {};
      app.context.uploadpath[name] = `${dirName}/${fileName}`;
    },

关键代码是下面两行:

因为是支持多文件上传的,因此 app.context.uploadpath 应该能够存放多个文件才可以,所以和 ctx.request.files 采用了一样的存储规则,通过 name 属性作为 key 。

其中需要判断一下 app.context.uploadpath 是否存在, 如果不存在则置空对象,否则会报错误,无法设置属性。

app.context.uploadpath = app.context.uploadpath ? app.context.uploadpath : {};
app.context.uploadpath[name] = `${dirName}/${fileName}`;

经过这两行之后,就可以在路由中的上下文中获取到上传后的文件信息:

router.post('/',async (ctx)=>{
  // console.log(ctx.request.files);
  console.log(ctx.uploadpath);
  ctx.body = JSON.stringify(ctx.request.files);
});

结果:

{ photo: '20180621/15295684564254069.txt' }

五、示例代码

在 github 上存了一份示例代码,可以通过修改index.html控制单个文件上传或者多个文件上传

1.jpg

结果:

{
"photo[]":[{"size":1200,"path":"E:\\Aprojects\\node_projects\\koa2\\20180619\\demo1\\public\\upload\\2018073/15306331757424476.png","name":"arrow.png","type":"image/png","mtime":"2018-07-03T15:52:55.743Z"},{"size":11536,"path":"E:\\Aprojects\\node_projects\\koa2\\20180619\\demo1\\public\\upload\\2018073/15306331757425633.jpg","name":"2.jpg","type":"image/jpeg","mtime":"2018-07-03T15:52:55.744Z"},{"size":15633,"path":"E:\\Aprojects\\node_projects\\koa2\\20180619\\demo1\\public\\upload\\2018073/15306331757442120.jpg","name":"1.jpg","type":"image/jpeg","mtime":"2018-07-03T15:52:55.748Z"}],
"photo2":{"size":1200,"path":"E:\\Aprojects\\node_projects\\koa2\\20180619\\demo1\\public\\upload\\2018073/15306331757486132.png","name":"arrow.png","type":"image/png","mtime":"2018-07-03T15:52:55.749Z"}
}