├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── .tslintrc.json ├── LICENSE ├── README.md ├── lib └── middleware.js ├── package-lock.json ├── package.json └── test ├── .eslintrc ├── fixtures ├── file1.txt └── file2.txt ├── helpers └── send.js └── middleware.test.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": "airbnb-base", 4 | "parserOptions": { 5 | "sourceType": "script" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /.gitignore 2 | /.npmignore 3 | /.travis.yml 4 | /.tslintrc.json 5 | /Makefile 6 | /lib.d/ 7 | /lib/**/*.ts 8 | !/lib/**/*.d.ts 9 | /lib/interface/ 10 | /node_modules/ 11 | /test/ 12 | 13 | /.last_build 14 | /.last_build_all 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 8 4 | -------------------------------------------------------------------------------- /.tslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "align": [true, 4 | "parameters", 5 | "arguments", 6 | "statements"], 7 | "ban": false, 8 | "class-name": true, 9 | "comment-format": [true, 10 | "check-space" 11 | ], 12 | "curly": true, 13 | "eofline": true, 14 | "forin": true, 15 | "indent": [true, 4], 16 | "interface-name": true, 17 | "jsdoc-format": true, 18 | "label-position": true, 19 | "label-undefined": true, 20 | "max-line-length": false, 21 | "member-ordering": [true, 22 | "public-before-private", 23 | "static-before-instance", 24 | "variables-before-functions" 25 | ], 26 | "no-arg": true, 27 | "no-bitwise": true, 28 | "no-console": [true, 29 | "debug", 30 | "time", 31 | "timeEnd", 32 | "trace" 33 | ], 34 | "no-construct": true, 35 | "no-constructor-vars": true, 36 | "no-debugger": true, 37 | "no-duplicate-key": true, 38 | "no-duplicate-variable": true, 39 | "no-empty": true, 40 | "no-eval": true, 41 | "no-string-literal": true, 42 | "no-switch-case-fall-through": true, 43 | "no-trailing-comma": false, 44 | "no-trailing-whitespace": true, 45 | "no-unused-expression": true, 46 | "no-unused-variable": true, 47 | "no-unreachable": true, 48 | "no-use-before-declare": true, 49 | "no-var-requires": false, 50 | "one-line": [true, 51 | "check-open-brace", 52 | "check-catch", 53 | "check-else", 54 | "check-whitespace" 55 | ], 56 | "quotemark": false, 57 | "radix": true, 58 | "semicolon": true, 59 | "triple-equals": [true, "allow-null-check"], 60 | "typedef": false, 61 | "typedef-whitespace": [true, { 62 | "call-signature": "nospace", 63 | "index-signature": "nospace", 64 | "parameter": "nospace", 65 | "property-declaration": "nospace", 66 | "variable-declaration": "nospace" 67 | }], 68 | "use-strict": false, 69 | "variable-name": false, 70 | "whitespace": [true, 71 | "check-branch", 72 | "check-decl", 73 | "check-operator", 74 | "check-separator", 75 | "check-type" 76 | ] 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2018 Hyunje Jun 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 all 13 | 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 THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # express-formidable [![Build Status](https://travis-ci.org/utatti/express-formidable.svg?branch=master)](https://travis-ci.org/utatti/express-formidable) 2 | 3 | An [Express](http://expressjs.com) middleware of 4 | [Formidable](https://github.com/felixge/node-formidable) that just works. 5 | 6 | ## What are Express, Formidable, and this? 7 | 8 | [Express](http://expressjs.com) is a fast, unopinionated, minimalist web 9 | framework for Node.js. 10 | 11 | [Formidable](https://github.com/felixge/node-formidable) is a Node.js module 12 | for parsing form data, including `multipart/form-data` file upload. 13 | 14 | So, **`express-formidable`** is something like a bridge between them, 15 | specifically an Express middleware implementation of Formidable. 16 | 17 | It aims to just work. 18 | 19 | ## Install 20 | 21 | ``` 22 | npm install express-formidable 23 | ``` 24 | 25 | ## How to use 26 | 27 | ```js 28 | const express = require('express'); 29 | const formidableMiddleware = require('express-formidable'); 30 | 31 | var app = express(); 32 | 33 | app.use(formidableMiddleware()); 34 | 35 | app.post('/upload', (req, res) => { 36 | req.fields; // contains non-file fields 37 | req.files; // contains files 38 | }); 39 | ``` 40 | 41 | And that's it. 42 | 43 | express-formidable can basically parse form types Formidable can handle, 44 | including `application/x-www-form-urlencoded`, `application/json`, and 45 | `multipart/form-data`. 46 | 47 | ## Option 48 | 49 | ```js 50 | app.use(formidableMiddleware(opts)); 51 | ``` 52 | 53 | `opts` are options which can be set to `form` in Formidable. For example: 54 | 55 | ```js 56 | app.use(formidableMiddleware({ 57 | encoding: 'utf-8', 58 | uploadDir: '/my/dir', 59 | multiples: true, // req.files to be arrays of files 60 | }); 61 | ``` 62 | 63 | For the detail, please refer to the 64 | [Formidable API](https://github.com/felixge/node-formidable#api). 65 | 66 | ## Events 67 | 68 | ```js 69 | app.use(formidableMiddleware(opts, events)); 70 | ``` 71 | 72 | `events` is an array of json with two field: 73 | 74 | | Field | Description | 75 | | ----- | ----------- | 76 | | `event` | The event emitted by the form of formidable. A complete list of all the possible events, please refer to the [Formidable Events](https://github.com/felixge/node-formidable#events) | 77 | | `action` | The callback to execute. The signature is `function (req, res, next, ...formidable_parameters)` | 78 | 79 | For example: 80 | 81 | ```js 82 | const events = [ 83 | { 84 | event: 'fileBegin', 85 | action: function (req, res, next, name, file) { /* your callback */ } 86 | }, 87 | { 88 | event: 'field', 89 | action: function (req, res, next, name, value) { /* your callback */ } 90 | } 91 | ]; 92 | ``` 93 | 94 | ### Error event 95 | 96 | Unless an `error` event are provided by the `events` array parameter, it will handle by the standard `next(error)`. 97 | 98 | ## Contribute 99 | 100 | ``` 101 | git clone https://github.com/utatti/express-formidable.git 102 | cd express-formidable 103 | npm install 104 | ``` 105 | 106 | To lint and test: 107 | 108 | ``` 109 | npm test 110 | ``` 111 | 112 | ## License 113 | 114 | [MIT](LICENSE) 115 | -------------------------------------------------------------------------------- /lib/middleware.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const formidable = require('formidable'); 4 | 5 | function parse(opts, events) { 6 | return (req, res, next) => { 7 | if (req.express_formidable && req.express_formidable.parsed) { 8 | next(); 9 | return; 10 | } 11 | 12 | const form = new formidable.IncomingForm(); 13 | Object.assign(form, opts); 14 | 15 | let manageOnError = false; 16 | if (events) { 17 | events.forEach((e) => { 18 | manageOnError = manageOnError || e.event === 'error'; 19 | form.on(e.event, (...parameters) => { e.action(req, res, next, ...parameters); }); 20 | }); 21 | } 22 | 23 | if (!manageOnError) { 24 | form.on('error', (err) => { 25 | next(err); 26 | }); 27 | } 28 | 29 | form.parse(req, (err, fields, files) => { 30 | if (err) { 31 | next(err); 32 | return; 33 | } 34 | 35 | Object.assign(req, { fields, files, express_formidable: { parsed: true } }); 36 | next(); 37 | }); 38 | }; 39 | } 40 | 41 | module.exports = parse; 42 | exports.parse = parse; 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-formidable", 3 | "version": "1.2.0", 4 | "description": "An Express middleware of Formidable that just works.", 5 | "author": "Jun ", 6 | "main": "./lib/middleware.js", 7 | "engines": { 8 | "node": ">= 8" 9 | }, 10 | "scripts": { 11 | "test": "eslint . && jest test/middleware.test.js --forceExit --detectOpenHandles" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/utatti/express-formidable.git" 16 | }, 17 | "keywords": [ 18 | "express", 19 | "middleware", 20 | "formidable" 21 | ], 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/utatti/express-formidable/issues" 25 | }, 26 | "homepage": "https://github.com/utatti/express-formidable", 27 | "dependencies": { 28 | "formidable": "^1.0.17" 29 | }, 30 | "devDependencies": { 31 | "eslint": "^3.7.1", 32 | "eslint-config-airbnb-base": "^8.0.0", 33 | "eslint-plugin-import": "^1.16.0", 34 | "express": "^4.14.0", 35 | "jest": "^23.6.0", 36 | "request": "^2.88.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true 4 | }, 5 | "rules": { 6 | "import/no-extraneous-dependencies": "off" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/file1.txt: -------------------------------------------------------------------------------- 1 | You're file 1. 2 | -------------------------------------------------------------------------------- /test/fixtures/file2.txt: -------------------------------------------------------------------------------- 1 | I'm file 2. 2 | -------------------------------------------------------------------------------- /test/helpers/send.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const request = require('request'); 4 | 5 | function send(type, data, context = '/') { 6 | const opts = { url: `http://localhost:1234${context}` }; 7 | 8 | if (type === 'application/x-www-form-urlencoded') { 9 | opts.form = data; 10 | } else if (type === 'multipart/form-data') { 11 | opts.formData = data; 12 | } else if (type === 'application/json') { 13 | opts.json = data; 14 | } 15 | 16 | request.post(opts); 17 | } 18 | 19 | module.exports = send; 20 | -------------------------------------------------------------------------------- /test/middleware.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | const express = require('express'); 5 | const formidable = require('../lib/middleware'); 6 | const fs = require('fs'); 7 | const path = require('path'); 8 | const send = require('./helpers/send'); 9 | 10 | const app = express(); 11 | 12 | let callback = null; 13 | 14 | app.post('/', formidable(), req => callback(req)); 15 | 16 | beforeAll(done => app.listen(1234, done)); 17 | 18 | test('parses application/x-www-form-urlencoded', (done) => { 19 | send('application/x-www-form-urlencoded', { 20 | hello: 'world', 21 | foo: 'bar', 22 | number: 20161006, 23 | }); 24 | 25 | callback = (req) => { 26 | assert.equal(req.fields.hello, 'world'); 27 | assert.equal(req.fields.foo, 'bar'); 28 | assert.equal(req.fields.number, '20161006'); 29 | done(); 30 | }; 31 | }); 32 | 33 | test('parses application/json', (done) => { 34 | send('application/json', { 35 | hello: 'world', 36 | foo: 'bar', 37 | number: 20161006, 38 | }); 39 | 40 | callback = (req) => { 41 | assert.equal(req.fields.hello, 'world'); 42 | assert.equal(req.fields.foo, 'bar'); 43 | assert.equal(req.fields.number, '20161006'); 44 | done(); 45 | }; 46 | }); 47 | 48 | test('parses multipart/form-data without a file', (done) => { 49 | send('multipart/form-data', { 50 | hello: 'world', 51 | foo: 'bar', 52 | number: 20161006, 53 | }); 54 | 55 | callback = (req) => { 56 | assert.equal(req.fields.hello, 'world'); 57 | assert.equal(req.fields.foo, 'bar'); 58 | assert.equal(req.fields.number, '20161006'); 59 | done(); 60 | }; 61 | }); 62 | 63 | function fileTest(file, content) { 64 | assert.equal(fs.readFileSync(file.path).toString(), content); 65 | } 66 | 67 | test('parses multipart/form-data with files', (done) => { 68 | send('multipart/form-data', { 69 | hello: 'world', 70 | foo: 'bar', 71 | number: 20161006, 72 | file1: fs.createReadStream(path.join(__dirname, '/fixtures/file1.txt')), 73 | file2: fs.createReadStream(path.join(__dirname, '/fixtures/file2.txt')), 74 | }); 75 | 76 | callback = (req) => { 77 | assert.equal(req.fields.hello, 'world'); 78 | assert.equal(req.fields.foo, 'bar'); 79 | assert.equal(req.fields.number, '20161006'); 80 | 81 | fileTest(req.files.file1, 'You\'re file 1.\n'); 82 | fileTest(req.files.file2, 'I\'m file 2.\n'); 83 | 84 | done(); 85 | }; 86 | }); 87 | 88 | let numberOfFields; 89 | let numberOfFilesBegin; 90 | let numberOfFiles; 91 | let endReached; 92 | 93 | const events = [ 94 | { 95 | event: 'fileBegin', 96 | action: (req, res, next, name, file) => { 97 | assert(name, 'name is null'); 98 | assert(file, 'file is null'); 99 | numberOfFilesBegin += 1; 100 | }, 101 | }, 102 | { 103 | event: 'field', 104 | action: (req, res, next, name, value) => { 105 | assert(name, 'name is null'); 106 | assert(value, 'value is null'); 107 | numberOfFields += 1; 108 | }, 109 | }, 110 | { 111 | event: 'progress', 112 | action: (req, res, next, bytesReceived, bytesExpected) => { 113 | assert(bytesExpected, 'bytesExpected is null'); 114 | }, 115 | }, 116 | { 117 | event: 'file', 118 | action: (req, res, next, name, file) => { 119 | assert(name, 'name is null'); 120 | assert(file, 'file is null'); 121 | numberOfFiles += 1; 122 | }, 123 | }, 124 | { 125 | event: 'error', 126 | action: (req, res, next, err) => { 127 | assert(err, 'err is null'); 128 | assert.fail('error event must not be recieved'); 129 | }, 130 | }, 131 | { 132 | event: 'aborted', 133 | action: (req, res, next) => { 134 | assert(next, 'next is null'); 135 | assert.fail('aborted event must not be recieved'); 136 | }, 137 | }, 138 | { 139 | event: 'end', 140 | action: (req, res, next) => { 141 | assert(next, 'next is null'); 142 | endReached = true; 143 | }, 144 | }, 145 | ]; 146 | 147 | app.post('/event', formidable(null, events), req => callback(req)); 148 | 149 | test('parses multipart/form-data with files and recieve all events', (done) => { 150 | numberOfFields = 0; 151 | numberOfFilesBegin = 0; 152 | numberOfFiles = 0; 153 | endReached = false; 154 | 155 | send('multipart/form-data', { 156 | hello: 'world', 157 | foo: 'bar', 158 | number: 20161006, 159 | file1: fs.createReadStream(path.join(__dirname, '/fixtures/file1.txt')), 160 | file2: fs.createReadStream(path.join(__dirname, '/fixtures/file2.txt')), 161 | }, '/event'); 162 | 163 | callback = (req) => { 164 | assert.equal(req.fields.hello, 'world'); 165 | assert.equal(req.fields.foo, 'bar'); 166 | assert.equal(req.fields.number, '20161006'); 167 | 168 | fileTest(req.files.file1, 'You\'re file 1.\n'); 169 | fileTest(req.files.file2, 'I\'m file 2.\n'); 170 | 171 | assert.equal(numberOfFields, 3); 172 | assert.equal(numberOfFilesBegin, 2); 173 | assert.equal(numberOfFiles, 2); 174 | 175 | assert(endReached, 'The end event is not reached'); 176 | 177 | done(); 178 | }; 179 | }); 180 | --------------------------------------------------------------------------------