Thursday, September 7, 2017

NodeJS/Restify: How can I recieve file upload in API?

Leave a Comment

I'm trying to upload an image file from a mobile application (which is written in react native and now running on iOS).

The file is send to my REST API, which is shown below. I got two problems with that:

  1. I do not get req.body, as it is always an empty object, although headers are submitted correctly.
  2. I want to write the recieved file to my DB (GridFS) via gridfs-stream, but I don't understand where to put that code.

API

const restify = require('restify') const winston = require('winston') const bunyanWinston = require('bunyan-winston-adapter') const mongoose = require('mongoose') const Grid = require('gridfs-stream') const config = require('../config')  // Configure mongoose to work with javascript promises mongoose.Promise = global.Promise  // Setting up server const server = restify.createServer({   name: config.name,   version: config.version,   log: bunyanWinston.createAdapter(log) })  server.use(restify.plugins.multipartBodyParser())  server.listen(config.port, () => {   mongoose.connection.on('open', (err) => {     server.post('/upload', (req, res, next) => {       console.log(req.headers) // <- returns headers as expected        /* Problem 1 */       console.log(req.body) // <- is empty object (unexpected)       res.send(200, { message: 'successful upload' })       res.end()     })   })    global.db = mongoose.connect(config.db.uri, { useMongoClient: true })    /* Problem 2: The recieved file should be stored to DB via `gridfs-stream` */   // I think this is the wrong place for this line...   var gfs = Grid(global.db, mongoose.mongo) }) 

I tried to find the error, but I did not find it, so here is the data, which I get in my API:

headers

{    host: 'localhost:3000',   'content-type': 'multipart/form-data; boundary=pUqK6oKvY65OfhaQ3h01xWg0j4ajlanAA_e3MXVSna4F8kbg-zT0V3-PeJQm1QZ2ymcmUM',   'user-agent': 'User/1 CFNetwork/808.2.16 Darwin/15.6.0',   connection: 'keep-alive',   accept: '*/*',   'accept-language': 'en-us',   'accept-encoding': 'gzip, deflate',   'content-length': '315196'  } 

body

{ } 

Why is body empty?


React Native file upload

This is how I am sending the file to the API. I also show you the content of some variables:

async function upload (photo) {   console.log('photo', photo); // OUTPUT SHOWN BELOW   if (photo.uri) {     // Create the form data object     var data = new FormData()     data.append('picture', { uri: photo.uri, name: 'selfie.jpg', type: 'image/jpg' })      // Create the config object for the POST     const config = {       method: 'POST',       headers: {         'Accept': 'application/json'       },       body: data     }     console.log('config', config); // OUTPUT SHOWN BELOW      fetchProgress('http://localhost:3000/upload', {       method: 'post',       body: data     }, (progressEvent) => {       const progress = progressEvent.loaded / progressEvent.total       console.log(progress)     }).then((res) => console.log(res), (err) => console.log(err))   } }  const fetchProgress = (url, opts = {}, onProgress) => {   console.log(url, opts)   return new Promise((resolve, reject) => {     var xhr = new XMLHttpRequest()     xhr.open(opts.method || 'get', url)     for (var k in opts.headers || {}) {       xhr.setRequestHeader(k, opts.headers[k])     }     xhr.onload = e => resolve(e.target)     xhr.onerror = reject     if (xhr.upload && onProgress) {       xhr.upload.onprogress = onProgress // event.loaded / event.total * 100 ; //event.lengthComputable     }     xhr.send(opts.body)   }) } 

photo

{   fileSize: 314945,   origURL: 'assets-library://asset/asset.JPG?id=106E99A1-4F6A-45A2-B320-B0AD4A8E8473&ext=JPG',   longitude: -122.80317833333334,   fileName: 'IMG_0001.JPG',   height: 2848,   width: 4288,   latitude: 38.0374445,   timestamp: '2011-03-13T00:17:25Z',   isVertical: false,   uri: 'file:///Users/User/Library/Developer/CoreSimulator/Devices/D3FEFFA8-7446-42AB-BC7E-B6EB88DDA840/data/Containers/Data/Application/17CE8C0A-B781-4E56-9347-857E74055119/Documents/images/69C2F27F-9EEE-4611-853E-FC7FF6E5C373.jpg' } 

config

'http://localhost:3000/upload',  {    method: 'post',     body:      {        _parts:          [            [ 'picture',             { uri: 'file:///Users/User/Library/Developer/CoreSimulator/Devices/D3FEFFA8-7446-42AB-BC7E-B6EB88DDA840/data/Containers/Data/Application/17CE8C0A-B781-4E56-9347-857E74055119/Documents/images/69C2F27F-9EEE-4611-853E-FC7FF6E5C373.jpg',               name: 'selfie.jpg',               type: 'image/jpg' }            ]          ]     } } 

I think data (which should be send as body in config) has wrong format. Why is there an array in an array?

3 Answers

Answers 1

The example below uses react-native-fetch-blob at React Native part, and Nodejs with Express and Formidable to parse form at the server side.

Let's first upload the file after determining whether user uploaded a photo or video:

RNFetchBlob.fetch(   'POST',   Constants.UPLOAD_URL + '/upload',   {     'Content-Type': 'multipart/form-data'   },   [     {       name: this.state.photoURL ? 'image' : 'video',       filename: 'avatar-foo.png',       type: 'image/foo',       data: RNFetchBlob.wrap(dataPath)     },     // elements without property `filename` will be sent as plain text     { name: 'email', data: this.props.email },     { name: 'title', data: this.state.text }   ] )   // listen to upload progress event   .uploadProgress((written, total) => {     console.log('uploaded', written / total);     this.setState({ uploadProgress: written / total });   })   // listen to download progress event   .progress((received, total) => {     console.log('progress', received / total);   })   .then(res => {     console.log(res.data); // we have the response of the server     this.props.navigation.goBack();   })   .catch(err => {     console.log(err);   }); }; 

Similarly, receive file and load the data accordingly:

exports.upload = (req, res) => {   var form = new formidable.IncomingForm();   let data = {     email: '',     title: '',     photoURL: '',     videoURL: '',   };    // specify that we want to allow the user to upload multiple files in a single request   form.multiples = true;   // store all uploads in the /uploads directory   form.uploadDir = path.join(__dirname, '../../uploads');    form.on('file', (field, file) => {     let suffix = field === 'image' ? '.png' : '.mp4';     let timestamp = new Date().getTime().toString();      fs.rename(file.path, path.join(form.uploadDir, timestamp + suffix)); //save file with timestamp.      data[field === 'image' ? 'photoURL' : 'videoURL'] = timestamp + suffix;   });   form.on('field', (name, value) => {     data[name] = value;   });   form.on('error', err => {     console.log('An error has occured: \n ' + err);   });   form.on('end', () => {     // now we have a data object with fields updated.   });   form.parse(req); }; 

And use the controller function:

let route = express.Router(); // other controller functions... route.post('/upload', uploadController.upload); app.use(route); 

Make sure you read the comments included in the code. Datapath is media's path (not base64 string) created after using react-native-image-picker . You can use react-native-progress to show upload progress.

Check out multipartform-data section of react-native-fetch-blob for further reference: https://github.com/wkh237/react-native-fetch-blob#multipartform-data-example-post-form-data-with-file-and-data

Answers 2

you can use co-busboy node module to write a middleware, this is an example for koa:

Firstly, you need to install co-busboy by npm or yarn:

npm i co-busboy -S or yarn add co-busboy

// upload.js  var parse = require('co-busboy')  var fs = require('fs')  var path = require('path')  var upload = function * (next) {    var parts = parse(this, {      autoFields: true    })    while(var part = yield parts) {      part.pipe(fs.createReadStream(path.join('uploadPath', part.filename)))    }    yield next  }    module.exports = () => upload      // app.js    var upload = require('upload')  app.use(upload())

Reference:

co-busboy

Answers 3

You need to base64 encode the image and then send the body as a json (with the header Content-Type set to application/json)

If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment