Saving Mime Types with Dragonfly

Cacheing mime types for Dragonfly attachments can save your server from a lot work. Keep reading to find out how!

We often reach for the Dragonfly gem when we need to provide photo and file upload access to an application's user base, and display those uploads in various sizes and shapes. On a recent project, users were allowed to upload either a photo or a PDF for a given attachment, which was all well and fine until we set out to display a thumbnail of the attachment.

The Problem

There was the challenge of rendering PDF thumbnails (see ImagePdfHelper gist if curious), but also the challenge of knowing what kind of attachment we were dealing with. You can always call #mime_type on a Dragonfly attachment instance, however if you have your files stored remotely (eg: S3), then doing so requires a full download of the attachment in order for image processing to take place on your server. This additional download should be avoided if possible, so what does it look like to cache an upload's mime type at upload time?

The Solution

    class Foo < ApplicationRecord

      dragonfly_accessor :file do
        after_assign do
          self.assign_attributes("file_mime_type" => attachment.mime_type)
        end

        after_unassign do
          self.assign_attributes("file_mime_type" => nil)
        end
      end
    end

Nothing too fancy going on here. We simply tap into Dragonfly's supplied after_assign and after_unassign hooks in order to cache an attachment's mime type on upload, and clear it on removal. This tactic requires the addition of a file_mime_type field to the model's database table to store the mime type string in the database on the parent model.

The Comprehensive Solution

We needed this behavior in a handful of models however, so I pulled the logic into a module which defined a custom override of the dragonfly_accessor method, and included the module in ApplicationRecord, effectively replacing the default implementation:

class ApplicationRecord
  include CaptureMimeType
end

class Foo < ApplicationRecord
  dragonfly_accessor :file, mime_type: true
end

module CaptureMimeType
  def self.included(base)
    base.instance_eval do
      def dragonfly_accessor(attribute, opts={}, &config_block)
        if opts.delete(:mime_type)
          # If the :mime_type option is passed in, cache the
          # attachment's mime_type after assignment
          super(attribute, opts) do
            config_block.call if config_block

            after_assign do |attachment|
              self.assign_attributes("#{attribute}_mime_type" => attachment.mime_type)
            end

            after_unassign do |attachment|
              self.assign_attributes("#{attribute}_mime_type" => nil)
            end
          end
        else
          # Otherwise, proceed with normal dragonfly setup
          super
        end
      end
    end
  end
end

This override recognizes the new :mime_type option, and declares the necessary hooks for caching and clearing the mime type. The rest of the method definition exists for backwards compatibility. If the mime_type option is not supplied, then a vanilla call to super will defer to Dragonfly's default behavior entirely. However if the option is supplied, a call to super is still utilized to trigger the default behavior and respect other passed in parameters, but with a custom block that defines the assignment hooks.

It's worth noting that this implementation has the potential break down should the method signature for dragonfly_accessor change in a future release, but for now, this works swimmingly.

Eli Fatsi

Eli uses his mathematics degree from Carnegie Mellon to blur the lines between the digital and physical worlds. He codes for Shure, Volunteers of America, and other clients from our Boulder, CO, office.

More articles by Eli