Fly-mart.com has been live now since the first of the year, yeah I know the logo sucks – I’m working on that! Traffic has been steadily growing, though the site is still in beta and is not taking payments, I’ve been very happy so far. One thing that’s been heaven’s sent is HopToad. Despite being diligent with regard to testing, some things slip. HopToad is right there and in one particular incident, I was busy fixing the code before the customer even had a chance to finish his email to support (me). Good stuff!
Anyway, one of the issues I ran into early in the project was the inability on Heroku’s app environment to write to the file system. If you’re using “paperclip” – this is kind of an issue, though it’s relatively easy to work around, Paperclip can easily integrate with Amazon S3. The big problem though came with testing. For some, still unexplained reason, I was never able to get my Cucumber or RSpec tests to successfully connect to the S3 service, I consistently got a connection exception. I didn’t really think this was worth chasing down as ultimately I really didn’t want the tests to hit S3 directly. My first thought was to use some sort of web tier mock to trick the code into thinking it was talking to S3. There were a few examples I found regarding how to get this to work but again I had little luck with my own codebase. I’m not sure it was because I choose R3 or some other reason, but again no joy.
I was thinking that what I really wanted was the ability to define different behavior depending on the environment. For example, production and development I wanted actual S3 calls and for testing I would be perfectly happy to have my Paperclip image files land on the local hard drive. I had a conversation with a good friend, Ryan Daigle who agreed with the approach and could also offer some code to get me bootstrapped. What follows is the relevant code with commentary.
Step 1 - Modify your app/model/asset.rb class slightly
The thing to notice is that the normal paperclip code that you’d expect in your asset class it replace by the “stores_file_as” helper. Most of the paperclip configuration as well as the mechanism to detect the environment and act accordingly has been moved into there.
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 31 32 33 34 35 36 37 38 39 40 41 42 class Asset < ActiveRecord::Baseinclude Helpers::AssetStoragebefore_validation :clear_assetbelongs_to :ad, :polymorphic => truestores_file_as :image,:styles => {:micro => "75x43", :thumb => "128x95", :medium => "300x300", :large => "500x500"}validates_attachment_presence :imagevalidates_attachment_size :image, :less_than => 1.megabytesvalidates_attachment_content_type :image, :content_type => ['image/jpeg', 'image/gif', 'image/png']def delete_asset=(value)@delete_asset = !value.to_i.zero?enddef delete_asset!!@delete_assetendalias_method :delete_asset?, :delete_assetdef clear_assetself.image = nil if delete_asset? && !image.dirty?endend# == Schema Information## Table name: assets## id :integer not null, primary key# ad_id :integer# created_at :datetime# updated_at :datetime# image_file_name :string(255)# image_content_type :string(255)# image_file_size :integer# image_updated_at :datetime#Step 2 - Add a app/helper/helpers.rb module
The idea here is to delegate the heavy lifting of the paperclip configuration to the helper module but more importantly to detect the environment and configure the appropriate behavior. In my case, I wanted 'dev' and 'prod' to use the actual S3 environment while the test environment should use the local file system. Not only does this keep the charges lower for S3 but has the added benefit of speeding up the tests. Alternatively you could externalize the configuration in a DSL but I really didn't see the point.Happy Hacking, Peter
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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 module Helpers# Conventient way to setup filesystem attachment uploading when in dev/testing# and use S3 otherwisemodule AssetStoragedef self.included(within)within.class_eval doextend ClassMethodsendendmodule ClassMethodsdef stores_file_as(asset_type, opts = {})filename_interpolation = opts[:filename_interpolation] || ':style.:extension'options = use_local_storage? ?filesystem_default_options(filename_interpolation) :s3_default_options(filename_interpolation)options = options.merge(opts)has_attached_file asset_type, optionsendprivatedef use_local_storage?@use_local_storage ||= !(Rails.env.production? || Rails.env.staging?)enddef s3_default_options(filename_interpolation)# Akamai URL -> origin URL -> Rails env# N/A -> origin.staticassets.development.theslap.digitaltoniq.com -> development# staticassets.theslap.digitaltoniq.com -> origin.staticassets.theslap.digitaltoniq.com -> staging# staticassets.beta.theslap.com -> origin.staticassets.beta.theslap.com -> beta# staticassets.theslap.com -> origin.staticassets.theslap.com -> production{:storage => :s3,:s3_credentials => "#{RAILS_ROOT}/config/s3.yml",:path => "/:style/:filename"}enddef filesystem_default_options(filename_interpolation){## Add any local file system options you may want here ...# :url => "/assets/:class/:slug/#{filename_interpolation}",# :path => ":rails_root/public/assets/:class/:slug/#{filename_interpolation}"}endendendend
{ 0 comments }
