Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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?

like image 308
user3142695 Avatar asked Aug 29 '17 21:08

user3142695


1 Answers

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

like image 196
eden Avatar answered Nov 15 '22 06:11

eden