Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🧪 QA (push) Failing after 34s
Build & Deploy / 🏗️ Build (push) Has started running
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Smoke Test (push) Has been cancelled
Build & Deploy / ⚡ Lighthouse (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
162 lines
6.3 KiB
Plaintext
162 lines
6.3 KiB
Plaintext
import Busboy from 'busboy';
|
|
import { status as httpStatus } from 'http-status';
|
|
import { APIError } from '../../errors/APIError.js';
|
|
import { fileFactory } from './fileFactory.js';
|
|
import { memHandler, tempFileHandler } from './handlers.js';
|
|
import { processNested } from './processNested.js';
|
|
import { createUploadTimer } from './uploadTimer.js';
|
|
import { buildFields, debugLog, isFunc, parseFileName } from './utilities.js';
|
|
const waitFlushProperty = Symbol('wait flush property symbol');
|
|
export const processMultipart = async ({ options, request })=>{
|
|
let parsingRequest = true;
|
|
let shouldAbortProccessing = false;
|
|
let fileCount = 0;
|
|
let filesCompleted = 0;
|
|
let allFilesHaveResolved;
|
|
let failedResolvingFiles;
|
|
const allFilesComplete = new Promise((res, rej)=>{
|
|
allFilesHaveResolved = res;
|
|
failedResolvingFiles = rej;
|
|
});
|
|
const result = {
|
|
fields: undefined,
|
|
files: undefined
|
|
};
|
|
const headersObject = {};
|
|
request.headers.forEach((value, name)=>{
|
|
headersObject[name] = value;
|
|
});
|
|
const reader = request.body?.getReader();
|
|
const busboy = Busboy({
|
|
...options,
|
|
headers: headersObject
|
|
});
|
|
function abortAndDestroyFile(file, err) {
|
|
file.destroy();
|
|
shouldAbortProccessing = true;
|
|
failedResolvingFiles(err);
|
|
}
|
|
// Build multipart req.body fields
|
|
busboy.on('field', (field, val)=>{
|
|
result.fields = buildFields(result.fields, field, val);
|
|
});
|
|
// Build req.files fields
|
|
busboy.on('file', (field, file, info)=>{
|
|
fileCount += 1;
|
|
// Parse file name(cutting huge names, decoding, etc..).
|
|
const { encoding, filename: name, mimeType: mime } = info;
|
|
const filename = parseFileName(options, name);
|
|
const inferredMimeType = filename && filename.endsWith('.glb') && 'model/gltf-binary' || filename && filename.endsWith('.gltf') && 'model/gltf+json' || mime;
|
|
// Define methods and handlers for upload process.
|
|
const { cleanup, complete, dataHandler, getFilePath, getFileSize, getHash, getWritePromise } = options.useTempFiles ? tempFileHandler(options, field, filename) // Upload into temporary file.
|
|
: memHandler(options, field, filename) // Upload into RAM.
|
|
;
|
|
const writePromise = options.useTempFiles ? getWritePromise().catch(()=>{
|
|
busboy.end();
|
|
cleanup();
|
|
}) : getWritePromise();
|
|
const uploadTimer = createUploadTimer(options.uploadTimeout, ()=>{
|
|
return abortAndDestroyFile(file, new APIError(`Upload timeout for ${field}->${filename}, bytes:${getFileSize()}`));
|
|
});
|
|
file.on('limit', ()=>{
|
|
debugLog(options, `Size limit reached for ${field}->${filename}, bytes:${getFileSize()}`);
|
|
uploadTimer.clear();
|
|
if (isFunc(options.limitHandler)) {
|
|
options.limitHandler({
|
|
request,
|
|
size: getFileSize()
|
|
});
|
|
}
|
|
// Return error and cleanup files if abortOnLimit set.
|
|
if (options.abortOnLimit) {
|
|
debugLog(options, `Upload file size limit reached ${field}->${filename}.`);
|
|
cleanup();
|
|
abortAndDestroyFile(file, new APIError(options.responseOnLimit, httpStatus.REQUEST_ENTITY_TOO_LARGE, {
|
|
size: getFileSize()
|
|
}));
|
|
}
|
|
});
|
|
file.on('data', (data)=>{
|
|
uploadTimer.set();
|
|
dataHandler(data);
|
|
});
|
|
file.on('end', ()=>{
|
|
const size = getFileSize();
|
|
debugLog(options, `Upload finished ${field}->${filename}, bytes:${size}`);
|
|
uploadTimer.clear();
|
|
if (!name && size === 0) {
|
|
fileCount -= 1;
|
|
if (options.useTempFiles) {
|
|
cleanup();
|
|
debugLog(options, `Removing the empty file ${field}->${filename}`);
|
|
}
|
|
return debugLog(options, `Don't add file instance if original name and size are empty`);
|
|
}
|
|
filesCompleted += 1;
|
|
result.files = buildFields(result.files, field, fileFactory({
|
|
name: filename,
|
|
buffer: complete(),
|
|
encoding,
|
|
hash: getHash(),
|
|
mimetype: inferredMimeType,
|
|
size,
|
|
tempFilePath: getFilePath(),
|
|
truncated: Boolean('truncated' in file && file.truncated) || false
|
|
}, options));
|
|
if (!request[waitFlushProperty]) {
|
|
request[waitFlushProperty] = [];
|
|
}
|
|
request[waitFlushProperty].push(writePromise);
|
|
if (filesCompleted === fileCount) {
|
|
allFilesHaveResolved();
|
|
}
|
|
});
|
|
file.on('error', (err)=>{
|
|
uploadTimer.clear();
|
|
debugLog(options, `File Error: ${err.message}`);
|
|
cleanup();
|
|
failedResolvingFiles(err);
|
|
});
|
|
// Start upload process.
|
|
debugLog(options, `New upload started ${field}->${filename}, bytes:${getFileSize()}`);
|
|
uploadTimer.set();
|
|
});
|
|
busboy.on('finish', async ()=>{
|
|
debugLog(options, `Busboy finished parsing request.`);
|
|
if (options.parseNested) {
|
|
result.fields = processNested(result.fields);
|
|
result.files = processNested(result.files);
|
|
}
|
|
if (request[waitFlushProperty]) {
|
|
try {
|
|
await Promise.all(request[waitFlushProperty]).then(()=>{
|
|
delete request[waitFlushProperty];
|
|
});
|
|
} catch (err) {
|
|
debugLog(options, `Error waiting for file write promises: ${err}`);
|
|
}
|
|
}
|
|
return result;
|
|
});
|
|
busboy.on('error', (err = new APIError('Busboy error parsing multipart request', httpStatus.BAD_REQUEST))=>{
|
|
debugLog(options, `Busboy error`);
|
|
throw err;
|
|
});
|
|
while(parsingRequest){
|
|
const { done, value } = await reader.read();
|
|
if (done) {
|
|
parsingRequest = false;
|
|
}
|
|
if (value && !shouldAbortProccessing) {
|
|
busboy.write(value);
|
|
}
|
|
}
|
|
if (fileCount !== 0) {
|
|
await allFilesComplete.catch((e)=>{
|
|
throw e;
|
|
});
|
|
}
|
|
return result;
|
|
};
|
|
|
|
//# sourceMappingURL=processMultipart.js.map |