Select Page

Managing file attachments in your rails app has been made easy thanks to gems like Paperclip. Just add a file attachment to a model and kapow, right?

has_attached_file :avatar, styles: { medium: "300x300>", thumb: "100x100>" }

But hang on, your app likely has multiple models that all want an image attachment, so we can get fancy and abstract by creating an Image model. And oh, the DRYness, an image can belong to many models through the magic of polymorphic association. It will probably look something like this with the “somethingable” rails convention.


class Image < ActiveRecord::Base 
  belongs_to :imageable, polymorphic: true 
  has_attached_file :avatar, styles: { medium: "300x300>", thumb: "100x100>" }
end

And hey, it works! Well, it works as long as products, users, widgets, and whatnots all need the same image with the same configuration. Life is easy and our clients will never require anything more complex than this, right?

Enter the real world scenario. You have avatars, profile photos, product photos, pdf docs, videos, user driven content applied to different objects from different kinds of users. That’s a lot of models and attachments. Each has a slightly different set of requirements like sizes and transformations.

And in this scenario, suppose you want to create a unifying feature set like scanning for inappropriate photos or aggregating file info. How do you bring together the data scattered across models and tables in your database? How do you tie it all together?

Here’s what I came up with. I started with a single class to act as an archetype for all file assets across my app – FileAsset. Then child classes would define specific types such as Avatar and Logo. Using single table inheritance (STI), that means we can keep all them all in the same database table.  FileAsset would also belong to other classes through polymorphic inheritance. Create the table like this:


class CreateFileAssets < ActiveRecord::Migration
  def change
    create_table :file_assets do |t|
      t.string      :type
      t.integer     :file_assetable_id
      t.string      :file_assetable_type
      t.attachment  :attachment

      t.timestamps
    end
  end
end

Then, FileAsset can be defined. BTW, the name FileAsset is not perfect, but names like File, Asset, or Attachment would conflict with other framework or gem naming.


 class FileAsset < ActiveRecord::Base 
  # accessible attrs in rails 3: 
  # attr_accessible :file_assetable_id, :file_assetable_type, :type belongs_to :file_assetable, :polymorphic => true
  delegate :url, to: :attachment

  ATTACHMENT_OPTIONS = {
                         storage: :s3,
                         s3_credentials: YAML.load_file("#{Rails.root}/config/s3.yml")
                       }
end

Above, we define the polymorphic relationship that will apply to all child classes. This class delegates the paperclip attribute :url to :attachment which will be defined in child classes. It also defines common options for paperclip that can be used by default across child classes.

Then the child class Avatar. Because this hypothetical real world scenario is large and complex, image related models will share a common namespace and live in app/models/images/.


class Images::Avatar < FileAsset 
  # accessible attrs in rails 3 
  # attr_accessible :attachment 
  has_attached_file :attachment, ATTACHMENT_OPTIONS.merge( styles: { thumb: "32x32>",
                                small:  "100x100>",
                                medium: "140x140>",
                                large:  "200x200>"
                              },
                      path: "/:style/:id/:filename"
                    )
end

In Avatar, we invoke paperclip’s wonderous has_attached_file which will always be :attachment. Notice the default options to which are merged options specific to this model.

Finally, you can associate Avatar with User.

  class User < ActiveRecord::Base
    has_one  :avatar, as: :file_assetable, class_name: "Images::Logo", dependent: :destroy
    accepts_nested_attributes_for :avatar, allow_destroy: true
  ...

Now, I’m going to reach in the oven and pull out the pre-baked app so we can have a look at how yummy it looks once all your models, controllers, view and other bits are fully cooked. When I fire up rails console, I can do this on Avatar:

irb(main):027:0> Images::Avatar.last.url(:small)
  Images::Logo Load (1.3ms)  SELECT "file_assets".* FROM "file_assets" WHERE "file_assets"."type" IN ('Images::Avatar') ORDER BY "file_assets"."id" DESC LIMIT 1
=> "http://s3.amazonaws.com/..../small/17/243.JPG?1383912357"

And behold, the same holds true, of course, for FileAsset itself.

irb(main):028:0> FileAsset.last.url(:small)
  FileAsset Load (1.3ms)  SELECT "file_assets".* FROM "file_assets" ORDER BY "file_assets"."id" DESC LIMIT 1
=> "http://s3.amazonaws.com/.../small/17/243.JPG?1383912357"

And why is this cool? Now you can perform administrative tasks or batch operations on all uploaded files in one place. Now you can create that super duper administrative screen in which an admin can preview all uploads and disable any unallowed content.

And as simply as that, you can make use of some impressive sounding concepts like polymorphic single table class inheritance – stuff you learned about when learning to program that you hoped you’d never have to use in everyday life.