Ruby on Rails: Model storing images in a database using concerns

In the past I was storing binary data on the filesystem instead of the database. Now I wanted to try the other approach.

The files (images in this particular case) will be stored in a binary typed column as raw data. The following concern will check if they are really images, their size and provides caching of the images.

#app/models/concerns/with_images.rb

class FileTypeValidator < ActiveModel::Validator
  def validate(record)
    record.class.get_image_fields.each do |f|
      field = f[0]
      type = f[1].is_a?(Array) ? f[1] : [f[1]]
      type = type.map{|t| "image/#{t}" }
      size = f[2]

      if record[field].is_a? ActionDispatch::Http::UploadedFile then
        if not type.include?(record[field].content_type) then
          record.errors.add field, "#{field.to_s.humanize} must be an PNG image!"
        elsif record.send("#{field}=", record[field].read).size > size then
          record.errors.add field, "#{field.to_s.humanize} maximum size can be #{size.to_s(:human_size)}"
        end
      end
    end
  end
end

module WithImages
  extend ActiveSupport::Concern
  include ActionView::Helpers::AssetTagHelper

  included do
    validates_with FileTypeValidator

    def self.get_image_fields
      @image_fields
    end

    def self.image_fields(fields)
      @image_fields = fields

      fields.map{|f| f[0]}.each do |field|
        define_method field do
          image_field field
        end
      end
    end
  end

  protected

  def image_field(field)
    path = "#{Rails.root}/public/images/app-#{self.id}-#{field}.png"

    Dir.mkdir(File.dirname(path)) unless Dir.exists? File.dirname(path)

    if not File.exists?(path) or File.mtime(path) < self.updated_at
      File.open(path, 'wb') do |f|
        f.write self[field]
      end
    end
    image_tag "/images/app-#{self.id}-#{field}.png"
  end
end

Now you have, let say, User model where you store the avatar in an avatar:binary column:

#app/models/user.rb

class User < ActiveRecord::Base
  include WithImages

  image_fields [[:avatar, 'png', 100.kilobytes]]
end

Leave a Reply

Your email address will not be published. Required fields are marked *