(hopefully) fixed upload for large files

This commit is contained in:
Ludwig Lehnert
2026-04-11 09:27:20 +02:00
parent d86139f574
commit 1adecba896
6 changed files with 203 additions and 122 deletions

110
nextjs/src/lib/multipart.js Normal file
View File

@@ -0,0 +1,110 @@
import fs from 'node:fs';
import { Readable } from 'node:stream';
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);
});
}

View File

@@ -90,11 +90,25 @@ export async function verifyCsrf(formData) {
providedToken = String(headerStore.get('x-csrf-token') || '');
}
await verifyCsrfTokenValue(expectedToken, providedToken);
}
async function verifyCsrfTokenValue(expectedToken, providedToken) {
if (!expectedToken || !providedToken || expectedToken !== providedToken) {
throw new Error('csrf-token-mismatch');
}
}
export async function verifyCsrfToken(token) {
await verifySameOrigin();
const cookieStore = await cookies();
const expectedToken = cookieStore.get(csrfCookieName)?.value || '';
const providedToken = String(token || '');
await verifyCsrfTokenValue(expectedToken, providedToken);
}
export async function getRequestMeta() {
const headerStore = await headers();
const forwardedFor = firstForwardedPart(headerStore.get('x-forwarded-for'));