Binary Data Upload - Multer

Introduction

In this tutorial, we discuss how to upload binary files using FormData and XMLHttpRequest, and parsing multipart/form-data requests server side using Multer. This method is one of several other possible methods to upload binary data.

Binary Data Upload Flow

In our code, once a user choses an image in AddRecipe, an UPLOAD_REQUEST action will be dispatched, and handled by AddRecipePageSagas.js as follows:

  1. uploadRequest redux-saga is executed to handle the UPLOAD_REQUEST.
  2. FormData is created to contain the image name, and image binary data.
  3. XMLHttpRequest channel is created, and FormData is sent via the channel.
  4. Server side:
    • Once received Multer is utilized to separate JSON from binary data.
    • Binary data is stored in file using base64Img.
    • URL is generated using absfilepath and returned to client.
  5. Once successful, an image URL is returned to the redux-saga.
  6. The redux-saga dispatches a success action.

uploadRequest Saga

In [ ]:
function* uploadRequest(action) {
  console.log('uploadRequest=', action);

  const channel = yield call(_createUploadFileChannel, '/api/upload/file', action.payload);
  const result = yield take(channel);

  if (result.success)
    action.payload.imageURL = result.response.url.replace(/\\/g,"/");
    yield put({type: action.type_success, payload: {...action}});
  if (result.err)
    yield put({type: action.type_failure, payload: {file: action.payload, err: result.err}});
}

The saga calls _createUploadFileChannel function which returns a channel created specifically for the received event, then it will retrieve the response from it once it done sending the data and receives a response.

If the result is successful, it will dispatch a sucess action, otherwise a failure action.

_createUploadFileChannel

In [ ]:
function _createUploadFileChannel(endpoint, file) {
  return eventChannel(emitter => {
      const xhr = new XMLHttpRequest();
      xhr.responseType = "json";

      const onFailure = (e) => {
        emitter({err: new Error('Upload failed')});
        emitter(END);
      };

      xhr.upload.addEventListener("error", onFailure);
      xhr.upload.addEventListener("abort", onFailure);
      xhr.onreadystatechange = () => {
        const {readyState, status} = xhr;
        if (readyState === 4) {
          if (status === 200) {
            emitter({success: true, response: xhr.response});
            emitter(END);
          } else {
            onFailure(null);
          }
        }
      };

      xhr.open("POST", endpoint, true);

      let formData = new FormData();
      formData.append('filename', file.filename);
      formData.append('buffer', file.data);
      xhr.send(formData);

      return () => {
        xhr.upload.removeEventListener("error", onFailure);
        xhr.upload.removeEventListener("abort", onFailure);
        xhr.onreadystatechange = null;
        xhr.abort();
      };
    },
    buffers.sliding(2));
}

This helper function returns an arrow function as a result of its execusion. Its main purpose is to create a configured instance following the received parameters as follows:

  1. XMLHttpRequest is created and configured as follows:
    • Response type is defined as JSON.
    • onFailure event handler to handle "error" and "abort" events.
    • Callback implemented for onreadystatechange property which is called when the readyState property of the XMLHttpRequest changes.
  2. FormData is created and the following fields are added:
    • filename to contain the file name
    • buffer to contain the file data

AddRecipePageSagas.js

In [ ]:
function* AddRecipePageSagas() {
  yield takeEvery(uploadConstants.UPLOAD_REQUEST, uploadRequest);
}

Express API - recipes.js

In [ ]:
let Recipe = require(MODEL_PATH + 'Recipe');
let upload = require('multer')();
let base64Img = require('base64-img');
let generateSafeId = require('generate-safe-id');

module.exports = (app) => {
    .
    .
    .   
  app.post('/api/upload/file', upload.any(), function (req, res, next) {
    console.log("app.post('/api/upload/file')");
    let baseURL = 'http://132.72.46.150:8080/';
    let filename = generateSafeId();
    let relativePath = "server/public/";
    let absfilepath = base64Img.imgSync(req.body.buffer, relativePath + 'images/', filename);
    let correctPath = absfilepath.substr(relativePath.length, absfilepath.length);
    //let img_url = new URL(correctPath, baseURL);
    let img_url = baseURL + correctPath;

    res.json({url: img_url});
    res.end();
  });
    .
    .
    .
}

The flow at server is as follows:

  1. upload.any(), found at the function header is a Multer function which is a middleware for handling multipart/form-data requests.
  2. File buffer object is stored as a file under images/ static file serving directory, and returns absolute path of the file.
  3. Image URL is generated using baseURL and image path.
  4. Response is returned to client.

Final Words

This tutorial provided the basic details of the following elements:

  1. Storing mixed types in FormData.
  2. Binary file upload using XMLHttpRequest.
  3. Multer middleware to parse multipart/form-data requests.
  4. Storing images using base64Img.

Created: August 5th, 2018 Updated: August 5th, 2018