×Welcome to Q&A for professional and enthusiast programmers — check out the FAQ!

I have a rails application that has multiple models with paperclip attachments that are all uploaded to S3. This app also has a large test suite that is run quite often. The downside with this is that a ton of files are uploaded to our S3 account on every test run, making the test suite run slowly. It also slows down development a bit, and requires you to have an internet connection in order to work on the code.

Is there a reasonable way to set the paperclip storage mechanism based on the Rails environment? Ideally, our test and development environments would use the local filesystem storage, and the production environment would use S3 storage.

I'd also like to extract this logic into a shared module of some kind, since we have several models that will need this behavior. I'd like to avoid a solution like this inside of every model:

### We don't want to do this in our models...
if Rails.env.production?
  has_attached_file
:image, :styles => {...},
                   
:path => "images/:uuid_partition/:uuid/:style.:extension",
                   
:storage => :s3,
                   
:url => ':s3_authenticated_url', # generates an expiring url
                   
:s3_credentials => File.join(Rails.root, 'config', 's3.yml'),
                   
:s3_permissions => 'private',
                   
:s3_protocol => 'https'
else
  has_attached_file
:image, :styles => {...},
                   
:storage => :filesystem
                   
# Default :path and :url should be used for dev/test envs.
end

Update: The sticky part is that the attachment's :path and :url options need to differ depending on which storage system is being used.

Any advice or suggestions would be greatly appreciated! :-)

link|edit|flag

3 Answers

I like Barry's suggestion better and there's nothing keeping you from setting the variable to a hash, that can then be merged with the paperclip options.

In config/environments/development.rb and test.rb set something like

PAPERCLIP_STORAGE_OPTIONS = {}

And in config/environments/production.rb

PAPERCLIP_STORAGE_OPTIONS = {:storage => :s3, 
                               
:s3_credentials => "#{Rails.root}/config/s3.yml",
                               
:path => "/:style/:filename"}

Finally in your paperclip model:

has_attached_file :image, {
   
:styles => {:thumb => '50x50#', :original => '800x800>'}
}.merge(PAPERCLIP_STORAGE_OPTIONS)
link|edit|flag
up vote 5 down vote accepted

After playing around with it for a while, I came up with a module that does what I want.

Inside app/models/shared/attachment_helper.rb:

module Shared
 
module AttachmentHelper

   
class << self
     
def included(base)
       
base.extend ClassMethods
     
end
   
end

   
module ClassMethods
     
def has_attachment(name, options = {})

       
# generates a string containing the singular model name and the pluralized attachment name.
       
# Examples: "user_avatars" or "asset_uploads" or "message_previews"
        attachment_owner    
= self.table_name.singularize
        attachment_folder  
= "#{attachment_owner}_#{name.to_s.pluralize}"

       
# we want to create a path for the upload that looks like:
       
# message_previews/00/11/22/001122deadbeef/thumbnail.png
        attachment_path    
= "#{attachment_folder}/:uuid_partition/:uuid/:style.:extension"

       
if Rails.env.production?
          options
[:path]            ||= attachment_path
          options
[:storage]         ||= :s3
          options
[:url]             ||= ':s3_authenticated_url'
          options
[:s3_credentials]  ||= File.join(Rails.root, 'config', 's3.yml')
          options
[:s3_permissions]  ||= 'private'
          options
[:s3_protocol]     ||= 'https'
       
else
         
# For local Dev/Test envs, use the default filesystem, but separate the environments
         
# into different folders, so you can delete test files without breaking dev files.
          options
[:path]  ||= ":rails_root/public/system/attachments/#{Rails.env}/#{attachment_path}"
          options
[:url]   ||= "/system/attachments/#{Rails.env}/#{attachment_path}"
       
end

       
# pass things off to paperclip.
        has_attached_file name
, options
     
end
   
end
 
end
end

(Note: I'm using some custom paperclip interpolations above, like :uuid_partition, :uuid and :s3_authenticated_url. You'll need to modify things as needed for your particular application)

Now, for every model that has paperclip attachments, you just have to include this shared module, and call the has_attachment method (instead of paperclip's has_attached_file)

An example model file: app/models/user.rb:

class User < ActiveRecord::Base
  include
Shared::AttachmentHelper  
  has_attachment
:avatar, :styles => { :thumbnail => "100x100>" }
end

With this in place, you'll have files saved to the following locations, depending on your environment:

Development:

RAILS_ROOT + public/attachments/development/user_avatars/aa/bb/cc/aabbccddeeff/thumbnail.jpg

Test:

RAILS_ROOT + public/attachments/test/user_avatars/aa/bb/cc/aabbccddeeff/thumbnail.jpg

Production:

https://s3.amazonaws.com/your-bucket-name/user_avatars/aa/bb/cc/aabbccddeeff/thumbnail.jpg

This does exactly what I'm looking for, hopefully it'll prove useful to someone else too. :)

-John

link|edit|flag
Good work. Yes, a lot more abstraction was needed than I provided! :) – Barry Hess May 14 '10 at 18:42
Very good work. Thanks. It really helped me. – Slobodan Kovacevic Feb 17 at 17:04

Couldn't you just set an environment variable in production/test/development.rb?

PAPERCLIP_STORAGE_MECHANISM = :s3

Then:

has_attached_file :image, :styles => {...},
                 
:storage => PAPERCLIP_STORAGE_MECHANISM,
                 
# ...etc...
link|edit|flag
Hey Barry, That's a good suggestion, but the various options inside of the "...etc..." cause trouble. I discovered that the :path and :url options need to differ depending on whether it's using :s3 or :filesystem storage. I'll update the question with a better example. Thanks, --John – John Reilly Apr 1 '10 at 19:42

Your Answer

 
or
never shown

Not the answer you're looking for? Browse other questions tagged or ask your own question.