Basic File Uploads With Rails

Jul 25, 2006

There are times in your Rails applications when you may want to be able to upload files to make them available from within the application. One way to do this, as shown in the Agile Web Development with Rails book, is to store them in the database. I’m going to show you how to store them on the file system.

The Upload Form

First thing do is create the form for uploading files.

<%= start_form_tag({:action => 'new_upload'}, :multipart => true) %>
Description:<br />
<%= text_field('upload', 'description', :size => '20') %><br />

Choose File:<br />
<%= file_field('upload', 'data', :size => '20') %><br />

<%= submit_tag('Upload') %>
<%= end_form_tag %>

The important things to note here is that I’ve added

:multipart => true
to the form tag. This allows our POST action to properly pass along the binary data from the file field.

The Controller Action

So now we have a way to send a file for upload to the application. We need to be able to accept that data and process it. The action is called “new_upload”, and you’ll notice that I have a request.post? condition in there. As mentioned in Combining Form Actions this saves us the effort of needing separate “new” and “create” actions. Here’s the controller action:

def new_upload
  @upload = Upload.new

  if request.post?
    path = "/files/" + params[:upload][:data].original_filename
    root = "#{RAILS_ROOT}/public" 

    #prevent overwriting
    if Dir[root+path].length > 0
      flash[:notice] = "File already exists!" 
      redirect_to :action => "new_upload" 
      return
    end

    #I'll explain these two lines of code next  
    data = params[:upload][:data].read
    upload(data, root+path)

    @upload.description = params[:upload][:description]
    @upload.path = path

    if @upload.save
      flash[:notice] = "File was successfully uploaded." 
    else
      flash[:notice] = "Upload error!" 
    end

    redirect_to :action => 'uploads'
  end
end

Now you’re not required to do this, but I have an actual Upload model to go along with the binary data, this way I can store the list of what files have been uploaded in the database. The “uploads” action is a listing of the uploaded files with delete links to remove the files if necessary.

Storing The Data

So what’s with these two lines?

data = params[:upload][:data].read
upload(data, root+path)

First, the “read” method reads the actual binary data of the file we uploaded. We still need to save this file to the file system. That’s where the “upload” method comes in.

def upload(data, path)
  File.open(path, 'w') do |f|
    f.write(data)
  end
end

The method will write the file we uploaded to the path we specifed on the file system. If you curious about what f.write does you can find all the specifics in the Ruby docs.

Final Notes

So now we have a reasonably elegant way to handle file uploads. It’s not completely bulletproof, but it will get the job done the vast majority of the time.

One caveat here is that this way of uploading files doesn’t deal particularly well with large files. If the file takes too long to upload it will just time out. I’m still investigating an elegant way of handling large file uploads, as it’s a common problem across the web.