UNPKG

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
"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;