Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

新人想请教egg-multipart问题:使用File 模式处理文件上传 #4820

Open
zenblofe opened this issue Dec 3, 2021 · 7 comments
Open
Labels

Comments

@zenblofe
Copy link

zenblofe commented Dec 3, 2021

问题现象:批量上传文件时会出现某几个文件始终处于上传状态,对应的上传请求处于挂起状态。
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文件。

@hyj1991
Copy link
Member

hyj1991 commented Dec 3, 2021

#4808 这个应该是同一个问题,需要看下

@zenblofe
Copy link
Author

zenblofe commented Dec 3, 2021

我排查发现是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

@zenblofe
Copy link
Author

zenblofe commented Dec 3, 2021

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

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

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

@hyj1991
Copy link
Member

hyj1991 commented Dec 3, 2021

切换到 node-v12.x 或者 node-v14.x 再试试,之前 co-busboy 有个 close 和 finish 先后时序问题导致的文件 pending:cojs/busboy#41

@zenblofe
Copy link
Author

zenblofe commented Dec 3, 2021

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

@zenblofe zenblofe closed this as completed Dec 3, 2021
@hyj1991 hyj1991 reopened this Dec 3, 2021
@hyj1991
Copy link
Member

hyj1991 commented Dec 3, 2021

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

@shuidian
Copy link

shuidian commented Jan 12, 2022

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants