The Right Way to Store and Serve Dragonfly Thumbnails

David Eisinger, Development Director

Article Categories: #Code, #Back-end Engineering

Posted on

An improved method for caching thumbnailed images with the Dragonfly Ruby library.

We love and use Dragonfly to manage file uploads in our Rails applications. Specifically, its API for generating thumbnails is a huge improvement over its predecessors. There is one area where the library falls short, though: out of the box, Dragonfly doesn't do anything to cache the result of a resize/crop, meaning a naïve implementation would rerun these operations every time we wanted to show a thumbnailed image to a user.

The Dragonfly documentation offers some suggestion about how to handle this issue, but makes it clear that you're pretty much on your own:

Dragonfly.app.configure do

  # Override the .url method...
  define_url do |app, job, opts|
    thumb = Thumb.find_by_signature(job.signature)
    # If (fetch 'some_uid' then resize to '40x40') has been stored already, give the datastore's remote url ...
    if thumb
      app.datastore.url_for(thumb.uid)
    # ...otherwise give the local Dragonfly server url
    else
      app.server.url_for(job)
    end
  end

  # Before serving from the local Dragonfly server...
  before_serve do |job, env|
    # ...store the thumbnail in the datastore...
    uid = job.store

    # ...keep track of its uid so next time we can serve directly from the datastore
    Thumb.create!(uid: uid, signature: job.signature)
  end

end

To summarize: create a Thumb model to track uploaded crops. The define_url callback executes when you ask for the URL for a thumbnail, checking if a record exists in the database with a matching signature and, if so, returning the URL to the stored image (e.g. on S3). The before_serve block defines what happens when Dragonfly receives a request for a thumbnailed image (the ones that look like /media/...), storing the thumbnail and then creating a corresponding record in the database.

The problem with this approach is that if someone gets ahold of the initial /media/... URL, they can cause your app to reprocess the same image multiple times, or store multiple copies of the same image, or just fail outright. Here's how we can do it better.

First, create the Thumbs table, and put unique indexes on both columns. This ensures we'll never store multiple versions of the same cropping of any given image.

class CreateThumbs < ActiveRecord::Migration[5.2]
  def change
    create_table :thumbs do |t|
      t.string :signature, null: false
      t.string :uid, null: false

      t.timestamps
    end

    add_index :thumbs, :signature, unique: true
    add_index :thumbs, :uid, unique: true
  end
end

Then, create the model. Same idea: ensure uniqueness of signature and UID.

class Thumb < ApplicationRecord
  validates :signature,
            :uid,
            presence: true,
            uniqueness: true
end

Then replace the before_serve block from above with the following:

before_serve do |job, env|
  thumb = Thumb.find_by_signature(job.signature)

  if thumb
    throw :halt,
      [301, { "Location" => job.app.remote_url_for(thumb.uid) }, [""]]
  else
    uid = job.store
    Thumb.create!(uid: uid, signature: job.signature)
  end
end

(Here's the full resulting config.)

The key difference here is that, before manipulating, storing, and serving an image, we check if we already have a thumbnail with the matching signature. If we do, we take advantage of a cool feature of Dragonfly (and of Ruby) and throw1 a Rack response that redirects to the existing asset which Dragonfly catches and returns to the user.


So that's that: a bare minimum approach to storing and serving your Dragonfly thumbnails without the risk of duplicates. Your app's needs may vary slightly, but I think this serves as a better default than what the docs recommend. Let me know if you have any suggestions for improvement in the comments below.

Dragonfly illustration courtesy of Vecteezy.

  1. For more information on Ruby's throw/catch mechanism, here is a good explanation from Programming Ruby or see chapter 4.7 of Avdi Grimm's Confident Ruby.
David Eisinger

David is Viget's managing development director. From our Durham, NC, office, he builds high-quality, forward-thinking software for PUMA, the World Wildlife Fund, NFLPA, and many others.

More articles by David

Related Articles