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.
- A Model for storing the files meta data in the database.
- A Controller for accepting the uploaded files and manipulating the model.
- 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:
- Upload the file
- Create the associated entry in the database
- 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.
- create
- upload
- 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:
- The hidden input containing all the ids of the attachment models associated with this model
- The visible list of uploaded files (with delete javascript)
- 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:
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.
- A user adds attachments, then fails to save thier model
- 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.
July 6th, 2007 at 05:59 PM FlashDevelop is opensource too.
July 10th, 2007 at 04:07 AM 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!
July 16th, 2007 at 10:16 AM 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
July 16th, 2007 at 12:16 PM 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
July 22nd, 2007 at 04:45 PM 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! :-)
July 22nd, 2007 at 04:50 PM 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!
July 23rd, 2007 at 02:36 AM 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
July 23rd, 2007 at 07:29 PM Hi Could you please upload a working rails application with the code above, I can't seem to get it to work. Thanks, Ivan
July 24th, 2007 at 06:15 AM 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
</macro:code> add the following to app/views/packages/edit.rhtml <macro:code lang="ruby"><%= f.attachments_field :attachment_id, { :add => "true", :edit =>"true", :filesize => 30720, :filetypes => [ "*.gif", "*.jpg", "*.png" ] } %>
Attachments
</macro:code> add the following to app/views/packages/show.rhtml <macro:code lang="ruby"><%= f.attachments_field :attachment_id, { :add => "true", :edit =>"true", :filesize => 30720, :filetypes => [ "*.gif", "*.jpg", "*.png" ] } %>
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>July 25th, 2007 at 01:54 AM 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
July 30th, 2007 at 07:43 PM 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
August 7th, 2007 at 02:08 PM 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.
August 11th, 2007 at 12:57 AM 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....
September 25th, 2007 at 01:54 AM 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
September 28th, 2007 at 02:56 AM 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.
October 6th, 2007 at 09:45 AM 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
November 1st, 2007 at 04:00 AM 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
November 5th, 2007 at 12:01 AM 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. Anyway, anyone knows how to upload with Firefox ? Thanks in advance, Nao
November 5th, 2007 at 07:48 AM 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!
November 6th, 2007 at 11:06 AM 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
November 7th, 2007 at 10:10 AM 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.
November 13th, 2007 at 11:18 AM Hello, Is there a way to use ActiveUpload to store the attachments in the database? Thanks, Joe
November 17th, 2007 at 04:32 AM 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
November 17th, 2007 at 04:45 AM 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
November 17th, 2007 at 06:37 AM 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
November 25th, 2007 at 05:26 AM 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.
December 12th, 2007 at 12:17 AM Has anyone gotten a Flash popup during upload, warning that the upload is taking too long? Does anyone know how to disable this?
December 12th, 2007 at 01:18 AM 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
January 2nd, 2008 at 06:42 AM 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!
January 3rd, 2008 at 10:14 AM 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!
January 12th, 2008 at 12:02 PM 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?
June 4th, 2008 at 03:22 PM 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
June 4th, 2008 at 03:29 PM 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