module Paperclip
module Storage
# The default place to store attachments is in the filesystem. Files on the local
# filesystem can be very easily served by Apache without requiring a hit to your app.
# They also can be processed more easily after they've been saved, as they're just
# normal files. There is one Filesystem-specific option for has_attached_file.
# * +path+: The location of the repository of attachments on disk. This can (and, in
# almost all cases, should) be coordinated with the value of the +url+ option to
# allow files to be saved into a place where Apache can serve them without
# hitting your app. Defaults to
# ":rails_root/public/:attachment/:id/:style/:basename.:extension"
# By default this places the files in the app's public directory which can be served
# directly. If you are using capistrano for deployment, a good idea would be to
# make a symlink to the capistrano-created system directory from inside your app's
# public directory.
# See Paperclip::Attachment#interpolate for more information on variable interpolaton.
# :path => "/var/app/attachments/:class/:id/:style/:basename.:extension"
module Filesystem
def self.extended base
end
def exists?(style = default_style)
if original_filename
File.exist?(path(style))
else
false
end
end
# Returns representation of the data of the file assigned to the given
# style, in the format most representative of the current storage.
def to_file style = default_style
@queued_for_write[style] || (File.new(path(style), 'rb') if exists?(style))
end
alias_method :to_io, :to_file
def flush_writes #:nodoc:
logger.info("[paperclip] Writing files for #{name}")
@queued_for_write.each do |style, file|
file.close
FileUtils.mkdir_p(File.dirname(path(style)))
logger.info("[paperclip] -> #{path(style)}")
FileUtils.mv(file.path, path(style))
FileUtils.chmod(0644, path(style))
end
@queued_for_write = {}
end
def flush_deletes #:nodoc:
logger.info("[paperclip] Deleting files for #{name}")
@queued_for_delete.each do |path|
begin
logger.info("[paperclip] -> #{path}")
FileUtils.rm(path) if File.exist?(path)
rescue Errno::ENOENT => e
# ignore file-not-found, let everything else pass
end
begin
while(true)
path = File.dirname(path)
FileUtils.rmdir(path)
end
rescue Errno::EEXIST, Errno::ENOTEMPTY, Errno::ENOENT, Errno::EINVAL, Errno::ENOTDIR
# Stop trying to remove parent directories
rescue SystemCallError => e
logger.info("[paperclip] There was an unexpected error while deleting directories: #{e.class}")
# Ignore it
end
end
@queued_for_delete = []
end
end
# 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 S3-specific options for has_attached_file:
# * +s3_credentials+: Takes a path, a File, or a Hash. The path (or File) must point
# to a YAML file containing the +access_key_id+ and +secret_access_key+ that Amazon
# gives you. You can 'environment-space' this just like you do to your
# database.yml file, so different environments can use different accounts:
# development:
# access_key_id: 123...
# secret_access_key: 123...
# test:
# access_key_id: abc...
# secret_access_key: abc...
# production:
# access_key_id: 456...
# secret_access_key: 456...
# This is not required, however, and the file may simply look like this:
# access_key_id: 456...
# secret_access_key: 456...
# In which case, those access keys will be used in all environments. You can also
# put your bucket name in this file, instead of adding it to the code directly.
# This is useful when you want the same account but a different bucket for
# development versus production.
# * +s3_permissions+: This is a String that should be one of the "canned" access
# policies that S3 provides (more information can be found here:
# http://docs.amazonwebservices.com/AmazonS3/2006-03-01/RESTAccessPolicy.html#RESTCannedAccessPolicies)
# The default for Paperclip is "public-read".
# * +s3_protocol+: The protocol for the URLs generated to your S3 assets. Can be either
# 'http' or 'https'. Defaults to 'http' when your :s3_permissions are 'public-read' (the
# default), and 'https' when your :s3_permissions are anything else.
# * +s3_headers+: A hash of headers such as {'Expires' => 1.year.from_now.httpdate}
# * +bucket+: 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. The bucket name will not be interpolated.
# You can define the bucket as a Proc if you want to determine it's name at runtime.
# Paperclip will call that Proc with attachment as the only argument.
# * +url+: There are two options for the S3 url. You can choose to have the bucket's name
# placed domain-style (bucket.s3.amazonaws.com) or path-style (s3.amazonaws.com/bucket).
# Normally, this won't matter in the slightest and you can leave the default (which is
# path-style, or :s3_path_url). But in some cases paths don't work and you need to use
# the domain-style (:s3_domain_url). Anything else here will be treated like path-style.
# * +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.
module S3
def self.extended base
require 'right_aws'
base.instance_eval do
@s3_credentials = parse_credentials(@options[:s3_credentials])
@bucket = @options[:bucket] || @s3_credentials[:bucket]
@bucket = @bucket.call(self) if @bucket.is_a?(Proc)
@s3_options = @options[:s3_options] || {}
@s3_permissions = @options[:s3_permissions] || 'public-read'
@s3_protocol = @options[:s3_protocol] || (@s3_permissions == 'public-read' ? 'http' : 'https')
@s3_headers = @options[:s3_headers] || {}
@url = ":s3_path_url" unless @url.to_s.match(/^:s3.*url$/)
end
base.class.interpolations[:s3_path_url] = lambda do |attachment, style|
"#{attachment.s3_protocol}://s3.amazonaws.com/#{attachment.bucket_name}/#{attachment.path(style).gsub(%r{^/}, "")}"
end
base.class.interpolations[:s3_domain_url] = lambda do |attachment, style|
"#{attachment.s3_protocol}://#{attachment.bucket_name}.s3.amazonaws.com/#{attachment.path(style).gsub(%r{^/}, "")}"
end
ActiveRecord::Base.logger.info("[paperclip] S3 Storage Initalized.")
end
def s3
@s3 ||= RightAws::S3.new(@s3_credentials[:access_key_id],
@s3_credentials[:secret_access_key],
@s3_options)
end
def s3_bucket
@s3_bucket ||= s3.bucket(@bucket, true, @s3_permissions)
end
def bucket_name
@bucket
end
def parse_credentials creds
creds = find_credentials(creds).stringify_keys
(creds[ENV['RAILS_ENV']] || creds).symbolize_keys
end
def exists?(style = default_style)
s3_bucket.key(path(style)) ? true : false
end
def s3_protocol
@s3_protocol
end
# Returns representation of the data of the file assigned to the given
# style, in the format most representative of the current storage.
def to_file style = default_style
@queued_for_write[style] || s3_bucket.key(path(style))
end
alias_method :to_io, :to_file
def flush_writes #:nodoc:
logger.info("[paperclip] Writing files for #{name}")
@queued_for_write.each do |style, file|
begin
logger.info("[paperclip] -> #{path(style)}")
key = s3_bucket.key(path(style))
key.data = file
key.put(nil, @s3_permissions, {'Content-type' => instance_read(:content_type)}.merge(@s3_headers))
rescue RightAws::AwsError => e
raise
end
end
@queued_for_write = {}
end
def flush_deletes #:nodoc:
logger.info("[paperclip] Writing files for #{name}")
@queued_for_delete.each do |path|
begin
logger.info("[paperclip] -> #{path}")
if file = s3_bucket.key(path)
file.delete
end
rescue RightAws::AwsError
# Ignore this.
end
end
@queued_for_delete = []
end
def find_credentials creds
case creds
when File
YAML.load_file(creds.path)
when String
YAML.load_file(creds)
when Hash
creds
else
raise ArgumentError, "Credentials are not a path, file, or hash."
end
end
private :find_credentials
end
# Store files in a database.
#
# Usage is identical to the file system storage version, except:
#
# 1. In your model specify the "database" storage option; for example:
# has_attached_file :avatar, :storage => :database
#
# The files will be stored in a new database table named with the plural attachment name
# by default, "avatars" in this example.
#
# 2. You need to create this new storage table with at least these columns:
# - file_contents
# - style
# - the primary key for the parent model (e.g. user_id)
#
# Note the "binary" migration will not work for the LONGBLOG type in MySQL for the
# file_cotents column. You may need to craft a SQL statement for your migration,
# depending on which database server you are using. Here's an example migration for MySQL:
#
# create_table :avatars do |t|
# t.string :style
# t.integer :user_id
# t.timestamps
# end
# execute 'ALTER TABLE avatars ADD COLUMN file_contents LONGBLOB'
#
# You can optionally specify any storage table name you want as follows:
# has_attached_file :avatar, :storage => :database, :database_table => 'avatar_files'
#
# 3. By default, URLs will be set to this pattern:
# /:relative_root/:class/:attachment/:id?style=:style
#
# Example:
# /app-root-url/users/avatars/23?style=original
#
# The idea here is that to retrieve a file from the database storage, you will need some
# controller's code to be executed.
#
# Once you pick a controller to use for downloading, you can add this line
# to generate the download action for the default URL/action (the plural attachment name),
# "avatars" in this example:
# downloads_files_for :user, :avatar
#
# Or you can write a download method manually if there are security, logging or other
# requirements.
#
# If you prefer a different URL for downloading files you can specify that in the model; e.g.:
# has_attached_file :avatar, :storage => :database, :url => '/users/show_avatar/:id/:style'
#
# 4. Add a route for the download to the controller which will handle downloads, if necessary.
#
# The default URL, /:relative_root/:class/:attachment/:id?style=:style, will be matched by
# the default route: :controller/:action/:id
#
module Database
def self.extended(base)
base.instance_eval do
setup_paperclip_files_model
override_default_options base
end
base.class.interpolations[:database_path] = lambda do |attachment, style|
attachment.database_path(style)
end
base.class.interpolations[:relative_root] = lambda do |attachment, style|
begin
if ActionController::AbstractRequest.respond_to?(:relative_url_root)
relative_url_root = ActionController::AbstractRequest.relative_url_root
end
rescue NameError
end
if !relative_url_root && ActionController::Base.respond_to?(:relative_url_root)
ActionController::Base.relative_url_root
end
end
ActiveRecord::Base.logger.info("[paperclip] Database Storage Initalized.")
end
def setup_paperclip_files_model
@paperclip_files = "#{instance.class.name.underscore}_#{name.to_s}_paperclip_files"
if !Object.const_defined?(@paperclip_files.classify)
@paperclip_file = Object.const_set(@paperclip_files.classify, Class.new(ActiveRecord::Base))
else
@paperclip_file = Object.const_get(@paperclip_files.classify)
end
@database_table = @options[:database_table] || name.to_s.pluralize
@paperclip_file.set_table_name @database_table
instance.class.has_many @paperclip_files
end
private :setup_paperclip_files_model
def override_default_options(base)
if @url == base.class.default_options[:url]
@url = ":relative_root/:class/:attachment/:id?style=:style"
end
@path = ":database_path"
end
private :override_default_options
def database_table
@database_table
end
def database_path(style)
paperclip_file = file_for(style)
if paperclip_file
"#{database_table}(id=#{paperclip_file.id},style=#{style.to_s})"
else
"#{database_table}(id=new,style=#{style.to_s})"
end
end
def exists?(style = default_style)
if original_filename
!file_for(style).nil?
else
false
end
end
# Returns representation of the data of the file assigned to the given
# style, in the format most representative of the current storage.
def to_file style = default_style
if @queued_for_write[style]
@queued_for_write[style]
elsif exists?(style)
tempfile = Tempfile.new instance_read(:file_name)
tempfile.write file_contents(style)
tempfile
else
nil
end
end
alias_method :to_io, :to_file
def file_for(style)
instance.send(@paperclip_files).detect { |file| file.style == style.to_s }
end
def file_contents(style)
file_for(style).file_contents
end
def flush_writes
logger.info("[paperclip] Writing files for #{name}")
@queued_for_write.each do |style, file|
paperclip_file = @paperclip_file.new
paperclip_file.file_contents = file.read
paperclip_file.style = style.to_s;
instance.send(@paperclip_files) << paperclip_file
end
@queued_for_write = {}
end
def flush_deletes #:nodoc:
logger.info("[paperclip] Deleting files for #{name}")
@queued_for_delete.each do |path|
/id=([0-9]+)/.match(path)
@paperclip_file.destroy $1
end
@queued_for_delete = []
end
module ControllerClassMethods
def self.included(base)
base.extend(self)
end
def downloads_files_for(model, attachment, options = {})
define_method("#{attachment.to_s.pluralize}") do
model_record = Object.const_get(model.to_s.camelize.to_sym).find(params[:id])
style = params[:style] ? params[:style] : 'original'
send_data model_record.send(attachment).file_contents(style),
:filename => model_record.send("#{attachment}_file_name".to_sym),
:type => model_record.send("#{attachment}_content_type".to_sym)
end
end
end
end
end
end