Files
files/nextjs/src/lib/multipart.js
2026-06-18 19:17:51 +00:00

156 lines
3.5 KiB
JavaScript

import fs from 'node:fs';
import { Readable, Transform } from 'node:stream';
import { pipeline } from 'node:stream/promises';
import Busboy from 'busboy';
export async function streamMultipartToPath(request, options) {
const { fileFieldName = 'file', targetPath, maxFileBytes = 0 } = options;
if (!request.body) {
throw new Error('missing-body');
}
return new Promise((resolve, reject) => {
const limits = { files: 1, fields: 20 };
if (maxFileBytes > 0) {
limits.fileSize = maxFileBytes;
}
const parser = Busboy({ headers: Object.fromEntries(request.headers), limits });
const fields = {};
let uploadInfo = null;
let writeStream = null;
let writeFinished = false;
let writeError = null;
let hitSizeLimit = false;
function fail(error) {
if (writeStream) {
writeStream.destroy();
}
fs.promises.rm(targetPath, { force: true }).catch(() => undefined).finally(() => reject(error));
}
parser.on('field', (name, value) => {
fields[name] = value;
});
parser.on('file', (name, file, info) => {
if (name !== fileFieldName || uploadInfo) {
file.resume();
return;
}
writeStream = fs.createWriteStream(targetPath, { flags: 'wx' });
uploadInfo = {
filename: info.filename || '',
mimeType: info.mimeType || '',
sizeBytes: 0,
};
file.on('data', (chunk) => {
uploadInfo.sizeBytes += chunk.length;
});
file.on('limit', () => {
hitSizeLimit = true;
});
file.on('error', (error) => {
writeError = error;
});
writeStream.on('error', (error) => {
writeError = error;
});
writeStream.on('close', () => {
writeFinished = true;
});
file.pipe(writeStream);
});
parser.on('error', (error) => {
fail(error);
});
parser.on('finish', () => {
const complete = () => {
if (writeError) {
fail(writeError);
return;
}
if (hitSizeLimit) {
fail(new Error('file-too-large'));
return;
}
if (!uploadInfo) {
fail(new Error('missing-file'));
return;
}
resolve({ fields, file: uploadInfo });
};
if (!writeStream) {
complete();
return;
}
if (writeFinished) {
complete();
return;
}
writeStream.on('close', complete);
});
Readable.fromWeb(request.body).pipe(parser);
});
}
export async function streamBodyToPath(request, options) {
const { targetPath, maxBytes = 0 } = options;
if (!request.body) {
throw new Error('missing-body');
}
let sizeBytes = 0;
const counter = new Transform({
transform(chunk, encoding, callback) {
sizeBytes += chunk.length;
if (maxBytes > 0 && sizeBytes > maxBytes) {
callback(new Error('file-too-large'));
return;
}
callback(null, chunk);
},
});
const writeStream = fs.createWriteStream(targetPath, { flags: 'wx' });
let opened = false;
writeStream.on('open', () => {
opened = true;
});
try {
await pipeline(Readable.fromWeb(request.body), counter, writeStream);
} catch (error) {
if (opened) {
await fs.promises.rm(targetPath, { force: true }).catch(() => undefined);
}
throw error;
}
if (sizeBytes <= 0) {
await fs.promises.rm(targetPath, { force: true }).catch(() => undefined);
throw new Error('missing-file');
}
return { sizeBytes };
}