← 中文资源

简体中文版本

Fastify 流式文件上传

用 Fastify 接收流式 multipart 上传:单文件体积与文件数双重上限、MIME 白名单校验、管道直写磁盘全程不整块缓冲。

阅读英文原文

示例说明

上传接口最大的风险是被大文件打爆内存。@fastify/multipart 的 files 迭代器把每个文件作为流交付,pipeline 直接把流对接到磁盘写流,内存占用与文件大小无关。limits 在解析层就掐断超限上传,连磁盘都不会被写到;MIME 白名单在落盘前拒绝不支持的类型。

技术要点

  • limits 声明体积、数量、字段三重上限
  • for await 逐个消费文件流
  • pipeline 连接上传流与文件写流

代码解读

注册插件时给出 25MB 单文件、最多 3 个文件、10 个表单字段的限制;处理循环里先校验 MIME 类型,不合规立即 415 返回。part.file 是可读流,bytesRead 属性在管道完成后给出实际字节数用于回执。

使用建议

文件名要做规范化防止路径穿越,最稳妥是服务端生成存储名。敏感场景应校验文件魔数而非仅信 MIME 头。上传目录与应用数据隔离并限制执行权限。

代码示例

import fastify from 'fastify';
import multipart from '@fastify/multipart';
import { createWriteStream } from 'node:fs';
import { pipeline } from 'node:stream/promises';

const app = fastify({ logger: true });
await app.register(multipart, {
  limits: { fileSize: 25 * 1024 * 1024, files: 3, fields: 10 },
});

const ALLOWED = new Set(['image/png', 'image/jpeg', 'application/pdf']);

app.post('/upload', async (req, reply) => {
  const saved = [];

  for await (const part of req.files()) {
    if (!ALLOWED.has(part.mimetype)) {
      return reply.code(415).send({ error: 'unsupported type', type: part.mimetype });
    }
    if (part.filename === undefined) continue;

    const target = createWriteStream('uploads/' + part.filename);
    await pipeline(part.file, target);
    saved.push({ name: part.filename, bytes: part.file.bytesRead });
  }
  return { saved };
});

await app.listen({ port: 3000 });