src/api/multer.js

/**
 * 专门用于处理`multipart/form-data`请求,对[`expressjs/multer`](https://github.com/expressjs/multer)进行简单的封装
 * 这种请求主要用于文件上传。
 *
 * 部分代码借鉴了[`koa-modules/multer`](https://github.com/koa-modules/multer)。
 *
 * @module api/multer
 */
const originalMulter = require('multer');
const mime = require('mime-types');
const {errorsEnum, coreCreateError, coreThrow} = require('../core/errors');
const {randomAlnumString, promisify} = require('../utils');

/**
 * 初始化一个multer对象。具有`any`、`array`、`fields`、`none`和`single`方法,会返回一个Koa
 * 中间件,具体使用方法见[`expressjs/multer`](https://github.com/expressjs/multer)。
 *
 * @param options {object} 选项,包含以下内容:
 *   - destination {string|Object.<string,string>} 上传文件路径
 *   - length {Number|Object.<string,Number>} 可选,随机文件名的长度,默认为40
 *   - types {Array.<string>|Object.<string,Array.<string>>} 可选,允许的mime-types,
 *     默认不做限定
 *   - maxSize {Number|Object.<string, Number>} 可选,最大文件的字节数,默认不做限定,
 *     如果传入对象,且希望某些字段不做限制,请使用`Infinity`
 *   - fields {Object.<string, function>} 可选,某些非文件字段的转换函数,用以从字符串
 *     转换为对应的类型
 */
function multer(options) {
  options.length = options.length || 40;
  options.fields = options.fields || {};
  const multerOptions = {
    storage: multer.diskStorage({
      destination: function (req, file, cb) {
        if (typeof options.destination === 'string')
          cb(null, options.destination);
        else {
          // Assume a map
          cb(null, options.destination[file.fieldname]);
        }
      },
      filename: function (req, file, cb) {
        let length;
        if (typeof options.length === 'number')
          length = options.length;
        else if (typeof options.length === 'object' &&
          options.length[file.fieldname] !== undefined)
          length = options.length[file.fieldname];
        else
          length = 40;
        let name = randomAlnumString(length);
        const ext = mime.extension(file.mimetype);
        if (ext)
          name += '.' + ext;
        cb(null, name);
      }
    }),
    fileFilter: function (req, file, cb) {
      if (Array.isArray(options.types)) {
        if (options.types.indexOf(file.mimetype) === -1)
          cb(coreCreateError(errorsEnum.SCHEMA, 'Wrong file type'));
        else
          cb(null, true);
      } else if (typeof options.types === 'object' &&
          Array.isArray(options.types[file.fieldname]) &&
          options.types[file.fieldname].indexOf(file.mimetype) === -1)
        cb(coreCreateError(errorsEnum.SCHEMA, 'Wrong file type'));
      else
        cb(null, true);
    }
  };
  if (typeof options.maxSize === 'number')
    multerOptions.limits = {fileSize: options.maxSize};
  else if (typeof options.maxSize === 'object') {
    const limits = Math.max(...Object.values(options.maxSize));
    if (limits !== Infinity)
      multerOptions.limits = {fileSize: limits};
  }
  const m = originalMulter(multerOptions);
  ['any', 'array', 'fields', 'none', 'single'].forEach(name => {
    if (!m[name])
      return;
    const fn = m[name];
    m[name] = function () {
      const middleware = promisify(fn.apply(this, arguments));
      return (ctx, next) => {
        return middleware(ctx.req, ctx.res).catch(err => {
          if (err.message === 'File too large')
            coreThrow(errorsEnum.SCHEMA, 'File too large');
          else if (err.message === 'Unexpected field')
            coreThrow(errorsEnum.SCHEMA, 'Unexpected field');
          else
            throw err;
        }).then(() => {
          if (ctx.req.file) {
            ctx.params.file = ctx.req.file;
            ctx.params._files.push(ctx.req.file.path);
          } if (Array.isArray(ctx.req.files)) {
            ctx.params.files = ctx.req.files;
            ctx.req.files.forEach(file => ctx.params._files.push(file.path));
          } else if (typeof ctx.req.files === 'object') {
            ctx.params.files = ctx.req.files;
            Object.values(ctx.req.files).forEach(files =>
              files.forEach(file => ctx.params._files.push(file.path))
            );
            if (typeof options.maxSize === 'object')
              Object.keys(options.maxSize).forEach(field =>
                ctx.params.files[field].forEach(file => {
                  if (file.size > options.maxSize[field])
                    coreThrow(errorsEnum.SCHEMA, 'File too large');
                })
              );
          }
          if (ctx.req.body) {
            const body = {};
            try {
              Object.keys(ctx.req.body).forEach(key => {
                if (options.fields[key])
                  body[key] = options.fields[key](ctx.req.body[key]);
                else
                  body[key] = ctx.req.body[key];
              });
            } catch (err) {
              coreThrow(errorsEnum.SCHEMA, 'Wrong field type');
            }
            ctx.params.data = body;
          }
        }).then(next);
      };
    };
  });
  return m;
}

multer.diskStorage = originalMulter.diskStorage;
multer.memoryStorage = originalMulter.memoryStorage;

module.exports = multer;