[eggjs/egg]新人想请教egg-multipart问题:使用File 模式处理文件上传

2025-10-27 512 views
6

问题现象:批量上传文件时会出现某几个文件始终处于上传状态,对应的上传请求处于挂起状态。 BUG排查:

// node_modules/egg-multipart/app/extend/context.js
async saveRequestFiles(options) {
    options = options || {};
    const ctx = this;

    const multipartOptions = {
      autoFields: false,
    };
    if (options.defCharset) multipartOptions.defCharset = options.defCharset;
    if (options.limits) multipartOptions.limits = options.limits;
    if (options.checkFile) multipartOptions.checkFile = options.checkFile;

    let storedir;

    const requestBody = {};
    const requestFiles = [];

    const parts = ctx.multipart(multipartOptions);
    let part;
    do {
      try {
        part = await parts();
      } catch (err) {
        await ctx.cleanupRequestFiles(requestFiles);
        throw err;
      }

      if (!part) break;

      if (part.length) {
        ctx.coreLogger.debug('[egg-multipart:storeMultipart] handle value part: %j', part);
        const fieldnameTruncated = part[2];
        const valueTruncated = part[3];
        if (valueTruncated) {
          await ctx.cleanupRequestFiles(requestFiles);
          limit('Request_fieldSize_limit', 'Reach fieldSize limit');
        }
        if (fieldnameTruncated) {
          await ctx.cleanupRequestFiles(requestFiles);
          limit('Request_fieldNameSize_limit', 'Reach fieldNameSize limit');
        }

        // arrays are busboy fields
        requestBody[part[0]] = part[1];
        continue;
      }

      // otherwise, it's a stream
      const meta = {
        field: part.fieldname,
        filename: part.filename,
        encoding: part.encoding,
        mime: part.mime,
      };
      // keep same property name as file stream
      // https://github.com/cojs/busboy/blob/master/index.js#L114
      meta.fieldname = meta.field;
      meta.transferEncoding = meta.encoding;
      meta.mimeType = meta.mime;

      ctx.coreLogger.debug('[egg-multipart:storeMultipart] handle stream part: %j', meta);
      // empty part, ignore it
      if (!part.filename) {
        await sendToWormhole(part);
        continue;
      }

      if (!storedir) {
        // ${tmpdir}/YYYY/MM/DD/HH
        storedir = path.join(ctx.app.config.multipart.tmpdir, moment().format('YYYY/MM/DD/HH'));
        const exists = await fs.exists(storedir);
        if (!exists) {
          await mkdirp(storedir);
        }
      }
      const filepath = path.join(storedir, uuid.v4() + path.extname(meta.filename));
      const target = fs.createWriteStream(filepath);
      await pump(part, target);
      // https://github.com/mscdex/busboy/blob/master/lib/types/multipart.js#L221
      meta.filepath = filepath;
      requestFiles.push(meta);

      // https://github.com/mscdex/busboy/blob/master/lib/types/multipart.js#L221
      if (part.truncated) {
        await ctx.cleanupRequestFiles(requestFiles);
        limit('Request_fileSize_limit', 'Reach fileSize limit');
      }
    } while (part != null);

    ctx.request.body = requestBody;
    ctx.request.files = requestFiles;
  },

image 导致该问题的原因是egg-multipart的saveRequestFiles()没有生成对应的part文件。

回答

9

我排查发现是co-busboy的parse有问题

// node_modules/egg-multipart/app/extend/context.js
const parse = require('co-busboy');
multipart(options) {
    // multipart/form-data
    if (!this.is('multipart')) {
      this.throw(400, 'Content-Type must be multipart/*');
    }
    if (this[HAS_CONSUMED]) throw new TypeError('the multipart request can\'t be consumed twice');

    this[HAS_CONSUMED] = true;
    const parseOptions = Object.assign({}, this.app.config.multipartParseOptions);
    options = options || {};
    if (typeof options.autoFields === 'boolean') parseOptions.autoFields = options.autoFields;
    if (options.defCharset) parseOptions.defCharset = options.defCharset;
    if (options.checkFile) parseOptions.checkFile = options.checkFile;
    // merge and create a new limits object
    if (options.limits) {
      const limits = options.limits;
      for (const key in limits) {
        if (/^\w+Size$/.test(key)) {
          limits[key] = bytes(limits[key]);
        }
      }
      parseOptions.limits = Object.assign({}, parseOptions.limits, limits);
    }
    return parse(this, parseOptions);
  },

我看了co-busboy的parse使用,文档推荐使用autoFields: true

5

我还看到一个问题,就是老版本的busboy在处理数据流的时候,会出现先 close 再 finish 的BUG

// node_modules/co-busboy/index.js
var Busboy = require('busboy')

这里的包依赖,我觉得试一下升级到最新的包,看一下数据流的处理。

9

cengzj@0201206761-NB MINGW64 /e/demo $ node -v v14.18.1 问题还是存在。 我现在的想法是改为使用 Stream 模式看一下。

4

@zenblofe 这两个上传文件 pending 的问题稍后我会看下的

7

我这边也遇到了一样的问题,然后也是调试到了co-busboy里面,确实是有问题。但是我测试了一下把co-busboy升级到1.5.0,问题貌似就没有了,好像是co-busboy修复了这个问题?我这边测试在node14和node16上,使用co-busboy@1.5.0,文件上传都是ok的。这边建议egg-multipart把co-busboy的依赖版本调整到1.5.0以上应该就ok了。