Amazon S3 is a great way to store files, but learning how to integrate it into your website can be a challenge. In this article, you will learn how to integrate Amazon S3 and Ruby on Rails through the construction of a simple music streaming application.
What is it and How does it Work?
Amazon S3 is "storage for the Internet"
As Amazon's website puts it, Amazon S3 is "storage for the Internet". Not only is it inexpensive, it is also a fast and reliable. It's a great way to serve content on your website, including images, videos, or pretty much anything that you want. Amazon S3 doesn't work exactly like the storage on your computer, so here are a few things that you should know:
- With Amazon S3, folders are called "buckets" and files are called "objects".
- Buckets on the top-level are used to determine the URL to access your data, so you can only use names that haven't already been taken. For example, if someone already created a top-level bucket with the name "videos", you can't use that name.
- It is a good idea to have one top-level bucket with the name of your website, and use sub-levels of buckets to separate files into different sections, such as images, videos, or music.
Before We Begin...
Before you continue with this tutorial, there are a few key things that should be in place:
- The Ruby interpreter and RubyGems installed on your computer, along with the Rails gem
- Knowledge of (or at least have access to) your Access Key ID and Secret Access Key for Amazon S3
- A basic understanding of Ruby on Rails
What We'll be Building
The final product of this tutorial is a simple music streaming and downloading application. In this application, the user will be able to do the following:
- View a list of all of the music currently uploaded and upload music themselves
- Download the music in a variety of ways, including streaming it with HTML5 audio, downloading it via HTTP, or downloading it via a .torrent file
By the time that this application is completed, you will have learned all of the major topics that you need to know about using Amazon S3 in your Ruby on Rails application.
Let's Get Started!
The aws-s3 gem allows you to interact with the Amazon S3 service in your application.
The first thing to do is to install the aws-s3
ruby gem. The gem allows you to interact with the Amazon S3 service in your application. To do this, if you are on Windows, simply run the command:
gem install aws-s3
After that is installed, generate our Rails application with the following command:
rails new mp3app
The final step to get your application started is to go into the mp3app/public directory and delete the file called "index.html". Once that is completed, your application is ready to start interacting with Amazon S3!
Logging into Amazon S3
Your Access Key ID and Secret Access Key allow you to connect to Amazon S3.
In order for us to interact with Amazon S3, we need to tell our application how to log into Amazon S3. This is where your Access Key ID and Secret Access Key come in handy. Your Access Key ID and Secret Access Key allow you to connect to Amazon S3. But first, we need to tell our application that we are using the aws-s3
gem. We do this in the Gemfile
:
gem 'aws-s3', :require => 'aws/s3'
In order to actually let our application use that gem, you'll need to type in command bundle install
. Now that it is working, we need to tell our application how to log into Amazon S3. We also do that in the config/application.rb
file, on a new line inside the Application class:
AWS::S3::Base.establish_connection!( :access_key_id => 'Put your Access Key ID here', :secret_access_key => 'Put your Secret Access Key here' )
This code tells our application to create a connection to Amazon S3 as soon as the application is started (the application.rb
file loads when your application starts up). One final thing that needs to be added to the application file is a constant with the value of the bucket that we will be using. The reason for doing this is that if we ever need to change which bucket we are using, it only needs to be updated in this one location. It should look something like this:
BUCKET='s3tutorialmusic'
For this tutorial, I decided to name the bucket s3tutorialmusic
, but you should replace that with whatever bucket you have on your account. In the end, your file should look something like this (but with your own login information):
require File.expand_path('../boot', __FILE__) require 'rails/all' Bundler.require(:default, Rails.env) if defined?(Bundler) module Mp3app class Application < Rails::Application config.encoding = "utf-8" config.filter_parameters += [:password] AWS::S3::Base.establish_connection!( :access_key_id => 'Put your Access Key ID Here', :secret_access_key => 'Put your Secred Access Key here' ) BUCKET = 's3tutorialmusic' end end
Generating the Controller
Now we can finally begin working on making our application actually display something in the browser. To start, let's generate the controller and views that we will need. In total, we will generate three actions for our controller (which we will call songs): index, upload
and delete
.
- The index action is going to be our main page.
- The upload action is for uploading new music to Amazon S3, so it doesn't need a view.
- Finally, the delete action won't have a view, and will be responsible for deleting music.
In the end, the only view that we will need for this application is the index view, because it will act as a central control panel for every action that you can do. Now, we'll combine all that into one nice command line statement:
rails g controller Songs index upload delete
Once that has finished running, go ahead and delete the views generated for upload
and delete
, because they won't be used. Let's move on to writing the code for the index action!
Working on the Index Action
In the index action, the finished product will allow users to upload new music and delete existing music. There isn't anything that needs to be done in this action's controller file for uploading new music, but we do need a list of current songs in order to let users delete them.
First, we need to get an object that refers to our music bucket (remember that the name of that bucket is stored in the constant BUCKET). Here is how we do that:
AWS::S3::Bucket.find(BUCKET)
In order for us to use the methods available in the aws-s3
gem, we need to tell the Ruby interpreter that we want to look for the functions in the AWS::S3
namespace, which is why that is part of the method call. The Bucket
class contains all the methods related to manipulating buckets. Finally, the find
method accepts one parameter, the name of the bucket, and returns an object that refers to that bucket. Now that we have the bucket, let's get all of its objects by doing this:
AWS::S3::Bucket.find(BUCKET).objects
The objects
method returns a hash with the names of all of the objects in that bucket. Finally, we need to store the result of that method call into an instance variable so that we can use it in our view. In the end, this is what the index action will look like:
def index @songs = AWS::S3::Bucket.find(BUCKET).objects end
Continuing with the Index View
Now we need to make the view for the user to upload and delete music. Let's start with the later and create an unordered list of all of the objects currently uploaded, with a link to delete that object. We can do that like so:
<ul> <% @songs.each do |song| %> <li><%= song.key %> - <%= link_to "Delete", "songs/delete/?song=" + song.key, :confirm => 'Are you sure you want to delete ' + song.key + '?' %></li> <% end %> </ul>
- First, we create an unordered list.
- Then, we loop through all of the songs in the
@songs
variable by using the each method. - For each song, we create a list item, and construct the text that will appear for each item. The first part is the songs key, because each song is a hash, and the key for that hash is the name of the song.
- Then, we put a link to the delete action, where the song can be deleted. For the url, we use a query string at the end to tell the delete action which song needs to be deleted.
- Finally, we have a confirmation message to warn the user before they actually delete the song.
if (params[:song]) AWS::S3::S3Object.find(params[:song], BUCKET).delete redirect_to root_path else render :text => "No song was found to delete!" end
- First, we check to make sure that the song paramter was specified.
- If it was, then we use the find method to get the object representing that song.
- Finally, we use the delete method to delete it from Amazon S3.
- Afterwards, we need to redirect the user to a new page because the delete action has no view. However, if the song parameter was never specified, we just render the text "No song was found to delete!".
Letting the User Upload Music
Now, we need to actually let the user upload music, because that was one of the core pieces of functionality for this application. First, we create a simple form that lets the user choose a file to upload. We can do that like so:
<h2>Upload a new MP3:</h2> <%= form_tag upload_path, :method => "post", :multipart => true do %> <%= file_field_tag "mp3file" %> <%= submit_tag "Upload" %> <% end %>
We create a form that submits to the upload action, which is the action that actually performs the upload to Amazon S3. We use post and multipart because we are submitting files. Other than that, this form is very simple and easy to understand, so now we can move onto implementing the controller portion of this action.
Submitting the File to Amazon S3
We need to take the file that was submitted and create a new S3 object for it, which will be performed in the upload action. We can do that with this line of code:
AWS::S3::S3Object.store(sanitize_filename(params[:mp3file].original_filename), params[:mp3file].read, BUCKET, :access => :public_read)
There is a lot going on in this one line of code, so I will explain each part individually.
As usual, we access the AWS::S3::S3Object
in order to interact with objects on Amazon S3.
We use the store
command to actually upload files to S3. The first parameter specifies what to call the file. We use the original_filename
parameter of the uploaded file for this so that the name stays the same. As for the sanitize_filename
method, that will be explained in the next paragraph. The second parameter is the actual file data, which is obtained from calling the read method on the uploaded file. The third parameter specifies the bucket to use, and the fourth determines who can access the file. Because we want everyone to be able to read the file (which includes downloading), we specify the access as :public_read.
The sanitize_filename
method is a method that has been used by many people and plugins, such as attachment_fu
, and it is used to solve a problem with Internet Explorer (shocking, isn't it?). Instead of just giving us the name of the file when we call the original_filename method, IE returns the whole path to the file; for example, if the file we wanted to upload was called mysong.mp3
, it would instead give us C:\rails\mp3app\mysong.mp3
when we call original_filename
. We can fix this by adding the following code to the end of the controller:
private def sanitize_filename(file_name) just_filename = File.basename(file_name) just_filename.sub(/[^\w\.\-]/,'_') end
Our final step in completing the upload action is to add some error checking and routes. The way that you do error checking in ruby is with a begin...rescue...end
statement. Many things could go wrong when uploading a file, so having error checking will prevent the user from seeing an error message that Rails would automatically generate. Here is the modified version of the upload action:
def upload begin AWS::S3::S3Object.store(sanitize_filename(params[:mp3file].original_filename), params[:mp3file].read, BUCKET, :access => :public_read) redirect_to root_path rescue render :text => "Couldn't complete the upload" end end
If an error occurs, we just render some text telling that to the user. Even though the user still sees an error message, it is better than a huge list of code that would appear in an error message generated by Rails.
Routing our Application
You may have noticed that throughout the code that we have written so far, there have been many times where something like upload_path
has been used in place of specifying a controller and action. We can do this because of a file called routes.rb. This tells our application what URL's can be accessed in our application. We also give names to certain paths to make it easier to update our code. Here is how you can name the paths that our Mp3app will use:
match "songs/upload", :as => "upload" match "songs/delete", :as => "delete" root :to => "songs#index"
The match method specifies a path, like songs/upload
, and give it a name, upload_path
. That name is specified using :as => "name"
as the second parameter to the match method. Finally, the root method specifies what action will be the root action, which acts similar to index.html in a static HTML based website.
The Completed Upload Action
Now, we are done implementing the functionality of the upload action. Here is the final code for the songs_controller.rb
file so far:
class SongsController < ApplicationController def index @songs = AWS::S3::Bucket.find(BUCKET).objects end def upload begin AWS::S3::S3Object.store(sanitize_filename(params[:mp3file].original_filename), params[:mp3file].read, BUCKET, :access => :public_read) redirect_to root_path rescue render :text => "Couldn't complete the upload" end end def delete if (params[:song]) AWS::S3::S3Object.find(params[:song], BUCKET).delete redirect_to root_path else render :text => "No song was found to delete!" end end private def sanitize_filename(file_name) just_filename = File.basename(file_name) just_filename.sub(/[^\w\.\-]/,'_') end end
And here is what the application looks like so far when viewed in the browser.
Downloading the Music
So far, our application has come a long way. The user can now upload music, view a list of currently uploaded music, and delete any existing music. Now, we have one last piece of core functionality to implement. That is letting the user actually download this music. As specified in the start of this tutorial, the user can do that in three ways:
- stream it with HTML5 Audio,
- download it via HTTP, and
- download it using a torrent file.
Right now, the list of music is just displayed using an unordered list. However, because we are going to end up adding an additional three links to the end of each line (one for each download method), it is more feasible to use a table to organize the list. Let's modify the index view to reflect this change:
<h2>Download and Delete Existing MP3's</h2> <table> <% @songs.each do |song| %> <tr> <td><%= song.key %></td> <td><%= link_to "Delete", "songs/delete/?song=" + song.key, :confirm => 'Are you sure you want to delete ' + song.key + '?' %></td> </tr> <% end %> </table>
- First, we update the header to reflect that we can also download the music.
- Second, we change the unordered list to a table, and put the name of the song and the download link on their own <td>.
Now we are ready to add the code to let the user download music. Let's start with downloading via HTTP, because it is the easiest one to implement.
Downloading via HTTP
To download via HTTP, we just need to add a new <td> to our table with a link to the .mp3 file. The aws-s3 gem has built in methods that let us generate the url for a file. However, the best practice is to put any "helping methods" like these in the helper file for that controller. Because we are using these methods throughout the whole application (especially if you decide to extend this application on your own), the helper methods will be placed in the application_helper.rb file
. Here is how you get the URL:
def download_url_for(song_key) AWS::S3::S3Object.url_for(song_key, BUCKET, :authenticated => false) end
This method just accepts one parameter, which is the name of the song. To help us remember that the name of the song is accessed by song.key, we call the parameter song_key
. As usual, we access the AWS::S3::S3Object
class to interact with Amazon S3 objects. The url_for
method takes two parameters, with the third being optional.
- The first is the name of the file you are looking for.
- The second is the name of the bucket where the file is located.
- Finally, the third parameter is used to give us a URL that won't expire. If we didn't specify
:authenticated => false
, the URL's would all expire in 5 minutes (by default).
<td><%= link_to "Download", download_url_for(song.key) %></td>
This <td>
goes in between the name of the song and the delete link (but that is personal preference, so you can have the links in any order you choose).
Downloading via Bit Torrent
Downloading files from Amazon S3 via Bit Torrent is very similar to downloading via HTTP. In fact, the only difference between the two download URL's is that the torrent one has ?torrent at the end of it. Therefore, our helper method to generate the torrent url will just add ?torrent to the end of the HTTP url. Here is how you would do that:
def torrent_url_for(song_key) download_url_for(song_key) + "?torrent" end
Now, we just need to add another <td> to our table:
<td><%= link_to "Torrent", torrent_url_for(song.key) %></td>
Streaming with HTML5 Audio
Streaming the songs through HTML5 audio is a little more difficult than just downloading the song, so let's start with the easy part: the <td>
for it. However, there are going to be some differences from the links we added for HTTP and Bit Torrent.
- First, we need to have a way to identify this link in order to add the <audio> tag to the page, so we will give it a class of html5.
- Second, we need a way to know the source of the mp3 to use for the
<source>
tag, so we will just give it the same url as the HTTP download. This will also serve as a fallback for browsers with javascript disabled, because we will use javascript to add the<audio>
tag to the page.
Here is the code for generating the link:
<td><%= link_to "HTML5 Audio", download_url_for(song.key), :class => "html5" %></td>
Now we need to work on the javascript to add the audio tag to the page when this link is clicked. To do this, we will use a technique similar to the technique that Jeffrey Way uses in his tutorial, The HTML 5 Audio Element. The first step is to add a few things to our view files. In our layout/application.html.erb
file, we need to include the latest version of jQuery, because that is the javascript library we will be using. Here is the code to add right before the first javascript include line:
<%= javascript_include_tag "https://ajax.googleapis.com/ajax/libs/jquery/1.4.3/jquery.min.js" %>
Then, change the first parameter for the original include tag from :defaults to application.js
, because that is where we will be storing our javascript code, and the other default JavaScript files aren't necessary. Next, we need to add a section to our index.html.erb view to put the audio tag in. At the top of that view, we need to put the following code:
<h2>Listen to a MP3 with HTML5 Audio</h2> <section id="audio"> </section>
Following with the HTML5 theme, we use a section tag instead of a div to create our audio section.
We get a reference to the audio section and cache it in a variable, which is considered a best practice. Next, we need to add a click event handler to our links with the html 5 class. When that event handler goes off, we need to do a few things:
- First, we need to make a new audio tag and give it some attributes like controls.
- Then, we need to add the source tag to it so it actually knows what to play.
- Finally, we need to replace the HTML in the audio section with the new audio tag, and return false so that the normal action of the link doesn't go through, which would be downloading the song. Here is how you can put that all together:
$(document).ready(function() { var audioSection = $('section#audio'); $('a.html5').click(function() { var audio = $('<audio>', { controls : 'controls' }); var url = $(this).attr('href'); $('<source>').attr('src', url).appendTo(audio); audioSection.html(audio); return false; }); });
Since this tutorial is about Ruby on Rails, and not JavaScript, I won't go into detail explaining how this code works. However, the code is fairly simple, so it should be easy for you to figure out. One thing that you should note is that this will only work in browsers that support HTML5 and support mp3's as valid sources for audio tags. For most browsers, the latest version will support this HTML 5 code, but older browsers don't support this.
Completed Index View
We have finally completed all of the core functionality for this application. The user can upload, download, and delete mp3's in a variety of ways, including HTML5 Audio, HTTP Downloads, and Bit Torrent. Here is what the index view should look like at this point:
<h2>Listen to a MP3 with HTML5 Audio</h2> <section id="audio"> </section> <h2>Upload a new MP3</h2> <%= form_tag upload_path, :method => "post", :multipart => true do %> <%= file_field_tag "mp3file" %> <%= submit_tag "Upload" %> <% end %> <h2>Download and Delete Existing MP3's</h2> <table> <% @songs.each do |song| %> <tr> <td><%= song.key %></td> <td><%= link_to "HTML5 Audio", download_url_for(song.key), :class => "html5" %></td> <td><%= link_to "Download", download_url_for(song.key) %></td> <td><%= link_to "Torrent", torrent_url_for(song.key) %></td> <td><%= link_to "Delete", "songs/delete/?song=" + song.key, :confirm => 'Are you sure you want to delete ' + song.key + '?' %></td> </tr> <% end %> </table>
If you haven't already, you should try running this code and trying it out for yourself. You can do this by running the command: rails s
. Even though we have completed the core functionality for this application, there are still things that need to be done, such as styling the page. Let's do that now.
Styling the Application
The first thing to do is to wrap the page in a container so that we can center it. All we need to do is put a div with an id of container around the yield statement in the layout file so that it looks something like this:
<div id="container"> <%= yield %> </div>
Next, we will use the styling from Ryan Bates gem, nifty_generators
, to give our application some basic styling. Here is the CSS that we will use from that gem:
#container { width: 75%; margin: 0 auto; background-color: #FFF; padding: 20px 40px; border: solid 1px black; margin-top: 20px; } body { background-color: #4B7399; font-family: Verdana, Helvetica, Arial; font-size: 14px; } .clear { clear: both; height: 0; overflow: hidden; }
Now we will work the index view. The first thing that we need to do is divide the page into three sections. Those sections will be a header, a main section, and a sidebar. At the top of the page, let's add a simple header:
<header> <h1>My First Music Streaming Application</h1> </header>
Next, let's divide the page into a main region and a sidebar region. Our main region will consist of the list of songs, while the sidebar will contain the HTML5 audio and the upload form. Here is how we will modify the code:
<div class="clear"></div> <section id="sidebar"> <h2>HTML5 Audio</h2> <section id="audio"> No song is currently playing. </section> <h2>Upload a Song</h2> <%= form_tag upload_path, :method => "post", :multipart => true do %> <%= file_field_tag "mp3file" %><br /> <%= submit_tag "Upload" %> <% end %> </section> <section id="main"> <h2>Download/Delete Songs</h2> <table> <% @songs.each do |song| %> <tr> <td><%= song.key %></td> <td><%= link_to "HTML5 Audio", download_url_for(song.key), :class => "html5" %></td> <td><%= link_to "Download", download_url_for(song.key) %></td> <td><%= link_to "Torrent", torrent_url_for(song.key) %></td> <td><%= link_to "Delete", "songs/delete/?song=" + song.key, :confirm => 'Are you sure you want to delete ' + song.key + '?' %></td> </tr> <% end %> </table> </section> <div class="clear"></div>
Since we will be using floats to design this page, we need to clear the floats before and after to make sure that the layout doesn't get messed up. Now let's add the CSS to adjust the layout of those sections:
#sidebar { width: 30%; float: left; } #main { width: 70%; float: left; } a, a:visited { color: #00f; text-decoration: none; } a:hover { text-decoration: underline; } td { padding: 5px; }
The sidebar will be 30% of the page, and the main section will be 70% of the page. In addition, there is CSS to remove the underline from the links unless the mouse is hovering over it, and there is also padding added to the <td> tags so that it doesn't look so cramped. In the end, that is really the only CSS we need to give the page a basic layout. Feel free to add as much styling to this application as you want on your own, because there are certainly ways to make this application look nicer.
Conclusion
Hopefully you now have a good understanding of how to interact with Amazon S3 from your Ruby on Rails application. With the aws-s3
gem, doing this is very easy, so adding it to an existing application will take very little time. Feel free to modify this application in any way you like to see if you can improve it in any way. Remember to add your own Amazon S3 login information and bucket constant to the application.rb
file, otherwise the application won't start!
For those of you who are Ruby on Rails experts, I'm sure that you can find a way to optimize this application even more. In addition, it would be great to share any optimizations that you make in the comments section so that readers can get even more out of this tutorial.
Comments