Not long ago, I needed a way for users to upload files and store them in a database. The platform for the application was Ruby on Rails, and I wanted to share my experience here.
The first thing we want to do is generate the table for the attachments:
script/generate model Attachment
exists app/models/
exists test/unit/
exists test/fixtures/
create app/models/attachment.rb
create test/unit/attachment_test.rb
create test/fixtures/attachments.yml
create db/migrate
create db/migrate/001_create_attachments.rb
This line does a number of things for us, including generating the model class, an associated unit test and fixture, as well as a migration class. The next thing we want to do is fill out the migration class so that we can create the database table. In our case, we will need a filename, content_type and attachment column.
class CreateAttachments < ActiveRecord::Migration
def self.up
create_table :attachments do |t|
t.column :filename, :string
t.column :content_type, :string
t.column :data, :binary
end
end
def self.down
drop_table :attachments
end
end
Let’s now run this migration to create the table in our database:
rake db:migrate
(in /Users/mattb/Projects/Attachments)
== CreateAttachments: migrating ===============================================
-- create_table(:attachments)
-> 0.0218s
== CreateAttachments: migrated (0.0220s) ======================================
The next thing that we’ll need to do is to create a controller to process the submitted attachment.
script/generate controller Attachments show create
exists app/controllers/
exists app/helpers/
create app/views/attachments
exists test/functional/
create app/controllers/attachments_controller.rb
create test/functional/attachments_controller_test.rb
create app/helpers/attachments_helper.rb
create app/views/attachments/show.rhtml
create app/views/attachments/create.rhtml
class AttachmentsController < ApplicationController
def show
@attachment = Attachment.find(params[:id])
send_data @attachment.data, :filename => @attachment.filename, :type => @attachment.content_type
end
def create
return if params[:attachment].blank?
@attachment = Attachment.new
@attachment.uploaded_file = params[:attachment]
if @attachment.save
flash[:notice] = "Thank you for your submission..."
redirect_to :action => "index"
else
flash[:error] = "There was a problem submitting your attachment."
render :action => "new"
end
end
end
The relevant snippet of the view code that utilizes this is in new.rhtml:
<% form_tag 'create', :multipart => true do %>
<%= file_field_tag 'attachment' %>
<%= submit_tag "Send Attachment" %>
<% end %>
The important parts of the view are the multipart declaration; without this, your file will not be submitted. Also, we take advantage of the file_field_tag helper method to output the file browser.
If we run the application now and try to upload a file, we will be presented with an error, because the uploaded_file method does not exist on the Attachment model. Let’s complete our model.
class Attachment < ActiveRecord::Base
def uploaded_file=(incoming_file)
self.filename = incoming_file.original_filename
self.content_type = incoming_file.content_type
self.data = incoming_file.read
end
def filename=(new_filename)
write_attribute("filename", sanitize_filename(new_filename))
end
private
def sanitize_filename(filename)
#get only the filename, not the whole path (from IE)
just_filename = File.basename(filename)
#replace all non-alphanumeric, underscore or periods with underscores
just_filename.gsub(/[^\w\.\-]/, '_')
end
end
The meat of this model is the uploaded_file method, which the controller calls, passing the uploaded file. This method is responsible for mapping the incoming file to the attributes expected by our database schema. Additionally, we’re sanitizing the filename, so that we are not only getting just the filename, but we’re also cleaning up any extra characters and replacing them with underscores.
I’ll leave it as an exercise for the reader to associate the attachment with a given user. The “magic” of Rails makes this really straightforward.