(hopefully) fixed upload for large files
This commit is contained in:
110
nextjs/src/lib/multipart.js
Normal file
110
nextjs/src/lib/multipart.js
Normal 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);
|
||||
});
|
||||
}
|
||||
@@ -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'));
|
||||
|
||||
Reference in New Issue
Block a user