├── .gitignore ├── images └── aws-api-gateway-1.jpg ├── package.json ├── README.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /images/aws-api-gateway-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dudewheresmycode/node-lambda-multipart/HEAD/images/aws-api-gateway-1.jpg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lambda-multipart", 3 | "version": "1.0.2", 4 | "description": "", 5 | "repository":"https://github.com/dudewheresmycode/node-lambda-multipart", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "pez": "^4.0.1" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lambda-multipart 2 | 3 | A simple multipart/form-data parser for AWS lambda functions. 4 | 5 | ```sh 6 | npm install -S lambda-multipart 7 | ``` 8 | 9 | ```js 10 | var Multipart = require('lambda-multipart'); 11 | 12 | exports.handler = function(event, context, callback){ 13 | 14 | var parser = new Multipart(event); 15 | 16 | parser.on('field',function(key, value){ 17 | console.log('received field', key, value); 18 | }); 19 | parser.on('file',function(file){ 20 | //file.headers['content-type'] 21 | file.pipe(fs.createWriteStream(__dirname+"/downloads/"+file.filename)); 22 | }); 23 | 24 | parser.on('finish',function(result){ 25 | //result.files (array of file streams) 26 | //result.fields (object of field key/values) 27 | console.log("Finished") 28 | }); 29 | } 30 | ``` 31 | 32 | ## AWS Setup 33 | There is a small bit of setup on the AWS side. Head to the API Gateway service you want to setup and select: 34 | 35 | **Settings** > **Binary Media Types** 36 | 37 | Add a new entry for `multipart/form-data` 38 | 39 | ![Screenshot](images/aws-api-gateway-1.jpg "Screenshot of API Gateway Settings") 40 | 41 | 42 | ### Credits and Acknowledgements 43 | - [@hapijs](https://github.com/hapijs) for https://github.com/hapijs/pez 44 | - [@myshenin](https://github.com/myshenin) for https://github.com/myshenin/aws-lambda-multipart-parser 45 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var Pez = require('pez'); 2 | var events = require("events"); 3 | var util = require("util"); 4 | 5 | var CONTENT_TYPE_RE = /^multipart\/(?:form-data|related)(?:;|$)/i; 6 | var CONTENT_TYPE_PARAM_RE = /;\s*([^=]+)=(?:"([^"]+)"|([^;]+))/gi; 7 | 8 | function multipart(event){ 9 | var that = this; 10 | events.EventEmitter.call(this); 11 | that.contentType = event.headers['content-type']; 12 | if(!that.contentType){ that.contentType = event.headers['Content-Type']; } 13 | const dispenser = new Pez.Dispenser({ boundary: that.getBoundary(that.contentType) }); 14 | that.fields = {}; 15 | that.files = []; 16 | dispenser.on('part', (part) => { 17 | that.files.push(part); 18 | that.emit('file', part) 19 | }); 20 | dispenser.on('field', (name, value) => { 21 | that.fields[name]=value; 22 | that.emit('field', that.fields[name]) 23 | }); 24 | dispenser.on('error',function(e){ 25 | that.emit('error', {error:e}) 26 | }); 27 | dispenser.on('close',function(){ 28 | //callback(null, {files:that.files, fields:that.fields}); 29 | that.emit('finish', {files:that.files, fields:that.fields}) 30 | }); 31 | dispenser.write(event.body, event.isBase64Encoded ? "base64" : "binary"); 32 | dispenser.end(); 33 | } 34 | 35 | multipart.prototype.getBoundary = function(contentType){ 36 | var m = CONTENT_TYPE_RE.exec(contentType); 37 | if (!m) { 38 | callback(null, {statusCode: 415, body: "unsupported content-type", headers: {"Content-Type":"text/plain"}}); 39 | console.log('unsupported content-type'); 40 | return; 41 | } 42 | 43 | var boundary; 44 | CONTENT_TYPE_PARAM_RE.lastIndex = m.index + m[0].length - 1; 45 | while ((m = CONTENT_TYPE_PARAM_RE.exec(contentType))) { 46 | if (m[1].toLowerCase() !== 'boundary') continue; 47 | boundary = m[2] || m[3]; 48 | break; 49 | } 50 | return boundary; 51 | } 52 | 53 | util.inherits(multipart, events.EventEmitter); 54 | 55 | module.exports = multipart; 56 | --------------------------------------------------------------------------------