Files
2023-10-24 20:48:09 -04:00

138 lines
4.8 KiB
Markdown

---
title: "The Right Way to Store and Serve Dragonfly Thumbnails"
date: 2018-06-29T00:00:00+00:00
draft: false
canonical_url: https://www.viget.com/articles/the-right-way-to-store-and-serve-dragonfly-thumbnails/
---
We love and use [Dragonfly](https://github.com/markevans/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](https://markevans.github.io/dragonfly/cache#processing-on-the-fly-and-serving-remotely)
about how to handle this issue, but makes it clear that you're pretty
much on your own:
```ruby
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.
```ruby
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.
```ruby
class Thumb < ApplicationRecord
validates :signature,
:uid,
presence: true,
uniqueness: true
end
```
Then replace the `before_serve` block from above with the following:
```ruby
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.](https://gist.github.com/dce/4e79183a105e415ca0e5e1f1709089b8))*
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](http://markevans.github.io/dragonfly/v0.9.15/file.URLs.html#Overriding_responses)
of Dragonfly (and of Ruby) and `throw`[^1] a Rack response that redirects
to the existing asset which Dragonfly
[catches](https://github.com/markevans/dragonfly/blob/a6835d2a9a1195df840c643d6f24df88b1981c91/lib/dragonfly/server.rb#L55)
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](https://www.vecteezy.com/vector-art/165467-free-insect-line-icon-vector).*
[^1]: For more information on Ruby's `throw`/`catch` mechanism, [here is
a good explanation from *Programming
Ruby*](http://phrogz.net/ProgrammingRuby/tut_exceptions.html#catchandthrow)
or see chapter 4.7 of Avdi Grimm's [*Confident
Ruby*](https://pragprog.com/book/agcr/confident-ruby).