formdata-node
Version:
FormData implementation for Node.js. Built over Readable stream and async generators. Can be used to communicate between servers with multipart/form-data format.
435 lines (347 loc) • 12.6 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _toStringTag = _interopRequireDefault(require("@babel/runtime/core-js/symbol/to-string-tag"));
var _map = _interopRequireDefault(require("@babel/runtime/core-js/map"));
var _symbol = _interopRequireDefault(require("@babel/runtime/core-js/symbol"));
var _iterator = _interopRequireDefault(require("@babel/runtime/core-js/symbol/iterator"));
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
var _wrapAsyncGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/wrapAsyncGenerator"));
var _awaitAsyncGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/awaitAsyncGenerator"));
var _asyncIterator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncIterator"));
var _asyncGeneratorDelegate2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncGeneratorDelegate"));
var _stream = require("stream");
var _path = require("path");
var _invariant = _interopRequireDefault(require("@octetstream/invariant"));
var _mimeTypes = _interopRequireDefault(require("mime-types"));
var _bind = _interopRequireDefault(require("./util/bind"));
var _concat = _interopRequireDefault(require("./util/concat"));
var _boundary = _interopRequireDefault(require("./util/boundary"));
var _getType = _interopRequireDefault(require("./util/getType"));
var _isString = _interopRequireDefault(require("./util/isString"));
var _isObject = _interopRequireDefault(require("./util/isObject"));
var _isBuffer = _interopRequireDefault(require("./util/isBuffer"));
var _isReadable = _interopRequireDefault(require("./util/isReadable"));
var _isFunction = _interopRequireDefault(require("./util/isFunction"));
var _StreamIterator = _interopRequireDefault(require("./util/StreamIterator"));
const isArray = Array.isArray;
/**
* FormData implementation for Node.js environments.
* Bult over Readable stream and async generators.
* Can be used to communicate between servers with multipart/form-data format.
*
* @api public
*/
var _Symbol$toStringTag = _toStringTag.default;
var _Symbol$iterator = _iterator.default;
var _Symbol$asyncIterator = _symbol.default.asyncIterator;
class FormData {
/**
* Check if given value is instance of FormData
* Note: This method is not a part of client-side FormData interface.
*
* @param {any} value
*
* @return {boolean}
*
* @public
*/
static isFormData(value) {
return value instanceof FormData;
}
/**
* @param {array} fields – an optional FormData initial fields.
* Each initial field should be passed as a collection of the objects
* with "name", "value" and "filename" props.
* See the FormData#append for more info about the available format.
*/
constructor(_fields = null) {
(0, _defineProperty2.default)(this, "__getMime", filename => _mimeTypes.default.lookup(filename) || this.__defaultContentType);
(0, _defineProperty2.default)(this, "__getFooter", () => (0, _concat.default)([this.__dashes, this.__boundary, this.__dashes, this.__carriage.repeat(2)]));
(0, _defineProperty2.default)(this, "__read", () => {
const onFulfilled = ({
done,
value
}) => {
if (done) {
return this.__stream.push(null);
}
this.__stream.push((0, _isBuffer.default)(value) ? value : Buffer.from(String(value)));
};
const onRejected = err => this.__stream.emit("error", err);
this.__curr.next().then(onFulfilled).catch(onRejected);
});
(0, _defineProperty2.default)(this, "__appendFromInitialFields", fields => {
for (const field of fields) {
if ((0, _isObject.default)(field)) {
this.append(field.name, field.value, field.filename);
}
}
});
(0, _defineProperty2.default)(this, "append", (name, value, filename) => this.__setField(name, value, filename, 1));
(0, _defineProperty2.default)(this, "set", (name, value, filename) => this.__setField(name, value, filename));
(0, _defineProperty2.default)(this, "has", name => this.__contents.has(name));
(0, _defineProperty2.default)(this, "get", name => {
const field = this.__contents.get(name);
if (!field) {
return undefined;
}
return field.values[0];
});
(0, _defineProperty2.default)(this, "getAll", name => {
const res = [];
const field = this.__contents.get(name);
if (field) {
for (const value of field.values) {
res.push(value);
}
}
return res;
});
(0, _defineProperty2.default)(this, "delete", name => void this.__contents.delete(name));
(0, _defineProperty2.default)(this, "pipe", (dest, options) => this.__stream.pipe(dest, options));
(0, _defineProperty2.default)(this, "forEach", (fn, ctx = null) => {
for (const [name, value] of this) {
fn.call(ctx, value, name, this);
}
});
(0, _bind.default)([_iterator.default, _symbol.default.asyncIterator, "toString", "inspect", "keys", "values", "entries"], this);
this.__carriage = "\r\n";
this.__defaultContentType = "application/octet-steam";
this.__dashes = "--";
this.__boundary = (0, _concat.default)(["NodeJSFormDataStream", (0, _boundary.default)()]);
this.__contents = new _map.default();
this.__entries = this.__contents.entries();
this.__curr = this.__getField();
const read = this.__read;
this.__stream = new _stream.Readable({
read
});
if (isArray(_fields)) {
this.__appendFromInitialFields(_fields);
}
}
/**
* @private
*/
/**
* @private
*/
__getHeader(name, filename) {
const head = [this.__dashes, this.__boundary, this.__carriage, "Content-Disposition: form-data; ", `name="${name}"`];
if (filename) {
head.push(`; filename="${filename}"${this.__carriage}`);
head.push(`Content-Type: "${this.__getMime(filename)}"`);
}
head.push(this.__carriage.repeat(2));
return (0, _concat.default)(head);
}
/**
* @private
*/
/**
* Get each field from internal Map
*
* @private
*/
__getField() {
var _this = this;
return (0, _wrapAsyncGenerator2.default)(function* () {
while (true) {
const curr = _this.__entries.next(); // Send a footer when iterator ends
if (curr.done === true) {
yield _this.__getFooter(); // In some cases I can't just return footer to stop iterator,
// so this might help.
// According to tests, it happens only Node 8.x
// eslint-disable-next-line max-len
// @see: https://travis-ci.org/octet-stream/apollo-link-form-data/jobs/465480482#L485-L490
return;
}
const [name, {
values,
filename
}] = curr.value; // Set field header
yield _this.__getHeader(name, filename); // Set the field body
for (const value of values) {
if ((0, _isReadable.default)(value)) {
// Read the stream content
yield* (0, _asyncGeneratorDelegate2.default)((0, _asyncIterator2.default)((0, _isFunction.default)(value[_symbol.default.asyncIterator]) ? value : new _StreamIterator.default(value)), _awaitAsyncGenerator2.default);
} else {
yield value;
}
} // Add trailing carriage
yield _this.__carriage;
}
})();
}
/**
* Read values from internal storage and push it to the internal stream
*
* @return {void}
*
* @private
*/
/**
* Appends a new value onto an existing key inside a FormData object,
* or adds the key if it does not already exist.
*
* @param {string} name – The name of the field whose data
* is contained in value
*
* @param {any} value – The field value. You can pass any primitive type
* (including null and undefined), Buffer or Readable stream.
* Note that Arrays and Object will be converted to string
* by using String function.
*
* @param {string} [filename = undefined] A filename of given field.
* Can be added only for Buffer and Readable
*
* @return {void}
*
* @private
*/
__setField(name, value, filename, append = false) {
(0, _invariant.default)(!(0, _isString.default)(name), TypeError, "Field name should be a string. Received %s", (0, _getType.default)(name));
(0, _invariant.default)(filename && !(0, _isString.default)(filename), TypeError, "Filename should be a string (if passed). Received %s", (0, _getType.default)(filename)); // Getting a filename for Buffer and Readable values
if ((0, _isBuffer.default)(value) && filename) {
filename = (0, _path.basename)(filename);
} else if ((0, _isReadable.default)(value) && (value.path || filename)) {
// Readable stream which created from fs.createReadStream
// have a "path" property. So, we can get a "filename"
// from the stream itself.
filename = (0, _path.basename)(value.path || filename);
} else {
// The regular values shouldn't have "filename" property
filename = undefined;
}
append = Boolean(append);
if (!((0, _isReadable.default)(value) || (0, _isBuffer.default)(value))) {
value = String(value);
}
const field = this.__contents.get(name); // Set a new field if given name is not exists
if (!field) {
return void this.__contents.set(name, {
append,
filename,
values: [value]
});
} // Replace a value of the existing field if "set" called
if (!append) {
return void this.__contents.set(name, {
append,
filename,
values: [value]
});
} // Do nothing if the field has been created from .set()
if (!field.append) {
return undefined;
} // Append a new value to the existing field
field.values.push(value);
this.__contents.set(name, field);
}
/**
* Returns boundary string
*
* @return {string}
*
* @public
*/
get boundary() {
return this.__boundary;
}
/**
* Returns headers for multipart/form-data
*/
get headers() {
return {
"content-type": (0, _concat.default)(["multipart/form-data; ", "boundary=", this.boundary])
};
}
/**
* Returns the internal stream
*
* @return {stream.Readable}
*
* @public
*/
get stream() {
return this.__stream;
}
/**
* Appends a new value onto an existing key inside a FormData object,
* or adds the key if it does not already exist.
*
* @param {string} name – The name of the field whose data
* is contained in value
*
* @param {any} value – The field value. You can pass any primitive type
* (including null and undefined), Buffer or Readable stream.
* Note that Arrays and Object will be converted to string
* by using String function.
*
* @param {string} [filename = undefined] A filename of given field.
* Can be added only for Buffer and Readable
*
* @return {void}
*
* @public
*/
toString() {
return "[object FormData]";
}
inspect() {
return "FormData";
}
get [_Symbol$toStringTag]() {
return "FormData";
}
*keys() {
for (const key of this.__contents.keys()) {
yield key;
}
}
*entries() {
for (const name of this.keys()) {
const values = this.getAll(name);
yield [name, values.length === 1 ? values[0] : values];
}
}
*values() {
for (const [, values] of this) {
yield values;
}
}
/**
* Executes a given callback for each field of the FormData instance
*
* @param {function} fn – Function to execute for each element,
* taking three arguments:
* + {any} value – A value(s) of the current field.
* + {string} – Name of the current field.
* + {FormData} fd – The FormData instance that forEach
* is being applied to
*
* @param {any} [ctx = null]
*
* @public
*/
[_Symbol$iterator]() {
return this.entries();
}
/**
* This method allows to read a content from internal stream
* using async generators and for-await-of APIs
*
* @return {StreamIterator}
*
* @public
*/
[_Symbol$asyncIterator]() {
return new _StreamIterator.default(this.__stream);
}
}
var _default = FormData;
exports.default = _default;