module Paperclip module Storage # fog is a modern and versatile cloud computing library for Ruby. # Among others, it supports Amazon S3 to store your files. In # contrast to the outdated AWS-S3 gem it is actively maintained and # supports multiple locations. # Amazon's S3 file hosting service is a scalable, easy place to # store files for distribution. You can find out more about it at # http://aws.amazon.com/s3 There are a few fog-specific options for # has_attached_file, which will be explained using S3 as an example: # * +fog_credentials+: Takes a Hash with your credentials. For S3, # you can use the following format: # aws_access_key_id: '' # aws_secret_access_key: '' # provider: 'AWS' # region: 'eu-west-1' # scheme: 'https' # * +fog_directory+: This is the name of the S3 bucket that will # store your files. Remember that the bucket must be unique across # all of Amazon S3. If the bucket does not exist, Paperclip will # attempt to create it. # * +fog_file+: This can be hash or lambda returning hash. The # value is used as base properties for new uploaded file. # * +path+: This is the key under the bucket in which the file will # be stored. The URL will be constructed from the bucket and the # path. This is what you will want to interpolate. Keys should be # unique, like filenames, and despite the fact that S3 (strictly # speaking) does not support directories, you can still use a / to # separate parts of your file name. # * +fog_public+: (optional, defaults to true) Should the uploaded # files be public or not? (true/false) # * +fog_host+: (optional) The fully-qualified domain name (FQDN) # that is the alias to the S3 domain of your bucket, e.g. # 'http://images.example.com'. This can also be used in # conjunction with Cloudfront (http://aws.amazon.com/cloudfront) # * +fog_options+: (optional) A hash of options that are passed # to fog when the file is created. For example, you could set # the multipart-chunk size to 100MB with a hash: # { :multipart_chunk_size => 104857600 } module Fog def self.extended(base) unless defined?(Fog) begin require "fog" rescue LoadError => e e.message << " (You may need to install the fog gem)" raise e end end base.instance_eval do unless @options[:url].to_s.match(/\A:fog.*url\z/) @options[:path] = @options[:path].gsub(/:url/, @options[:url]).gsub(/\A:rails_root\/public\/system\//, "") @options[:url] = ":fog_public_url" end unless Paperclip::Interpolations.respond_to? :fog_public_url Paperclip.interpolates(:fog_public_url) do |attachment, style| attachment.public_url(style) end end end end AWS_BUCKET_SUBDOMAIN_RESTRICTON_REGEX = /\A(?:[a-z]|\d(?!\d{0,2}(?:\.\d{1,3}){3}\z))(?:[a-z0-9]|\.(?![\.\-])|\-(?![\.])){1,61}[a-z0-9]\z/.freeze def exists?(style = default_style) if original_filename !!directory.files.head(path(style)) else false end end def fog_credentials @fog_credentials ||= parse_credentials(@options[:fog_credentials]) end def fog_file @fog_file ||= begin value = @options[:fog_file] if !value {} elsif value.respond_to?(:call) value.call(self) else value end end end def fog_public(style = default_style) if @options.key?(:fog_public) value = @options[:fog_public] if value.respond_to?(:key?) && value.key?(style) value[style] elsif value.respond_to?(:call) value.call(self) else value end else true end end def flush_writes @queued_for_write.each do |style, file| log("saving #{path(style)}") retried = false begin attributes = fog_file.merge( body: file, key: path(style), public: fog_public(style), content_type: file.content_type ) attributes.merge!(@options[:fog_options]) if @options[:fog_options] directory.files.create(attributes) rescue Excon::Errors::NotFound raise if retried retried = true directory.save file.rewind retry ensure file.rewind end end after_flush_writes # allows attachment to clean up temp files @queued_for_write = {} end def flush_deletes @queued_for_delete.each do |path| log("deleting #{path}") directory.files.new(key: path).destroy end @queued_for_delete = [] end def public_url(style = default_style) if @options[:fog_host] "#{dynamic_fog_host_for_style(style)}/#{path(style)}" else if fog_credentials[:provider] == "AWS" "#{scheme}://#{host_name_for_directory}/#{path(style)}" else directory.files.new(key: path(style)).public_url end end end def expiring_url(time = (Time.now + 3600), style_name = default_style) time = convert_time(time) http_url_method = "get_#{scheme}_url" if path(style_name) && directory.files.respond_to?(http_url_method) expiring_url = directory.files.public_send(http_url_method, path(style_name), time) if @options[:fog_host] expiring_url.gsub!(/#{host_name_for_directory}/, dynamic_fog_host_for_style(style_name)) end else expiring_url = url(style_name) end expiring_url end def parse_credentials(creds) creds = find_credentials(creds).stringify_keys (creds[RailsEnvironment.get] || creds).symbolize_keys end def copy_to_local_file(style, local_dest_path) log("copying #{path(style)} to local file #{local_dest_path}") ::File.open(local_dest_path, "wb") do |local_file| file = directory.files.get(path(style)) return false unless file local_file.write(file.body) end rescue ::Fog::Errors::Error => e warn("#{e} - cannot copy #{path(style)} to local file #{local_dest_path}") false end private def convert_time(time) time = Time.now + time if time.is_a?(Integer) time end def dynamic_fog_host_for_style(style) if @options[:fog_host].respond_to?(:call) @options[:fog_host].call(self) else @options[:fog_host] =~ /%d/ ? @options[:fog_host] % (path(style).hash % 4) : @options[:fog_host] end end def host_name_for_directory if directory_name.to_s =~ Fog::AWS_BUCKET_SUBDOMAIN_RESTRICTON_REGEX "#{directory_name}.s3.amazonaws.com" else "s3.amazonaws.com/#{directory_name}" end end def find_credentials(creds) case creds when File YAML::safe_load(ERB.new(File.read(creds.path)).result) when String, Pathname YAML::safe_load(ERB.new(File.read(creds)).result) when Hash creds else if creds.respond_to?(:call) creds.call(self) else raise ArgumentError, "Credentials are not a path, file, hash or proc." end end end def connection @connection ||= ::Fog::Storage.new(fog_credentials) end def directory @directory ||= connection.directories.new(key: directory_name) end def directory_name if @options[:fog_directory].respond_to?(:call) @options[:fog_directory].call(self) else @options[:fog_directory] end end def scheme @scheme ||= fog_credentials[:scheme] || "https" end end end end