diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2820926 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +**/node_modules/** +**/target/** +.project +.jsbeautifyrc diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1a65b45 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Francis Pasoquen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..7fbeac1 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# lambda-multipart-parser + +## Introduction +This module will parse the multipart-form containing files and fields from the lambda event object. + +## Description +``` +@param {event} - an event containing the multipart-form in the body +@return {object} - a JSON object containing array of files and fields, sample below. + +{ + files: [ + { + filename: 'test.pdf', + content: , + contentType: 'application/pdf', + encoding: '7bit', + fieldname: 'uploadFile1' + } + ], + field1: 'VALUE1', + field2: 'VALUE2', +} +``` + +## Usage diff --git a/index.js b/index.js new file mode 100644 index 0000000..87afc1c --- /dev/null +++ b/index.js @@ -0,0 +1,67 @@ +'use strict'; + +const Busboy = require('busboy'); + +/* + * This module will parse the multipart-form containing files and fields from the lambda event object. + * @param {event} - an event containing the multipart-form in the body + * @return {object} - a JSON object containing array of files and fields, sample below. + { + files: [ + { + filename: 'test.pdf', + content: , + contentType: 'application/pdf', + encoding: '7bit', + fieldname: 'uploadFile1' + } + ], + field1: 'VALUE1', + field2: 'VALUE2', + } + */ +const parse = (event) => new Promise((resolve, reject) => { + const busboy = new Busboy({ + headers: { + 'content-type': event.headers['content-type'] || event.headers['Content-Type'] + } + }); + const result = { + files: [] + }; + + busboy.on('file', (fieldname, file, filename, encoding, mimetype) => { + const uploadFile = {}; + + file.on('data', data => { + uploadFile.content = data; + }); + + file.on('end', () => { + if (uploadFile.content) { + uploadFile.filename = filename; + uploadFile.contentType = mimetype; + uploadFile.encoding = encoding; + uploadFile.fieldname = fieldname; + result.files.push(uploadFile); + } + }); + }); + + busboy.on('field', (fieldname, value) => { + result[fieldname] = value; + }); + + busboy.on('error', error => { + reject(error); + }); + + busboy.on('finish', () => { + resolve(result); + }); + + busboy.write(event.body, event.isBase64Encoded ? 'base64' : 'binary'); + busboy.end(); +}); + +module.exports.parse = parse; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..47806a4 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,345 @@ +{ + "name": "lambda-multipart-parser", + "version": "0.0.1", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@sinonjs/commons": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.3.0.tgz", + "integrity": "sha512-j4ZwhaHmwsCb4DlDOIWnI5YyKDNMoNThsmwEpfHx6a1EpsGZ9qYLxP++LMlmBRjtGptGHFsGItJ768snllFWpA==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + }, + "dependencies": { + "type-detect": { + "version": "4.0.8", + "resolved": "http://build.fxdms.net/artifactory/api/npm/npm-virtual/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha1-dkb7XxiHHPu3dJ5pvTmmOI63RQw=", + "dev": true + } + } + }, + "@sinonjs/formatio": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.1.0.tgz", + "integrity": "sha512-ZAR2bPHOl4Xg6eklUGpsdiIJ4+J1SNag1DHHrG/73Uz/nVwXqjgUtRPLoS+aVyieN9cSbc0E4LsU984tWcDyNg==", + "dev": true, + "requires": { + "@sinonjs/samsam": "2.1.3" + } + }, + "@sinonjs/samsam": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-2.1.3.tgz", + "integrity": "sha512-8zNeBkSKhU9a5cRNbpCKau2WWPfan+Q2zDlcXvXyhn9EsMqgYs4qzo0XHNVlXC6ABQL8fT6nV+zzo5RTHJzyXw==", + "dev": true + }, + "assertion-error": { + "version": "1.1.0", + "resolved": "http://build.fxdms.net/artifactory/api/npm/npm-virtual/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha1-5gtrDo8wG9l+U3UhW9pAbIURjAs=", + "dev": true + }, + "busboy": { + "version": "0.3.0", + "resolved": "http://build.fxdms.net/artifactory/api/npm/npm-virtual/busboy/-/busboy-0.3.0.tgz", + "integrity": "sha1-buPLHIRPwfaR2PnYJPcBKLO15IU=", + "requires": { + "dicer": "0.3.0" + } + }, + "chai": { + "version": "3.5.0", + "resolved": "http://build.fxdms.net/artifactory/api/npm/npm-virtual/chai/-/chai-3.5.0.tgz", + "integrity": "sha1-TQJjewZ/6Vi9v906QOxW/vc3Mkc=", + "dev": true, + "requires": { + "assertion-error": "1.1.0", + "deep-eql": "0.1.3", + "type-detect": "1.0.0" + } + }, + "commander": { + "version": "2.3.0", + "resolved": "http://build.fxdms.net/artifactory/api/npm/npm-virtual/commander/-/commander-2.3.0.tgz", + "integrity": "sha1-/UMOiJgy7DU7ms0d4hfBHLPu+HM=", + "dev": true + }, + "debug": { + "version": "2.2.0", + "resolved": "http://build.fxdms.net/artifactory/api/npm/npm-virtual/debug/-/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "dev": true, + "requires": { + "ms": "0.7.1" + } + }, + "deep-eql": { + "version": "0.1.3", + "resolved": "http://build.fxdms.net/artifactory/api/npm/npm-virtual/deep-eql/-/deep-eql-0.1.3.tgz", + "integrity": "sha1-71WKyrjeJSBs1xOQbXTlaTDrafI=", + "dev": true, + "requires": { + "type-detect": "0.1.1" + }, + "dependencies": { + "type-detect": { + "version": "0.1.1", + "resolved": "http://build.fxdms.net/artifactory/api/npm/npm-virtual/type-detect/-/type-detect-0.1.1.tgz", + "integrity": "sha1-C6XsKohWQORw6k6FBZcZANrFiCI=", + "dev": true + } + } + }, + "dicer": { + "version": "0.3.0", + "resolved": "http://build.fxdms.net/artifactory/api/npm/npm-virtual/dicer/-/dicer-0.3.0.tgz", + "integrity": "sha1-6s2Ys7+/kuirXC/bcaqsRLsGuHI=", + "requires": { + "streamsearch": "0.1.2" + } + }, + "diff": { + "version": "1.4.0", + "resolved": "http://build.fxdms.net/artifactory/api/npm/npm-virtual/diff/-/diff-1.4.0.tgz", + "integrity": "sha1-fyjS657nsVqX79ic5j3P2qPMur8=", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.2", + "resolved": "http://build.fxdms.net/artifactory/api/npm/npm-virtual/escape-string-regexp/-/escape-string-regexp-1.0.2.tgz", + "integrity": "sha1-Tbwv5nTnGUnK8/smlc5/LcHZqNE=", + "dev": true + }, + "glob": { + "version": "3.2.11", + "resolved": "http://build.fxdms.net/artifactory/api/npm/npm-virtual/glob/-/glob-3.2.11.tgz", + "integrity": "sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "minimatch": "0.3.0" + } + }, + "growl": { + "version": "1.9.2", + "resolved": "http://build.fxdms.net/artifactory/api/npm/npm-virtual/growl/-/growl-1.9.2.tgz", + "integrity": "sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8=", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "http://build.fxdms.net/artifactory/api/npm/npm-virtual/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "inherits": { + "version": "2.0.3", + "resolved": "http://build.fxdms.net/artifactory/api/npm/npm-virtual/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "isarray": { + "version": "0.0.1", + "resolved": "http://build.fxdms.net/artifactory/api/npm/npm-virtual/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "jade": { + "version": "0.26.3", + "resolved": "http://build.fxdms.net/artifactory/api/npm/npm-virtual/jade/-/jade-0.26.3.tgz", + "integrity": "sha1-jxDXl32NefL2/4YqgbBRPMslaGw=", + "dev": true, + "requires": { + "commander": "0.6.1", + "mkdirp": "0.3.0" + }, + "dependencies": { + "commander": { + "version": "0.6.1", + "resolved": "http://build.fxdms.net/artifactory/api/npm/npm-virtual/commander/-/commander-0.6.1.tgz", + "integrity": "sha1-+mihT2qUXVTbvlDYzbMyDp47GgY=", + "dev": true + }, + "mkdirp": { + "version": "0.3.0", + "resolved": "http://build.fxdms.net/artifactory/api/npm/npm-virtual/mkdirp/-/mkdirp-0.3.0.tgz", + "integrity": "sha1-G79asbqCevI1dRQ0kEJkVfSB/h4=", + "dev": true + } + } + }, + "just-extend": { + "version": "4.0.2", + "resolved": "http://build.fxdms.net/artifactory/api/npm/npm-virtual/just-extend/-/just-extend-4.0.2.tgz", + "integrity": "sha1-8/R/ffyg+YnFVBCn68iFSwcQivw=", + "dev": true + }, + "lodash.get": { + "version": "4.4.2", + "resolved": "http://build.fxdms.net/artifactory/api/npm/npm-virtual/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", + "dev": true + }, + "lolex": { + "version": "2.7.5", + "resolved": "http://build.fxdms.net/artifactory/api/npm/npm-virtual/lolex/-/lolex-2.7.5.tgz", + "integrity": "sha1-ETAB1Wv8fgLVbjYpHMXEE9GqBzM=", + "dev": true + }, + "lru-cache": { + "version": "2.7.3", + "resolved": "http://build.fxdms.net/artifactory/api/npm/npm-virtual/lru-cache/-/lru-cache-2.7.3.tgz", + "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=", + "dev": true + }, + "minimatch": { + "version": "0.3.0", + "resolved": "http://build.fxdms.net/artifactory/api/npm/npm-virtual/minimatch/-/minimatch-0.3.0.tgz", + "integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=", + "dev": true, + "requires": { + "lru-cache": "2.7.3", + "sigmund": "1.0.1" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "http://build.fxdms.net/artifactory/api/npm/npm-virtual/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "http://build.fxdms.net/artifactory/api/npm/npm-virtual/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "mocha": { + "version": "2.5.3", + "resolved": "http://build.fxdms.net/artifactory/api/npm/npm-virtual/mocha/-/mocha-2.5.3.tgz", + "integrity": "sha1-FhvlvetJZ3HrmzV0UFC2IrWu/Fg=", + "dev": true, + "requires": { + "commander": "2.3.0", + "debug": "2.2.0", + "diff": "1.4.0", + "escape-string-regexp": "1.0.2", + "glob": "3.2.11", + "growl": "1.9.2", + "jade": "0.26.3", + "mkdirp": "0.5.1", + "supports-color": "1.2.0", + "to-iso-string": "0.0.2" + } + }, + "ms": { + "version": "0.7.1", + "resolved": "http://build.fxdms.net/artifactory/api/npm/npm-virtual/ms/-/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", + "dev": true + }, + "nise": { + "version": "1.4.8", + "resolved": "http://build.fxdms.net/artifactory/api/npm/npm-virtual/nise/-/nise-1.4.8.tgz", + "integrity": "sha1-zpHDHobPmyxMrEnX/Nf1Z3m/1rA=", + "dev": true, + "requires": { + "@sinonjs/formatio": "3.1.0", + "just-extend": "4.0.2", + "lolex": "2.7.5", + "path-to-regexp": "1.7.0", + "text-encoding": "0.6.4" + } + }, + "path-to-regexp": { + "version": "1.7.0", + "resolved": "http://build.fxdms.net/artifactory/api/npm/npm-virtual/path-to-regexp/-/path-to-regexp-1.7.0.tgz", + "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", + "dev": true, + "requires": { + "isarray": "0.0.1" + } + }, + "sigmund": { + "version": "1.0.1", + "resolved": "http://build.fxdms.net/artifactory/api/npm/npm-virtual/sigmund/-/sigmund-1.0.1.tgz", + "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=", + "dev": true + }, + "sinon": { + "version": "6.3.5", + "resolved": "http://build.fxdms.net/artifactory/api/npm/npm-virtual/sinon/-/sinon-6.3.5.tgz", + "integrity": "sha1-D21qW066rR9ujgGTlVQtHQLBRKA=", + "dev": true, + "requires": { + "@sinonjs/commons": "1.3.0", + "@sinonjs/formatio": "3.1.0", + "@sinonjs/samsam": "2.1.3", + "diff": "3.5.0", + "lodash.get": "4.4.2", + "lolex": "2.7.5", + "nise": "1.4.8", + "supports-color": "5.5.0", + "type-detect": "4.0.8" + }, + "dependencies": { + "diff": { + "version": "3.5.0", + "resolved": "http://build.fxdms.net/artifactory/api/npm/npm-virtual/diff/-/diff-3.5.0.tgz", + "integrity": "sha1-gAwN0eCov7yVg1wgKtIg/jF+WhI=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "http://build.fxdms.net/artifactory/api/npm/npm-virtual/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha1-4uaaRKyHcveKHsCzW2id9lMO/I8=", + "dev": true, + "requires": { + "has-flag": "3.0.0" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "http://build.fxdms.net/artifactory/api/npm/npm-virtual/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha1-dkb7XxiHHPu3dJ5pvTmmOI63RQw=", + "dev": true + } + } + }, + "streamsearch": { + "version": "0.1.2", + "resolved": "http://build.fxdms.net/artifactory/api/npm/npm-virtual/streamsearch/-/streamsearch-0.1.2.tgz", + "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" + }, + "supports-color": { + "version": "1.2.0", + "resolved": "http://build.fxdms.net/artifactory/api/npm/npm-virtual/supports-color/-/supports-color-1.2.0.tgz", + "integrity": "sha1-/x7R5hFp0Gs88tWI4YixjYhH4X4=", + "dev": true + }, + "text-encoding": { + "version": "0.6.4", + "resolved": "http://build.fxdms.net/artifactory/api/npm/npm-virtual/text-encoding/-/text-encoding-0.6.4.tgz", + "integrity": "sha1-45mpgiV6J22uQou5KEXLcb3CbRk=", + "dev": true + }, + "to-iso-string": { + "version": "0.0.2", + "resolved": "http://build.fxdms.net/artifactory/api/npm/npm-virtual/to-iso-string/-/to-iso-string-0.0.2.tgz", + "integrity": "sha1-TcGeZk38y+Jb2NtQiwDG2hWCVdE=", + "dev": true + }, + "type-detect": { + "version": "1.0.0", + "resolved": "http://build.fxdms.net/artifactory/api/npm/npm-virtual/type-detect/-/type-detect-1.0.0.tgz", + "integrity": "sha1-diIXzAbbJY7EiQihKY6LlRIejqI=", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..3011b9c --- /dev/null +++ b/package.json @@ -0,0 +1,34 @@ +{ + "name": "lambda-multipart-parser", + "version": "0.0.1", + "description": "This module will parse the multipart-form containing files and fields from the lambda event object", + "main": "index.js", + "author": "francismeynard", + "license": "MIT", + "scripts": { + "test": "./node_modules/.bin/mocha --recursive --reporter spec" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/francismeynard/lambda-multipart-parser.git" + }, + "homepage": "https://github.com/francismeynard/lambda-multipart-parser#readme", + "keywords": [ + "aws", + "lambda", + "parser", + "multipart", + "form-data", + "formdata", + "multipart/form-data", + "multipart/formdata" + ], + "dependencies": { + "busboy": "^0.3.0" + }, + "devDependencies": { + "chai": "^3.5.0", + "mocha": "^2.5.3", + "sinon": "^6.3.1" + } +} \ No newline at end of file diff --git a/test/MultipartParserTest.js b/test/MultipartParserTest.js new file mode 100644 index 0000000..045d058 --- /dev/null +++ b/test/MultipartParserTest.js @@ -0,0 +1,79 @@ +'use strict'; + +const sinon = require('sinon'); +const { assert } = require('chai'); + +const parser = require('../index.js'); + +describe('MultipartParser', () => { + + describe('#parser()', () => { + + beforeEach(() => { + this.callback = sinon.fake(); + }); + + afterEach(() => { + sinon.restore(); + }); + + it('should parse the multipart form-data successfully given raw multipart form data', async () => { + // GIVEN + const event = { + headers: { + "Content-Type": "multipart/form-data; boundary=----WebKitFormBoundaryDP6Z1qHQSzB6Pf8c" + }, + body: ['------WebKitFormBoundaryDP6Z1qHQSzB6Pf8c', + 'Content-Disposition: form-data; name="uploadFile1"; filename="test.txt"', + 'Content-Type: text/plain', + '', + 'Hello World!', + '------WebKitFormBoundaryDP6Z1qHQSzB6Pf8c--' + ].join('\r\n'), + isBase64Encoded: false + }; + + // WHEN + const result = await parser.parse(event); + + // THEN + assert.isNotNull(result.files); + assert.equal(result.files.length, 1); + + const file = result.files[0]; + assert.equal(file.filename, "test.txt"); + assert.equal(file.contentType, "text/plain"); + assert.equal(file.encoding, "7bit"); + assert.equal(file.fieldname, "uploadFile1"); + }); + + it('should parse the multipart form-data successfully given base64 encoded form data', async () => { + // GIVEN + const event = { + headers: { + "Content-Type": "multipart/form-data; boundary=----WebKitFormBoundaryDP6Z1qHQSzB6Pf8c" + }, + body: `LS0tLS0tV2ViS2l0Rm9ybUJvdW5kYXJ5RFA2WjFxSFFTekI2UGY4Yw0KQ29udGVudC1EaXNwb3NpdGlvbjogZm9ybS1kYXRhOyBuYW1lPSJ1cGxvYWRGaWxlMSI7IGZpb + GVuYW1lPSJ0ZXN0LnR4dCINCkNvbnRlbnQtVHlwZTogdGV4dC9wbGFpbg0KDQpIZWxsbyBXb3JsZCENCi0tLS0tLVdlYktpdEZvcm1Cb3VuZGFyeURQNloxcUhRU3pCNl + BmOGMNCkNvbnRlbnQtRGlzcG9zaXRpb246IGZvcm0tZGF0YTsgbmFtZT0idXBsb2FkRmlsZTIiOyBmaWxlbmFtZT0iIg0KQ29udGVudC1UeXBlOiBhcHBsaWNhdGlvbi9 + vY3RldC1zdHJlYW0NCg0KDQotLS0tLS1XZWJLaXRGb3JtQm91bmRhcnlEUDZaMXFIUVN6QjZQZjhjLS0NCg==`, + isBase64Encoded: true + }; + + // WHEN + const result = await parser.parse(event); + + // THEN + assert.isNotNull(result.files); + assert.equal(result.files.length, 1); + + const file = result.files[0]; + assert.equal(file.filename, "test.txt"); + assert.equal(file.contentType, "text/plain"); + assert.equal(file.encoding, "7bit"); + assert.equal(file.fieldname, "uploadFile1"); + }); + + }); + +}); \ No newline at end of file