require 'aws/s3' module Imagery module S3 def self.included(base) base.extend Configs class << base attr_writer :s3_bucket, :s3_distribution_domain, :s3_host end end module Configs def s3_bucket(bucket = nil) @s3_bucket = bucket if bucket @s3_bucket || raise(UndefinedBucket, BUCKET_ERROR_MSG) end BUCKET_ERROR_MSG = (<<-MSG).gsub(/^ {6}/, '') You need to define a bucket name. Example: class Imagery::Model include Imagery::S3 s3_bucket 'my-bucket-name' end MSG def s3_distribution_domain(domain = nil) @s3_distribution_domain = domain if domain @s3_distribution_domain end # Allows you to customize the S3 host. Usually happens when you use # amazon S3 EU. # # @param [String] host the custom host you want to use instead. # @return [String] the s3 host currently set. def s3_host(host = nil) @s3_host = host if host @s3_host || S3_HOST end end UndefinedBucket = Class.new(StandardError) S3_HOST = "http://s3.amazonaws.com" # Returns a url publicly accessible. If a distribution domain is set, # then the url will be based on that. # # @example # # class Imagery::Model # include Imagery::S3 # # s3_bucket 'bucket-name' # end # # Photo = Class.new(Struct.new(:id)) # i = Imagery.new(Photo.new(1001)) # # i.url == 'http://s3.amazonaws.com/bucket-name/photo/1001/original.png' # # => true # # Imagery::Model.s3_distribution_domain = 'assets.site.com' # i.url == 'http://assets.site.com/photo/1001/original.png' # # => true # # # You may also subclass this of course since it's just a ruby object # # and configure them differently as needed. # # class CloudFront < Imagery::Model # include Imagery::S3 # s3_bucket 'cloudfront' # s3_distribution_domain 'assets.site.com' # end # # class RegularS3 < Imagery::Model # include Imagery::S3 # s3_bucket 'cloudfront' # end # # @param [Symbol] size the preferred size you want for the url. # @return [String] the size specific url. def url(size = self.default_size) if domain = self.class.s3_distribution_domain [domain, namespace, key, filename(size)].join('/') else [self.class.s3_host, self.class.s3_bucket, namespace, key, filename(size)].join('/') end end # In addition to saving the files and resizing them locally, uploads all # the different sizes to amazon S3. def save(io) if super s3_object_keys.each do |key, size| Gateway.store(key, File.open(file(size)), self.class.s3_bucket, :access => :public_read, :content_type => "image/png" ) end end end # Deletes all the files related to this Imagery instance and also # all the S3 keys. def delete super s3_object_keys.each do |key, size| Gateway.delete key, self.class.s3_bucket end end protected def s3_object_keys sizes.keys.map do |size| [[namespace, key, filename(size)].join('/'), size] end end module Gateway # A wrapper for AWS::S3::S3Object.store. Basically auto-connects # using the environment vars. # # @example # # AWS::S3::Base.connected? # # => false # # Imagery::S3::Gateway.store( # 'avatar', File.open('avatar.jpg'), 'bucket' # ) # AWS::S3::Base.connected? # # => true # def store(*args) execute(:store, *args) end module_function :store # A wrapper for AWS::S3::S3Object.delete. Basically auto-connects # using the environment vars. # # @example # # AWS::S3::Base.connected? # # => false # # Imagery::S3::Gateway.delete('avatar', 'bucket') # AWS::S3::Base.connected? # # => true def delete(*args) execute(:delete, *args) end module_function :delete private def execute(command, *args) begin AWS::S3::S3Object.__send__(command, *args) rescue AWS::S3::NoConnectionEstablished AWS::S3::Base.establish_connection!( :access_key_id => ENV["AMAZON_ACCESS_KEY_ID"], :secret_access_key => ENV["AMAZON_SECRET_ACCESS_KEY"] ) retry end end module_function :execute end end end