You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
207 lines
5.4 KiB
207 lines
5.4 KiB
'use strict';
|
|
|
|
const Busboy = require('busboy');
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
|
|
const getDescriptor = Object.getOwnPropertyDescriptor;
|
|
|
|
module.exports = function (request, options) {
|
|
options = options || {};
|
|
options.headers = request.headers;
|
|
const customOnFile = typeof options.onFile === "function" ? options.onFile : false;
|
|
delete options.onFile;
|
|
const busboy = new Busboy(options);
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const fields = {};
|
|
const filePromises = [];
|
|
|
|
request.on('close', cleanup);
|
|
|
|
busboy
|
|
.on('field', onField.bind(null, fields))
|
|
.on('file', customOnFile || onFile.bind(null, filePromises))
|
|
.on('close', cleanup)
|
|
.on('error', onError)
|
|
.on('end', onEnd)
|
|
.on('finish', onEnd);
|
|
|
|
busboy.on('partsLimit', function(){
|
|
const err = new Error('Reach parts limit');
|
|
err.code = 'Request_parts_limit';
|
|
err.status = 413;
|
|
onError(err);
|
|
});
|
|
|
|
busboy.on('filesLimit', () => {
|
|
const err = new Error('Reach files limit');
|
|
err.code = 'Request_files_limit';
|
|
err.status = 413;
|
|
onError(err);
|
|
});
|
|
|
|
busboy.on('fieldsLimit', () => {
|
|
const err = new Error('Reach fields limit');
|
|
err.code = 'Request_fields_limit';
|
|
err.status = 413;
|
|
onError(err);
|
|
});
|
|
|
|
request.pipe(busboy);
|
|
|
|
function onError(err) {
|
|
cleanup();
|
|
return reject(err);
|
|
}
|
|
|
|
function onEnd(err) {
|
|
if(err) {
|
|
return reject(err);
|
|
}
|
|
if (customOnFile) {
|
|
cleanup();
|
|
resolve({ fields });
|
|
} else {
|
|
Promise.all(filePromises)
|
|
.then((files) => {
|
|
cleanup();
|
|
resolve({fields, files});
|
|
})
|
|
.catch(reject);
|
|
}
|
|
}
|
|
|
|
function cleanup() {
|
|
busboy.removeListener('field', onField);
|
|
busboy.removeListener('file', onFile);
|
|
busboy.removeListener('close', cleanup);
|
|
busboy.removeListener('end', cleanup);
|
|
busboy.removeListener('error', onEnd);
|
|
busboy.removeListener('partsLimit', onEnd);
|
|
busboy.removeListener('filesLimit', onEnd);
|
|
busboy.removeListener('fieldsLimit', onEnd);
|
|
busboy.removeListener('finish', onEnd);
|
|
}
|
|
});
|
|
};
|
|
|
|
function onField(fields, name, val, fieldnameTruncated, valTruncated) {
|
|
// don't overwrite prototypes
|
|
if (getDescriptor(Object.prototype, name)) return;
|
|
|
|
// This looks like a stringified array, let's parse it
|
|
if (name.indexOf('[') > -1) {
|
|
const obj = objectFromBluePrint(extractFormData(name), val);
|
|
reconcile(obj, fields);
|
|
|
|
} else {
|
|
if (fields.hasOwnProperty(name)) {
|
|
if (Array.isArray(fields[name])) {
|
|
fields[name].push(val);
|
|
} else {
|
|
fields[name] = [fields[name], val];
|
|
}
|
|
} else {
|
|
fields[name] = val;
|
|
}
|
|
}
|
|
}
|
|
|
|
function onFile(filePromises, fieldname, file, filename, encoding, mimetype) {
|
|
const tmpName = file.tmpName = Math.random().toString(16).substring(2) + '-' + filename;
|
|
const saveTo = path.join(os.tmpdir(), path.basename(tmpName));
|
|
const writeStream = fs.createWriteStream(saveTo);
|
|
|
|
const filePromise = new Promise((resolve, reject) => writeStream
|
|
.on('open', () => file
|
|
.pipe(writeStream)
|
|
.on('error', reject)
|
|
.on('finish', () => {
|
|
const readStream = fs.createReadStream(saveTo);
|
|
readStream.fieldname = fieldname;
|
|
readStream.filename = filename;
|
|
readStream.transferEncoding = readStream.encoding = encoding;
|
|
readStream.mimeType = readStream.mime = mimetype;
|
|
resolve(readStream);
|
|
})
|
|
)
|
|
.on('error', (err) => {
|
|
file
|
|
.resume()
|
|
.on('error', reject);
|
|
reject(err);
|
|
})
|
|
);
|
|
filePromises.push(filePromise);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* Extract a hierarchy array from a stringified formData single input.
|
|
*
|
|
*
|
|
* i.e. topLevel[sub1][sub2] => [topLevel, sub1, sub2]
|
|
*
|
|
* @param {String} string: Stringify representation of a formData Object
|
|
* @return {Array}
|
|
*
|
|
*/
|
|
const extractFormData = (string) => {
|
|
const arr = string.split('[');
|
|
const first = arr.shift();
|
|
const res = arr.map( v => v.split(']')[0] );
|
|
res.unshift(first);
|
|
return res;
|
|
};
|
|
|
|
/**
|
|
*
|
|
* Generate an object given an hierarchy blueprint and the value
|
|
*
|
|
* i.e. [key1, key2, key3] => { key1: {key2: { key3: value }}};
|
|
*
|
|
* @param {Array} arr: from extractFormData
|
|
* @param {[type]} value: The actual value for this key
|
|
* @return {[type]} [description]
|
|
*
|
|
*/
|
|
const objectFromBluePrint = (arr, value) => {
|
|
return arr
|
|
.reverse()
|
|
.reduce((acc, next) => {
|
|
if (Number(next).toString() === 'NaN') {
|
|
return {[next]: acc};
|
|
} else {
|
|
const newAcc = [];
|
|
newAcc[ Number(next) ] = acc;
|
|
return newAcc;
|
|
}
|
|
}, value);
|
|
};
|
|
|
|
/**
|
|
* Reconciles formatted data with already formatted data
|
|
*
|
|
* @param {Object} obj extractedObject
|
|
* @param {Object} target the field object
|
|
* @return {Object} reconciled fields
|
|
*
|
|
*/
|
|
const reconcile = (obj, target) => {
|
|
const key = Object.keys(obj)[0];
|
|
const val = obj[key];
|
|
|
|
// The reconciliation works even with array has
|
|
// Object.keys will yield the array indexes
|
|
// see https://jsbin.com/hulekomopo/1/
|
|
// Since array are in form of [ , , valu3] [value1, value2]
|
|
// the final array will be: [value1, value2, value3] has expected
|
|
if (target.hasOwnProperty(key)) {
|
|
return reconcile(val, target[key]);
|
|
} else {
|
|
return target[key] = val;
|
|
}
|
|
|
|
};
|
|
|