Upload Files to Amazon S3 with Meteor Slingshot

Recently I started working on an app based on Meteor platform. There is one particular feature in the app which lets users upload images and my search for a viable solution started with this. There are many Meteor packages like slingshot, Collection FS, S3 uploader to upload files to Amazon S3. I decided to go with slingshot, as this package uploads files directly from the browser without using the Meteor app's server for resource consuming file transfers. Plus it has got support for other cloud services like Google Cloud, Rackspace, etc. In this tutorial I'd be happy to share whatever I learnt along the way.

What are we learning?

This stand alone app will let users sign in to the app, select, upload and display the latest image uploaded by the user.

Here is what we're going to learn:

  • creating and setting up Amazon S3 account
  • creating Meteor projects
  • templates
  • upload and progress
  • events
  • helpers
  • displaying the newest image
  • server configuration for S3

Amazon S3 Setup

Before touching any code, let's get done with the S3 account configuration. Head over to aws.amazon.com/s3 and click on "Try Amazon S3 for Free". Just follow the instructions given on the screen to complete the verification and registration process.

S3 Bucket

Once you're inside AWS Management Console, click on S3.

Amazon S3

The next step for you is to create a bucket and select a region that's nearest to most of your target users. Amazon S3 Bucket

CORS Policy

S3 uses CORS policy to primarily control different types of access to the bucket made from other domains.

Now click on the bucket to open the bucket specific page. Click on the Properties tab present at the top-right. Finally click on the Edit CORS Configuration and paste the following (as per slingshot's official documentaion):

    <?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
    <CORSRule>
        <AllowedOrigin>*</AllowedOrigin>
        <AllowedMethod>PUT</AllowedMethod>
        <AllowedMethod>POST</AllowedMethod>
        <AllowedMethod>GET</AllowedMethod>
        <AllowedMethod>HEAD</AllowedMethod>
        <MaxAgeSeconds>3000</MaxAgeSeconds>
        <AllowedHeader>*</AllowedHeader>
    </CORSRule>
</CORSConfiguration>

Basically we'saying that PUT, POST, GET, and HEAD requests are allowed on this bucket from any domain and any HTTP header can be included.

Access Keys

We will be using access keys to authenticate the requests sent to the bucket. Now, click on your name present on the top navigation bar and click on the Security Credentials link present in the resulting drop down. Toggle the Access Keys (Access Key ID and Secret Access Key) and click on Create New Access Key. Download the key file and we're done with all the S3 configuration.

Note This key file has highly sensitive information. So, don't disclose the content to anyone and keep in mind that it can be downloaded only once.

Meteor project

Exciting part has arrived! Now let's get down to code our way to create the image uploader. Fire up your shell and assuming you have already installed Meteor, write the following and hit enter:

meteor create photoshot

Note that photoshot is the name of the sample app. It will create a folder named photoshot and three files -- photoshot.html, photoshot.css and photoshot.js.

Delete all of them and wipe the slate clean. For the sake of simplicity, we'll have the following folder structure:

+-- client

|---- photoshot.html

|---- photoshot.js

+-- server

|---- photoshot.js

Note As per Meteor convention, any code present inside a folder named client, will only run on the client. Similarly any code present inside a folder named as server, will only run on the server.

Templates

Templates are used link application interface and the JavaScript code. The application logic can be used to reference and manipulate interface elements placed inside a template. They are placed inside body tag as per Spacebar syntax.

To start off, open photoshot.html and place the following code:

<head>
  <title>PhotoShot</title>
</head>

<body>
   {{> loginButtons}}

   {{> imageUploader}}
</body>

In this app we'll be using two templates: loginButtons and imageUploader.

Now create the template by placing the following code below the closing body tag:

<template name="imageUploader">
</template>

Note The loginButtons template is already included in the accounts-ui meteor package.

Image upload and progress

Inside the newly created imageUploader template place the following code:

<div class="container">
  <div class="col-md-6 col-md-offset-3">
   <h1>PhotoShot</h1>
   <span style="font-size:15px; margin-left:10px;">
    <div class="form-group">
      <label for="selectFile">Select</label>
      <input type="file" class="uploadFile" id="uploadFile">
      <p class="help-block">Upload image here.</p>
    </div>
    <div class="row">
      <div class="col-md-6">
        <div class="progress">
          <div class="progress-bar" role="progressbar" aria-valuenow="{{progress}}" aria-valuemin="0" aria-valuemax="100" style="width: {{progress}}%;">
            <span class="sr-only">{{progress}}% Complete</span>
          </div>
        </div>
      </div>
    </div>
    </span>
  </div>

{{isUploading}}

There are three important things to notice here:

  1. Creation of field to browse and select file <input type="file" class="uploadFile" id="uploadFile">

  2. Basic Bootstrap progress bar

  3. Double braced isUploading tag for string replacement. We'll learn how the status of upload will be shown here in subsequent section.

Apart from this, as you can see we've also added some styling and positioning elements.

Events

Now is the time to write some client side code. We’ll start with the code that gets triggered depending on various action taken by the user (example: click, mouseover, double-click, etc.). Open the photoshot.js file present inside the client folder and paste the code given below:

var uploader = new ReactiveVar();
var imageDetails = new Mongo.Collection('images'); 
var currentUserId = Meteor.userId();

       Template.imageUploader.events({'change .uploadFile': function(event, template) {

             event.preventDefault();

             var upload = new Slingshot.Upload("myImageUploads");
             var timeStamp = Math.floor(Date.now());                 
         upload.send(document.getElementById('uploadFile').files[0], function (error, downloadUrl) {
             uploader.set();
             if (error) {
               console.error('Error uploading');
               alert (error);
             }
             else{
               console.log("Success!");
               console.log('uploaded file available here: '+downloadUrl);
               imageDetails.insert({
                   imageurl: downloadUrl,
                   time: timeStamp,
                   uploadedBy: currentUserId
               });
             }
             });
             uploader.set(upload);
           }
       });

Give a close look to the first line var uploader = new ReactiveVar();. Here, we’re declaring a custom reactive object by making use of reactivevar meteor package. This is one of most powerful features of Meteor. Reactive variables are used to keep tab on the value of the variable that changes over time and perform various actions depending on the change. In our case we'll use to track upload progress.

The next line is for referencing the images collection in the database and currentUserId stores the unique user id created by accounts-password Meteor package.

Next thing that we’re doing is triggering an event when .uploadFile attached with the following element changes: <input type="file" class="uploadFile" id="uploadFile">. Then we’re defining a new object upload and assigning it to an instance of Slingshot.Upload() and passing the name of a “directive"—myImageUploads which will be covered in server later on.

Then, we'll call our upload instance’s send method and pass the file retrieved from the file input element. At the same time a call is made to S3 in the background to upload the image. We'll cover the required configuration in server section. In the end our Reactivevar uploader is set by passing the upload instance.

Note that in case of successful upload we're inserting the uploaded image URL, timestamp and user id into the images collection.

Helpers

Helper functions are attached to templates and we can use them to execute code inside the templates.

Template.imageUploader.helpers({

    isUploading: function () {
        return Boolean(uploader.get());
    },

    progress: function () {
    var upload = uploader.get();
    if (upload)
    return Math.round(upload.progress() * 100);
    },

    url: function () {

    return imageDetails.findOne({uploadedBy: currentUserId},{sort:{ time : -1 } });

    },

});

As you can see, here the helper functions are attached to imageUploader template.

The first function returns true/false depending on whether file is getting uploaded or not. If you open the photoshot.html file, you'll see that inside imageUploader template there is {{isUploading}}, which gets replaced with the value returned from isUploading helper function. Next function returns the upload progress percentage.

Last function is used to return the url of the last upladed image, by retrieving from the images collection. Here the time stamp and unique user id of the user stored earlier via events are coming into play.

Display the image

Go back to photoshot.html and add the following just before the closing template tag:

<div class="container">
  <div class="col-md-6 col-md-offset-3">

    <strong>Uploaded image:</strong>

    <img src="{{url.imageurl}}"/> 

  </div>    
</div>

The url function needs to be referenced in the template here via {{url}}. But, we are only concerned with the URL of the image and that value can be retrieved via dot notation. So it becomes {{url.imageurl}}.

Server configuration for slingshot

Open the photoshot.js file present inside the server folder and add the following code:

var imageDetails = new Mongo.Collection('images');

Slingshot.fileRestrictions("myImageUploads", {
  allowedFileTypes: ["image/png", "image/jpeg", "image/gif"],
  maxSize: 2 * 1024 * 1024,
});

Slingshot.createDirective("myImageUploads", Slingshot.S3Storage, {
  AWSAccessKeyId: "AWS_ACCESS_KEY_ID",
  AWSSecretAccessKey: "AWS_SECRET_ACCESS_KEY",
  bucket: "BUCKET_NAME",
  acl: "public-read",
  region: "S3_REGION",

  authorize: function () {
    if (!this.userId) {
      var message = "Please login before posting images";
      throw new Meteor.Error("Login Required", message);
    }

    return true;
  },

  key: function (file) {
    var currentUserId = Meteor.user().emails[0].address;
    return currentUserId + "/" + file.name;
  }

});

Have a look at following methods: Slingshot.fileRestrictions() and Slingshot.createDirective().The first method is quite straight forward. Here, we're configuring two things: an array of allowed file types and a maximum 2 MB file size restriction.

Next thing that we need to do is use the upload directive myImageUploads mentioned in photoshot.js in client side. Now we inform Slongshot that we'd like to upload files to S3 by passing Slingshot.S3Storage. Here we're passing the keyid and access key generated earlier while setting up S3 configuration in AWS console. Apart from that, the bucket name, ACL (access control list) and region are also passed.

The next method is used to check whether the user is logged in or not. If not, a message is shown that asks the user to log in before uploading image.

In the end we're calling a method key that takes the file argument passed from the client. This method is used to return a valid file storage structure to exactly define where the image will reside in the bucket. In our case a folder named as the email address of the uploader will be created and the file will be uploaded to that directory.

Conclusion

In this tutorial we learnt configuration and CORS policy required for Amazon S3. Used Meteor package slingshot to upload image to S3 bucket, stored the image url in MongoDB database and in the meanwhile also got an idea of how Meteor templates, helpers and events work. This example is a vanilla implementation for uploading images, so feel free modify and improve it by adding new functionalities.

You can download the working sample from GitHub.

Write your comment…

10 comments

Really good article! Simple and clear. Thank you !

Hashnode is building a friendly and inclusive dev community. Come jump on the bandwagon!

  • 💬 A beginner friendly place

  • 🧠 Stay in the loop and grow your knowledge

  • 🍕 >500K developers share programming wisdom here

  • ❤️ Support the growing dev community!

Register ( 500k+ developers strong 👊)

Very nice article, and I agree the progress bars adds a nice touch. Could I use the same approach for uploading other files? For example my use case would be to let users upload PDFs and then send these via email through my application? Do you know if this would be possible with this set up or is there an easier way?

Also, as I am investigating a service like cloudinary.com, one of their features is automatic transform of images to other formats, such as thumbnail, mobile size, low resolution, etc.

Can we expect that AWS servers could do that? (By them or by our own implementation)?

Another concern is how private the access is. Does this Slingshot+AWS pattern allow anybody to access it one on the server?

I am not an expert (far from that), but a nebulously documented element in all ressources is: Do we need to do something with our server so our secure key is sent securly to the 3rd party API?

Or there is already a protocol in the API that encrypt sending these request?

In other words: Do we need to implement SSL on our server so calling secure APIs is secure?

Thanks!

Load more responses