示例说明
上传接口最大的风险是被大文件打爆内存。@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 });