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.
The next step for you is to create a bucket
and select a region that's nearest to most of your target users.
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="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 asserver
, 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 theaccounts-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:
Creation of field to browse and select file
<input type="file" class="uploadFile" id="uploadFile">
Basic Bootstrap progress bar
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.