Blog

RubyOnRails

Rails Dragonfly: Keep external image links valid

Many Rails file handling solutions process images when uploading. I like the solution of the Dragonfly Gem to only create image versions when requested. Here a solution how to update the image cache.

The Dragonfly Gem is an image handling gem for RubyOnRails. It is useless without a proxy cache in production, because the gem uses it for storing generated images.

I faced the problem, that the user wanted to link to an image, but another user could edit the image, and it should then update at all linked places. The way dragonfly uses the cache is: Give everything an “Expire” header far in the future, if something changes, it gets a new url. That is a fine solution if you get away with it, because requests will not hit your app, but only the proxy server once an image was generated. But the consequence is, the same url always responses with the same image. You cannot update things and it magically updates everywhere.

As you might know:

There are only two hard things in Computer Science: cache invalidation and naming things.

Phil Karlton

That is, where conditional cache validation comes into play. Conditional cache validation works with “Last-Modified-Since” (sent by you) and “If-Modified-Since” (sent by your browser). A proxy like rack cache falls back to conditional cache validation if the “Exprires” time has past. The disadvantage is: the request hits your app. The advantage is: you can build some logic in your caching mechanism.

class ImagesController < ApplicationController
  def show
    skip_authorization

    im = Image.find(params[:id])

    if params[:size] == 'small'
      thumb_params = "100x100#"
    elsif params[:size] == 'large'
      thumb_params = "800x600"
    else
      thumb_params = "400x300"
    end

    if stale?(etag: im, last_modified: im.updated_at.utc)
      self.response.cache_control[:public] = true
      self.response.headers['Expires'] = 10.seconds.from_now.httpdate
      self.response.headers['Cache-Control'] = "max-age=10"

      send_data im.image.thumb(thumb_params).compress.data, disposition: :inline, type: im.image.mime_type  
    end

  end
end

So what this does: Give the response a short expire time, then revalidate future requests with the Rails stale? method. Only if the image was modified after it was added to your proxy cache, it is generated new.

That way if you have an image url like /images/123-me-eating-cake-jpg and you visit the same url after it has been updated, you will get the right version of the image.

Took me some time to figure it out, hope it saves somebody some time.