123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286 |
- import util from 'util'
- import Busboy from 'busboy'
- import { WriteStream } from 'fs-capacitor'
- import createError from 'http-errors'
- import objectPath from 'object-path'
- import { SPEC_URL } from './constants'
- import { ignoreStream } from './ignoreStream'
- import { isEnumerableObject } from './isEnumerableObject'
- class Upload {
- constructor() {
- this.promise = new Promise((resolve, reject) => {
- this.resolve = file => {
- this.file = file
- resolve(file)
- }
- this.reject = reject
- })
- this.promise.catch(() => {})
- }
- }
- export const processRequest = (
- request,
- response,
- { maxFieldSize = 1000000, maxFileSize = Infinity, maxFiles = Infinity } = {}
- ) =>
- new Promise((resolve, reject) => {
- let released
- let exitError
- let currentStream
- let operations
- let operationsPath
- let map
- const parser = new Busboy({
- headers: request.headers,
- limits: {
- fieldSize: maxFieldSize,
- fields: 2,
- fileSize: maxFileSize,
- files: maxFiles
- }
- })
- const exit = error => {
- if (exitError) return
- exitError = error
- reject(exitError)
- parser.destroy()
- if (currentStream) currentStream.destroy(exitError)
- if (map)
- for (const upload of map.values())
- if (!upload.file) upload.reject(exitError)
- request.unpipe(parser)
- setImmediate(() => {
- request.resume()
- })
- }
- const release = () => {
- // istanbul ignore next
- if (released) return
- released = true
- if (map)
- for (const upload of map.values())
- if (upload.file) upload.file.capacitor.destroy()
- }
- const abort = () => {
- exit(
- createError(
- 499,
- 'Request disconnected during file upload stream parsing.'
- )
- )
- }
- parser.on(
- 'field',
- (fieldName, value, fieldNameTruncated, valueTruncated) => {
- if (exitError) return
- if (valueTruncated)
- return exit(
- createError(
- 413,
- `The ‘${fieldName}’ multipart field value exceeds the ${maxFieldSize} byte size limit.`
- )
- )
- switch (fieldName) {
- case 'operations':
- try {
- operations = JSON.parse(value)
- } catch (error) {
- return exit(
- createError(
- 400,
- `Invalid JSON in the ‘operations’ multipart field (${SPEC_URL}).`
- )
- )
- }
- if (!isEnumerableObject(operations) && !Array.isArray(operations))
- return exit(
- createError(
- 400,
- `Invalid type for the ‘operations’ multipart field (${SPEC_URL}).`
- )
- )
- operationsPath = objectPath(operations)
- break
- case 'map': {
- if (!operations)
- return exit(
- createError(
- 400,
- `Misordered multipart fields; ‘map’ should follow ‘operations’ (${SPEC_URL}).`
- )
- )
- let parsedMap
- try {
- parsedMap = JSON.parse(value)
- } catch (error) {
- return exit(
- createError(
- 400,
- `Invalid JSON in the ‘map’ multipart field (${SPEC_URL}).`
- )
- )
- }
- if (!isEnumerableObject(parsedMap))
- return exit(
- createError(
- 400,
- `Invalid type for the ‘map’ multipart field (${SPEC_URL}).`
- )
- )
- const mapEntries = Object.entries(parsedMap)
- if (mapEntries.length > maxFiles)
- return exit(
- createError(413, `${maxFiles} max file uploads exceeded.`)
- )
- map = new Map()
- for (const [fieldName, paths] of mapEntries) {
- if (!Array.isArray(paths))
- return exit(
- createError(
- 400,
- `Invalid type for the ‘map’ multipart field entry key ‘${fieldName}’ array (${SPEC_URL}).`
- )
- )
- map.set(fieldName, new Upload())
- for (const [index, path] of paths.entries()) {
- if (typeof path !== 'string')
- return exit(
- createError(
- 400,
- `Invalid type for the ‘map’ multipart field entry key ‘${fieldName}’ array index ‘${index}’ value (${SPEC_URL}).`
- )
- )
- try {
- operationsPath.set(path, map.get(fieldName).promise)
- } catch (error) {
- return exit(
- createError(
- 400,
- `Invalid object path for the ‘map’ multipart field entry key ‘${fieldName}’ array index ‘${index}’ value ‘${path}’ (${SPEC_URL}).`
- )
- )
- }
- }
- }
- resolve(operations)
- }
- }
- }
- )
- parser.on('file', (fieldName, stream, filename, encoding, mimetype) => {
- if (exitError) {
- ignoreStream(stream)
- return
- }
- if (!map) {
- ignoreStream(stream)
- return exit(
- createError(
- 400,
- `Misordered multipart fields; files should follow ‘map’ (${SPEC_URL}).`
- )
- )
- }
- currentStream = stream
- stream.on('end', () => {
- currentStream = null
- })
- const upload = map.get(fieldName)
- if (!upload) {
- ignoreStream(stream)
- return
- }
- const capacitor = new WriteStream()
- capacitor.on('error', () => {
- stream.unpipe()
- stream.resume()
- })
- stream.on('limit', () => {
- stream.unpipe()
- capacitor.destroy(
- createError(
- 413,
- `File truncated as it exceeds the ${maxFileSize} byte size limit.`
- )
- )
- })
- stream.on('error', error => {
- stream.unpipe() // istanbul ignore next
- capacitor.destroy(exitError || error)
- })
- stream.pipe(capacitor)
- const file = {
- filename,
- mimetype,
- encoding,
- createReadStream() {
- const error = capacitor.error || (released ? exitError : null)
- if (error) throw error
- return capacitor.createReadStream()
- }
- }
- let capacitorStream
- Object.defineProperty(file, 'stream', {
- get: util.deprecate(function() {
- if (!capacitorStream) capacitorStream = this.createReadStream()
- return capacitorStream
- }, 'File upload property ‘stream’ is deprecated. Use ‘createReadStream()’ instead.')
- })
- Object.defineProperty(file, 'capacitor', {
- value: capacitor
- })
- upload.resolve(file)
- })
- parser.once('filesLimit', () =>
- exit(createError(413, `${maxFiles} max file uploads exceeded.`))
- )
- parser.once('finish', () => {
- request.unpipe(parser)
- request.resume()
- if (!operations)
- return exit(
- createError(
- 400,
- `Missing multipart field ‘operations’ (${SPEC_URL}).`
- )
- )
- if (!map)
- return exit(
- createError(400, `Missing multipart field ‘map’ (${SPEC_URL}).`)
- )
- for (const upload of map.values())
- if (!upload.file)
- upload.reject(createError(400, 'File missing in the request.'))
- })
- parser.once('error', exit)
- response.once('finish', release)
- response.once('close', release)
- request.once('close', abort)
- request.once('end', () => {
- request.removeListener('close', abort)
- })
- request.pipe(parser)
- })
|