四好公路
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

'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;
}
};