Using Paperclip for attachments

Doing something more than a toy example

This is not as straightforward as it seems.

In development, it works fine. Simply declare that the model has an attachment:

class Download < ActiveRecord::Base
        ...
        has_attached_file :attachment

and stuff will work automagically. A directory “system” will be created in your public folder and the attachments will be created in there in a nested fashion.

Your templates can give a link to file, for downloads etc, like this:

Download this file
<a href="#{this.attachment}">here</a>       # in DRYML
<%= link_to "PDF" , this.attachment.url %>  # in Erb

'attachment' or 'attachment.url' seem to be synonyms, as far as passing back an url that points to the physical file.

Stuff gets weird when you move beyond this simple case. For a start, if you're serving from a suburl, you need to tell Paperclip about this. In config/production.rb, do something like this:

ENV["DOWNLOAD_ATTACHMENT_URL"] =
        '/saird/system/:class/:attachment/:id/:basename.:extension'

That is, return an url that points into the public folder with the appropriate path. The symbols in the string are interpolated, so you end up with '/saird/system/mymodel/myattachment/foo.txt'.

Of course, this url has to use the appropriate set of directories / symbols / path that Paperclip is using to store the file. Should you need to change the location of those, or want to change the nested hierarchy, you need to set the path explicitly. Again, in config/production.rb:

ENV["DOWNLOAD_ATTACHMENT_PATH"] =
':rails_root/public/system/:class/:attachment/:id/:basename.:extension'

And then the model details are modified:

has_attached_file :attachment, :path =>
        ENV.fetch('DOWNLOAD_ATTACHMENT_PATH', ':rails_root/public:url'),
        :url => ENV.fetch('DOWNLOAD_ATTACHMENT_URL',
        '/system/:attachment/:id/:style/:filename')

(Ignore the default details as being peculiar to my dev setup.)

So, what if you don't want the attachments to be stored in the public directory? In the HPA case, we have two separate instances in different locations serving off the same db. If they store files in public, the two will become unsynchronised. Some files will be with one instance and some with the other. So have them use a single central location, right?

Here's the rub: Paperclip doesn't really handle that. Paperclip wants to assume that your stuff is in the public directory.

(This appears to be because they're all using Capistrano or some setup where public/system is sym-linked to central file storage. So, the problem has already been solved for them.)

So let's assume I've been given central storage at ”/home/saird”. I modify the path setting to be:

ENV["DOWNLOAD_ATTACHMENT_PATH"] =
        '/home/saird/system/:class/:attachment/:id/:basename.:extension'

But what's the url to that location? I naively assumed that Paperclip was doing some clever routing (e.g. any requests that come in are mapped to the actual filepath on the system). No. No, Paperclip just takes the url and looks for the appropriate file in the public directory.

Right, so I put a symbolic link in the public folder that points at the central storage:

% ln -s /home/saird public/storage

and set the url as:

ENV["DOWNLOAD_ATTACHMENT_URL"] =
        '/saird/storage/system/:class/:attachment/:id/:basename.:extension'

i.e. prefix it with the suburl appropriately and then route it through the symbolic link to the central directory.

You might be tempted to use the symlink for the path setting as well, but this resulted in a permissions error. This may be related to our server setup however.

Note: another peculiar error resulted when the file storage was given just as the directory in which it would be created, e.g. '/home/saird', as some examples seemed to imply. Here it seemed that Paperclip tried to copy the dir and corrupted all its permissions. Charming.