Uploading Files with acts_as_attachment
Problem
Contributed by: Rick Olson
You want to add file upload support to your Rails application but you need more options than are available with the file_column plug-in. Specifically, you need to be able to configure details about how file uploading is handled on a per-model basis. For example, one model may store images in a database while another saves them on the filesystem.
Solution
Use the acts_as_attachment plug-in to allow you to configure file-uploading capabilities individually, for each model that supports uploads.
Suppose you want to allow DVD collectors to upload cover art for each item in their collection. For this recipe, assume you have a Rails application configured to access your database. Start by adding this URL to your plug-in source list:
$ ruby script/plugin source http://svn.techno-weenie.net/projects/plugins
Next, download the acts_as_attachment plug-in:
$ ruby script/plugin install acts_as_attachment
Because this plug-in can depend on RMagick being installed, it's a good idea to run its test to make sure it finds everything it needs on your system:
$ rake test:plugins PLUGIN=acts_as_attachment
Now use the plug-in's attachment_model generator to generate an attachment model named dvd_cover:
$ script/generate attachment_model dvd_cover
Running this command generates the model stubs as well as an attachment migration to get started. Here's the database migration you'll use to set up the table structure:
class CreateDvdCovers < ActiveRecord::Migration
def self.up
create_table :dvd_covers do |t|
t.column "content_type", :string
t.column "filename", :string
t.column "size", :integer
# used with thumbnails, always required
t.column "parent_id", :integer
t.column "thumbnail", :string
# required for images only
t.column "width", :integer
t.column "height", :integer
end
# only for db-based files
# create_table :db_files, :force => true do |t|
# t.column :data, :binary
# end
end
def self.down
drop_table :dvd_covers
# only for db-based files
# drop_table :db_files
end
end
The columns content_type, filename, size, parent_id, and thumbnail are all vital for acts_as_attachment. Width and height are optional and used for images only. Here's what the initial model will look like:
class DvdCover < ActiveRecord::Base
belongs_to :dvd
acts_as_attachment :storage => :file_system
validates_as_attachment end
The :file_system storage option specifies that uploaded files are to go in your application's public directory. For example, if you uploaded a file called logo.gif, you'd end up with the following file path on your server: public/dvd_covers/1/logo.gif.
The validates method sets up the essential validations: checking that the file size is within the limits you've specified, that the content type matches what you want, and that the filename, size, and content_type fields are present. The default file size ranges from 1 B to 1 MB. Because DVD covers typically won't be that large, set up some constraints on what files are allowed. You can always use the :image shortcut to specify any common image type (e.g., GIF, JPG, PNG).
app/models/dvd_cover.rb:
class DvdCover < ActiveRecord::Base
belongs_to :dvd
acts_as_attachment :storage => :file_system,
:max_size => 300.kilobytes,
:content_type => :image,
:thumbnails => {
:thumb => [50, 50],
:geometry => 'x50'
}
validates_as_attachment end
Setting up a controller and some initial views does not require any special code. acts_as_attachment creates an uploaded_data= setter that does all the processing for you. Here's everything you need for a working example:
app/controllers/dvd_covers_controller.rb:
class DvdCoversController < ApplicationController
def index
@dvd_covers = DvdCover.find(:all)
end
def new
@dvd_cover = DvdCover.new
end
def show
@dvd_cover = DvdCover.find params[:id]
end
def create
@dvd_cover = DvdCover.create! params[:dvd_cover]
redirect_to :action => 'show', :id => @dvd_cover
rescue ActiveRecord::RecordInvalid
render :action => 'new'
end end
Here's a view to list all uploaded files or images:
app/views/dvd_covers/index.rhtml:
<h1>DVD Covers</h1>
<ul>
<% @dvd_covers.each do |dvd_cover| -%>
<li><%= link_to dvd_cover.filename, :action => 'show',
:id => dvd_cover %></li>
<% end -%>
</ul>
<p><%= link_to 'New', :action => 'new' %></p>
Next, here's a form, containing a multipart, file selection element:
app/views/dvd_covers/new.rhtml:
<h1>New DVD Cover</h1>
<% form_for :dvd_cover, :url => { :action => 'create' },
:html => { :multipart => true } do |f| -%>
<p><%= f.file_field :uploaded_data %></p>
<p><%= submit_tag :Create %></p>
<% end -%>
Finally, here's some code to display individual DVD cover images:
app/views/dvd_covers/show.rhtml:
<p><%= @dvd_cover.filename %></p>
<%= image_tag @dvd_cover.public_filename,
:size => @dvd_cover.image_size %>
Discussion
The acts_as_attachment plug-in is designed to be specified on multiple models in your application, rather than having a global Attachment model that other models depend on.
Like file_column, acts_as_attachment supports thumbnail images. The first way to trigger the generation of thumbnails is with the resize_to option:
acts_as_attachment :storage => :file_system, :resize_to => '300x200'
The option takes two forms of parameters: a standard width/height array ([300, 200]), or an RMagick geometry string. The various codes can give you a lot of power.
Resizing the original image is not always desired. Sometimes you will want to change thumbnail sizes and regenerate. Not having the original around makes this impossible. So instead, we'll create various thumbnail sizes.
acts_as_attachment :storage => :file_system,
:thumbnails => { :normal => '300>', :thumb => '75' }
The'300>' geometry code resizes the width to 300 if it's larger and keeps aspect ratio. The '75' geometry code always resizes the width to 75, while keeping the aspect ratio.
Now let's change the show view to accommodate for these new thumbnails:
<p>Original: <%= link_to @dvd_cover.filename, @dvd_cover.public_filename %></p>
<% @dvd_cover.thumbnails.each do |thumb| -%>
<p><%= thumb.thumbnail.to_s.humanize %>:
<%= link_to thumb.filename, thumb.public_filename %></p>
<% end -%>
There are a few things to explain here:
See Also
|