How to Upload Files with Ease Using DragonFly

File uploads are generally a tricky area in web development. In this tutorial, we will learn how to use Dragonfly, a powerful Ruby gem that makes it easy and efficient to add any kind of upload functionality to a Rails project.

What We're Going to Build

Our sample application will display a list of users, and for each one of them, we will be able to upload an avatar and have it stored. Additionally, Dragonfly will allow us to:

Dynamically manipulate images without saving additional copies

Leverage HTTP caching to optimize our application load

In this lesson, we will follow a BDD [Behavior Driven Development] approach, using Cucumber and RSpec.

Prerequisites

You'll need to have Imagemagick installed: you can refer to this page for the binaries to install. As I am based on a Mac platform, use Homebrew, I can simply type brew install imagemagick.

You will also need to clone a basic Rails application that we will use as a starting point.

Setup

We will begin by cloning the starting repository and setting up our dependencies:

This application requires at least Ruby 1.9.2 to run, however, I encourage you to use 1.9.3. The Rails version is 3.2.1. The project does not include a .rvmrc or a .rbenv file.

Next, we run:

bundle install
bundle exec rake db:setup db:test:prepare db:seed

This will take care of the gem dependencies and database setup (we will be using sqlite, so no need to worry about database config).

To test that everything is working as expected, we can run:

bundle exec rspec
bundle exec cucumber

You should find that all tests have passed. Let's review Cucumber's output:

Feature: managing user profile
As a user
In order to manage my data
I want to access my user profile page
Background:
Given a user exists with email "email@example.com"
Scenario: viewing my profile
Given I am on the home page
When I follow "Profile" for "email@example.com"
Then I should be on the profile page for "email@example.com"
Scenario: editing my profile
Given I am on the profile page for "email@example.com"
When I follow "Edit"
And I change my email with "new_email@example.com"
And I click "Save"
Then I should be on the profile page for "email@example.com"
And I should see "User updated"
2 scenarios (2 passed)
11 steps (11 passed)
0m0.710s

As you can see, these features describe a typical user workflow: we open a user page from a list, press "Edit" to edit the user data, change the email, and save.

Now, try running the app:

rails s

If you open http:://localhost:3000 in the browser, you will find a list of users (we pre-populated the database with 40 random records thanks to the Faker gem).

For now, each one of the users will have a small, 16x16px avatar and a big placeholder avatar in their profile page. If you edit the user, you will be able to change its details (first name, last name and password), but if you try to upload an avatar, it will not be saved.

Feel free to browse the codebase: the application uses Simple Form to generate form views and Twitter Bootstrap for CSS and layout, as they integrate perfectly and help a lot in speeding up the prototyping process.

Features for Avatar Upload

We will start by adding a new scenario to features/managing_profile.feature:

...
Scenario: adding an avatar
Given I am on the profile page for "email@example.com"
When I follow "Edit"
And I upload the mustache avatar
And I click "Save"
Then I should be on the profile page for "email@example.com"
And the profile should show "the mustache avatar"

This feature is fairly self-explanatory, but it requires a few additional steps to add to features/step_definitions/user_steps.rb:

This step assumes that you have an image, called mustache_avatar.jpg inside spec/fixtures. As you might guess, this is just an example; it can be anything you want.

The first step uses Capybara to find the user[avatar_image] file field and upload the file. Note that we're assuming that we will have a an avatar_image attribute on the User model.

The second step uses Nokogiri (a powerful HTML/XML parsing library) and XPath to parse the content of the resulting profile page and search for the first img tag with a thumbnail class and test that the src attribute contains mustache_avatar.

If you run cucumber now, this scenario will trigger an error, as there is no file field with the name we specified. It's now time to focus on the User model.

Adding Dragonfly Support to the User Model

Before integrating Dragonfly with the User model, let's add a couple of specs to user_spec.rb.

We can append a new block right after the attributes context:

context "avatar attributes" do
it { should respond_to(:avatar_image) }
it { should allow_mass_assignment_of(:avatar_image) }
end

We test that the User has a avatar_image attribute and, as we will be updating this attribute through a form, it needs to be accessible (second spec).

Now we can install Dragonfly: by doing that, we will get these specs to go green.

Let's add the following lines to the Gemfile:

gem 'rack-cache', require: 'rack/cache'
gem 'dragonfly', '~>0.9.10'

Next, we can run bundle install. Rack-cache is needed in development, as it's the simplest option to have HTTP caching. It can be used in production as well, even if more robust solutions (like Varnish or Squid) would be better.

We also need to add the Dragonfly initializer. Let's create the config/initializers/dragonfly.rb file and add the following:

This is the vanilla Dragonfly configuration: it sets up a Dragonfly application and configures it with the needed module. It also adds a new macro to ActiveRecord that we will be able to use to extend our User model.

We need to update config/application.rb, and add a new directive to the configuration (right before the config.generators block):

Once again, this is straight from Dragonfly's documentation: we need to have a avatar_image_uid column to uniquely identify the avatar file and a avatar_image_name to store its original filename (the latter column is not strictly needed, but it enables the generation of image urls that end with the original filename).

The image_accessor method is made available by the Dragonfly initializer, and it requires just an attribute name. We also make the same attribute accessible in the line below.

Running rspec now should show all specs green.

Uploading and Displaying the Avatar

To test the upload function, we can add a context to users_controller_spec.rb in the PUT update block:

context "avatar image" do
let!(:image_file) { fixture_file_upload('/mustache_avatar.jpg', 'image/jpg') }
context "uploading an avatar" do
before do
put :update, id: user.id, user: { avatar_image: image_file }
end
it "should effectively save the image record on the user" do
user.reload
user.avatar_image_name.should =~ /mustache_avatar/
end
end
end

We will reuse the same fixture and create a mock for the upload with fixture_file_upload.

As this functionality leverages on Dragonfly, we don't need to write code to get it passing.

We now have to update our views to show the avatar. Let's start from the user show page and open app/views/users/show.html.erb and update it with the following content:

We can show the user avatar with a simple call to @user.avatar_image.url. This will return a url to a non-modified version of the avatar uploaded by the user.

If you run cucumber now, you'll see the green feature. Feel free to try it out in the browser too!

We're implicitly relying on CSS to resize the image if it's too big for its container. It's a shaky approach: our user could upload non-square avatars, or a very small image. In addition, we're always serving the same image, without too much concern for page size or bandwidth.

We need to work on two different areas: adding some validation rules to the avatar upload and specifying image size and ratio with Dragonfly.

Upload Validations

We will start by opening the user_spec.rb file and adding a new spec block:

context "avatar attributes" do
%w(avatar_image retained_avatar_image remove_avatar_image).each do |attr|
it { should respond_to(attr.to_sym) }
end
%w(avatar_image retained_avatar_image remove_avatar_image).each do |attr|
it { should allow_mass_assignment_of(attr.to_sym) }
end
it "should validate the file size of the avatar" do
user.avatar_image = Rails.root + 'spec/fixtures/huge_size_avatar.jpg'
user.should_not be_valid # size is > 100 KB
end
it "should validate the format of the avatar" do
user.avatar_image = Rails.root + 'spec/fixtures/dummy.txt'
user.should_not be_valid
end
end

We are testing for presence and are allowing "mass assignment" for additional attributes that we will use to enhance the user form (:retained_avatar_image and :remove_avatar_image).

In addition, we are also testing that our user model will not accept big uploads (more than 200 KB) and files that are not images. For both cases, we need to add two fixture files (an image with the specified name and whose size is more than 200 KB and a text file with any content).

As usual, running these specs will not get us to green. Let's update the user model to add those validation rules:

These methods will create a processed version of this image with a unique hash and thanks to our caching layer, ImageMagick will be called just once per image. After that, the image will be served directly from cache.

You can use many built-in methods or simply build your own, Dragonfly's documentation has got plenty of examples.

We're forcing the image size to 400 x 400 pixels. The # parameter also instructs ImageMagick to crop the image keeping a central gravity. You can see that we have the same code in two places, so let's refactor this into a partial called views/users/_avatar_image.html.erb

Removing the Avatar

retained_avatar_image: this stores the uploaded image between reloads. If validations for another form field (say email) fail and the page is reloaded, the uploaded image is still available without need to reupload it. We will use it directly in the form.

remove_avatar_image: when it's true, the current avatar image will be deleted both from the user record and disk.

We can test the avatar removal by adding an additional spec to users_controller_spec.rb, in the avatar image block:

...
context "removing an avatar" do
before do
user.avatar_image = Rails.root + 'spec/fixtures/mustache_avatar.jpg'
user.save
end
it "should remove the avatar from the user" do
put :update, id: user.id, user: { remove_avatar_image: "1" }
user.reload
user.avatar_image_name.should be_nil
end
end
...

Once again, Dragonfly will get this spec to pass automatically as we already have the remove_avatar_image attribute available for the user instance.

Let's then add another feature to managing_profile.feature:

Scenario: removing an avatar
Given the user with email "email@example.com" has the mustache avatar
And I am on the profile page for "email@example.com"
When I follow "Edit"
And I check "Remove avatar image"
And I click "Save"
Then I should be on the profile page for "email@example.com"
And the profile should show "the placeholder avatar"

As usual, we need to add some steps to user_steps.rb and update one to add a Regex for the placeholder avatar:

The spec encapsulates all of the steps we used to define our main feature. It thoroughly tests the markup and the flow, so we can make sure that everything works properly at a granular level.

Now we can shorten the managing_profile.feature:

Feature: managing user profile
As a user
In order to manage my data
I want to access my user profile page
Background:
Given a user exists with email "email@example.com"
Scenario: editing my profile
Given I change the email with "new_mail@example.com" for "email@example.com"
Then I should see "User updated"
Scenario: adding an avatar
Given I upload the mustache avatar for "email@example.com"
Then the profile should show "the mustache avatar"
Scenario: removing an avatar
Given the user "email@example.com" has the mustache avatar and I remove it
Then the user "email@example.com" should have "the placeholder avatar"

Claudio works as a full stack developer at New Bamboo in London, where he's been living for two years. His main areas of interest are Ruby, Rails, client side Javascript and [Elixir}(http://elixir-lang.org/).