module Technoweenie # :nodoc: module AttachmentFu # :nodoc: module Backends # = CloudFiles Storage Backend # # Enables use of {Rackspace Cloud Files}[http://www.mosso.com/cloudfiles.jsp] as a storage mechanism # # Based heavily on the Amazon S3 backend. # # == Requirements # # Requires the {Cloud Files Gem}[http://www.mosso.com/cloudfiles.jsp] by Rackspace # # == Configuration # # Configuration is done via RAILS_ROOT/config/rackspace_cloudfiles.yml and is loaded according to the RAILS_ENV. # The minimum connection options that you must specify are a container name, your Mosso login name and your Mosso API key. # You can sign up for Cloud Files and get access keys by visiting https://www.mosso.com/buy.htm # # Example configuration (RAILS_ROOT/config/rackspace_cloudfiles.yml) # # development: # container_name: appname_development # username: # api_key: # # test: # container_name: appname_test # username: # api_key: # # production: # container_name: appname # username: # apik_key: # # You can change the location of the config path by passing a full path to the :cloudfiles_config_path option. # # has_attachment :storage => :cloud_files, :cloudfiles_config_path => (RAILS_ROOT + '/config/mosso.yml') # # === Required configuration parameters # # * :username - The username for your Rackspace Cloud (Mosso) account. Provided by Rackspace. # * :secret_access_key - The api key for your Rackspace Cloud account. Provided by Rackspace. # * :container_name - The name of a container in your Cloud Files account. # # If any of these required arguments is missing, a AuthenticationException will be raised from CloudFiles::Connection. # # == Usage # # To specify Cloud Files as the storage mechanism for a model, set the acts_as_attachment :storage option to :cloud_files/tt>. # # class Photo < ActiveRecord::Base # has_attachment :storage => :cloud_files # end # # === Customizing the path # # By default, files are prefixed using a pseudo hierarchy in the form of :table_name/:id, which results # in Cloud Files object names (and urls) that look like: http://:server/:container_name/:table_name/:id/:filename with :table_name # representing the customizable portion of the path. You can customize this prefix using the :path_prefix # option: # # class Photo < ActiveRecord::Base # has_attachment :storage => :cloud_files, :path_prefix => 'my/custom/path' # end # # Which would result in public URLs like http(s)://:server/:container_name/my/custom/path/:id/:filename. # # === Permissions # # File permisisons are determined by the permissions of the container. At present, the options are public (and distributed # by the Limelight CDN), and private (only available to your login) # # === Other options # # Of course, all the usual configuration options apply, such as content_type and thumbnails: # # class Photo < ActiveRecord::Base # has_attachment :storage => :cloud_files, :content_type => ['application/pdf', :image], :resize_to => 'x50' # has_attachment :storage => :cloud_files, :thumbnails => { :thumb => [50, 50], :geometry => 'x50' } # end # # === Accessing Cloud Files URLs # # You can get an object's public URL using the cloudfiles_url accessor. For example, assuming that for your postcard app # you had a container name like 'postcard_world_development', and an attachment model called Photo: # # @postcard.cloudfiles_url # => http://cdn.cloudfiles.mosso.com/c45182/uploaded_files/20/london.jpg # # The resulting url is in the form: http://:server/:container_name/:table_name/:id/:file. # The optional thumbnail argument will output the thumbnail's filename (if any). # # Additionally, you can get an object's base path relative to the container root using # base_path: # # @photo.file_base_path # => uploaded_files/20 # # And the full path (including the filename) using full_filename: # # @photo.full_filename # => uploaded_files/20/london.jpg # # Niether base_path or full_filename include the container name as part of the path. # You can retrieve the container name using the container_name method. module CloudFileBackend class RequiredLibraryNotFoundError < StandardError; end class ConfigFileNotFoundError < StandardError; end def self.included(base) #:nodoc: mattr_reader :container_name, :cloudfiles_config begin require 'cloudfiles' rescue LoadError raise RequiredLibraryNotFoundError.new('CloudFiles could not be loaded') end begin @@cloudfiles_config_path = base.attachment_options[:cloudfiles_config_path] || (RAILS_ROOT + '/config/rackspace_cloudfiles.yml') @@cloudfiles_config = @@cloudfiles_config = YAML.load(ERB.new(File.read(@@cloudfiles_config_path)).result)[RAILS_ENV].symbolize_keys rescue #raise ConfigFileNotFoundError.new('File %s not found' % @@cloudfiles_config_path) end @@container_name = @@cloudfiles_config[:container_name] @@cf = CloudFiles::Connection.new(@@cloudfiles_config[:username], @@cloudfiles_config[:api_key]) @@container = @@cf.container(@@container_name) base.before_update :rename_file end # Overwrites the base filename writer in order to store the old filename def filename=(value) @old_filename = filename unless filename.nil? || @old_filename write_attribute :filename, sanitize_filename(value) end # The attachment ID used in the full path of a file def attachment_path_id ((respond_to?(:parent_id) && parent_id) || id).to_s end # The pseudo hierarchy containing the file relative to the container name # Example: :table_name/:id def base_path File.join(attachment_options[:path_prefix], attachment_path_id) end # The full path to the file relative to the container name # Example: :table_name/:id/:filename def full_filename(thumbnail = nil) File.join(base_path, thumbnail_name_for(thumbnail)) end # All public objects are accessible via a GET request to the Cloud Files servers. You can generate a # url for an object using the cloudfiles_url method. # # @photo.cloudfiles_url # # The resulting url is in the CDN URL for the object # # The optional thumbnail argument will output the thumbnail's filename (if any). # # If you are trying to get the URL for a nonpublic container, nil will be returned. def cloudfiles_url(thumbnail = nil) if @@container.public? File.join(@@container.cdn_url, full_filename(thumbnail)) else nil end end alias :public_filename :cloudfiles_url def create_temp_file write_to_temp_file current_data end def current_data @@container.get_object(full_filename).data end protected # Called in the after_destroy callback def destroy_file @@container.delete_object(full_filename) end def rename_file # Cloud Files doesn't rename right now, so we'll just nuke. return unless @old_filename && @old_filename != filename old_full_filename = File.join(base_path, @old_filename) @@container.delete_object(old_full_filename) @old_filename = nil true end def save_to_storage if save_attachment? @object = @@container.create_object(full_filename) @object.write((temp_path ? File.open(temp_path) : temp_data)) end @old_filename = nil true end end end end end