home about

ActiveUpload - Easy File Uploads in Rails

July 6th, 2007 cpetersen

Introduction

Our goal was to create a Rails plugin that allows the user to upload multiple files to a website, quickly and easily. This plugin should also give the developer control over what is uploaded, such as maximum filesize allowed, and what filetypes are acceptable. To accomplish this goal, we created the ActiveUpload project. After researching some alternatives, we settled on a Flash application called SWFUpload. SWFUpload is a Flash movie that was written using FlashDevelop. It is a Flash movie that runs on the client, and handles the client end of the uploads. However, the movie itself is invisible, it is styled with HTML and works via a series of JavaScript callbacks. It is simple yet flexible for the developer, and the user interface has all the functionality we require. SWFUpload is licensed under the MIT license and Flash Develop is free. For a demonstration of SWFUpload, visit their site.

Design

We need to design a Rails plugin that integrates SWFUpload with a Rails application. Our plugin will consist of three parts.
  1. A Model for storing the files meta data in the database.
  2. A Controller for accepting the uploaded files and manipulating the model.
  3. A FormBuilder extension for rendering the User Interface.

The Model

We started with the model. We knew we would need to store basic information about the file, such as filename and size. We also knew that we would want to attach this model to other models. We decided on a single table, single model design, which stores the filename and size, and attaches to other models using a Polymorphic Association. The migration script looks like:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class CreateAttachments < ActiveRecord::Migration
  def self.up
    create_table :attachments, :force => true do |t|
      t.column :filename,        :string
      t.column :size,            :integer
      t.column :attachable_id,   :integer
      t.column :attachable_type, :string
      t.column :created_at,      :datetime
    end
  end

  def self.down
    drop_table :attachments
  end
end
and the model looks like:
1
2
3
4
5
6
7
8
9
10
11
12
13
class Attachment < ActiveRecord::Base
  belongs_to :attachable, :polymorphic => true

  def dirname
    padded_id = sprintf("%6.6d", id)
    dirnames = padded_id.match("(...)(...)")
    dirname = "public/attachments/#{dirnames[1]}/#{dirnames[2]}"
  end

  def path
    "#{dirname}/#{filename}"
  end
end

The Controller

Next we decided to tackle the controller. The attachments controller has to do three things:
  1. Upload the file
  2. Create the associated entry in the database
  3. Allow a user to download the file
After many attempts at getting the controller to do this, we settled on a controller with three methods.
  1. create
  2. upload
  3. show
You are probably wondering why create and upload are separate methods. The short answer is, if you create the attachment model inside of upload, there is no way to get the newly created id back out to the web page where you will be creating (or updating) one of your models that has attachments. The long answer, well if you want the long answer, read the "Modifying SWFUpload" section. It is remarkably easy to accept a file upload inside of rails, all you have to do is:
1
2
3
file = File.new(filename, "wb")
file.write(params[:Filedata].read)
file.close
The full controller looks like:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
require 'fileutils'

class AttachmentsController < ApplicationController
  def create
    @f = Attachment.new()
    if @f.save
      render :text => "#{@f.id}"
    end
  end

  def upload
    @f = Attachment.find(params[:id])
    @f.filename = params[:Filename]
    @f.size = params[:Filedata].size

    if @f.save
      dirname = @f.dirname
      FileUtils.mkdir_p(dirname)
      file = File.new(@f.path, "wb")
      file.write(params[:Filedata].read)
      file.close
      render :text => "#{@f.id}"
    end
  end

  def show
    attachment = Attachment.find(params[:id])
    send_file attachment.path
  end
end

The FormBuilder

So now we have a model to store the uploaded files meta data and we have a controller to accept the uploaded file and manage the model. Now we need a way of displaying the UI, that is the job of the FormBuilder. The FormBuilder is a little too big to show here, but I encourage you to look it over, you can find it here. Basically the FormBuilder write the HTML for you, that HTML includes:
  1. The hidden input containing all the ids of the attachment models associated with this model
  2. The visible list of uploaded files (with delete javascript)
  3. The JavaScript to display SWFUpload
The most complicated part of the helper is the JavaScript to display the Flash application, it looks like:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<script>
<!--
  var swfu;
  var result;
  window.onload = function() {
    swfu = new SWFUpload({
      upload_script : "/attachments/upload",
      target : "SWFUploadTarget",
      flash_path : "/flash/SWFUpload.swf",
      allowed_filesize : 30720,        // 30 MB
      allowed_filetypes : "*.*",
      allowed_filetypes_description : "All files...",
      browse_link_innerhtml : "Browse",
      upload_link_innerhtml : "Upload queue",
      browse_link_class : "swfuploadbtn browsebtn",
      upload_link_class : "swfuploadbtn uploadbtn",
      flash_loaded_callback : 'swfu.flashLoaded',
      upload_file_queued_callback : "fileQueued",
      upload_file_start_callback : 'uploadFileStart',
      upload_progress_callback : 'uploadProgress',
      upload_file_complete_callback : 'uploadFileComplete',
      upload_file_cancel_callback : 'uploadFileCancelled',
      upload_queue_complete_callback : 'uploadQueueComplete',
      upload_error_callback : 'uploadError',
      upload_cancel_callback : 'uploadCancel',
      auto_upload : true
    });
  };
-->
</script>
That's it, we now have all the pieces we need to accept uploaded files and attach them to any model. Normally I would jump directly into how you use it however, this project had a particularly interesting side note that I want to delve into. Please feel free to skip to the "Using ActiveUpload" section if you just want to get moving.

Modifying SWFUpload

As I alluded to in "The Controller" there are some limitation within SWFUpload. Ideally, SWFUpload would call its upload script and that would be it, that script would take care of creating the model, uploading the file and associating the file to the model you are actually working on. However, since the files are uploaded before you save the model that is impossible. The next best thing would be for the upload script to create the model and upload the file, then return the newly created id to the web page for associating when you save your model. Unfortunately, there is no way to get information from the upload script back into the page. Luckily, SWFUpload has numerous callbacks. We settled on a method by which we create the model in the call back that is called directly before you upload the file, the function is called uploadFileStart. This works great, except there is no way (that I could find) to get information from that callback into the upload script. Essentially, we could either associate the file with the attachment model, or the attachment model with your model, but not both. The solution was to modify the way SWFUpload works. As I mentioned in the Introduction, SWFUpload is open source and FlashDevelop is free, so anyone can modify the source. We examined the code and found where the call back was called:
1
2
3
4
5
6
7
8
9
// Call home E.T. - file obj, file count & file queue length
if(uploadFileStartCallback.length > 0)
        ExternalInterface.call(uploadFileStartCallback, getFileObject(currentFileId, currentFile), position, fileQueueLength);

// Add listener
currentFile.addListener(listener);
                        
// Start upload
currentFile.upload(uploadScript);
You can see the ExternalInterface is what allows SWFUpload to call JavaScript functions. We changed it to the following:
1
2
3
4
5
6
7
8
9
10
11
12
13
// Call home E.T. - file obj, file count & file queue length
if(uploadFileStartCallback.length > 0)
        callbackReturnValue = ExternalInterface.call(uploadFileStartCallback, getFileObject(currentFileId, currentFile), position, fileQueueLength);

// If there was a return value, append it to the uploadScript
if (callbackReturnValue != null)
        uploadScript = uploadScript + callbackReturnValue;

// Add listener
currentFile.addListener(listener);
                        
// Start upload
currentFile.upload(uploadScript);
We wanted to keep the solution as generic as possible, so we simply took the result of the callback (if any) and appended it to upload script. So we created a JavaScript call back that returned "?id=XXX" and we were able to get the id of the attachment model that was created in the call back into the upload script. The call back looks like:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function uploadFileStart(file, position, queuelength) {
  var div = document.getElementById("queueinfo");
  div.innerHTML = "Uploading file " + position + " of " + queuelength;

  var li = document.getElementById(file.id);
  li.className += " fileUploading";

  var id;
  var url = '/attachments/create';
  new Ajax.Request(url, {
    asynchronous: false,
    onComplete: function(transport) {
      id = transport.responseText;
    }
  });

  $('_object_attachment_id').value += ","+id;
}

Using ActiveUpload

There are six fast and easy steps to using ActiveUpload. First install the plugin:
./script/plugin install http://activeupload.googlecode.com/svn/trunk/activeupload/
Second, generate the migration, model and controller, and migrate your database:
./script/generate activeupload
rake db:migrate
Third, add the following lines to any model you wish to attach files to:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  has_many :attachments, :as => :attachable

  def attachment_id=(attachment_id)
    unless attachment_id.blank?
      attachment_ids = attachment_id.split(",")
      attachment_ids.each do |a_id|
        unless a_id.blank?
          attachment = Attachment.find(a_id)
          attachments << attachment
        end
      end
    end
  end

  def attachment_id
    result = ""
    if attachments
      attachments.each do |attachment|
        result << ",#{attachment.id}"
      end
    end
    result
  end
Fourth, add the form helpers to your new and edit views:
1
2
3
4
  <p>
    <b>Attachments</b><br />
    <%= f.attachments_field :attachment_id, { :add => "true", :edit =>"true", :filesize => 30720, :filetypes => [ "*.gif", "*.jpg", "*.png" ] } %>
  </p>
Fifth, add the view helper to your show view:
1
2
3
4
  <p>
    <b>Attachments</b>
    <%= view_attachments_field(@model_name, {})  %>
  </p>
Sixth and lastly, add the JavaScript and Stylesheets to your layout:
1
2
3
4
  <%= javascript_include_tag :defaults %>
  <%= stylesheet_link_tag 'swfupload_theme' %>
  <%= javascript_include_tag "swfupload_callbacks.js" %>
  <%= javascript_include_tag "SWFUpload.js" %>
You should now be able to upload files to your Rails application.

ActiveScaffold

ActiveUpload works well with ActiveScaffold. To get ActiveScaffold to render the correct upload forms you must create a partial form override. Luckily, ActiveUpload comes with one. In the plugin's public directory ($PROJECT/vendor/plugins/activeupload/public/) you will find a file named _attachment_id_form_column.rhtml. Copy that file to your models views directory ($PROJECT/app/views/$MODEL/).
cp vendor/plugins/activeupload/public/_attachment_id_form_column.rhtml app/views/model
Since attachment_id is a virtual column, you will need to tell ActiveScaffold to use it, you can do that by adding the following code to your model's controller:
1
2
3
4
active_scaffold :model_name do |config|
  config.create.columns = [:column1, :column2, :attachment_id]
  config.update.columns = [:column1, :column2, :attachment_id]
end
Finally, you should add the following lines to your attachments_controller.rb:

  active_scaffold

Notes

You will want to periodically cleanse your Attachments table and the associated files. There are two different ways entries can get stuck in there.
  1. A user adds attachments, then fails to save thier model
  2. A user disassociates an attachment with a model
In both these cases entries are made in the table and files are uploaded. Unfortunately, they are completely inaccessible. You can delete any entries from your attachments table that have a empty attachable_id and attachable_type columns. Don't forget to delete the associated files! Additionally, since create and upload are seperate methods, it is theoretically possible to create an entry in the table and not upload the corresponding file. I've set SWFUpload to automatically upload (as opposed to creating a queue) so I've never seen it happen, but it is possible.

33 Responses to “ActiveUpload - Easy File Uploads in Rails”

  1. Philippe Says:
    FlashDevelop is opensource too.
  2. Christopher L Petersen Says:
    Thank you Philippe, you're right, it is open source. I didn't know what license it used when I wrote the post, so I didn't want to jump to conclusions. Thanks!
  3. Shane Vitarana Says:
    Hey Chris- Nice work! I submitted a minor issue to Google Code. Also, I'm working on a single file upload version of this and will send you a patch when done. Btw, you can re-write the attachment_id method as like this
  4. Christopher L Petersen Says:
    Hi Shane, Thanks for submitting that issue, appearently some extra images crept into my CSS, I'll remove them shortly. Also, I like your attachment_id method, I changed the README file to reflect it. I look forward to your single file version, Chris
  5. Jim Says:
    I think I've added the code properly, but upon selecting any file, I'm getting an error 500 regarding the file name, and so when I handle my create method, the plugin is being supplied (in the midst of the regular params) this: "attachment_id"=>", undefined, undefined, undefined", which kills of the attempt to associate the attachments to my model. I was offering up files with spaces, underscores, etc. in the names... is SWFUpload limited to a more classical 8.3 type file name? I'll keep plugging away at this while hoping someone has an answer! :-)
  6. Jim Says:
    Alas, I tried simple file names, test.doc, test.txt and test.pdf. All produced a browser alert of Error Code: HTTP Error, File name: test.pdf, Message: 500 Must be a red-herring... will look for other sources of errors!
  7. Christopher L Petersen Says:
    Hi Jim, It sounds like the Javascript callback is returning "undefined", which would point to a failure in the first Rails method (the one that creates the place holder attachment model). Could you post the applicable portion of your log file? That might help diagnose the problem. Chris
  8. Ivan Vanderbyl Says:
    Hi Could you please upload a working rails application with the code above, I can't seem to get it to work. Thanks, Ivan
  9. Christopher L Petersen Says:
    Hi Ivan, I created an example application, you can find it here: Example ActiveUpload Application I noted the exact steps I took to create it the application. You can find them below. While I was creating this app, I noticed that if Flash isn't installed, the plugin does not degrade nicely. Given the way the forms work, I couldn't make it degrade as nicely as the standard SWFUpload. However, I did at least add a check that alerts the user if flash isn't installed. I hope that helps, please post back here if it gives you any trouble. Chris Steps I took to create the example app: <macro:code> mkdir Upload cd Upload rails . vi config/database.yml ./script/plugin install http://activeupload.googlecode.com/svn/trunk/activeupload/ ./script/generate activeupload ./script/generate scaffold_resource Package name:string rake db:migrate </macro:code> add the following to app/models/package.rb <macro:code lang="ruby"> has_many :attachments, :as => :attachable def attachment_id=(attachment_id) unless attachment_id.blank? attachments.clear attachment_ids = attachment_id.split(",") attachment_ids.each do |a_id| unless a_id.blank? attachment = Attachment.find(a_id) attachments << attachment end end end end def attachment_id result = "" if attachments attachments.each do |attachment| result << ",#{attachment.id}" end end result end </macro:code> add the following to app/views/packages/new.rhtml <macro:code lang="ruby">

    Attachments
    <%= f.attachments_field :attachment_id, { :add => "true", :edit =>"true", :filesize => 30720, :filetypes => [ "*.gif", "*.jpg", "*.png" ] } %>

    </macro:code> add the following to app/views/packages/edit.rhtml <macro:code lang="ruby">

    Attachments
    <%= f.attachments_field :attachment_id, { :add => "true", :edit =>"true", :filesize => 30720, :filetypes => [ "*.gif", "*.jpg", "*.png" ] } %>

    </macro:code> add the following to app/views/packages/show.rhtml <macro:code lang="ruby">

    Attachments <%= view_attachments_field(@package, {}) %>

    </macro:code> add the following to app/views/layouts/packages.rhtml <macro:code lang="ruby"> <%= javascript_include_tag :defaults %> <%= stylesheet_link_tag 'swfupload_theme' %> <%= javascript_include_tag "swfupload_callbacks.js" %> <%= javascript_include_tag "SWFUpload.js" %> </macro:code> <macro:code> ./script/server </macro:code>
  10. Jim Says:
    Hi Chris, I had to strip the SWFUpload stuff out of my app to keep it up. Today I'll create a new app *the same way* and see if I can reproduce the problem. Thanks! Jim
  11. Olivier Says:
    Hi, First of all, many thanks for this plugin ! Jim> You can check your prototype library, i've got the same problem with 500 error and my prototype.js was a 1.5.0_rc0, i changed for 1.5.0 and it's ok now : no more 500 error. logs before changing prototype version (pb with javascript : no values for the query): INSERT INTO attachments (`size`, `filename`, `attachable_type`, `attachable_id`, `created_at`) VALUES(NULL, NULL, NULL, NULL... and : ActiveRecord::RecordNotFound (Couldn't find Attachment with ID=undefined) Maybe it is the same problem for you... Olivier
  12. Aaron Says:
    Does anyone know of a way to combine the SWFupload with the File_Column ? I found that File_Column upload handles the creation/resize/deleting of files to be very smooth between the model and file structure.
  13. ejay Says:
    hi... this project is interesting... can i join the mailing list using my email so as to see future improvements and discusssions on this project.... its great as well as exciting to see that integration with RoR with SWFUpload would be truelly great!!! so as to improve the experience of surfers.... i would appreciate to be part of the mailing list.... thank you... hope to hear more good integrations....
  14. dazza Says:
    hi there, has anyone had any luck integrating this with an exisiting attachment_fu working app? I am getting undefined method `content_type' for "DSCF8784.JPG":String in attachment_fu.rb:257:in `uploaded_data=' and 406 Not Acceptable errors any clues??? cheers dazza
  15. Kevin Says:
    Great work. Thx. I'm facing the ScriptLimits issue with the .SWF file you have though. Did you patch it to overcome that issue? See "Updating ScriptLimits" on the SWFUPload site. Thx again. Hoping to hear back re: this.
  16. Christopher L Petersen Says:
    Hi Dazza, I haven't tried using it with attachment_fu, but I would be really interested if someone had... has anyone out there tried this? Chris
  17. Simone Fumagalli Says:
    Hello. FYI I've installed your plugin but I couldn't get it work on my Dev machine (Ubuntu + FF). I connected with my Laptop (Win + IE6) and everything was fine. This is a kinda strange because the demo on SWFUpload website works well on Ubuntu. Do I have to change some configuration ? Bye
  18. Nao Says:
    Has anyone tested with ActiveScaffold ? I could successfully upload files on IE6, but could not download the files. "Show" link doesn't work well in attachments sub-list. So I have added following helper method to AttachmensHelper.
    module AttachmentsHelper def filename_column(record) link_to record.filename, { :controller => 'attachments', :action => 'show', :id => record.id } end end
    Anyway, anyone knows how to upload with Firefox ? Thanks in advance, Nao
  19. Nate P Says:
    Hey this is a great plugin, but I have a question about the partial form override used with active scaffold. I can't quite seem to figure out how to have the existing attachments appear in the "edit" mode. Looking at the partial form template I don't quite see how it's supposed to be populated with existing uploaded items? If you could point me in the right direction I'd be appreciative! Thanks for your great plugin!
  20. Andrew Says:
    Dazza, I believe that Flash mucks up the MIME headers during upload. I haven't tried this yet, but apparently you can fix this with the mime_fu plugin: http://www.railsontherun.com/2007/6/14/new-rails-plugin-mimetype_fu. Also read the very complete explanation of the MIME problem, among others, here: http://blog.airbladesoftware.com/2007/8/8/uploading-files-with-swfupload
  21. PJ Says:
    When I go to my 'show' page, i get a link to the image that was uploaded. How do i get the actual image itself to be displayed? PJ.
  22. Joe Says:
    Hello, Is there a way to use ActiveUpload to store the attachments in the database? Thanks, Joe
  23. Christopher L Petersen Says:
    Hi Simone, I'm not sure what that would be, if it works on your Mac, and the SWFUpload site works on Ubuntu, I suspect it might have something to do with the Flash security model. Check out this site: http://bob.pythonmac.org/archives/2005/08/07/flash-8s-backwards-security-model/ Hope that helps, Chris
  24. Christopher L Petersen Says:
    Hi PJ, The plugin comes with some helpers that display links to the uploaded file. However, your model should "have many attachments" so you can loop through them and display whatever you like. Additionally, the attachment model has a path attribute that will give you the path the file. Chris
  25. Christopher L Petersen Says:
    Hi Nate P, Good catch, I guess I forgot to update the partial when I was tweaking the code. It took me a little bit to figure it out, but ActiveScaffold populates a variable called @record in the form partial. That gives you access to the current object. I updated the form partial in the project. Its not perfect (removing objects still doesn't quite work) but it should point you in the right direction. Thanks for posting, Chris
  26. Bulak Says:
    Great plug-in. I was looking for a decent file upload plug-in that would work with my current activescaffold install and your code does it neatly. However I am getting an IO Error unless I remove the line 'active_scaffold' from attachments_controller.rb. As I am new to RoR, I am not sure what exactly was not going right but now it works great with my active_scaffold objects. Thanks for creating and sharing.
  27. Kevin Says:
    Has anyone gotten a Flash popup during upload, warning that the upload is taking too long? Does anyone know how to disable this?
  28. Pedro Says:
    Hi Chris, Thanks for the plug-in. I'm experiencing though a 404 error when I integrate the plugin into ActiveScaffold. I'm a newcomer to rails but it looks like the server is not able to find the update action of the attachment controller after the upload is finished. I include the log from webrick: [2007-12-12 17:02:44] ERROR `/attachments/upload' not found. 127.0.0.1 - - [12/Dec/2007:17:02:44 Romance Standard Time] "POST /attachments/upload?id=65 HTTP/1.1" 404 288 - -> /attachments/upload?id=65 Needless to say, the controller and the method are there (the create method for example is resolved) but I don't know where to go from this, I could definitely use some help. I'm using rails 1.2.6 and gems 0.9.4, and I'm pretty sure that I followed all the steps that you mentioned. Regards Pedro
  29. Scott Says:
    I am trying to use activeupload with activescaffold. I can upload the files fine, but I cannot remove them. I click the icon to remove the file, but it does nothing at all. It is performing the javascript method removeAttachment, but cannot find a match on id's. This is because the hidden field _object_attachment_id in _attachment_id_form_column.rhtml has a value of blank and never gets populated with anything. Any ideas how to get this to work? It appears the hidden field value just needs to be populated with all the attachment id's seperated with a comma. But I am not sure how to get access to the attachment_ids from the rhtml file. Thanks!
  30. Jason Says:
    I am having a similar issue to the one mentioned above: INSERT INTO attachments (`size`, `filename`, `attachable_type`, `attachable_id`, `created_at`) VALUES(NULL, NULL, NULL, NULL… Using prototype version 1.5.0. Things upload fine, no errors in the logs to speak of, however these associations aren't stored in the database. Would anyone have an idea on what to look at? Thanks!
  31. Ben Says:
    Do you have any plans on integrating SWFUpload v2 into ActiveUpload? Also, is anyone aware of a description of how to get ActiveUpload working with attachment_fu?
  32. mjsim Says:
    hi everybody i need your help. This amazing program work well in my application in files with 1mb or less but if i try with 10mb or more i get an error that say me if i donīt close the movie script maybe mi computer crash. I think its an error in flash script. Please help me. thanks
  33. David Says:
    Bulak, could you please send me a working rails application that ActiveUpload is integrated into ActiveScaffold ? I tried to integrate and got the IO error, I removed the "active_scaffold" from the Attachment_controller and the problem is gone but I can't remove attachment or see it in the show or edit modes. Please, I need it for my graduation project and I'm working on it for quite a long time, I need something that works. David

Leave a Reply