Uploading files to a database using Rails

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.