Author Note 10/9/2018: This article references Angular 1 and should only be used for Angular 1


Allow users to upload images or other files to store them in a CDN

Many times you may want to allow users to upload a file, say a user image for a profile page. This post will show you how to do it using the following stack:

  • Angular on the front end
  • Node/Express on the server
  • Azure Blob for Content Delivery Network (CDN)... but it could also be done with Amazon S3 or another CDN like Max CDN

I will walk you through using the following front-end and back-end modules that make the upload process easy.

  • ng-file-upload (angular module)
  • multer (node.js module)
  • azure storage (node.js module)

Okay, let's start on the front end.
From your project repo, install ng-file-upload.

bower install ng-file-upload --save
bower install ng-file-upload-shim --save 

The second one is a required shim. --save installs and adds to the bower.json file.

Then, you need to inject $upload into your Angular Controller

angular.module('myApp')
  .controller('UserController', ['$upload', ....

Then we watch the scope for file uploads

//watch for image file upload
  $scope.$watch('files', function() {  
    $scope.upload($scope.files);
  });

This would be a good time to note what should happen in the html form for file uploads.

<section data-ng-controller="UserController">
	<div ng-if="isProfileOwner" style="text-align: center;">
    	<button class="btn btn-default" ng-file-select ng-file-change="upload($files)" ng-multiple="multiple">
      Upload Image
    	</button>
    	<div style="color:red;" ng-if="user.uploadError">{{user.uploadError}}</div>
  	</div>
  </section>

I've got my controller called in the section element and a button with ng-file-select which tells the browser to open my file dialog box when the button is clicked. Then we have ng-file-change="upload($files)" which interacts with the watcher with what we saw in our Controller file when we have $scope.$watch. We also have ng-multiple="multiple" which wuld allow me to upload multiple files.
Back in the Controller file now, we have

  $scope.upload = function(files) { 
      var thisUser = contextUsername;
      if (files && files.length) {
        var file = files[0];
        $upload.upload({
          url: 'user/image',
          fields: {
            'username': thisUser
          },
          file: file
        }).progress(function(evt) {
          var progressPercentage = parseInt(100.0 * evt.loaded / evt.total);
          console.log('progress: ' + progressPercentage + '% ' +
          evt.config.file.name);
        }).success(function(data, status, headers, config) {
          $scope.image = data;
          if ($scope.image.uploadError) {
            $scope.user.uploadError = $scope.image.uploadError;
            console.log('error on hand');
          } else {
            $scope.user.uploadError = '';
            UserImage.saveUserImage(thisUser, $scope.image.path, function(data) {
              $scope.loadUserImage(data.username);
            });
          }
        });
      }
    };

I'm using this for uploading a user profile image so the variables are specific for that. You can also take a look at the instructions for using the module on Github here. So, all this code will handle the upload on the client side.

Now let's turn our attention to the server side. I'm running Node.js with Express 4.0 for my server. I first install Multer, a middleware module that works with Express to http POSTS that include a file. So, to install,

  npm install multer --save

Then, I set up my middleware. In this case, the middleware will work for any incoming request that includes a file. In my routes file on the server side, I have

  var multer = require('multer');
  var fs = require('fs');
  app.use(function(req, res, next) {
  var fileTooLarge = false;
  var handler = multer({
    dest: 'packages/theme/public/assets/img/uploads/',
    limits: {
      fileSize: 500000
    },
    rename: function (fieldname, filename, req, res) {
      var username = req.user.username;
      return username + '001';
    },
    onFileSizeLimit: function (file) {
      fileTooLarge = true;
      res.json({
        uploadError: 'Upload failed. File must be less than 500 KB'
      });
    },
    onFileUploadStart: function (file) {
      console.log(file.originalname + ' is starting ...');
    },
    onFileUploadComplete: function (file, req, res) {
      console.log(file.fieldname + ' uploaded to  ' + file.path);
      var newFileName = req.files.file[0].name;
      if(!fileTooLarge) {
        articles.uploadUserImage(req, res, newFileName, function() {
          file.path = 'http://<myblobstorage>.blob.core.windows.net/userpictures/' + newFileName;
          //file param is actually an object with the path as a property
          res.send(file);
          //delete file from local uploads folder
          fs.unlink('packages/theme/public/assets/img/uploads/' + newFileName);
        });
      } else {
        fs.unlink('packages/theme/public/assets/img/uploads/' + newFileName);
      }
    }
  });
  handler(req, res, next);
});

I set up a handler function and then call it after so that I could still go to the route below after the middleware in case I wanted to do something else. In my case, I am checking if the file is larger than 500 kb and responding with an Error if the file is too large. If the file is under 500 kb, then I call articles.upLoadUserImage which is a method that uploads the image to my CDN, in this case an Azure blob.

You'll also note that fs.unlink is being used to delete the file from local storage after it has been successfully uploaded to the azure blob. So, to recap, Angular is uploading the file to Node.js/Express which stores the file in a temporary folder (called uploads here) and then the file is uploaded to the Azure blob. Then the temp file is deleted from the server storage and the URL of the newly created image on the CDN can be returned to the client by bode.

The following code will be very specific to AZURE and storing images on Azure Blob. If you are using another CDN, disregard. If using Azure blob, you can also find more information here.

In my server side controller file, I have

var azure = require('azure-storage');
var retryOperations = new azure.ExponentialRetryPolicyFilter();
var blobSvc = azure.createBlobService().withFilter(retryOperations);
blobSvc.createContainerIfNotExists('userpictures', {publicAccessLevel: 'blob'}, function(error, result, response) {
 if (!error) {
	console.log(result);
	console.log(response);
  } else {
	console.log('error creating azure blob container ', error);
}
});

And to make the a method available to my routes file

exports.uploadUserImage = function(req, res, imageName, cb) {
var localPath = 'packages/theme/public/assets/img/uploads/' + imageName;
blobSvc.createBlockBlobFromLocalFile('userpictures', imageName, localPath, function(error, result, response) {
 if (!error) {
   console.log('file uploaded');
	  cb();
 } else {
   console.log('error on image upload is ', error);
   return error;
 }
});
};

We use a callback (cb) here to asynchronously send a response to the client browser with the new image URL. This is a common pattern in asynchronous JavaScript and you will see this pattern all over Node.js and Express applications.

I won't show the code here but back on the client side, we receive the response and can load the new user image. This will also be asynchrounous code.

I hope this helps you make uploading files a snap from your Angular & Node.js/Express App. I didn't go into it here but you can also have file type verification and I imagine some compression of uploaded images happening on the server.