Search code examples
javascriptnode.jshttpmultipartform-dataform-data

Reading multipart/form-data is saving incorectly


I am trying to read data from an http image file upload. It seems to read the file, but for some reason the output file is about twice the size of the original file.

Original: 94.7KB
Temp File: 168.8 KB

When I open both files in a text editor, they have the same number of lines, and the starting and ending characters are the same. This leaves me to believe that the uploaded file is getting saved as a string instead of binary data.

I believe that the important parts are the following

The reading of the data:

let body = ''
req.on('data', data => {
  body += data.toString()
}).on('end', data => {
  if (data) body += data.toString()
  // Rest of block
})

The saving of the data:

// If there is a filename grab the file data
if (result.filename.length > 0) {
  // Create a temporary url
  let temp = join(os.tmpdir(), (Math.random() * 10000).toString(12).substr(5, 10))

  // Get the data between the blocks after the first two newlines
  let matches = item.match(/^.+?(\r\n\r\n|\n\n)(.+)/s)

  // Write the data to file
  fs.createWriteStream(temp).write(matches[2])
}

Here is the full parsing of the data:

http.createServer((req, res) => {
  let body = ''
  req.on('data', data => {
    body += data.toString()
  }).on('end', data => {
    if (data) body += data.toString()
    let boundary = req.headers['content-type'].split('boundary=')[1]

    // Split all the boundary items and loop over them
    body.split(new RegExp(`(--${boundary}|--${boundary}--)`)).forEach(item => {
      if (item.trim().toLowerCase().startsWith('content-disposition')) {
        item = item.trim()
        // Find the name and filename
        let result = item.split(':')[1].split(';').map(i => i.trim()).reduce((obj, itm) => {
          if (itm.startsWith('name=')) obj.name = itm.match(/^name="(.+)"/)[1]
          if (itm.startsWith('filename=')) obj.filename = itm.match(/^filename="(.+)"/)[1]
          return obj
        }, { name: '', filename: '' })

        // If there is a filename grab the file data
        if (result.filename.length > 0) {
          // Create a temporary url
          let temp = join(os.tmpdir(), (Math.random() * 10000).toString(12).substr(5, 10))

          // Get the data
          let matches = item.match(/^.+?(\r\n\r\n|\n\n)(.+)/s)

          // Write the data to file
          fs.createWriteStream(temp).write(matches[2])
        }
      }
    })
  })
})

Solution

  • So, I was somewhat correct in both cases...

    First, I must read the data as binary data like this:

    body += data.toString('binary')
    

    Next when saving, I need to save as binary data like this:

    fs.createWriteStream(temp).write(matches[2], 'binary')
    

    This now saves as the correct file size and the image is readable!