├── .eslintrc ├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── main.js ├── package-lock.json ├── package.json └── test ├── bufferAsync.js ├── fixture └── fixture.jpg └── streamAsync.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base", 3 | "env": { 4 | "node": true, 5 | "mocha": true 6 | }, 7 | "parserOptions": { 8 | "sourceType": "script" 9 | }, 10 | "rules": { 11 | "comma-dangle": 0, 12 | "consistent-return": 0, 13 | "func-names": 0, 14 | "new-cap": [2, { "newIsCap": true, "capIsNew": false }], 15 | "no-param-reassign": 0, 16 | "no-plusplus": 0, 17 | "no-restricted-syntax": 0, 18 | "no-shadow": [2, { "allow": ["err", "n"] }], 19 | "no-underscore-dangle": 0, 20 | "prefer-rest-params": 0, 21 | "strict": 0 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [12.x, 14.x, 16.x, 18.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'npm' 29 | - run: npm ci 30 | - run: npm run lint && npm run cover 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2019 Alexey Bystrov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | multi-part [![License](https://img.shields.io/npm/l/multi-part.svg)](https://github.com/strikeentco/multi-part/blob/master/LICENSE) [![npm](https://img.shields.io/npm/v/multi-part.svg)](https://www.npmjs.com/package/multi-part) 2 | ========== 3 | [![Build Status](https://travis-ci.org/strikeentco/multi-part.svg)](https://travis-ci.org/strikeentco/multi-part) [![node](https://img.shields.io/node/v/multi-part.svg)](https://www.npmjs.com/package/multi-part) [![Test Coverage](https://api.codeclimate.com/v1/badges/9876ebf194e36617bcea/test_coverage)](https://codeclimate.com/github/strikeentco/multi-part/test_coverage) 4 | 5 | A `multi-part` allows you to create multipart/form-data `Stream` and `Buffer`, which can be used to submit forms and file uploads to other web applications. 6 | 7 | It extends [`multi-part-lite`](https://github.com/strikeentco/multi-part-lite) and adds automatic data type detection. 8 | 9 | Supports: `Strings`, `Numbers`, `Arrays`, `ReadableStreams`, `Buffers` and `Vinyl`. 10 | 11 | ## Install 12 | ```sh 13 | $ npm install multi-part --save 14 | ``` 15 | 16 | ## Usage 17 | Usage with `got` as `Stream`: 18 | 19 | ```js 20 | const got = require('got'); 21 | const Multipart = require('multi-part'); 22 | const form = new Multipart(); 23 | 24 | form.append('photo', got.stream('https://avatars1.githubusercontent.com/u/2401029')); 25 | form.append('field', 'multi-part test'); 26 | 27 | (async () => { 28 | const body = await form.stream(); 29 | got.post('127.0.0.1:3000', { headers: form.getHeaders(), body }); 30 | })() 31 | ``` 32 | Usage with `got` as `Buffer`: 33 | 34 | ```js 35 | const got = require('got'); 36 | const Multipart = require('multi-part'); 37 | const form = new Multipart(); 38 | 39 | form.append('photo', got.stream('https://avatars1.githubusercontent.com/u/2401029')); 40 | form.append('field', 'multi-part test'); 41 | 42 | (async () => { 43 | const body = await form.buffer(); 44 | got.post('127.0.0.1:3000', { headers: form.getHeaders(false), body }); 45 | })() 46 | ``` 47 | Usage with `http`/`https` as `Stream`: 48 | 49 | ```js 50 | const http = require('http'); 51 | const https = require('https'); 52 | const Multipart = require('multi-part'); 53 | const form = new Multipart(); 54 | 55 | form.append('photo', https.request('https://avatars1.githubusercontent.com/u/2401029')); 56 | 57 | (async () => { 58 | const stream = await form.stream(); 59 | stream.pipe(http.request({ headers: form.getHeaders(), hostname: '127.0.0.1', port: 3000, method: 'POST' })); 60 | })() 61 | ``` 62 | Usage with `http`/`https` as `Buffer`: 63 | 64 | ```js 65 | const http = require('http'); 66 | const https = require('https'); 67 | const Multipart = require('multi-part'); 68 | const form = new Multipart(); 69 | 70 | form.append('photo', https.request('https://avatars1.githubusercontent.com/u/2401029')); 71 | 72 | (async () => { 73 | const body = await form.buffer(); 74 | const req = http.request({ headers: form.getHeaders(false), hostname: '127.0.0.1', port: 3000, method: 'POST' }); 75 | req.end(body); 76 | })() 77 | ``` 78 | 79 | # API 80 | 81 | ### new Multipart([options]) 82 | ### new MultipartAsync([options]) 83 | 84 | Constructor. 85 | 86 | ### Params: 87 | * **[options]** (*Object*) - `Object` with options: 88 | * **[boundary]** (*String|Number*) - Custom boundary for `multipart` data. Ex: if equal `CustomBoundary`, boundary will be equal exactly `CustomBoundary`. 89 | * **[boundaryPrefix]** (*String|Number*) - Custom boundary prefix for `multipart` data. Ex: if equal `CustomBoundary`, boundary will be equal something like `--CustomBoundary567689371204`. 90 | * **[defaults]** (*Object*) - `Object` with defaults values: 91 | * **[name]** (*String*) - File name which will be used, if `filename` is not specified in the options of `.append` method. By default `file`. 92 | * **[ext]** (*String*) - File extension which will be used, if `filename` is not specified in the options of `.append` method. By default `bin`. 93 | * **[type]** (*String*) - File content-type which will be used, if `contentType` is not specified in the options of `.append` method. By default `application/octet-stream`. 94 | 95 | ```js 96 | const Multipart = require('multi-part'); 97 | const { MultipartAsync } = require('multi-part'); 98 | ``` 99 | 100 | ### .append(name, value, [options]) 101 | 102 | Adds a new data to the `multipart/form-data` stream. 103 | 104 | ### Params: 105 | * **name** (*String|Number*) - Field name. Ex: `photo`. 106 | * **value** (*Mixed*) - Value can be `String`, `Number`, `Array`, `Buffer`, `ReadableStream` or even [Vynil](https://www.npmjs.com/package/vinyl). 107 | * **[options]** (*Object*) - Additional options: 108 | * **filename** (*String*) - File name. Ex: `anonim.jpg`. 109 | * **contentType** (*String*) - File content type. It's not necessary if you have already specified file name. If you are not sure about the content type - leave `filename` and `contentType` empty and it will be automatically determined, if possible. Ex: `image/jpeg`. 110 | 111 | If `value` is an array, `append` will be called for each value: 112 | ```js 113 | form.append('array', [0, [2, 3], 1]); 114 | 115 | // similar to 116 | 117 | form.append('array', 0); 118 | form.append('array', 2); 119 | form.append('array', 3); 120 | form.append('array', 1); 121 | ``` 122 | 123 | `Null`, `false` and `true` will be converted to `'0'`, `'0'` and `'1'`. Numbers will be converted to strings also. 124 | 125 | For `Buffer` and `ReadableStream` content type will be automatically determined, if it's possible, and name will be specified according to content type. If content type is `image/jpeg`, file name will be set as `file.jpeg` (if `filename` option is not specified).
In case content type is undetermined, content type and file name will be set as `application/octet-stream` and `file.bin`. 126 | 127 | ### .stream() 128 | 129 | Returns a `Promise` with a `multipart/form-data` stream. 130 | 131 | ### .buffer() 132 | 133 | Returns a `Promise` with a buffer of the `multipart/form-data` stream data. 134 | 135 | ### .getBoundary() 136 | 137 | Returns the form boundary used in the `multipart/form-data` stream. 138 | 139 | ```js 140 | form.getBoundary(); // -> '--MultipartBoundary352840693617' 141 | ``` 142 | 143 | ### .getLength() 144 | 145 | Returns the length of a buffer of the `multipart/form-data` stream data. 146 | 147 | Should be called after `.buffer()`; 148 | 149 | For `.stream()` it's always `0`. 150 | 151 | ```js 152 | await form.buffer(); 153 | form.getLength(); // -> 12345 154 | ``` 155 | 156 | ### .getHeaders(chunked = true) 157 | 158 | Returns the headers. 159 | 160 | If you want to get correct `content-length`, you should call it after `.buffer()`. There is no way to know `content-length` of the `.stream()`, so it will be always `0`. 161 | 162 | ### Params: 163 | * **chunked** (*Boolean*) - If `false` - headers will include `content-length` header, otherwise there will be `transfer-encoding: 'chunked'`. 164 | 165 | ```js 166 | form.getHeaders(); // -> 167 | //{ 168 | // 'transfer-encoding': 'chunked', 169 | // 'content-type': 'multipart/form-data; boundary="--MultipartBoundary352840693617"' 170 | //} 171 | ``` 172 | With `.buffer()`: 173 | ```js 174 | form.getHeaders(false); // -> 175 | //{ 176 | // 'content-length': '0', 177 | // 'content-type': 'multipart/form-data; boundary="--MultipartBoundary352840693617"' 178 | //} 179 | 180 | await form.buffer(); 181 | form.getHeaders(false); // -> 182 | //{ 183 | // 'content-length': '12345', 184 | // 'content-type': 'multipart/form-data; boundary="--MultipartBoundary352840693617"' 185 | //} 186 | ``` 187 | 188 | ## License 189 | 190 | The MIT License (MIT)
191 | Copyright (c) 2015-2022 Alexey Bystrov 192 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint-disable class-methods-use-this, no-await-in-loop, max-len, no-promise-executor-return */ 4 | 5 | const { basename } = require('path'); 6 | const MultipartLite = require('multi-part-lite'); 7 | const { 8 | isBuffer, isStream, isHTTPStream, isVinyl 9 | } = require('multi-part-lite/lib/helpers'); 10 | const mime = require('mime-kind'); 11 | 12 | const { 13 | init, started, ended, stack, generate, next, length 14 | } = MultipartLite.symbols; 15 | 16 | const CRLF = '\r\n'; 17 | 18 | class Multipart extends MultipartLite { 19 | /** 20 | * Returns file name of val 21 | * @param {Object} val 22 | * @param {String} [val.filename] 23 | * @param {String} [val.path] 24 | * @param {Object} defaults 25 | * @param {String} defaults.name 26 | * @param {String} defaults.ext 27 | * @returns {Promise} 28 | * @async 29 | */ 30 | async getFileName(val, { name, ext, type }) { 31 | if (isBuffer(val)) { 32 | const m = await mime.async(val, type); 33 | return `${name}.${m.ext}`; 34 | } 35 | 36 | const filename = val.filename || val.path; 37 | 38 | if (filename) { 39 | return basename(filename); 40 | } 41 | 42 | return `${name}.${ext}`; 43 | } 44 | 45 | /** 46 | * Returns content-type of val 47 | * @param {Object} val 48 | * @param {String} [val.contentType] 49 | * @param {Object} defaults 50 | * @param {String} defaults.type 51 | * @returns {Promise} 52 | * @async 53 | */ 54 | async getContentType(val, { type }) { 55 | if (val.contentType) { 56 | return val.contentType; 57 | } 58 | const m = await mime.async(val.filename, type); 59 | return m.mime; 60 | } 61 | 62 | async [init]() { 63 | if (this[ended] || this[started]) { 64 | return; 65 | } 66 | this[started] = true; 67 | let value = this[stack].shift(); 68 | while (value) { 69 | await this[generate](...value); 70 | value = this[stack].shift(); 71 | } 72 | this._append(`--${this.getBoundary()}--`, CRLF); 73 | this[ended] = true; 74 | this[next](); 75 | } 76 | 77 | async [generate](field, value, { filename, contentType }) { 78 | this._append(`--${this.getBoundary()}${CRLF}`); 79 | this._append(`Content-Disposition: form-data; name="${field}"`); 80 | if (isBuffer(value) || isStream(value) || isHTTPStream(value) || isVinyl(value)) { 81 | if (isVinyl(value)) { 82 | filename = filename || value.basename; 83 | value = value.contents; 84 | } 85 | const file = await this.getFileName(filename ? { filename } : value, this.opts.defaults); 86 | this._append(`; filename="${file}"${CRLF}`); 87 | const type = await this.getContentType({ filename: filename || file, contentType }, this.opts.defaults); 88 | this._append(`Content-Type: ${type}${CRLF}`); 89 | } else { 90 | this._append(CRLF); 91 | } 92 | 93 | return this._append(CRLF, value, CRLF); 94 | } 95 | 96 | /** 97 | * Returns stream 98 | * @returns {Promise} 99 | * @async 100 | */ 101 | async stream() { 102 | await this[init](); 103 | return this; 104 | } 105 | 106 | /** 107 | * Returns buffer of the stream 108 | * @returns {Promise} 109 | * @async 110 | */ 111 | async buffer() { 112 | return new Promise((resolve, reject) => { 113 | this.once('error', reject); 114 | const buffer = []; 115 | this.on('data', (data) => { 116 | buffer.push(data); 117 | }); 118 | this.on('end', () => { 119 | const body = Buffer.concat(buffer); 120 | this[length] = Buffer.byteLength(body); 121 | return resolve(body); 122 | }); 123 | return this[init]().catch(reject); 124 | }); 125 | } 126 | } 127 | 128 | module.exports = Multipart; 129 | module.exports.MultipartAsync = Multipart; 130 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "multi-part", 3 | "author": "Alexey Bystrov ", 4 | "version": "4.0.0", 5 | "engines": { 6 | "node": ">=10" 7 | }, 8 | "description": "Simple multipart/form-data implementation with automatic data type detection. Supports: Strings, Numbers, Arrays, Streams, Buffers and Vinyl.", 9 | "keywords": [ 10 | "multi-part", 11 | "form", 12 | "data", 13 | "buffer", 14 | "stream", 15 | "vinyl", 16 | "form-data", 17 | "multipart" 18 | ], 19 | "main": "./main.js", 20 | "files": [ 21 | "main.js" 22 | ], 23 | "scripts": { 24 | "test": "mocha test", 25 | "lint": "eslint main.js", 26 | "check": "npm run lint && npm run test", 27 | "cover": "nyc ./node_modules/mocha/bin/_mocha && nyc report --reporter=html" 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "git+https://github.com/strikeentco/multi-part.git" 32 | }, 33 | "bugs": { 34 | "url": "https://github.com/strikeentco/multi-part/issues" 35 | }, 36 | "dependencies": { 37 | "mime-kind": "^4.0.0", 38 | "multi-part-lite": "^1.0.0" 39 | }, 40 | "devDependencies": { 41 | "eslint": "^8.24.0", 42 | "eslint-config-airbnb-base": "^15.0.0", 43 | "eslint-plugin-import": "^2.26.0", 44 | "express": "^4.18.1", 45 | "got": "^9.6.0", 46 | "mocha": "^6.2.3", 47 | "multer": "^1.4.4", 48 | "nyc": "^14.1.1", 49 | "should": "^13.2.3", 50 | "vinyl": "^2.2.1" 51 | }, 52 | "license": "MIT" 53 | } 54 | -------------------------------------------------------------------------------- /test/bufferAsync.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const should = require('should/as-function'); 4 | const http = require('http'); 5 | const https = require('https'); 6 | const fs = require('fs'); 7 | const Stream = require('stream'); 8 | 9 | const File = require('vinyl'); 10 | const got = require('got'); 11 | const app = require('express')(); 12 | const multer = require('multer'); 13 | 14 | const { MultipartAsync: Multipart } = require('../main'); 15 | 16 | const upload = multer({ dest: `${__dirname}/uploads/` }); 17 | const photoFile = `${__dirname}/fixture/fixture.jpg`; 18 | 19 | function chunkSync(data, length) { 20 | const buf = Buffer.alloc(length); 21 | const fd = fs.openSync(data.path, data.flags); 22 | 23 | fs.readSync(fd, buf, 0, length); 24 | fs.closeSync(fd); 25 | 26 | return buf; 27 | } 28 | 29 | const photoVinyl = new File({ 30 | path: 'anon.jpg', 31 | contents: chunkSync({ path: photoFile, flags: 'r' }, 9379) 32 | }); 33 | 34 | describe('multi-part.async().buffer()', function () { 35 | let server; 36 | this.timeout(10000); 37 | before((done) => { 38 | app.post('/', upload.single('photo'), (req, res) => { 39 | if (req.file) { 40 | return res.json({ 41 | filename: req.file.originalname, 42 | mime: req.file.mimetype, 43 | fields: req.body 44 | }); 45 | } 46 | return res.json('Nothing'); 47 | }); 48 | 49 | server = http.createServer(app); 50 | server.listen(4000, done); 51 | }); 52 | 53 | after(() => server.close()); 54 | 55 | describe('get custom boundary', () => { 56 | it('should be ok', () => { 57 | const form = new Multipart({ boundary: '--CustomBoundary12345' }); 58 | should(form.getBoundary()).be.eql('--CustomBoundary12345'); 59 | should(form.getHeaders()).be.eql({ 60 | 'transfer-encoding': 'chunked', 61 | 'content-type': 'multipart/form-data; boundary="--CustomBoundary12345"' 62 | }); 63 | }); 64 | }); 65 | 66 | describe('append nothing', () => { 67 | it('should be ok', async () => { 68 | const form = new Multipart(); 69 | const body = await form.buffer(); 70 | return got.post('http://127.0.0.1:4000', { headers: form.getHeaders(), body }).then((res) => { 71 | should(res.body).be.eql('"Nothing"'); 72 | }); 73 | }); 74 | 75 | it('should be ok', (done) => { 76 | const form = new Multipart(); 77 | const req = http.request({ 78 | headers: form.getHeaders(), hostname: '127.0.0.1', port: 4000, method: 'POST' 79 | }, (res) => { 80 | const chunks = []; 81 | res.on('data', (chunk) => { 82 | chunks.push(chunk); 83 | }); 84 | res.on('end', () => { 85 | should(Buffer.concat(chunks).toString('utf8')).be.eql('"Nothing"'); 86 | done(); 87 | }); 88 | }); 89 | form.buffer().then((body) => { 90 | req.write(body); 91 | req.end(); 92 | }); 93 | }); 94 | 95 | it('should throw', () => { 96 | const form = new Multipart(); 97 | should(() => form.append({}, null)).throw('Field must be specified and must be a string or a number'); 98 | should(() => form.append('test')).throw('Value can\'t be undefined'); 99 | }); 100 | }); 101 | 102 | describe('append vinyl', () => { 103 | it('should be ok', async () => { 104 | const form = new Multipart(); 105 | form.append('photo', photoVinyl); 106 | const body = await form.buffer(); 107 | return got.post('http://127.0.0.1:4000', { headers: form.getHeaders(), body }).then((res) => { 108 | should(JSON.parse(res.body)).be.eql({ filename: 'anon.jpg', mime: 'image/jpeg', fields: {} }); 109 | }); 110 | }); 111 | 112 | it('should be ok', async () => { 113 | const form = new Multipart(); 114 | form.append('photo', photoVinyl, { filename: 'anon.jpg' }); 115 | const body = await form.buffer(); 116 | return got.post('http://127.0.0.1:4000', { headers: form.getHeaders(), body }).then((res) => { 117 | should(JSON.parse(res.body)).be.eql({ filename: 'anon.jpg', mime: 'image/jpeg', fields: {} }); 118 | }); 119 | }); 120 | 121 | it('should be ok', (done) => { 122 | const form = new Multipart(); 123 | form.append('photo', photoVinyl); 124 | const req = http.request({ 125 | headers: form.getHeaders(), hostname: '127.0.0.1', port: 4000, method: 'POST' 126 | }, (res) => { 127 | const chunks = []; 128 | res.on('data', (chunk) => { 129 | chunks.push(chunk); 130 | }); 131 | res.on('end', () => { 132 | should(JSON.parse(Buffer.concat(chunks).toString('utf8'))).be.eql({ filename: 'anon.jpg', mime: 'image/jpeg', fields: {} }); 133 | done(); 134 | }); 135 | }); 136 | form.buffer().then((body) => { 137 | req.write(body); 138 | req.end(); 139 | }); 140 | }); 141 | }); 142 | 143 | describe('append array', () => { 144 | it('should be ok', async () => { 145 | const form = new Multipart(); 146 | form.append('array', ['arr', ['arr1', 'arr2'], 'arr3', null]); 147 | form.append('photo', photoVinyl); 148 | form.append('array', []); 149 | const body = await form.buffer(); 150 | return got.post('http://127.0.0.1:4000', { headers: form.getHeaders(), body }).then((res) => { 151 | should(JSON.parse(res.body)).be.eql({ filename: 'anon.jpg', mime: 'image/jpeg', fields: { array: ['arr', 'arr1', 'arr2', 'arr3', '0', ''] } }); 152 | }); 153 | }); 154 | 155 | it('should be ok', (done) => { 156 | const form = new Multipart(); 157 | form.append('array', ['arr', ['arr1', 'arr2'], 'arr3', null]); 158 | form.append('photo', photoVinyl); 159 | form.append('array', []); 160 | const req = http.request({ 161 | headers: form.getHeaders(), hostname: '127.0.0.1', port: 4000, method: 'POST' 162 | }, (res) => { 163 | const chunks = []; 164 | res.on('data', (chunk) => { 165 | chunks.push(chunk); 166 | }); 167 | res.on('end', () => { 168 | should(JSON.parse(Buffer.concat(chunks).toString('utf8'))).be.eql({ filename: 'anon.jpg', mime: 'image/jpeg', fields: { array: ['arr', 'arr1', 'arr2', 'arr3', '0', ''] } }); 169 | done(); 170 | }); 171 | }); 172 | form.buffer().then((body) => { 173 | req.write(body); 174 | req.end(); 175 | }); 176 | }); 177 | }); 178 | 179 | describe('append stream', () => { 180 | describe('chunked', () => { 181 | it('should be ok', async () => { 182 | const form = new Multipart(); 183 | form.append('field', 12345); 184 | form.append('photo', fs.createReadStream(photoFile)); 185 | form.append('field', null); 186 | const body = await form.buffer(); 187 | return got.post('http://127.0.0.1:4000', { headers: form.getHeaders(), body }).then((res) => { 188 | should(JSON.parse(res.body)).be.eql({ filename: 'fixture.jpg', mime: 'image/jpeg', fields: { field: ['12345', '0'] } }); 189 | }); 190 | }); 191 | 192 | it('should be ok', async () => { 193 | const form = new Multipart(); 194 | form.append('field', 12345); 195 | form.append('photo', fs.createReadStream(photoFile), { filename: 'a.jpg', contentType: 'image/jpeg' }); 196 | form.append('field', null); 197 | const body = await form.buffer(); 198 | return got.post('http://127.0.0.1:4000', { headers: form.getHeaders(), body }).then((res) => { 199 | should(res.body).be.eql('{"filename":"a.jpg","mime":"image/jpeg","fields":{"field":["12345","0"]}}'); 200 | }); 201 | }); 202 | 203 | it('should be ok', async () => { 204 | const form = new Multipart(); 205 | form.append('field', 12345); 206 | form.append('photo', fs.createReadStream(photoFile), { filename: 'a.jpg' }); 207 | form.append('field', null); 208 | const body = await form.buffer(); 209 | return got.post('http://127.0.0.1:4000', { headers: form.getHeaders(), body }).then((res) => { 210 | should(res.body).be.eql('{"filename":"a.jpg","mime":"image/jpeg","fields":{"field":["12345","0"]}}'); 211 | }); 212 | }); 213 | 214 | it('should be ok', async () => { 215 | const form = new Multipart(); 216 | form.append('field', 12345); 217 | form.append('photo', fs.createReadStream(photoFile), { contentType: 'image/jpeg' }); 218 | form.append('field', null); 219 | const body = await form.buffer(); 220 | return got.post('http://127.0.0.1:4000', { headers: form.getHeaders(), body }).then((res) => { 221 | should(res.body).be.eql('{"filename":"fixture.jpg","mime":"image/jpeg","fields":{"field":["12345","0"]}}'); 222 | }); 223 | }); 224 | 225 | it('should be ok', async () => { 226 | const form = new Multipart(); 227 | form.append('field', 12345); 228 | form.append('photo', https.request('https://avatars1.githubusercontent.com/u/2401029')); 229 | form.append('field', null); 230 | const body = await form.buffer(); 231 | await got.post('http://127.0.0.1:4000', { headers: form.getHeaders(), body }).then((res) => { 232 | should(res.body).be.eql('{"filename":"2401029","mime":"application/octet-stream","fields":{"field":["12345","0"]}}'); 233 | }); 234 | await got.post('http://127.0.0.1:4000', { headers: form.getHeaders(), body }).then((res) => { 235 | should(res.body).be.eql('{"filename":"2401029","mime":"application/octet-stream","fields":{"field":["12345","0"]}}'); 236 | }); 237 | }); 238 | 239 | it('should be ok', (done) => { 240 | const form = new Multipart(); 241 | form.append('field', 12345); 242 | form.append('photo', https.request('https://avatars1.githubusercontent.com/u/2401029')); 243 | form.append('field', null); 244 | const req = http.request({ 245 | headers: form.getHeaders(), hostname: '127.0.0.1', port: 4000, method: 'POST' 246 | }, (res) => { 247 | const chunks = []; 248 | res.on('data', (chunk) => { 249 | chunks.push(chunk); 250 | }); 251 | res.on('end', () => { 252 | should(Buffer.concat(chunks).toString('utf8')).be.eql('{"filename":"2401029","mime":"application/octet-stream","fields":{"field":["12345","0"]}}'); 253 | done(); 254 | }); 255 | }); 256 | form.buffer().then((body) => { 257 | req.write(body); 258 | req.end(); 259 | }); 260 | }); 261 | 262 | it('should be ok', async () => { 263 | const stream = new Stream(); 264 | stream.readable = true; 265 | 266 | setTimeout(() => { 267 | stream.emit('end'); 268 | stream.emit('close'); 269 | }, 50); 270 | 271 | const form = new Multipart(); 272 | form.append('field', stream); 273 | const body = await form.buffer(); 274 | return got.post('http://127.0.0.1:4000', { headers: form.getHeaders(), body }).then((res) => { 275 | should(res.body).be.eql('"Nothing"'); 276 | }); 277 | }); 278 | 279 | it('should be ok', async () => { 280 | const stream = new Stream(); 281 | stream.readable = true; 282 | 283 | const form = new Multipart(); 284 | form.append('field', stream); 285 | setTimeout(() => { 286 | stream.emit('end'); 287 | stream.emit('close'); 288 | }, 50); 289 | const body = await form.buffer(); 290 | return got.post('http://127.0.0.1:4000', { headers: form.getHeaders(), body }).then((res) => { 291 | should(res.body).be.eql('"Nothing"'); 292 | }); 293 | }); 294 | 295 | it('should throw', async () => { 296 | const stream = new Stream(); 297 | stream.readable = true; 298 | stream.destroy = () => { 299 | stream.emit('end'); 300 | }; 301 | 302 | const form = new Multipart(); 303 | form.append('field', stream); 304 | setTimeout(() => { 305 | stream.emit('error'); 306 | }, 50); 307 | const body = await form.buffer(); 308 | return got.post('http://127.0.0.1:4000', { headers: form.getHeaders(), body }).catch((e) => { 309 | should(e.message).be.eql('Response code 500 (Internal Server Error)'); 310 | }); 311 | }); 312 | 313 | it('should be ok', async () => { 314 | const stream = new Stream(); 315 | stream.readable = true; 316 | stream.destroy = () => { 317 | stream.emit('end'); 318 | }; 319 | 320 | stream.emit('data', 'Text'); 321 | setTimeout(() => { 322 | stream.emit('end'); 323 | }, 50); 324 | 325 | const form = new Multipart(); 326 | form.append('field', 12345); 327 | form.append('photo', https.request('https://avatars1.githubusercontent.com/u/2401029')); 328 | form.append('field', stream); 329 | const body = await form.buffer(); 330 | return got.post('http://127.0.0.1:4000', { headers: form.getHeaders(), body }).then((res) => { 331 | should(res.body).be.eql('{"filename":"2401029","mime":"application/octet-stream","fields":{"field":["12345",""]}}'); 332 | }); 333 | }); 334 | 335 | it('should be ok', (done) => { 336 | https.get('https://avatars1.githubusercontent.com/u/2401029') 337 | .on('response', async (photo) => { 338 | const form = new Multipart(); 339 | form.append('field', 12345); 340 | form.append('photo', photo); 341 | form.append('field', null); 342 | const body = await form.buffer(); 343 | got.post('http://127.0.0.1:4000', { headers: form.getHeaders(), body }).then((res) => { 344 | should(res.body).be.eql('{"filename":"file.bin","mime":"application/octet-stream","fields":{"field":["12345","0"]}}'); 345 | done(); 346 | }); 347 | }); 348 | }); 349 | 350 | it('should throw', async () => { 351 | const form = new Multipart(); 352 | form.append('photo', got.stream('http://127.0.0.1', { retries: 0 })); 353 | return form.buffer().catch(e => should(e.message).startWith('connect ECONNREFUSED')); 354 | }); 355 | 356 | it('should throw', () => { 357 | const form = new Multipart(); 358 | form.append('field', 12345); 359 | form.append('field', null); 360 | form._append = {}; 361 | return form.buffer().catch(e => should(e.message).be.eql('this._append is not a function')); 362 | }); 363 | 364 | it('should throw', () => { 365 | const form = new Multipart(); 366 | form.append('field', 12345); 367 | form.append('photo', http.request({ hostname: '127.0.0.1' })); 368 | form.append('photo', fs.createReadStream(photoFile)); 369 | form.append('field', null); 370 | return form.buffer().catch(e => should(e.message).startWith('connect ECONNREFUSED')); 371 | }); 372 | }); 373 | 374 | describe('length', () => { 375 | it('should be ok', async () => { 376 | const form = new Multipart(); 377 | form.append('field', 12345); 378 | form.append('photo', fs.createReadStream(photoFile)); 379 | form.append('field', null); 380 | const body = await form.buffer(); 381 | return got.post('http://127.0.0.1:4000', { headers: form.getHeaders(false), body }).then((res) => { 382 | should(JSON.parse(res.body)).be.eql({ filename: 'fixture.jpg', mime: 'image/jpeg', fields: { field: ['12345', '0'] } }); 383 | }); 384 | }); 385 | 386 | it('should be ok', async () => { 387 | const form = new Multipart(); 388 | form.append('field', 12345); 389 | form.append('photo', fs.createReadStream(photoFile), { filename: 'a.jpg', contentType: 'image/jpeg' }); 390 | form.append('field', null); 391 | const body = await form.buffer(); 392 | return got.post('http://127.0.0.1:4000', { headers: form.getHeaders(false), body }).then((res) => { 393 | should(res.body).be.eql('{"filename":"a.jpg","mime":"image/jpeg","fields":{"field":["12345","0"]}}'); 394 | }); 395 | }); 396 | 397 | it('should be ok', async () => { 398 | const form = new Multipart(); 399 | form.append('field', 12345); 400 | form.append('photo', fs.createReadStream(photoFile), { filename: 'a.jpg' }); 401 | form.append('field', null); 402 | const body = await form.buffer(); 403 | return got.post('http://127.0.0.1:4000', { headers: form.getHeaders(false), body }).then((res) => { 404 | should(res.body).be.eql('{"filename":"a.jpg","mime":"image/jpeg","fields":{"field":["12345","0"]}}'); 405 | }); 406 | }); 407 | 408 | it('should be ok', async () => { 409 | const form = new Multipart(); 410 | form.append('field', 12345); 411 | form.append('photo', fs.createReadStream(photoFile), { contentType: 'image/jpeg' }); 412 | form.append('field', null); 413 | const body = await form.buffer(); 414 | return got.post('http://127.0.0.1:4000', { headers: form.getHeaders(false), body }).then((res) => { 415 | should(res.body).be.eql('{"filename":"fixture.jpg","mime":"image/jpeg","fields":{"field":["12345","0"]}}'); 416 | }); 417 | }); 418 | 419 | it('should be ok', async () => { 420 | const form = new Multipart(); 421 | form.append('field', 12345); 422 | form.append('photo', https.request('https://avatars1.githubusercontent.com/u/2401029')); 423 | form.append('field', null); 424 | const body = await form.buffer(); 425 | await got.post('http://127.0.0.1:4000', { headers: form.getHeaders(false), body }).then((res) => { 426 | should(res.body).be.eql('{"filename":"2401029","mime":"application/octet-stream","fields":{"field":["12345","0"]}}'); 427 | }); 428 | await got.post('http://127.0.0.1:4000', { headers: form.getHeaders(false), body }).then((res) => { 429 | should(res.body).be.eql('{"filename":"2401029","mime":"application/octet-stream","fields":{"field":["12345","0"]}}'); 430 | }); 431 | }); 432 | 433 | it('should be ok', (done) => { 434 | const form = new Multipart(); 435 | form.append('field', 12345); 436 | form.append('photo', https.request('https://avatars1.githubusercontent.com/u/2401029')); 437 | form.append('field', null); 438 | form.buffer().then((body) => { 439 | const req = http.request({ 440 | headers: form.getHeaders(false), hostname: '127.0.0.1', port: 4000, method: 'POST' 441 | }, (res) => { 442 | const chunks = []; 443 | res.on('data', (chunk) => { 444 | chunks.push(chunk); 445 | }); 446 | res.on('end', () => { 447 | should(Buffer.concat(chunks).toString('utf8')).be.eql('{"filename":"2401029","mime":"application/octet-stream","fields":{"field":["12345","0"]}}'); 448 | done(); 449 | }); 450 | }); 451 | req.write(body); 452 | req.end(); 453 | }); 454 | }); 455 | 456 | it('should be ok', async () => { 457 | const stream = new Stream(); 458 | stream.readable = true; 459 | 460 | setTimeout(() => { 461 | stream.emit('end'); 462 | stream.emit('close'); 463 | }, 50); 464 | 465 | const form = new Multipart(); 466 | form.append('field', stream); 467 | const body = await form.buffer(); 468 | return got.post('http://127.0.0.1:4000', { headers: form.getHeaders(false), body }).then((res) => { 469 | should(res.body).be.eql('"Nothing"'); 470 | }); 471 | }); 472 | 473 | it('should be ok', async () => { 474 | const stream = new Stream(); 475 | stream.readable = true; 476 | 477 | const form = new Multipart(); 478 | form.append('field', stream); 479 | setTimeout(() => { 480 | stream.emit('end'); 481 | stream.emit('close'); 482 | }, 50); 483 | const body = await form.buffer(); 484 | return got.post('http://127.0.0.1:4000', { headers: form.getHeaders(false), body }).then((res) => { 485 | should(res.body).be.eql('"Nothing"'); 486 | }); 487 | }); 488 | 489 | it('should throw', async () => { 490 | const stream = new Stream(); 491 | stream.readable = true; 492 | stream.destroy = () => { 493 | stream.emit('end'); 494 | }; 495 | 496 | const form = new Multipart(); 497 | form.append('field', stream); 498 | setTimeout(() => { 499 | stream.emit('error'); 500 | }, 50); 501 | const body = await form.buffer(); 502 | return got.post('http://127.0.0.1:4000', { headers: form.getHeaders(false), body }).catch((e) => { 503 | should(e.message).be.eql('Response code 500 (Internal Server Error)'); 504 | }); 505 | }); 506 | 507 | it('should be ok', async () => { 508 | const stream = new Stream(); 509 | stream.readable = true; 510 | stream.destroy = () => { 511 | stream.emit('end'); 512 | }; 513 | 514 | stream.emit('data', 'Text'); 515 | setTimeout(() => { 516 | stream.emit('end'); 517 | }, 50); 518 | 519 | const form = new Multipart(); 520 | form.append('field', 12345); 521 | form.append('photo', https.request('https://avatars1.githubusercontent.com/u/2401029')); 522 | form.append('field', stream); 523 | const body = await form.buffer(); 524 | return got.post('http://127.0.0.1:4000', { headers: form.getHeaders(false), body }).then((res) => { 525 | should(res.body).be.eql('{"filename":"2401029","mime":"application/octet-stream","fields":{"field":["12345",""]}}'); 526 | }); 527 | }); 528 | 529 | it('should be ok', (done) => { 530 | https.get('https://avatars1.githubusercontent.com/u/2401029') 531 | .on('response', async (photo) => { 532 | const form = new Multipart(); 533 | form.append('field', 12345); 534 | form.append('photo', photo); 535 | form.append('field', null); 536 | const body = await form.buffer(); 537 | got.post('http://127.0.0.1:4000', { headers: form.getHeaders(false), body }).then((res) => { 538 | should(res.body).be.eql('{"filename":"file.bin","mime":"application/octet-stream","fields":{"field":["12345","0"]}}'); 539 | done(); 540 | }); 541 | }); 542 | }); 543 | }); 544 | }); 545 | 546 | describe('append buffer', () => { 547 | const photoBuffer = chunkSync({ path: photoFile, flags: 'r' }, 9379); 548 | describe('chunked', () => { 549 | it('should be ok', async () => { 550 | const form = new Multipart(); 551 | form.append('field', 12345); 552 | form.append('photo', photoBuffer); 553 | form.append('field', null); 554 | const body = await form.buffer(); 555 | return got.post('http://127.0.0.1:4000', { headers: form.getHeaders(), body }).then((res) => { 556 | should(res.body).be.eql('{"filename":"file.jpg","mime":"image/jpeg","fields":{"field":["12345","0"]}}'); 557 | }); 558 | }); 559 | 560 | it('should be ok', async () => { 561 | const form = new Multipart(); 562 | form.append('field', 12345); 563 | form.append('photo', photoBuffer, { filename: 'a.jpg', contentType: 'image/jpeg' }); 564 | form.append('field', null); 565 | const body = await form.buffer(); 566 | return got.post('http://127.0.0.1:4000', { headers: form.getHeaders(), body }).then((res) => { 567 | should(res.body).be.eql('{"filename":"a.jpg","mime":"image/jpeg","fields":{"field":["12345","0"]}}'); 568 | }); 569 | }); 570 | 571 | it('should be ok', async () => { 572 | const form = new Multipart(); 573 | form.append('field', 12345); 574 | form.append('photo', photoBuffer, { filename: 'a.jpg' }); 575 | form.append('field', null); 576 | const body = await form.buffer(); 577 | return got.post('http://127.0.0.1:4000', { headers: form.getHeaders(), body }).then((res) => { 578 | should(res.body).be.eql('{"filename":"a.jpg","mime":"image/jpeg","fields":{"field":["12345","0"]}}'); 579 | }); 580 | }); 581 | 582 | it('should be ok', async () => { 583 | const form = new Multipart(); 584 | form.append('field', 12345); 585 | form.append('photo', photoBuffer, { contentType: 'image/jpeg' }); 586 | form.append('field', null); 587 | const body = await form.buffer(); 588 | return got.post('http://127.0.0.1:4000', { headers: form.getHeaders(), body }).then((res) => { 589 | should(res.body).be.eql('{"filename":"file.jpg","mime":"image/jpeg","fields":{"field":["12345","0"]}}'); 590 | }); 591 | }); 592 | 593 | it('should be ok', (done) => { 594 | const form = new Multipart(); 595 | form.append('field', 12345); 596 | form.append('photo', photoBuffer); 597 | form.append('field', null); 598 | const req = http.request({ 599 | headers: form.getHeaders(), hostname: '127.0.0.1', port: 4000, method: 'POST' 600 | }, (res) => { 601 | const chunks = []; 602 | res.on('data', (chunk) => { 603 | chunks.push(chunk); 604 | }); 605 | res.on('end', () => { 606 | should(Buffer.concat(chunks).toString('utf8')).be.eql('{"filename":"file.jpg","mime":"image/jpeg","fields":{"field":["12345","0"]}}'); 607 | done(); 608 | }); 609 | }); 610 | form.buffer().then((body) => { 611 | req.write(body); 612 | req.end(); 613 | }); 614 | }); 615 | }); 616 | 617 | describe('length', () => { 618 | it('should be ok', async () => { 619 | const form = new Multipart(); 620 | form.append('field', 12345); 621 | form.append('photo', photoBuffer); 622 | form.append('field', null); 623 | const body = await form.buffer(); 624 | return got.post('http://127.0.0.1:4000', { headers: form.getHeaders(false), body }).then((res) => { 625 | should(res.body).be.eql('{"filename":"file.jpg","mime":"image/jpeg","fields":{"field":["12345","0"]}}'); 626 | }); 627 | }); 628 | 629 | it('should be ok', async () => { 630 | const form = new Multipart(); 631 | form.append('field', 12345); 632 | form.append('photo', photoBuffer, { filename: 'a.jpg', contentType: 'image/jpeg' }); 633 | form.append('field', null); 634 | const body = await form.buffer(); 635 | return got.post('http://127.0.0.1:4000', { headers: form.getHeaders(false), body }).then((res) => { 636 | should(res.body).be.eql('{"filename":"a.jpg","mime":"image/jpeg","fields":{"field":["12345","0"]}}'); 637 | }); 638 | }); 639 | 640 | it('should be ok', async () => { 641 | const form = new Multipart(); 642 | form.append('field', 12345); 643 | form.append('photo', photoBuffer, { filename: 'a.jpg' }); 644 | form.append('field', null); 645 | const body = await form.buffer(); 646 | return got.post('http://127.0.0.1:4000', { headers: form.getHeaders(false), body }).then((res) => { 647 | should(res.body).be.eql('{"filename":"a.jpg","mime":"image/jpeg","fields":{"field":["12345","0"]}}'); 648 | }); 649 | }); 650 | 651 | it('should be ok', async () => { 652 | const form = new Multipart(); 653 | form.append('field', 12345); 654 | form.append('photo', photoBuffer, { contentType: 'image/jpeg' }); 655 | form.append('field', null); 656 | const body = await form.buffer(); 657 | return got.post('http://127.0.0.1:4000', { headers: form.getHeaders(false), body }).then((res) => { 658 | should(res.body).be.eql('{"filename":"file.jpg","mime":"image/jpeg","fields":{"field":["12345","0"]}}'); 659 | }); 660 | }); 661 | 662 | it('should be ok', (done) => { 663 | const form = new Multipart(); 664 | form.append('field', 12345); 665 | form.append('photo', photoBuffer); 666 | form.append('field', null); 667 | form.buffer().then((body) => { 668 | const req = http.request({ 669 | headers: form.getHeaders(false), hostname: '127.0.0.1', port: 4000, method: 'POST' 670 | }, (res) => { 671 | const chunks = []; 672 | res.on('data', (chunk) => { 673 | chunks.push(chunk); 674 | }); 675 | res.on('end', () => { 676 | should(Buffer.concat(chunks).toString('utf8')).be.eql('{"filename":"file.jpg","mime":"image/jpeg","fields":{"field":["12345","0"]}}'); 677 | done(); 678 | }); 679 | }); 680 | req.write(body); 681 | req.end(); 682 | }); 683 | }); 684 | }); 685 | }); 686 | }); 687 | -------------------------------------------------------------------------------- /test/fixture/fixture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strikeentco/multi-part/06968a326cd7921fb86acf209b591290e71418ff/test/fixture/fixture.jpg -------------------------------------------------------------------------------- /test/streamAsync.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const should = require('should/as-function'); 4 | const http = require('http'); 5 | const https = require('https'); 6 | const fs = require('fs'); 7 | const Stream = require('stream'); 8 | 9 | const File = require('vinyl'); 10 | const got = require('got'); 11 | const app = require('express')(); 12 | const multer = require('multer'); 13 | 14 | const { MultipartAsync: Multipart } = require('../main'); 15 | 16 | const upload = multer({ dest: `${__dirname}/uploads/` }); 17 | const photoFile = `${__dirname}/fixture/fixture.jpg`; 18 | 19 | function chunkSync(data, length) { 20 | const buf = Buffer.alloc(length); 21 | const fd = fs.openSync(data.path, data.flags); 22 | 23 | fs.readSync(fd, buf, 0, length); 24 | fs.closeSync(fd); 25 | 26 | return buf; 27 | } 28 | 29 | const photoVinyl = new File({ 30 | path: 'anon.jpg', 31 | contents: chunkSync({ path: photoFile, flags: 'r' }, 9379) 32 | }); 33 | 34 | describe('multi-part.async().stream()', function () { 35 | let server; 36 | this.timeout(10000); 37 | before((done) => { 38 | app.post('/', upload.single('photo'), (req, res) => { 39 | if (req.file) { 40 | return res.json({ 41 | filename: req.file.originalname, 42 | mime: req.file.mimetype, 43 | fields: req.body 44 | }); 45 | } 46 | return res.json('Nothing'); 47 | }); 48 | 49 | server = http.createServer(app); 50 | server.listen(4000, done); 51 | }); 52 | 53 | after(() => server.close()); 54 | 55 | describe('get custom boundary', () => { 56 | it('should be ok', () => { 57 | const form = new Multipart({ boundary: '--CustomBoundary12345' }); 58 | should(form.getBoundary()).be.eql('--CustomBoundary12345'); 59 | should(form.getHeaders()).be.eql({ 60 | 'transfer-encoding': 'chunked', 61 | 'content-type': 'multipart/form-data; boundary="--CustomBoundary12345"' 62 | }); 63 | }); 64 | }); 65 | 66 | describe('append nothing', () => { 67 | it('should be ok', async () => { 68 | const form = new Multipart(); 69 | const body = await form.stream(); 70 | return got.post('http://127.0.0.1:4000', { headers: form.getHeaders(), body }).then((res) => { 71 | should(res.body).be.eql('"Nothing"'); 72 | }); 73 | }); 74 | 75 | it('should be ok', (done) => { 76 | const form = new Multipart(); 77 | const req = http.request({ 78 | headers: form.getHeaders(), hostname: '127.0.0.1', port: 4000, method: 'POST' 79 | }, (res) => { 80 | const chunks = []; 81 | res.on('data', (chunk) => { 82 | chunks.push(chunk); 83 | }); 84 | res.on('end', () => { 85 | should(Buffer.concat(chunks).toString('utf8')).be.eql('"Nothing"'); 86 | done(); 87 | }); 88 | }); 89 | form.stream().then((body) => body.pipe(req)); 90 | }); 91 | 92 | it('should throw', () => { 93 | const form = new Multipart(); 94 | should(() => form.append({}, null)).throw('Field must be specified and must be a string or a number'); 95 | should(() => form.append('test')).throw('Value can\'t be undefined'); 96 | }); 97 | }); 98 | 99 | describe('append vinyl', () => { 100 | it('should be ok', async () => { 101 | const form = new Multipart(); 102 | form.append('photo', photoVinyl); 103 | const body = await form.stream(); 104 | return got.post('http://127.0.0.1:4000', { headers: form.getHeaders(), body }).then((res) => { 105 | should(JSON.parse(res.body)).be.eql({ filename: 'anon.jpg', mime: 'image/jpeg', fields: {} }); 106 | }); 107 | }); 108 | 109 | it('should be ok', async () => { 110 | const form = new Multipart(); 111 | form.append('photo', photoVinyl, { filename: 'anon.jpg' }); 112 | const body = await form.stream(); 113 | return got.post('http://127.0.0.1:4000', { headers: form.getHeaders(), body }).then((res) => { 114 | should(JSON.parse(res.body)).be.eql({ filename: 'anon.jpg', mime: 'image/jpeg', fields: {} }); 115 | }); 116 | }); 117 | 118 | it('should be ok', (done) => { 119 | const form = new Multipart(); 120 | form.append('photo', photoVinyl); 121 | const req = http.request({ 122 | headers: form.getHeaders(), hostname: '127.0.0.1', port: 4000, method: 'POST' 123 | }, (res) => { 124 | const chunks = []; 125 | res.on('data', (chunk) => { 126 | chunks.push(chunk); 127 | }); 128 | res.on('end', () => { 129 | should(JSON.parse(Buffer.concat(chunks).toString('utf8'))).be.eql({ filename: 'anon.jpg', mime: 'image/jpeg', fields: {} }); 130 | done(); 131 | }); 132 | }); 133 | form.stream().then((body) => body.pipe(req)); 134 | }); 135 | }); 136 | 137 | describe('append array', () => { 138 | it('should be ok', async () => { 139 | const form = new Multipart(); 140 | form.append('array', ['arr', ['arr1', 'arr2'], 'arr3', null]); 141 | form.append('photo', photoVinyl); 142 | form.append('array', []); 143 | const body = await form.stream(); 144 | return got.post('http://127.0.0.1:4000', { headers: form.getHeaders(), body }).then((res) => { 145 | should(JSON.parse(res.body)).be.eql({ filename: 'anon.jpg', mime: 'image/jpeg', fields: { array: ['arr', 'arr1', 'arr2', 'arr3', '0', ''] } }); 146 | }); 147 | }); 148 | 149 | it('should be ok', (done) => { 150 | const form = new Multipart(); 151 | form.append('array', ['arr', ['arr1', 'arr2'], 'arr3', null]); 152 | form.append('photo', photoVinyl); 153 | form.append('array', []); 154 | const req = http.request({ 155 | headers: form.getHeaders(), hostname: '127.0.0.1', port: 4000, method: 'POST' 156 | }, (res) => { 157 | const chunks = []; 158 | res.on('data', (chunk) => { 159 | chunks.push(chunk); 160 | }); 161 | res.on('end', () => { 162 | should(JSON.parse(Buffer.concat(chunks).toString('utf8'))).be.eql({ filename: 'anon.jpg', mime: 'image/jpeg', fields: { array: ['arr', 'arr1', 'arr2', 'arr3', '0', ''] } }); 163 | done(); 164 | }); 165 | }); 166 | form.stream().then((body) => body.pipe(req)); 167 | }); 168 | }); 169 | 170 | describe('append stream', () => { 171 | it('should be ok', async () => { 172 | const form = new Multipart(); 173 | form.append('field', 12345); 174 | form.append('photo', fs.createReadStream(photoFile)); 175 | form.append('field', null); 176 | const body = await form.stream(); 177 | return got.post('http://127.0.0.1:4000', { headers: form.getHeaders(), body }).then((res) => { 178 | should(JSON.parse(res.body)).be.eql({ filename: 'fixture.jpg', mime: 'image/jpeg', fields: { field: ['12345', '0'] } }); 179 | }); 180 | }); 181 | 182 | it('should be ok', async () => { 183 | const form = new Multipart(); 184 | form.append('field', 12345); 185 | form.append('photo', fs.createReadStream(photoFile), { filename: 'a.jpg', contentType: 'image/jpeg' }); 186 | form.append('field', null); 187 | const body = await form.stream(); 188 | return got.post('http://127.0.0.1:4000', { headers: form.getHeaders(), body }).then((res) => { 189 | should(res.body).be.eql('{"filename":"a.jpg","mime":"image/jpeg","fields":{"field":["12345","0"]}}'); 190 | }); 191 | }); 192 | 193 | it('should be ok', async () => { 194 | const form = new Multipart(); 195 | form.append('field', 12345); 196 | form.append('photo', fs.createReadStream(photoFile), { filename: 'a.jpg' }); 197 | form.append('field', null); 198 | const body = await form.stream(); 199 | return got.post('http://127.0.0.1:4000', { headers: form.getHeaders(), body }).then((res) => { 200 | should(res.body).be.eql('{"filename":"a.jpg","mime":"image/jpeg","fields":{"field":["12345","0"]}}'); 201 | }); 202 | }); 203 | 204 | it('should be ok', async () => { 205 | const form = new Multipart(); 206 | form.append('field', 12345); 207 | form.append('photo', fs.createReadStream(photoFile), { contentType: 'image/jpeg' }); 208 | form.append('field', null); 209 | const body = await form.stream(); 210 | return got.post('http://127.0.0.1:4000', { headers: form.getHeaders(), body }).then((res) => { 211 | should(res.body).be.eql('{"filename":"fixture.jpg","mime":"image/jpeg","fields":{"field":["12345","0"]}}'); 212 | }); 213 | }); 214 | 215 | it('should be ok', async () => { 216 | const form = new Multipart(); 217 | form.append('field', 12345); 218 | form.append('photo', https.request('https://avatars1.githubusercontent.com/u/2401029')); 219 | form.append('field', null); 220 | let body = await form.stream(); 221 | await got.post('http://127.0.0.1:4000', { headers: form.getHeaders(), body }).then((res) => { 222 | should(res.body).be.eql('{"filename":"2401029","mime":"application/octet-stream","fields":{"field":["12345","0"]}}'); 223 | }); 224 | body = await form.stream(); 225 | await got.post('http://127.0.0.1:4000', { headers: form.getHeaders(), body }).then((res) => { 226 | should(res.body).be.eql('"Nothing"'); 227 | }); 228 | }); 229 | 230 | it('should be ok', (done) => { 231 | const form = new Multipart(); 232 | form.append('field', 12345); 233 | form.append('photo', https.request('https://avatars1.githubusercontent.com/u/2401029')); 234 | form.append('field', null); 235 | const req = http.request({ 236 | headers: form.getHeaders(), hostname: '127.0.0.1', port: 4000, method: 'POST' 237 | }, (res) => { 238 | const chunks = []; 239 | res.on('data', (chunk) => { 240 | chunks.push(chunk); 241 | }); 242 | res.on('end', () => { 243 | should(Buffer.concat(chunks).toString('utf8')).be.eql('{"filename":"2401029","mime":"application/octet-stream","fields":{"field":["12345","0"]}}'); 244 | done(); 245 | }); 246 | }); 247 | form.stream().then((body) => body.pipe(req)); 248 | }); 249 | 250 | it('should be ok', async () => { 251 | const stream = new Stream(); 252 | stream.readable = true; 253 | 254 | setTimeout(() => { 255 | stream.emit('end'); 256 | stream.emit('close'); 257 | }, 50); 258 | 259 | const form = new Multipart(); 260 | form.append('field', stream); 261 | const body = await form.stream(); 262 | return got.post('http://127.0.0.1:4000', { headers: form.getHeaders(), body }).then((res) => { 263 | should(res.body).be.eql('"Nothing"'); 264 | }); 265 | }); 266 | 267 | it('should be ok', async () => { 268 | const stream = new Stream(); 269 | stream.readable = true; 270 | 271 | const form = new Multipart(); 272 | form.append('field', stream); 273 | setTimeout(() => { 274 | // stream.emit('close'); 275 | stream.emit('end'); 276 | stream.emit('close'); 277 | }, 50); 278 | const body = await form.stream(); 279 | return got.post('http://127.0.0.1:4000', { headers: form.getHeaders(), body }).then((res) => { 280 | should(res.body).be.eql('"Nothing"'); 281 | }); 282 | }); 283 | 284 | it('should throw', async () => { 285 | const stream = new Stream(); 286 | stream.readable = true; 287 | stream.destroy = () => { 288 | stream.emit('end'); 289 | }; 290 | 291 | const form = new Multipart(); 292 | form.append('field', stream); 293 | setTimeout(() => { 294 | stream.emit('error'); 295 | }, 50); 296 | const body = await form.stream(); 297 | return got.post('http://127.0.0.1:4000', { headers: form.getHeaders(), body }).catch((e) => { 298 | should(e.message).be.eql('Response code 500 (Internal Server Error)'); 299 | }); 300 | }); 301 | 302 | it('should be ok', async () => { 303 | const stream = new Stream(); 304 | stream.readable = true; 305 | stream.destroy = () => { 306 | stream.emit('end'); 307 | }; 308 | 309 | stream.emit('data', 'Text'); 310 | setTimeout(() => { 311 | stream.emit('end'); 312 | }, 50); 313 | 314 | const form = new Multipart(); 315 | form.append('field', 12345); 316 | form.append('photo', https.request('https://avatars1.githubusercontent.com/u/2401029')); 317 | form.append('field', stream); 318 | const body = await form.stream(); 319 | return got.post('http://127.0.0.1:4000', { headers: form.getHeaders(), body }).then((res) => { 320 | should(res.body).be.eql('{"filename":"2401029","mime":"application/octet-stream","fields":{"field":["12345",""]}}'); 321 | }); 322 | }); 323 | 324 | it('should be ok', (done) => { 325 | https.get('https://avatars1.githubusercontent.com/u/2401029') 326 | .on('response', async (photo) => { 327 | const form = new Multipart(); 328 | form.append('field', 12345); 329 | form.append('photo', photo); 330 | form.append('field', null); 331 | const body = await form.stream(); 332 | got.post('http://127.0.0.1:4000', { headers: form.getHeaders(), body }).then((res) => { 333 | should(res.body).be.eql('{"filename":"file.bin","mime":"application/octet-stream","fields":{"field":["12345","0"]}}'); 334 | done(); 335 | }); 336 | }); 337 | }); 338 | 339 | it('should throw', () => { 340 | const form = new Multipart(); 341 | form.append('photo', got.stream('http://127.0.0.1', { retries: 0 })); 342 | return form.stream().catch((e) => should(e.message).startWith('connect ECONNREFUSED')); 343 | }); 344 | 345 | it('should throw', () => { 346 | const form = new Multipart(); 347 | form.append('field', 12345); 348 | form.append('field', null); 349 | form._append = {}; 350 | return form.stream().catch((e) => should(e.message).startWith('this._append is not a function')); 351 | }); 352 | 353 | it('should throw', (done) => { 354 | const form = new Multipart(); 355 | form.append('field', 12345); 356 | form.append('photo', http.request({ hostname: '127.0.0.1' })); 357 | form.append('photo', fs.createReadStream(photoFile)); 358 | form.append('field', null); 359 | form.once('error', (e) => { 360 | should(e.message).startWith('connect ECONNREFUSED'); 361 | done(); 362 | }); 363 | form.stream(); 364 | }); 365 | }); 366 | 367 | describe('append buffer', () => { 368 | const photoBuffer = chunkSync({ path: photoFile, flags: 'r' }, 9379); 369 | it('should be ok', async () => { 370 | const form = new Multipart(); 371 | form.append('field', 12345); 372 | form.append('photo', photoBuffer); 373 | form.append('field', null); 374 | const body = await form.stream(); 375 | return got.post('http://127.0.0.1:4000', { headers: form.getHeaders(), body }).then((res) => { 376 | should(res.body).be.eql('{"filename":"file.jpg","mime":"image/jpeg","fields":{"field":["12345","0"]}}'); 377 | }); 378 | }); 379 | 380 | it('should be ok', async () => { 381 | const form = new Multipart(); 382 | form.append('field', 12345); 383 | form.append('photo', photoBuffer, { filename: 'a.jpg', contentType: 'image/jpeg' }); 384 | form.append('field', null); 385 | const body = await form.stream(); 386 | return got.post('http://127.0.0.1:4000', { headers: form.getHeaders(), body }).then((res) => { 387 | should(res.body).be.eql('{"filename":"a.jpg","mime":"image/jpeg","fields":{"field":["12345","0"]}}'); 388 | }); 389 | }); 390 | 391 | it('should be ok', async () => { 392 | const form = new Multipart(); 393 | form.append('field', 12345); 394 | form.append('photo', photoBuffer, { filename: 'a.jpg' }); 395 | form.append('field', null); 396 | const body = await form.stream(); 397 | return got.post('http://127.0.0.1:4000', { headers: form.getHeaders(), body }).then((res) => { 398 | should(res.body).be.eql('{"filename":"a.jpg","mime":"image/jpeg","fields":{"field":["12345","0"]}}'); 399 | }); 400 | }); 401 | 402 | it('should be ok', async () => { 403 | const form = new Multipart(); 404 | form.append('field', 12345); 405 | form.append('photo', photoBuffer, { contentType: 'image/jpeg' }); 406 | form.append('field', null); 407 | const body = await form.stream(); 408 | return got.post('http://127.0.0.1:4000', { headers: form.getHeaders(), body }).then((res) => { 409 | should(res.body).be.eql('{"filename":"file.jpg","mime":"image/jpeg","fields":{"field":["12345","0"]}}'); 410 | }); 411 | }); 412 | 413 | it('should be ok', (done) => { 414 | const form = new Multipart(); 415 | form.append('field', 12345); 416 | form.append('photo', photoBuffer); 417 | form.append('field', null); 418 | const req = http.request({ 419 | headers: form.getHeaders(), hostname: '127.0.0.1', port: 4000, method: 'POST' 420 | }, (res) => { 421 | const chunks = []; 422 | res.on('data', (chunk) => { 423 | chunks.push(chunk); 424 | }); 425 | res.on('end', () => { 426 | should(Buffer.concat(chunks).toString('utf8')).be.eql('{"filename":"file.jpg","mime":"image/jpeg","fields":{"field":["12345","0"]}}'); 427 | done(); 428 | }); 429 | }); 430 | form.stream().then((body) => body.pipe(req)); 431 | }); 432 | }); 433 | }); 434 | --------------------------------------------------------------------------------