Image Upload

The use case:

Allow a user to upload a profile photo and profile background image to be displayed on public profile page.

Seems simple but in a clustered environment we can not rely on retrieving an image from the server's hard drive and storing them in the database is generally not a great option. Fortunately cheap and robust services such as Amazon s3 allow storage of files in a high availability centrally accessible location.

We need to be able to:

  • upload image for user to be used a profile "photo"
  • upload "background" image for user
  • get current "photo" image for user (original or cropped)
  • get "background" image for user
  • save a "cropped" version of a photo using aviary to use in preference to the original photo.

Once a file is uploaded and saved on S3, a reference to its final location is stored in the database with some metadata about the file and associated with the user so we can later retrieve it as needed. We also save a few resized versions at the same time for various UI features. For example, for mobile devices we don't need a 1024px wide background image so we create one that is 480 wide for mobile browsers.

Install component-storage, s3-uploader and the Upload model factory

Component storage is the loopback storage container for s3. We use this to help with deleting s3 files.

npm install loopback-component-storage --save

Setup component storage in server/server.js

// setup component storage for s3
var ds = loopback.createDataSource({  
  connector: require('loopback-component-storage'),
  provider: 'amazon',
  key: process.env.AWS_S3_KEY,
  keyId: process.env.AWS_S3_KEY_ID,
});
var container = ds.createModel('container');  
app.model(container, {  
  'dataSource': null,
  'public': false
});

s3-uploader does the upload heavy lift. It does all the uploading and resizing returning an array with all the final image details that we save in the Upload instance associated with the user.

npm install s3-uploader --save

Define Amazon S3 credentials in the environment when launching the server. See "keeping secrets" for more on that.


The upload handler configuration factory

We use a function to perform this task as we probably will want to give several models in our project upload capability and the setup is tediously repetitive. This function sets up the needed relationships between the MyUser and the Upload model, sets up the api endpoints and watches some events to keep s3 in sync with changed in the database.

This all works as a black-box but peruse the source to get an understanding of how it is all put together.

Uploadable configuration Factory:
server/lib/uploadable.js

The Upload model definition:
common/models/Upload.json

The Upload model behavior:
common/models/Upload.js

Note: in the server/model-config.json configuration file the Upload model definition must be before the MyUser model in the list of models so that it is instantiated before MyUser or any other models that might use it.

Relate the MyUser model to the Upload model creating api endpoints for uploading files to MyUser

common/models/MyUser.js

// Once the MyUser model is set up, use the uploadable 
// factory to add the uploadable behavior to MyUser.

MyUser.on('attached', function () {

    // Define the variations for various UI uses that 
    // will be created by s3-uploader on upload
    var versions = [{
        suffix: 'large',
        quality: 90,
        maxHeight: 1024,
        maxWidth: 1024,
    }, {
        suffix: 'medium',
        quality: 90,
        maxHeight: 480,
        maxWidth: 480
    }, ... ];

    var uploadable = require('../../server/lib/uploadable')();
    // Pass the model class, model name and the array of variations
    uploadable(MyUser, 'MyUser', versions); 
});

This exposes endpoints for uploading files, in our case:

/api/MyUsers/me/upload/photo /api/MyUsers/me/upload/background

Add to the ACL for MyUser to allow authenticated access to the api endpoint

common/models/MyUser.json

{
  "accessType": "EXECUTE",
  "principalType": "ROLE",
  "principalId": "$owner",
  "permission": "ALLOW",
  "property": "upload"
}

Now the user can upload images to MyUser.

This would normally be invoked using Ajax but by way of example a logged in user can post directly to the endpoint:

extends ../wrapper

block seo  
    title.
        Upload

block content  
    h3.
        Upload Photo
    form(action="/api/MyUsers/me/upload/photo" method="post" enctype="multipart/form-data") 
        input(type="file" name="uploadedFile")
        input(type="submit")

For a full front end example checkout the Image Upload: Front End post.


Photo: Window Washers, Dubai (2015)
Document version 2.0