davideisinger.com

My personal website
Log | Files | Refs | README

index.md (4909B)


      1 ---
      2 title: "The Right Way to Store and Serve Dragonfly Thumbnails"
      3 date: 2018-06-29T00:00:00+00:00
      4 draft: false
      5 canonical_url: https://www.viget.com/articles/the-right-way-to-store-and-serve-dragonfly-thumbnails/
      6 ---
      7 
      8 We love and use [Dragonfly](https://github.com/markevans/dragonfly) to
      9 manage file uploads in our Rails applications. Specifically, its API for
     10 generating thumbnails is a huge improvement over its predecessors. There
     11 is one area where the library falls short, though: out of the box,
     12 Dragonfly doesn't do anything to cache the result of a resize/crop,
     13 meaning a naïve implementation would rerun these operations every time
     14 we wanted to show a thumbnailed image to a user.
     15 
     16 [The Dragonfly documentation offers some
     17 suggestion](https://markevans.github.io/dragonfly/cache#processing-on-the-fly-and-serving-remotely)
     18 about how to handle this issue, but makes it clear that you're pretty
     19 much on your own:
     20 
     21 ```ruby
     22 Dragonfly.app.configure do
     23 
     24   # Override the .url method...
     25   define_url do |app, job, opts|
     26     thumb = Thumb.find_by_signature(job.signature)
     27     # If (fetch 'some_uid' then resize to '40x40') has been stored already, give the datastore's remote url ...
     28     if thumb
     29       app.datastore.url_for(thumb.uid)
     30     # ...otherwise give the local Dragonfly server url
     31     else
     32       app.server.url_for(job)
     33     end
     34   end
     35 
     36   # Before serving from the local Dragonfly server...
     37   before_serve do |job, env|
     38     # ...store the thumbnail in the datastore...
     39     uid = job.store
     40 
     41     # ...keep track of its uid so next time we can serve directly from the datastore
     42     Thumb.create!(uid: uid, signature: job.signature)
     43   end
     44 
     45 end
     46 ```
     47 
     48 To summarize: create a `Thumb` model to track uploaded crops. The
     49 `define_url` callback executes when you ask for the URL for a thumbnail,
     50 checking if a record exists in the database with a matching signature
     51 and, if so, returning the URL to the stored image (e.g. on S3). The
     52 `before_serve` block defines what happens when Dragonfly receives a
     53 request for a thumbnailed image (the ones that look like `/media/...`),
     54 storing the thumbnail and then creating a corresponding record in the
     55 database.
     56 
     57 The problem with this approach is that if someone gets ahold of the
     58 initial `/media/...` URL, they can cause your app to reprocess the same
     59 image multiple times, or store multiple copies of the same image, or
     60 just fail outright. Here's how we can do it better.
     61 
     62 First, create the `Thumbs` table, and put unique indexes on both
     63 columns. This ensures we'll never store multiple versions of the same
     64 cropping of any given image.
     65 
     66 ```ruby
     67 class CreateThumbs < ActiveRecord::Migration[5.2]
     68   def change
     69     create_table :thumbs do |t|
     70       t.string :signature, null: false
     71       t.string :uid, null: false
     72 
     73       t.timestamps
     74     end
     75 
     76     add_index :thumbs, :signature, unique: true
     77     add_index :thumbs, :uid, unique: true
     78   end
     79 end
     80 ```
     81 
     82 Then, create the model. Same idea: ensure uniqueness of signature and
     83 UID.
     84 
     85 ```ruby
     86 class Thumb < ApplicationRecord
     87   validates :signature,
     88             :uid,
     89             presence: true,
     90             uniqueness: true
     91 end
     92 ```
     93 
     94 Then replace the `before_serve` block from above with the following:
     95 
     96 ```ruby
     97 before_serve do |job, env|
     98   thumb = Thumb.find_by_signature(job.signature)
     99 
    100   if thumb
    101     throw :halt,
    102       [301, { "Location" => job.app.remote_url_for(thumb.uid) }, [""]]
    103   else
    104     uid = job.store
    105     Thumb.create!(uid: uid, signature: job.signature)
    106   end
    107 end
    108 ```
    109 
    110 *([Here's the full resulting
    111 config.](https://gist.github.com/dce/4e79183a105e415ca0e5e1f1709089b8))*
    112 
    113 The key difference here is that, before manipulating, storing, and
    114 serving an image, we check if we already have a thumbnail with the
    115 matching signature. If we do, we take advantage of a [cool
    116 feature](http://markevans.github.io/dragonfly/v0.9.15/file.URLs.html#Overriding_responses)
    117 of Dragonfly (and of Ruby) and `throw`[^1] a Rack response that redirects
    118 to the existing asset which Dragonfly
    119 [catches](https://github.com/markevans/dragonfly/blob/a6835d2a9a1195df840c643d6f24df88b1981c91/lib/dragonfly/server.rb#L55)
    120 and returns to the user.
    121 
    122 ------------------------------------------------------------------------
    123 
    124 So that's that: a bare minimum approach to storing and serving your
    125 Dragonfly thumbnails without the risk of duplicates. Your app's needs
    126 may vary slightly, but I think this serves as a better default than what
    127 the docs recommend. Let me know if you have any suggestions for
    128 improvement in the comments below.
    129 
    130 *Dragonfly illustration courtesy of
    131 [Vecteezy](https://www.vecteezy.com/vector-art/165467-free-insect-line-icon-vector).*
    132 
    133 [^1]:  For more information on Ruby's `throw`/`catch` mechanism, [here is
    134 a good explanation from *Programming
    135 Ruby*](http://phrogz.net/ProgrammingRuby/tut_exceptions.html#catchandthrow)
    136 or see chapter 4.7 of Avdi Grimm's [*Confident
    137 Ruby*](https://pragprog.com/book/agcr/confident-ruby).