Multiple Attachments with Validations In Rails with Paperclip

I wanted a project to allow attachments to be added to tasks. My requirements were that: users should be able to add multiple attachments to a task up to some maximum limit; there should be validation on the size of the attachments (combined with the number limit this should ensure I don’t run out of disk space too fast); and, that there should be some security on downloading a task’s attachments. Paperclip seems to be the popular Rails attachment at the moment and is still under active development, so I hoped to use that. However, it does not handle multiple attachments for a model. There is a plugin PaperclipPolymorph which adds multiple attachments to Paperclip, but I just couldn’t get it to meet my validation and security requirements. In the end I wrote my own solution and this article details it.

Update: This tutorial was completed using Rails 2.1.0, since then there have been a few changes, see the first comment (by Flyc) below. If you are using Rails 2.3+ you will at a minimum need to rename app/controller/appilication.rb to apps/controller/application_controller.rb

Firstly, if you don’t need multiple attachments, take a look at the Paperclip documentation and this webfellas article and you should be fine. Also, if you just need multiple attachments without any of the extra stuff, then PaperclipPolymorph will probably meet your needs. Lastly, I found this article by Brian Getting detailing how to handle multiple attachments with Attachment Fu (a Paperclip alternative). My tutorial closely follows his as I used it as a base for my investigations.

To demonstrate how I met my requirements I will use the example of creating a simple Rails app with a single model class, task, and allow that class to handle multiple attachments. You can download this project here.

Step 1 : Create the project

Here I just create a very simple Rails project called attachments with a single model class Task which contains only title and body attributes. Then I install Paperclip using Git. As I don’t need any of the image based functionality in Paperclip I haven’t installed any of its associated images packages, see the documentation if you think you might need them.

Below is the migration I used to create a table for storing the associations between model objects and their attachments. I have made it polymorphic so it can be used with other model classes. It is called Assets as if it is called Attachments there is an error in some versions of Rails. Use rake db:migrate to create the tables.

To have a standard multiple attachments model like is seen in Brian Getting article and PaperclipPolymorph we just need to add an association to the Task model. In task.rb add the line: has_many :assets, :as => :attachable, :dependent => :destroy.

Step 3: Add Validations

To ensure that there are never more than 5 attachments on a Task and that each attachment is less than a megabyte, add the following lines to task.rb. Putting validations on the Task model allows the Asset model to be reused by other classes if they need multiple attachments. These limits can obviously be easily modified by changing the constants. You will see these constants used throughout the rest of the project, so that the control and display of validations is in a single place.

validate:validate_attachmentsMax_Attachments=5Max_Attachment_Size=1.megabytedefvalidate_attachmentserrors.add_to_base("Too many attachments - maximum is #{Max_Attachments}")ifassets.length>Max_Attachmentsassets.each{|a|errors.add_to_base("#{a.name} is over #{Max_Attachment_Size/1.megabyte}MB")ifa.file_size>Max_Attachment_Size}end

Step 4: Add Security

By default Paperclip stores attachments under the public directory, thus they are generally available to everyone. To secure the attachments Paperclip needs to be told to store the attachments in a different place, and provided with a controller method to retrieve them. To store the attachments in an assets directory directly under the project root, and have the show method on the AssetsController retrieve them, then modify the association in asset.rb to be the following:

This will require the creation of assets_controller.rb as below. I have just put the comment # do security check here in the method where you need to add any security checking as it tends to be application specific. Likely security includes checking the user is logged in or has a particular role or has permission to view the object which has the attachment.

To handle the display for this simple app there is a new page that allows users to add attachments to a task, a show page that displays the attachments for task and an edit page that allows both. To do this some javascript needs to be set up. Also the rest of this tutorial assumes that the image public/images/attachment.png is present and that the css includes the following (I just added it to public/stylesheets/scaffold.css):

I have used a slightly modified version of the multifile javascript library used by Brian Getting and originally by the Stickman. This assumes you are using Prototype and Scriptaculous. It allows users to add a number of files at once which are then stored in page and sent on an update. Create the file public/javascripts/multifile.js as below:

Firstly, to add new attachments to the scaffold new.html.erb the form needs to be updated to include :multipart => true, this allows the attachments to be sent along with the HTTP request. Then the Pending Attachment paragraph creates the input field and MultiSelector from multifile.js handles attachment upload on submit. The empty pending_files will contain references to the attachments. Note the use of the Task constants for the allowable number and size of the attachments.

In the controller the attachments need to be read in and added to the new task. The below code script needs to be added into app/controllers/tasks_controller.rb. Here most of the work is offloaded to the protected process_file_uploads method, so it can be reused in other methods. Before the save or update of a task which may have attachments added to it, call process_file_uploads which just loops through the HTTP parameters and builds any given attachments. They will then be validated and persisted to the database with a save or update.

The show action is a little different – it doesn’t allow any attachments to be added, instead just displaying a list of the current task attachments. Below in app/views/tasks/show.html.erb this is done with a partial.

The partial app/views/tasks/_attachment.html.erb just displays some basic information about the attachment, including a link for it to be downloaded using attachment.url. Remember from Step 4 that this will return the url of the AssetsController’s show method to add a layer of security. I have left the remove link in the partial, this will be explained in the next step.

The attachment.html.erb partial included a link to remove the attachment. This requires the following destroy method added to the assetscontroller.rb. It simply finds the attachment and deletes it. The page is updated through javascript returned to the user’s browser.

The destroy method’s view is app/views/assets/destroy.rjs which using javascript finds the attachment deleted and removes it from the page. It then checks for an attachment add section (by looking for a newfile_data id) and if it finds one updates the number of attachments that may still be added. With this check the RJS can still be used without error on pages that do not have a newfile_data input nor a MultiSelector.