make-middleware.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. var is = require('type-is')
  2. var Busboy = require('busboy')
  3. var appendField = require('append-field')
  4. var Counter = require('./counter')
  5. var MulterError = require('./multer-error')
  6. var FileAppender = require('./file-appender')
  7. var removeUploadedFiles = require('./remove-uploaded-files')
  8. function drainStream (stream) {
  9. stream.on('readable', () => {
  10. while (stream.read() !== null) {}
  11. })
  12. }
  13. function makeMiddleware (setup) {
  14. return function multerMiddleware (req, res, next) {
  15. if (!is(req, ['multipart'])) return next()
  16. var options = setup()
  17. var limits = options.limits
  18. var storage = options.storage
  19. var fileFilter = options.fileFilter
  20. var fileStrategy = options.fileStrategy
  21. var preservePath = options.preservePath
  22. var defParamCharset = options.defParamCharset
  23. req.body = Object.create(null)
  24. var busboy
  25. var appender = null
  26. var isDone = false
  27. var readFinished = false
  28. var errorOccured = false
  29. var pendingWrites = new Counter()
  30. var uploadedFiles = []
  31. function done (err) {
  32. var called = false
  33. function onFinished () {
  34. if (called) return
  35. called = true
  36. next(err)
  37. }
  38. if (isDone) return
  39. isDone = true
  40. if (busboy) {
  41. req.unpipe(busboy)
  42. setImmediate(() => {
  43. busboy.removeAllListeners()
  44. })
  45. }
  46. drainStream(req)
  47. req.resume()
  48. // - if responding with an error, drain request body before calling
  49. // next(err) -- avoids EPIPE on the client (server closing connection
  50. // while the client is still sending the request body)
  51. // - also listen for 'close' so we don't hang when the client aborts (stream may never 'end')
  52. // - skip waiting if the stream is already destroyed (e.g. client aborted)
  53. if (err && req.readable && !req.destroyed) {
  54. req.once('end', onFinished)
  55. req.once('error', onFinished)
  56. req.once('close', onFinished)
  57. return
  58. }
  59. next(err)
  60. }
  61. function indicateDone () {
  62. if (readFinished && pendingWrites.isZero() && !errorOccured) done()
  63. }
  64. function abortWithError (uploadError, skipPendingWait) {
  65. if (errorOccured) return
  66. errorOccured = true
  67. function finishAbort () {
  68. function remove (file, cb) {
  69. storage._removeFile(req, file, cb)
  70. }
  71. removeUploadedFiles(uploadedFiles, remove, function (err, storageErrors) {
  72. if (err) return done(err)
  73. uploadError.storageErrors = storageErrors
  74. done(uploadError)
  75. })
  76. }
  77. if (skipPendingWait) {
  78. finishAbort()
  79. } else {
  80. pendingWrites.onceZero(finishAbort)
  81. }
  82. }
  83. function abortWithCode (code, optionalField) {
  84. abortWithError(new MulterError(code, optionalField))
  85. }
  86. function handleRequestFailure (err) {
  87. if (isDone) return
  88. if (busboy) {
  89. req.unpipe(busboy)
  90. busboy.destroy(err)
  91. }
  92. abortWithError(err, true)
  93. }
  94. req.on('error', function (err) {
  95. handleRequestFailure(err || new Error('Request error'))
  96. })
  97. req.on('aborted', function () {
  98. handleRequestFailure(new Error('Request aborted'))
  99. })
  100. req.on('close', function () {
  101. if (req.readableEnded) return
  102. handleRequestFailure(new Error('Request closed'))
  103. })
  104. try {
  105. busboy = Busboy({
  106. headers: req.headers,
  107. limits: limits,
  108. preservePath: preservePath,
  109. defParamCharset: defParamCharset
  110. })
  111. } catch (err) {
  112. return next(err)
  113. }
  114. appender = new FileAppender(fileStrategy, req)
  115. // handle text field data
  116. busboy.on('field', function (fieldname, value, { nameTruncated, valueTruncated }) {
  117. if (fieldname == null) return abortWithCode('MISSING_FIELD_NAME')
  118. if (nameTruncated) return abortWithCode('LIMIT_FIELD_KEY')
  119. if (valueTruncated) return abortWithCode('LIMIT_FIELD_VALUE', fieldname)
  120. // Work around bug in Busboy (https://github.com/mscdex/busboy/issues/6)
  121. if (limits && Object.prototype.hasOwnProperty.call(limits, 'fieldNameSize')) {
  122. if (fieldname.length > limits.fieldNameSize) return abortWithCode('LIMIT_FIELD_KEY')
  123. }
  124. appendField(req.body, fieldname, value)
  125. })
  126. // handle files
  127. busboy.on('file', function (fieldname, fileStream, { filename, encoding, mimeType }) {
  128. var pendingWritesIncremented = false
  129. fileStream.on('error', function (err) {
  130. if (pendingWritesIncremented) {
  131. pendingWrites.decrement()
  132. }
  133. abortWithError(err)
  134. })
  135. if (fieldname == null) return abortWithCode('MISSING_FIELD_NAME')
  136. // don't attach to the files object, if there is no file
  137. if (!filename) return fileStream.resume()
  138. // Work around bug in Busboy (https://github.com/mscdex/busboy/issues/6)
  139. if (limits && Object.prototype.hasOwnProperty.call(limits, 'fieldNameSize')) {
  140. if (fieldname.length > limits.fieldNameSize) return abortWithCode('LIMIT_FIELD_KEY')
  141. }
  142. var file = {
  143. fieldname: fieldname,
  144. originalname: filename,
  145. encoding: encoding,
  146. mimetype: mimeType
  147. }
  148. var placeholder = appender.insertPlaceholder(file)
  149. fileFilter(req, file, function (err, includeFile) {
  150. if (errorOccured) {
  151. appender.removePlaceholder(placeholder)
  152. return fileStream.resume()
  153. }
  154. if (err) {
  155. appender.removePlaceholder(placeholder)
  156. return abortWithError(err)
  157. }
  158. if (!includeFile) {
  159. appender.removePlaceholder(placeholder)
  160. return fileStream.resume()
  161. }
  162. var aborting = false
  163. pendingWritesIncremented = true
  164. pendingWrites.increment()
  165. Object.defineProperty(file, 'stream', {
  166. configurable: true,
  167. enumerable: false,
  168. value: fileStream
  169. })
  170. fileStream.on('limit', function () {
  171. aborting = true
  172. abortWithCode('LIMIT_FILE_SIZE', fieldname)
  173. })
  174. storage._handleFile(req, file, function (err, info) {
  175. if (aborting) {
  176. appender.removePlaceholder(placeholder)
  177. uploadedFiles.push({ ...file, ...info })
  178. return pendingWrites.decrement()
  179. }
  180. if (err) {
  181. appender.removePlaceholder(placeholder)
  182. pendingWrites.decrement()
  183. return abortWithError(err)
  184. }
  185. var fileInfo = { ...file, ...info }
  186. appender.replacePlaceholder(placeholder, fileInfo)
  187. uploadedFiles.push(fileInfo)
  188. pendingWrites.decrement()
  189. indicateDone()
  190. })
  191. })
  192. })
  193. busboy.on('error', function (err) { abortWithError(err) })
  194. busboy.on('partsLimit', function () { abortWithCode('LIMIT_PART_COUNT') })
  195. busboy.on('filesLimit', function () { abortWithCode('LIMIT_FILE_COUNT') })
  196. busboy.on('fieldsLimit', function () { abortWithCode('LIMIT_FIELD_COUNT') })
  197. busboy.on('close', function () {
  198. readFinished = true
  199. indicateDone()
  200. })
  201. req.pipe(busboy)
  202. }
  203. }
  204. module.exports = makeMiddleware