1 | import BusBoy from '@fastify/busboy'
|
2 | import { createError } from '@middy/util'
|
3 |
|
4 | const mimePattern = /^multipart\/form-data(;.*)?$/
|
5 | const fieldnamePattern = /(.+)\[(.*)]$/
|
6 |
|
7 | const defaults = {
|
8 |
|
9 | busboy: {},
|
10 | charset: 'utf8',
|
11 | disableContentTypeError: false
|
12 | }
|
13 |
|
14 | const httpMultipartBodyParserMiddleware = (opts = {}) => {
|
15 | const options = { ...defaults, ...opts }
|
16 |
|
17 | const httpMultipartBodyParserMiddlewareBefore = async (request) => {
|
18 | const { headers } = request.event
|
19 |
|
20 | const contentType = headers?.['Content-Type'] ?? headers?.['content-type']
|
21 |
|
22 | if (!mimePattern.test(contentType)) {
|
23 | if (options.disableContentTypeError) {
|
24 | return
|
25 | }
|
26 | throw createError(415, 'Unsupported Media Type', {
|
27 | cause: { package: '@middy/multipart-body-parser', data: contentType }
|
28 | })
|
29 | }
|
30 |
|
31 | return parseMultipartData(request.event, options)
|
32 | .then((multipartData) => {
|
33 |
|
34 | request.event.body = multipartData
|
35 | })
|
36 | .catch((err) => {
|
37 |
|
38 | throw createError(
|
39 | 415,
|
40 | 'Invalid or malformed multipart/form-data was provided',
|
41 | { cause: { package: '@middy/multipart-body-parser', data: err } }
|
42 | )
|
43 | })
|
44 | }
|
45 |
|
46 | return {
|
47 | before: httpMultipartBodyParserMiddlewareBefore
|
48 | }
|
49 | }
|
50 |
|
51 | const parseMultipartData = (event, options) => {
|
52 | const multipartData = {}
|
53 | const charset = event.isBase64Encoded ? 'base64' : options.charset
|
54 |
|
55 | const busboy = BusBoy({
|
56 | ...options.busboy,
|
57 | headers: {
|
58 | 'content-type':
|
59 | event.headers?.['Content-Type'] ?? event.headers?.['content-type']
|
60 | }
|
61 | })
|
62 |
|
63 | return new Promise((resolve, reject) => {
|
64 | busboy
|
65 | .on('file', (fieldname, file, filename, encoding, mimetype) => {
|
66 | const attachment = {
|
67 | filename,
|
68 | mimetype,
|
69 | encoding
|
70 | }
|
71 |
|
72 | const chunks = []
|
73 |
|
74 | file.on('data', (data) => {
|
75 | chunks.push(data)
|
76 | })
|
77 | file.on('end', () => {
|
78 | attachment.truncated = file.truncated
|
79 | attachment.content = Buffer.concat(chunks)
|
80 | if (!multipartData[fieldname]) {
|
81 | multipartData[fieldname] = attachment
|
82 | } else {
|
83 | const current = multipartData[fieldname]
|
84 | multipartData[fieldname] = [attachment].concat(current)
|
85 | }
|
86 | })
|
87 | })
|
88 | .on('field', (fieldname, value) => {
|
89 | const matches = fieldname.match(fieldnamePattern)
|
90 | if (!matches) {
|
91 | multipartData[fieldname] = value
|
92 | } else {
|
93 | if (!multipartData[matches[1]]) {
|
94 | multipartData[matches[1]] = []
|
95 | }
|
96 | multipartData[matches[1]].push(value)
|
97 | }
|
98 | })
|
99 | .on('finish', () => resolve(multipartData))
|
100 | .on('error', (e) => reject(e))
|
101 |
|
102 | busboy.write(event.body, charset)
|
103 | busboy.end()
|
104 | })
|
105 | }
|
106 | export default httpMultipartBodyParserMiddleware
|