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