Uploading Files In Sinatra

Please note that there are some problems with the code in this example - particularly the part about uploading a file to Heroku. You might still get something out of the post (and there are a lot of helpful comments) but please bear in mind that it might not work exactly how it should. I plan to write an updated guide soon that will hopefully work much better

There are often cases when you want to upload a file in a web app - it might be an image, or a pdf attachment. In this ditty I’m going to go through the code required for uploading files to your application. It’s only a simple solution but it gets the job done.

The Sinatra App

First of all we need a basic Sinatra application with a page called ‘upload’:

require 'rubygems'
require 'sinatra'

get '/upload' do
  haml :upload
end

__END__

@@upload
%h2 Upload
  %form{:action=>"/upload",:method=>"post",:enctype=>"multipart/form-data"}
%input{:type=>"file",:name=>"file"}
%input{:type=>"submit",:value=>"Upload"}

(Note that I’ve not used a layout for this example)

This will show a very simple form that let’s you select a file from your system. The important part is the ‘enctype’ attribute, this needs to be set to ‘multipart/form-data’.

Now we need to deal with what happens when the form is posted, by adding another handler (this code goes before the __END__ declaration above):

post '/upload' do
  unless params[:file] && (tmpfile = params[:file][:tempfile]) && (name = params[:file][:filename])
    return haml(:upload)
  end
  while blk = tmpfile.read(65536)
      File.open(File.join(Dir.pwd,"public/uploads", name), "wb") { |f| f.write(tmpfile.read) }
  end
 'success'
end

Let’s go through what happens here. When you select a file and click on the upload button, a file object is sent via the params hash. This contains a property called tempfile, which is a temporary file that is created while the file is uploaded. There is also a property called filename which is the name of the file as it is saved on your system. The first line checks to see that a file was actually selected and sets these properties as the variables tmpfile and name. If a file wasn’t chosen then the upload form is shown again.

A while block is then used to deal with the file while it’s being uploaded. This is done in blocks of 65536 bytes, which I think acts as a sort of buffer to limit RAM usage. This is written to a file in the public directory in a folder called uploads (I can’t remember if you have to create this manually of if it is created the first time you upload a file). The ‘wb’ argument tells Ruby that it is a binary file and is write only (this is called a mode string, you can see a list of them all by scrolling to the top of this page). Once the file has been uploaded there is a simple message of ‘success’, although this could obviously be improved, it is enough for now.

If you want to know a bit more about file objects in Ruby, you can read all about them in the online Ruby Pickaxe Book.

Heroku

The code above is all well and good, but Heroku doesn’t allow you to store lots of files on their servers (Only files that are to do with the actual code are supposed to be there). Instead, you are supposed to host your files elsewhere. The most obvious option is to use Amazon’s Simple Storage Service (S3). This allows you to store files on Amazon’s servers for a very small charge. Heroku is also hosted on Amazon’s servers, so by hosting your files here too, you will reduce any possible latency issues when retreiving your files.

Sign Up for S3

The first thing you need to do is sign up for S3, which you can do by going to the website and clicking on the Sign Up button. If you are already an Amazon customer you can use these login details. Once you have completed the sign up process, you can start adding files to your account. Amazon have a web based console for this and you can also use the excellent Firefox addon S3Fox. Amazon S3 uses the concept of buckets to hold your files. Within each bucket you can create folders. I tend to use a separate bucket for each website.

The Sinatra Code

Now you’ve got your Amazon S3 account up and running, you can add files to it via a web interface by making a few alterations to the code above. Before you do that, you need to install the aws gem:

sudo gem install aws-s3

When you signed up for Amazon S3, you should have been given two important bits of information - a 20-digit Access Key ID and a Secret Access Key. You can get these by signing into the Amazon Web Services page and then clicking on the ‘Account’ tab. You then click on the ‘Security Credentials’ link. The information you need is in a box halfway down the page (you need to click to get the secret key, which is 40-digits long).

Once you have this information you can use the set command in Sinatra to store them. We also need to add a require statement so that the aws-s3 gem is included in our code:

require 'rubygems'
require 'sinatra'
require 'aws/s3'

set :bucket, 'mybucket'
set :s3_key, THISISANEXAMPLEKEYID
set :s3_secret, Thi$isJu5taNExamp/etO0itSh0u1dBel0NgeR

Note that in production you might want add these variables using Heroku’s Environment Settings in order to keep your keys secret. See this post on settings and cofiguration for more details.

Next we need to deal with our handlers. The first get handler is the same as before and the second, post handler, is similar, although it uses the aws-s3 gem to establish a connection and then store the file. Notice that it uses the settings above (these can be hard-coded into the handler if you prefer). The view code stays exactly the same as before:

get '/upload' do
  haml :upload
end

post '/upload' do
  unless params[:file] && (tmpfile = params[:file][:tempfile]) && (name = params[:file][:filename])
    return haml(:upload)
  end
  while blk = tmpfile.read(65536)
    AWS::S3::Base.establish_connection!(
    :access_key_id     => settings.s3_key,
    :secret_access_key => settings.s3_secret)
    AWS::S3::S3Object.store(name,open(tmpfile),settings.bucket,:access => :public_read)     
  end
 'success'
end

__END__

@@upload
%h2 Upload
%form{:action=>"/upload",:method=>"post",:enctype=>"multipart/form-data"}
%input{:type=>"file",:name=>"file"}
%input{:type=>"submit",:value=>"Upload"}

And that’s all folks! This should be enough to get you started uploading basic files. This can definitely be improved - off the top of my head I can think of:

  • A nicer looking form
  • A better feedback page or message (including some feedback if a file isn’t selected)
  • A progress bar as the file is uploading
  • The ability to upload files into different folders
  • Checks to see what type of file you are uploading
  • Post Formatting of images (such as creating thumbnails automatically), although this is probably worthy of a whole new post in itself
  • Upload multiple files at once

Can any of the Crooners out there think of any more? Can anybody implement any improvements and post their code in the comments? There will also be a number of gems that do this for you and probably do it a lot better, but one of the reasons I like Sinatra is that you can write basic code yourself. If anybody has any recommendations of any good gems or Sinatra extensions for uploading files then please let me know in the comments.

blog comments powered by Disqus