Chris Ebert Chris Ebert - 6 months ago 14
Javascript Question

Uploading photographs using MEAN.js

I’m using this tutorial, which is based on the MEAN stack.
I want to store photographs that users can upload with MongoDB on a server.

included: Angular directive to upload files

create-spot.client.view.html

<div data-ng-controller="SpotsCreateController">
<form class="form-signin" data-ng-submit="create(picFile)" novalidate>
<label>Upload an image</label>
<input type="file" id="articleimage" ng-model="picFile" ng-file-select="" ng-file-change="generateThumb(picFile[0], $files)" multiple name="file" accept="image/*">
<img ng-show="picFile[0].dataUrl != null" ng-src="{{picFile[0].dataUrl}}" class="img-thumbnail" height="50" width="100">
<span class="progress" ng-show="picFile[0].progress >= 0">
<div style="width:{{picFile[0].progress}}%" ng-bind="picFile[0].progress + '%'" class="ng-binding"></div>
</span>
<span ng-show="picFile[0].result">Upload Successful</span>

<input type="submit" class="btn btn-lg btn-primary btn-block" ng-click="uploadPic(picFile)">

<div data-ng-show="error">
<strong data-ng-bind="error"></strong>
</div>
</form>
</div>


view-spot.client.view.html

<div data-ng-controller="SpotsViewController">
<section data-ng-init="findOne()">
<img ng-src="data:image/jpeg;base64,{{spot.image}}" id="image-id" width="200" height="200"/>
</section>
</div>


application.js

var app = angular.module('newApp', ['ngAnimate', 'ngCookies', 'ngResource', 'ngRoute', 'ngSanitize', 'ngTouch', 'ui.bootstrap', 'users', 'spots']);


spots.create.client.controller.js

angular.module('spots').controller('SpotsCreateController', ['$scope', '$timeout', 'Authentication', 'Spots', '$location'
function($scope, $timeout, Authentication, Spots, $location) {
$scope.authentication = Authentication;
$scope.fileReaderSupported = window.FileReader !== null;

$scope.create = function(picFile) {
var spot = new Spots({
title: this.title,
description: this.description,
image: null
});
spot.$save(function(response) {
$location.path('spots/' + response._id);
}, function(errorResponse) {
$scope.error = errorResponse.data.message;
});
};

$scope.doTimeout = function(file) {
$timeout( function() {
var fileReader = new FileReader();
fileReader.readAsDataURL(file);
fileReader.onload = function(e) {
$timeout(function() {
file.dataUrl = e.target.result;
});
};
});
};
$scope.generateThumb = function(file) {
if (file) {
if ($scope.fileReaderSupported && file.type.indexOf('image') > -1) {
$scope.doTimeout(file);
}
}
};


spot.server.model.js

var mongoose = require('mongoose'),
Schema = mongoose.Schema;

var SpotSchema = new Schema({
...
image: {
type: String,
default: '',
required: false
}
});

mongoose.model('Spot', SpotSchema);


spots.server.routes.js

var multiparty = require('connect-multiparty'),
multipartyMiddleware = multiparty();

module.exports = function(app) {
app.route('/api/spots')
.get(spots.list)
.post(users.requiresLogin, multipartyMiddleware, spots.create);

app.route('/api/spots/:spotId')
.get(spots.read)
.put(users.requiresLogin, spots.hasAuthorization, spots.update)
.delete(users.requiresLogin, spots.hasAuthorization, spots.delete);

app.param('spotId', spots.spotByID);
};


spots.server.controller.js

var mongoose = require('mongoose'),
fs = require('fs'),
Spot = mongoose.model('Spot');

exports.create = function(req, res) {
if (req.files.file) {
var file = req.files.file;
}

var spot = new Spot(req.body);
spot.creator = req.user;

fs.readFile(file.path, function (err,original_data) {
if (err) {
return res.status(400).send({
message: getErrorMessage(err)
});
}
var base64Image = original_data.toString('base64');
fs.unlink(file.path, function (err) {
if (err) {
console.log('failed to delete ' + file.path);
} else {
console.log('successfully deleted ' + file.path);
}
});
spot.image = base64Image;
spot.save(function(err) {
if (err) {
return res.status(400).send({
message: getErrorMessage(err)
});
} else {
res.json(spot);
}
});
});
};


What did I do wrong? Please consider that I want to limit the file sizes, and I thought using
base64
is good start. The issue is that the photograph is not stored in the database because the controller doesn’t work with the rest.

Answer

What exactly is the problem you are experiencing, and at what step is it going wrong?

For Express backend apps I usually use multer middleware for handling file uploads. Also, I create separate routes/controllers for dealing with the files rather than trying to process them at the same time I'm saving the parent object. This allows me to separate the logic nicely and not worry about the parent object not being saved when the file upload fails. You could use the JS API for ng-file-upload to handle that in Angular.

Example routes in Express (we have a "club" with a "logo" image here):

router.post(
  '/logo',
  ensureAuthenticated, ensureAdmin,
  logoCtrl.upload,
  logoCtrl.save
);
router.get(
  '/:clubId/logo.*',
  logoCtrl.stream
);

Example controller methods:

let multer = require('multer');
module.exports = {

  /**
   * Upload logo
   */
  save(req, res, next) {

    //Get club and file
    let club = req.user.club;
    let file = req.file;

    //Update
    club.logo = {
      data: file.buffer,
      mimeType: file.mimetype
    };

    //Save
    club.save()
      .then(() => {
        res.end();
      })
      .catch(next);
  },

  /**
   * Stream logo
   */
  stream(req, res, next) {
    let club = req.club;
    res.contentType(club.logo.mimeType);
    res.send(club.logo.data);
  },

  /**
   * Upload middleware
   */
  upload(req, res, next) {

    //Create upload middleware
    let upload = multer({
      storage: multer.memoryStorage(),
      limits: {
        fileSize: 50000000
      }
    }).single('logo');

    //Use middleware
    upload(req, res, next);
  }
};

So as you can see, it's quite simple with multer and all you really need is one route for uploading the logo, with two controller methods, one to register the multer middleware and process the file, and the other to save it to the MongoDB (in this case attached to the club in the request).

Just make sure that ng-file-upload uses the same field name for uploading the file as multer is expecting. In the above example that's "logo". If you're unsure, check in the request what your client app is sending to the server and make sure the server app is expecting the same field name.

Let me know if you have further trouble.