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:

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:

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

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:

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:

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:

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:

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):

Without going into much detail, we are setting up Rack::Cache (except for production, where it's enabled by default), and setting up Dragonfly to use it.

We will store our images on disk, however, we need a way to track the association with a user. The simplest option is to add two columns to the user table with a migration:

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).

Finally, we can update the User model:

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:

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:

Next, we can update app/views/users/edit.html.erb:

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:

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 rules are fairly effective: note that, in addition to checking the format, we also check the mime type for better safety. Being an image, we allow jpg, png and gif files.

Our specs should be passing now, so it's time to update the views to optimize the image load.


Dynamic Image Processing

By default, Dragonfly uses ImageMagick to dynamically process images when requested. Assuming we have a user instance in one of our views, we could then:

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.

Let's revisit our user edit page and update the view code:

We'll do the same for the user show page:

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

Then we can replace the content of the .thumbnail container with a simple call to:

We can do even better by moving the argument of thumb out of the partial. Let's update _avatar_image.html.erb:

We can now call our partial with two arguments: one for the desired aspect and one for the user:

We can use the snippet above in edit and show views, while we can call it in the following way inside views/users/_user_table.html.erb, where we are showing the small thumbnails.

In both cases, we also perform a Regex on the aspect to extract a string compatible with the placehold.it service (i.e. removing non alphanumerical characters).


Removing the Avatar


Dragonfly creates two additional attributes that we can use in a form:

  • 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:

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:

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

We need also to add two additional fields to the edit form:

This will get our feature to pass.


Refactoring Features

To avoid having a large and too detailed feature, we can test the same functionality in a request spec.

Let's create a new file called spec/requests/user_flow_spec.rb and add this content to it:

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:

Updated user_steps.rb:


Adding S3 Support

As a last step, we can easily add S3 support to store the avatar files. Let's reopen config/initializers/dragonfly.rb and update the configuration block:

This will work out of the box, and will only affect production (or any other environment that is not specified in the file). Dragonfly will default to file system storage for all other cases.


Conclusion

I hope you found this tutorial interesting, and managed to pick up a few interesting tidbits of information.

I encourage you to refer to the Dragonfly GitHub page for extensive documentation and other examples of use cases - even outside of a Rails application.

Tags:

Comments

Related Articles