Commit 66b47339 by jyurek

A little reorg, plus added S3 support.

git-svn-id: https://svn.thoughtbot.com/plugins/paperclip/trunk@222 7bbfaf0e-4d1d-0410-9690-a8bb5f8ef2aa
parent 16b60a52
......@@ -10,495 +10,8 @@
#
# See the +has_attached_file+ documentation for more details.
module Thoughtbot #:nodoc:
# Paperclip defines an attachment as any file, though it makes special considerations
# for image files. You can declare that a model has an attached file with the
# +has_attached_file+ method:
#
# class User < ActiveRecord::Base
# has_attached_file :avatar, :thumbnails => { :thumb => "100x100" }
# end
#
# See the +has_attached_file+ documentation for more details.
module Paperclip
require 'paperclip/paperclip'
require 'paperclip/storage'
require 'paperclip/storage/filesystem'
require 'paperclip/storage/s3'
PAPERCLIP_OPTIONS = {
:whiny_deletes => false,
:whiny_thumbnails => true,
:image_magick_path => nil
}
def self.options
PAPERCLIP_OPTIONS
end
DEFAULT_ATTACHMENT_OPTIONS = {
:path_prefix => ":rails_root/public",
:url_prefix => "",
:path => ":attachment/:id/:style_:name",
:attachment_type => :image,
:thumbnails => {},
:delete_on_destroy => true,
:default_style => :original,
:missing_url => "",
:missing_path => ""
}
class PaperclipError < StandardError #:nodoc:
attr_accessor :attachment
def initialize attachment
@attachment = attachment
end
end
module ClassMethods
# == Methods
# +has_attached_file+ attaches a file (or files) with a given name to a model. It creates seven instance
# methods using the attachment name (where "attachment" in the following is the name
# passed in to +has_attached_file+):
# * attachment: Returns the name of the file that was attached, with no path information.
# * attachment?: Alias for _attachment_ for clarity in determining if the attachment exists.
# * attachment=(file): Sets the attachment to the file and creates the thumbnails (if necessary).
# +file+ can be anything normally accepted as an upload (+StringIO+ or +Tempfile+) or a +File+
# if it has had the +Upfile+ module included. +file+ can also be a URL object pointing to a valid
# resource. This resource will be downloaded using +open-uri+[http://www.ruby-doc.org/stdlib/libdoc/open-uri/rdoc/]
# and processed as a regular file object would. Finally, you can set this property to +nil+ to clear
# the attachment, which is the same thing as calling +destroy_attachment+.
# Note this does not save the attachments.
# user.avatar = File.new("~/pictures/me.png")
# user.avatar = params[:user][:avatar] # When :avatar is a file_field
# user.avatar = URI.parse("http://www.avatars-r-us.com/spiffy.png")
# * attachment_file_name(style): The name of the file, including path information. Pass in the
# name of a thumbnail to get the path to that thumbnail.
# user.avatar_file_name(:thumb) # => "public/users/44/thumb/me.png"
# user.avatar_file_name # => "public/users/44/original/me.png"
# * attachment_url(style): The public URL of the attachment, suitable for passing to +image_tag+
# or +link_to+. Pass in the name of a thumbnail to get the url to that thumbnail.
# user.avatar_url(:thumb) # => "http://assethost.com/users/44/thumb/me.png"
# user.avatar_url # => "http://assethost.com/users/44/original/me.png"
# * attachment_valid?: If unsaved, returns true if all thumbnails have data (that is,
# they were successfully made). If saved, returns true if all expected files exist and are
# of nonzero size.
# * destroy_attachment(complain = false): Flags the attachment and all thumbnails for deletion. Sets
# the +attachment_file_name+ column and +attachment_content_type+ column to +nil+. Set +complain+
# to true to override the +whiny_deletes+ option. NOTE: this does not actually delete the attachment.
# You must still call +save+ on the model to actually delete the file and commit the change to the
# database.
#
# == Options
# There are a number of options you can set to change the behavior of Paperclip.
# * +path_prefix+: The location of the repository of attachments on disk. See Interpolation below
# for more control over where the files are located.
# :path_prefix => ":rails_root/public"
# :path_prefix => "/var/app/repository"
# * +url_prefix+: The root URL of where the attachment is publically accessible. See Interpolation below
# for more control over where the files are located.
# :url_prefix => "/"
# :url_prefix => "/user_files"
# :url_prefix => "http://some.other.host/stuff"
# * +path+: Where the files are stored underneath the +path_prefix+ directory and underneath the +url_prefix+ URL.
# See Interpolation below for more control over where the files are located.
# :path => ":class/:style/:id/:name" # => "users/original/13/picture.gif"
# * +attachment_type+: If this is set to :image (which it is, by default), Paperclip will attempt to make thumbnails.
# * +thumbnails+: A hash of thumbnail styles and their geometries. You can find more about geometry strings
# at the ImageMagick website (http://www.imagemagick.org/script/command-line-options.php#resize). Paperclip
# also adds the "#" option, which will resize the image to fit maximally inside the dimensions and then crop
# the rest off (weighted at the center).
# * +delete_on_destroy+: When records are deleted, the attachment that goes with it is also deleted. Set
# this to +false+ to prevent the file from being deleted.
# * +default_style+: The thumbnail style that will be used by default for +attachment_file_name+ and +attachment_url+
# Defaults to +original+.
# has_attached_file :avatar, :thumbnails => { :normal => "100x100#" },
# :default_style => :normal
# user.avatar_url # => "/avatars/23/normal_me.png"
# * +missing_url+: The URL that will be returned if there is no attachment assigned. It should be an absolute
# URL, not relative to the +url_prefix+. This field is interpolated.
# has_attached_file :avatar, :missing_url => "/images/default_:style_avatar.png"
# User.new.avatar_url(:small) # => "/images/default_small_avatar.png"
#
# == Interpolation
# The +path_prefix+, +url_prefix+, and +path+ options can have dynamic interpolation done so that the
# locations of the files can vary depending on a variety of factors. Each variable looks like a Ruby symbol
# and is searched for with +gsub+, so a variety of effects can be achieved. The list of possible variables
# follows:
# * +rails_root+: The value of the +RAILS_ROOT+ constant for your app. Typically used when putting your
# attachments into the public directory. Probably not useful in the +path+ definition.
# * +class+: The underscored, pluralized version of the class in which the attachment is defined.
# * +attachment+: The pluralized name of the attachment as given to +has_attached_file+
# * +style+: The name of the thumbnail style for the current thumbnail. If no style is given, "original" is used.
# * +id+: The record's id.
# * +name+: The file's name, as stored in the attachment_file_name column.
# * +base+: The base of the file's name, e.g. "myself" from "myself.jpg", or "my.picture" from "my.picture.png".
# It is defined as everything except the final period and what follows it. If there is no extension, :base works
# the same as :name.
# * +ext+: The extension of the file, e.g. "jpg" from "myself.jpg". It is defined as everything following the final
# period
#
# When interpolating, you are not confined to making any one of these into its own directory. This is
# perfectly valid:
# :path => ":attachment/:style/:id-:name" # => "avatars/thumb/44-me.png"
#
# == Model Requirements
# For any given attachment _foo_, the model the attachment is in needs to have both a +foo_file_name+
# and +foo_content_type+ column, as a type of +string+. The +foo_file_name+ column contains only the name
# of the file and none of the path information. However, the +foo_file_name+ column accessor is overwritten
# by the one (defined above) which returns the full path to whichever style thumbnail is passed in.
# In a pinch, you can either use +read_attribute+ or the plain +foo+ accessor, which returns the database's
# +foo_file_name+ column.
#
# Note that if these columns are not found in the model (according to +ActiveRecord::Base#column_names+) then
# Paperclip will throw a +PaperclipError+ informing you of the fact.
#
# == Event Triggers
# When an attachment is set by using he setter (+model.attachment=+), the thumbnails are created and held in
# memory. They are not saved until the +after_save+ trigger fires, at which point the attachment and all
# thumbnails are written to disk.
#
# Attached files are destroyed when the associated record is destroyed in a +before_destroy+ trigger. Set
# the +delete_on_destroy+ option to +false+ to prevent this behavior. Also note that using the ActiveRecord's
# +delete+ method instead of the +destroy+ method will prevent the +before_destroy+ trigger from firing.
#
# == Validation
# If there is a problem in the thumbnail-making process, Paperclip will add errors to your model on save. These
# errors appear if there is an error with +convert+ (e.g. +convert+ doesn't exist, the file wasn't an image, etc).
def has_attached_file *attachment_names
options = attachment_names.last.is_a?(Hash) ? attachment_names.pop : {}
options = DEFAULT_ATTACHMENT_OPTIONS.merge(options)
include InstanceMethods
attachments = (@attachments ||= {})
attachment_names.each do |attr|
attachments[attr] = (attachments[attr] || {:name => attr}).merge(options)
whine_about_columns_for attachments[attr]
define_method "#{attr}=" do |uploaded_file|
uploaded_file = fetch_uri(uploaded_file) if uploaded_file.is_a? URI
return send("destroy_#{attr}") if uploaded_file.nil?
return unless is_a_file? uploaded_file
attachments[attr].merge!({
:dirty => true,
:files => {:original => uploaded_file},
:content_type => uploaded_file.content_type,
:filename => sanitize_filename(uploaded_file.original_filename),
:errors => [],
:delete_on_save => false
})
write_attribute(:"#{attr}_file_name", attachments[attr][:filename])
write_attribute(:"#{attr}_content_type", attachments[attr][:content_type])
if attachments[attr][:attachment_type] == :image
send("process_#{attr}_thumbnails")
end
uploaded_file
end
define_method attr do
read_attribute("#{attr}_file_name")
end
alias_method "#{attr}?", attr
define_method "#{attr}_attachment" do
attachments[attr]
end
private :"#{attr}_attachment"
define_method "#{attr}_file_name" do |*args|
style = args.shift || attachments[attr][:default_style] # This prevents arity warnings
path_for(attachments[attr], style) || interpolate(attachments[attr], attachments[attr][:missing_path], style)
end
define_method "#{attr}_url" do |*args|
style = args.shift || attachments[attr][:default_style] # This prevents arity warnings
url_for(attachments[attr], style) || interpolate(attachments[attr], attachments[attr][:missing_url], style)
end
define_method "#{attr}_valid?" do
attachments[attr][:thumbnails].merge(:original => nil).all? do |style, geometry|
if read_attribute("#{attr}_file_name")
if attachments[attr][:dirty]
!attachments[attr][:files][style].blank? && attachments[attr][:errors].empty?
else
File.file?( path_for(attachments[attr], style) )
end
else
false
end
end
end
define_method "process_#{attr}_thumbnails" do
make_thumbnails attachments[attr]
end
define_method "destroy_#{attr}" do |*args|
complain = args.first || false
if attachments[attr].keys.any?
attachments[attr][:files] = nil
attachments[attr][:delete_on_save] = true
attachments[attr][:complain_on_delete] = complain
write_attribute("#{attr}_file_name", nil)
write_attribute("#{attr}_content_type", nil)
end
true
end
validates_each attr do |r, a, v|
attachments[attr][:errors].each{|e| r.errors.add(attr, e) } if attachments[attr][:errors]
end
define_method "#{attr}_before_save" do
if attachments[attr].keys.any?
write_attachment attachments[attr] if attachments[attr][:files]
delete_attachment attachments[attr], attachments[attr][:complain_on_delete] if attachments[attr][:delete_on_save]
attachments[attr][:delete_on_save] = false
attachments[attr][:dirty] = false
attachments[attr][:files] = nil
end
end
private :"#{attr}_before_save"
after_save :"#{attr}_before_save"
define_method "#{attr}_before_destroy" do
if attachments[attr].keys.any?
delete_attachment attachments[attr] if attachments[attr][:delete_on_destroy]
end
end
private :"#{attr}_before_destroy"
before_destroy :"#{attr}_before_destroy"
end
end
def attachment_names
@attachments.keys
end
def attachment name
@attachments[name]
end
# Adds errors if the attachments you specify are either missing or had errors on them.
# Essentially, acts like validates_presence_of for attachments.
def validates_attached_file *attachment_names
validates_each *attachment_names do |r, a, v|
r.errors.add(a, "requires a valid attachment.") unless r.send("#{a}_valid?")
end
end
def whine_about_columns_for attachment #:nodoc:
name = attachment[:name]
unless column_names.include?("#{name}_file_name") && column_names.include?("#{name}_content_type")
error = "Class #{self.name} does not have the necessary columns to have an attachment named #{name}. " +
"(#{name}_file_name and #{name}_content_type)"
raise PaperclipError.new(attachment), error
end
end
end
module InstanceMethods #:nodoc:
private
def interpolate attachment, source, style
file_name = read_attribute("#{attachment[:name]}_file_name")
returning source.dup do |s|
s.gsub!(/:rails_root/, RAILS_ROOT)
s.gsub!(/:id/, self.id.to_s) if self.id
s.gsub!(/:class/, self.class.to_s.underscore.pluralize)
s.gsub!(/:style/, style.to_s)
s.gsub!(/:attachment/, attachment[:name].to_s.pluralize)
if file_name
file_bits = file_name.split(".")
s.gsub!(/:name/, file_name)
s.gsub!(/:base/, [file_bits[0], *file_bits[1..-2]].join("."))
s.gsub!(/:ext/, file_bits.last )
end
end
end
def path_for attachment, style = nil
style ||= attachment[:default_style]
file = read_attribute("#{attachment[:name]}_file_name")
return nil unless file && self.id
prefix = interpolate attachment, "#{attachment[:path_prefix]}/#{attachment[:path]}", style
File.join( prefix.split("/") )
end
def url_for attachment, style = nil
style ||= attachment[:default_style]
file = read_attribute("#{attachment[:name]}_file_name")
return nil unless file && self.id
interpolate attachment, "#{attachment[:url_prefix]}/#{attachment[:path]}", style
end
def ensure_directories_for attachment
attachment[:files].each do |style, file|
dirname = File.dirname(path_for(attachment, style))
FileUtils.mkdir_p dirname
end
end
def write_attachment attachment
return if attachment[:files].blank?
ensure_directories_for attachment
attachment[:files].each do |style, atch|
atch.rewind
data = atch.read
File.open( path_for(attachment, style), "w" ) do |file|
file.rewind
file.write(data)
end
end
attachment[:files] = nil
attachment[:dirty] = false
end
def delete_attachment attachment, complain = false
(attachment[:thumbnails].keys + [:original]).each do |style|
file_path = path_for(attachment, style)
begin
FileUtils.rm file_path if file_path
rescue SystemCallError => e
raise PaperclipError.new(attachment), "Could not delete thumbnail." if ::Thoughtbot::Paperclip.options[:whiny_deletes] || complain
end
end
end
def make_thumbnails attachment
attachment[:files] ||= {}
attachment[:files][:original] ||= File.new( path_for(attachment, :original) )
attachment[:thumbnails].each do |style, geometry|
begin
attachment[:files][style] = make_thumbnail(attachment, attachment[:files][:original], geometry)
rescue PaperclipError => e
attachment[:errors] << "thumbnail '#{style}' could not be created."
end
end
end
def make_thumbnail attachment, orig_io, geometry
operator = geometry[-1,1]
begin
geometry, crop_geometry = geometry_for_crop(geometry, orig_io) if operator == '#'
command = "#{path_for_command "convert"} - -scale '#{geometry}' #{operator == '#' ? "-crop '#{crop_geometry}'" : ""} - 2>/dev/null"
thumb = IO.popen(command, "w+") do |io|
orig_io.rewind
io.write(orig_io.read)
io.close_write
StringIO.new(io.read)
end
rescue Errno::EPIPE => e
raise PaperclipError.new(attachment), "Could not create thumbnail. Is ImageMagick or GraphicsMagick installed and available?"
rescue SystemCallError => e
raise PaperclipError.new(attachment), "Could not create thumbnail."
end
if ::Thoughtbot::Paperclip.options[:whiny_thumbnails] && !$?.success?
raise PaperclipError.new(attachment), "Convert returned with result code #{$?.exitstatus}: #{thumb.read}"
end
thumb
end
def geometry_for_crop geometry, orig_io
IO.popen("#{path_for_command "identify"} - 2>/dev/null", "w+") do |io|
orig_io.rewind
io.write(orig_io.read)
io.close_write
if match = io.read.split[2].match(/(\d+)x(\d+)/)
src = match[1,2].map(&:to_f)
srch = src[0] > src[1]
dst = geometry.match(/(\d+)x(\d+)/)[1,2].map(&:to_f)
dsth = dst[0] > dst[1]
ar = src[0] / src[1]
scale_geometry, scale = if dst[0] == dst[1]
if srch
[ "x#{dst[1]}", src[1] / dst[1] ]
else
[ "#{dst[0]}x", src[0] / dst[0] ]
end
elsif dsth
[ "#{dst[0]}x", src[0] / dst[0] ]
else
[ "x#{dst[1]}", src[1] / dst[1] ]
end
crop_geometry = if dsth
"%dx%d+%d+%d" % [ dst[0], dst[1], 0, (src[1] / scale - dst[1]) / 2 ]
else
"%dx%d+%d+%d" % [ dst[0], dst[1], (src[0] / scale - dst[0]) / 2, 0 ]
end
[ scale_geometry, crop_geometry ]
end
end
end
def fetch_uri uri
image = if uri.scheme == 'file'
path = url.gsub(%r{^file://}, '/')
open(path)
else
require 'open-uri'
uri
end
begin
data = StringIO.new(image.read)
uri.extend(Upfile)
class << data
attr_accessor :original_filename, :content_type
end
data.original_filename = uri.original_filename
data.content_type = uri.content_type
data
rescue OpenURI::HTTPError => e
self.errors.add_to_base("The file at #{uri.to_s} could not be found.")
$stderr.puts "#{e.message}: #{uri.to_s}"
return nil
end
end
def is_a_file? data
[:content_type, :original_filename, :read].map do |meth|
data.respond_to? meth
end.all?
end
def sanitize_filename filename
File.basename(filename).gsub(/[^\w\.\_]/,'_')
end
def path_for_command command
File.join([::Thoughtbot::Paperclip.options[:image_magick_path], command].compact)
end
end
# The Upfile module is a convenience module for adding uploaded-file-type methods
# to the +File+ class. Useful for testing.
# user.avatar = File.new("test/test_avatar.jpg")
module Upfile
# Infer the MIME-type of the file from the extension.
def content_type
type = self.path.match(/\.(\w+)$/)[1] || "data"
case type
when "jpg", "png", "gif" then "image/#{type}"
when "txt", "csv", "xml", "html", "htm" then "text/#{type}"
else "x-application/#{type}"
end
end
# Returns the file's normal name.
def original_filename
self.path
end
# Returns the size of the file.
def size
File.size(self)
end
end
end
end
module Thoughtbot #:nodoc:
# Paperclip defines an attachment as any file, though it makes special considerations
# for image files. You can declare that a model has an attached file with the
# +has_attached_file+ method:
#
# class User < ActiveRecord::Base
# has_attached_file :avatar, :thumbnails => { :thumb => "100x100" }
# end
#
# See the +has_attached_file+ documentation for more details.
module Paperclip
PAPERCLIP_OPTIONS = {
:whiny_deletes => false,
:whiny_thumbnails => true,
:image_magick_path => nil
}
def self.options
PAPERCLIP_OPTIONS
end
DEFAULT_ATTACHMENT_OPTIONS = {
:path_prefix => ":rails_root/public",
:url_prefix => "",
:path => ":attachment/:id/:style_:name",
:attachment_type => :image,
:thumbnails => {},
:delete_on_destroy => true,
:default_style => :original,
:missing_url => "",
:missing_path => ""
}
class PaperclipError < StandardError #:nodoc:
attr_accessor :attachment
def initialize attachment
@attachment = attachment
end
end
module ClassMethods
# == Methods
# +has_attached_file+ attaches a file (or files) with a given name to a model. It creates seven instance
# methods using the attachment name (where "attachment" in the following is the name
# passed in to +has_attached_file+):
# * attachment: Returns the name of the file that was attached, with no path information.
# * attachment?: Alias for _attachment_ for clarity in determining if the attachment exists.
# * attachment=(file): Sets the attachment to the file and creates the thumbnails (if necessary).
# +file+ can be anything normally accepted as an upload (+StringIO+ or +Tempfile+) or a +File+
# if it has had the +Upfile+ module included. +file+ can also be a URL object pointing to a valid
# resource. This resource will be downloaded using +open-uri+[http://www.ruby-doc.org/stdlib/libdoc/open-uri/rdoc/]
# and processed as a regular file object would. Finally, you can set this property to +nil+ to clear
# the attachment, which is the same thing as calling +destroy_attachment+.
# Note this does not save the attachments.
# user.avatar = File.new("~/pictures/me.png")
# user.avatar = params[:user][:avatar] # When :avatar is a file_field
# user.avatar = URI.parse("http://www.avatars-r-us.com/spiffy.png")
# * attachment_file_name(style): The name of the file, including path information. Pass in the
# name of a thumbnail to get the path to that thumbnail.
# user.avatar_file_name(:thumb) # => "public/users/44/thumb/me.png"
# user.avatar_file_name # => "public/users/44/original/me.png"
# * attachment_url(style): The public URL of the attachment, suitable for passing to +image_tag+
# or +link_to+. Pass in the name of a thumbnail to get the url to that thumbnail.
# user.avatar_url(:thumb) # => "http://assethost.com/users/44/thumb/me.png"
# user.avatar_url # => "http://assethost.com/users/44/original/me.png"
# * attachment_valid?: If unsaved, returns true if all thumbnails have data (that is,
# they were successfully made). If saved, returns true if all expected files exist and are
# of nonzero size.
# * destroy_attachment(complain = false): Flags the attachment and all thumbnails for deletion. Sets
# the +attachment_file_name+ column and +attachment_content_type+ column to +nil+. Set +complain+
# to true to override the +whiny_deletes+ option. NOTE: this does not actually delete the attachment.
# You must still call +save+ on the model to actually delete the file and commit the change to the
# database.
#
# == Options
# There are a number of options you can set to change the behavior of Paperclip.
# * +path_prefix+: The location of the repository of attachments on disk. See Interpolation below
# for more control over where the files are located.
# :path_prefix => ":rails_root/public"
# :path_prefix => "/var/app/repository"
# * +url_prefix+: The root URL of where the attachment is publically accessible. See Interpolation below
# for more control over where the files are located.
# :url_prefix => "/"
# :url_prefix => "/user_files"
# :url_prefix => "http://some.other.host/stuff"
# * +path+: Where the files are stored underneath the +path_prefix+ directory and underneath the +url_prefix+ URL.
# See Interpolation below for more control over where the files are located.
# :path => ":class/:style/:id/:name" # => "users/original/13/picture.gif"
# * +attachment_type+: If this is set to :image (which it is, by default), Paperclip will attempt to make thumbnails.
# * +thumbnails+: A hash of thumbnail styles and their geometries. You can find more about geometry strings
# at the ImageMagick website (http://www.imagemagick.org/script/command-line-options.php#resize). Paperclip
# also adds the "#" option, which will resize the image to fit maximally inside the dimensions and then crop
# the rest off (weighted at the center).
# * +delete_on_destroy+: When records are deleted, the attachment that goes with it is also deleted. Set
# this to +false+ to prevent the file from being deleted.
# * +default_style+: The thumbnail style that will be used by default for +attachment_file_name+ and +attachment_url+
# Defaults to +original+.
# has_attached_file :avatar, :thumbnails => { :normal => "100x100#" },
# :default_style => :normal
# user.avatar_url # => "/avatars/23/normal_me.png"
# * +missing_url+: The URL that will be returned if there is no attachment assigned. It should be an absolute
# URL, not relative to the +url_prefix+. This field is interpolated.
# has_attached_file :avatar, :missing_url => "/images/default_:style_avatar.png"
# User.new.avatar_url(:small) # => "/images/default_small_avatar.png"
#
# == Interpolation
# The +path_prefix+, +url_prefix+, and +path+ options can have dynamic interpolation done so that the
# locations of the files can vary depending on a variety of factors. Each variable looks like a Ruby symbol
# and is searched for with +gsub+, so a variety of effects can be achieved. The list of possible variables
# follows:
# * +rails_root+: The value of the +RAILS_ROOT+ constant for your app. Typically used when putting your
# attachments into the public directory. Probably not useful in the +path+ definition.
# * +class+: The underscored, pluralized version of the class in which the attachment is defined.
# * +attachment+: The pluralized name of the attachment as given to +has_attached_file+
# * +style+: The name of the thumbnail style for the current thumbnail. If no style is given, "original" is used.
# * +id+: The record's id.
# * +name+: The file's name, as stored in the attachment_file_name column.
# * +base+: The base of the file's name, e.g. "myself" from "myself.jpg", or "my.picture" from "my.picture.png".
# It is defined as everything except the final period and what follows it. If there is no extension, :base works
# the same as :name.
# * +ext+: The extension of the file, e.g. "jpg" from "myself.jpg". It is defined as everything following the final
# period
#
# When interpolating, you are not confined to making any one of these into its own directory. This is
# perfectly valid:
# :path => ":attachment/:style/:id-:name" # => "avatars/thumb/44-me.png"
#
# == Model Requirements
# For any given attachment _foo_, the model the attachment is in needs to have both a +foo_file_name+
# and +foo_content_type+ column, as a type of +string+. The +foo_file_name+ column contains only the name
# of the file and none of the path information. However, the +foo_file_name+ column accessor is overwritten
# by the one (defined above) which returns the full path to whichever style thumbnail is passed in.
# In a pinch, you can either use +read_attribute+ or the plain +foo+ accessor, which returns the database's
# +foo_file_name+ column.
#
# Note that if these columns are not found in the model (according to +ActiveRecord::Base#column_names+) then
# Paperclip will throw a +PaperclipError+ informing you of the fact.
#
# == Event Triggers
# When an attachment is set by using he setter (+model.attachment=+), the thumbnails are created and held in
# memory. They are not saved until the +after_save+ trigger fires, at which point the attachment and all
# thumbnails are written to disk.
#
# Attached files are destroyed when the associated record is destroyed in a +before_destroy+ trigger. Set
# the +delete_on_destroy+ option to +false+ to prevent this behavior. Also note that using the ActiveRecord's
# +delete+ method instead of the +destroy+ method will prevent the +before_destroy+ trigger from firing.
#
# == Validation
# If there is a problem in the thumbnail-making process, Paperclip will add errors to your model on save. These
# errors appear if there is an error with +convert+ (e.g. +convert+ doesn't exist, the file wasn't an image, etc).
def has_attached_file *attachment_names
options = attachment_names.last.is_a?(Hash) ? attachment_names.pop : {}
options = DEFAULT_ATTACHMENT_OPTIONS.merge(options)
include InstanceMethods
attachments = (@attachments ||= {})
define_method :after_initialize do
attachments.each do |name, options|
options[:instance] = self
end
end
attachment_names.each do |attr|
attachments[attr] = (attachments[attr] || {:name => attr}).merge(options)
whine_about_columns_for attachments[attr]
if attachments[attr][:storage]
attachments[attr][:storage] = Thoughtbot::Paperclip::Storage.const_get(attachments[attr][:storage].to_s.camelize).new
else
attachments[attr][:storage] = Thoughtbot::Paperclip::Storage::Filesystem.new
end
define_method "#{attr}=" do |uploaded_file|
uploaded_file = fetch_uri(uploaded_file) if uploaded_file.is_a? URI
return send("destroy_#{attr}") if uploaded_file.nil?
return unless is_a_file? uploaded_file
attachments[attr].merge!({
:dirty => true,
:files => {:original => uploaded_file},
:content_type => uploaded_file.content_type,
:file_name => sanitize_filename(uploaded_file.original_filename),
:errors => [],
:delete_on_save => false
})
write_attribute(:"#{attr}_file_name", attachments[attr][:file_name])
write_attribute(:"#{attr}_content_type", attachments[attr][:content_type])
if attachments[attr][:attachment_type] == :image
send("process_#{attr}_thumbnails")
end
uploaded_file
end
define_method attr do
read_attribute("#{attr}_file_name")
end
alias_method "#{attr}?", attr
define_method "#{attr}_attachment" do
attachments[attr]
end
private :"#{attr}_attachment"
define_method "#{attr}_file_name" do |*args|
style = args.shift || attachments[attr][:default_style] # This prevents arity warnings
attachments[attr][:storage].path_for(attachments[attr], style) ||
attachments[attr][:storage].interpolate(attachments[attr], attachments[attr][:missing_path], style)
end
define_method "#{attr}_url" do |*args|
style = args.shift || attachments[attr][:default_style] # This prevents arity warnings
attachments[attr][:storage].url_for(attachments[attr], style) ||
attachments[attr][:storage].interpolate(attachments[attr], attachments[attr][:missing_url], style)
end
define_method "#{attr}_valid?" do
attachments[attr][:storage].attachment_valid? attachments[attr]
end
define_method "process_#{attr}_thumbnails" do
attachments[attr][:storage].make_thumbnails attachments[attr]
end
define_method "destroy_#{attr}" do |*args|
complain = args.first || false
if attachments[attr].keys.any?
attachments[attr][:files] = nil
attachments[attr][:delete_on_save] = true
attachments[attr][:complain_on_delete] = complain
write_attribute("#{attr}_file_name", nil)
write_attribute("#{attr}_content_type", nil)
end
true
end
validates_each attr do |r, a, v|
attachments[attr][:errors].each{|e| r.errors.add(attr, e) } if attachments[attr][:errors]
end
define_method "#{attr}_before_save" do
if attachments[attr].keys.any?
if attachments[attr][:files]
attachments[attr][:storage].write_attachment attachments[attr]
end
if attachments[attr][:delete_on_save]
attachments[attr][:storage].delete_attachment attachments[attr], attachments[attr][:complain_on_delete]
end
attachments[attr][:delete_on_save] = false
attachments[attr][:dirty] = false
attachments[attr][:files] = nil
end
end
private :"#{attr}_before_save"
after_save :"#{attr}_before_save"
define_method "#{attr}_before_destroy" do
if attachments[attr].keys.any?
attachments[attr][:storage].delete_attachment attachments[attr] if attachments[attr][:delete_on_destroy]
end
end
private :"#{attr}_before_destroy"
before_destroy :"#{attr}_before_destroy"
end
[attachments, options]
end
def attachment_names
@attachments.keys
end
def attachment name
@attachments[name]
end
# Adds errors if the attachments you specify are either missing or had errors on them.
# Essentially, acts like validates_presence_of for attachments.
def validates_attached_file *attachment_names
validates_each *attachment_names do |r, a, v|
r.errors.add(a, "requires a valid attachment.") unless r.send("#{a}_valid?")
end
end
def whine_about_columns_for attachment #:nodoc:
name = attachment[:name]
unless column_names.include?("#{name}_file_name") && column_names.include?("#{name}_content_type")
error = "Class #{self.name} does not have the necessary columns to have an attachment named #{name}. " +
"(#{name}_file_name and #{name}_content_type)"
raise PaperclipError.new(attachment), error
end
end
end
module InstanceMethods #:nodoc:
def is_a_file? data
[:content_type, :original_filename, :read].map do |meth|
data.respond_to? meth
end.all?
end
def sanitize_filename filename
File.basename(filename).gsub(/[^\w\.\_]/,'_')
end
def fetch_uri uri
image = if uri.scheme == 'file'
path = url.gsub(%r{^file://}, '/')
open(path)
else
require 'open-uri'
uri
end
begin
data = StringIO.new(image.read)
uri.extend(Upfile)
class << data
attr_accessor :original_filename, :content_type
end
data.original_filename = uri.original_filename
data.content_type = uri.content_type
data
rescue OpenURI::HTTPError => e
self.errors.add_to_base("The file at #{uri.to_s} could not be found.")
$stderr.puts "#{e.message}: #{uri.to_s}"
return nil
end
end
end
# The Upfile module is a convenience module for adding uploaded-file-type methods
# to the +File+ class. Useful for testing.
# user.avatar = File.new("test/test_avatar.jpg")
module Upfile
# Infer the MIME-type of the file from the extension.
def content_type
type = self.path.match(/\.(\w+)$/)[1] || "data"
case type
when "jpg", "png", "gif" then "image/#{type}"
when "txt", "csv", "xml", "html", "htm" then "text/#{type}"
else "x-application/#{type}"
end
end
# Returns the file's normal name.
def original_filename
self.path
end
# Returns the size of the file.
def size
File.size(self)
end
end
end
end
module Thoughtbot
module Paperclip
class Storage
def interpolate attachment, source, style
style ||= attachment[:default_style]
file_name = attachment[:instance]["#{attachment[:name]}_file_name"]
returning source.dup do |s|
s.gsub!(/:rails_root/, RAILS_ROOT)
s.gsub!(/:id/, attachment[:instance].id.to_s) if attachment[:instance].id
s.gsub!(/:class/, attachment[:instance].class.to_s.underscore.pluralize)
s.gsub!(/:style/, style.to_s )
s.gsub!(/:attachment/, attachment[:name].to_s.pluralize)
if file_name
file_bits = file_name.split(".")
s.gsub!(/:name/, file_name)
s.gsub!(/:base/, [file_bits[0], *file_bits[1..-2]].join("."))
s.gsub!(/:ext/, file_bits.last )
end
end
end
def make_thumbnails attachment
attachment[:files] ||= {}
attachment[:files][:original] ||= File.new( path_for(attachment, :original) )
attachment[:thumbnails].each do |style, geometry|
begin
attachment[:files][style] = make_thumbnail(attachment, attachment[:files][:original], geometry)
rescue PaperclipError => e
attachment[:errors] << "thumbnail '#{style}' could not be created."
end
end
end
def make_thumbnail attachment, orig_io, geometry
operator = geometry[-1,1]
begin
geometry, crop_geometry = geometry_for_crop(geometry, orig_io) if operator == '#'
command = "#{path_for_command "convert"} - -scale '#{geometry}' #{operator == '#' ? "-crop '#{crop_geometry}'" : ""} - 2>/dev/null"
thumb = IO.popen(command, "w+") do |io|
orig_io.rewind
io.write(orig_io.read)
io.close_write
StringIO.new(io.read)
end
rescue Errno::EPIPE => e
raise PaperclipError.new(attachment), "Could not create thumbnail. Is ImageMagick or GraphicsMagick installed and available?"
rescue SystemCallError => e
raise PaperclipError.new(attachment), "Could not create thumbnail."
end
if ::Thoughtbot::Paperclip.options[:whiny_thumbnails] && !$?.success?
raise PaperclipError.new(attachment), "Convert returned with result code #{$?.exitstatus}: #{thumb.read}"
end
thumb
end
def geometry_for_crop geometry, orig_io
IO.popen("#{path_for_command "identify"} - 2>/dev/null", "w+") do |io|
orig_io.rewind
io.write(orig_io.read)
io.close_write
if match = io.read.split[2].match(/(\d+)x(\d+)/)
src = match[1,2].map(&:to_f)
srch = src[0] > src[1]
dst = geometry.match(/(\d+)x(\d+)/)[1,2].map(&:to_f)
dsth = dst[0] > dst[1]
ar = src[0] / src[1]
scale_geometry, scale = if dst[0] == dst[1]
if srch
[ "x#{dst[1]}", src[1] / dst[1] ]
else
[ "#{dst[0]}x", src[0] / dst[0] ]
end
elsif dsth
[ "#{dst[0]}x", src[0] / dst[0] ]
else
[ "x#{dst[1]}", src[1] / dst[1] ]
end
crop_geometry = if dsth
"%dx%d+%d+%d" % [ dst[0], dst[1], 0, (src[1] / scale - dst[1]) / 2 ]
else
"%dx%d+%d+%d" % [ dst[0], dst[1], (src[0] / scale - dst[0]) / 2, 0 ]
end
[ scale_geometry, crop_geometry ]
end
end
end
def path_for_command command
File.join([::Thoughtbot::Paperclip.options[:image_magick_path], command].compact)
end
def to_s
self.class.name
end
end
end
end
\ No newline at end of file
module Thoughtbot
module Paperclip
module ClassMethods
def has_attached_file_with_fs *attachment_names
has_attached_file_without_fs *attachment_names
end
alias_method_chain :has_attached_file, :fs
end
class Storage
class Filesystem < Storage
def path_for attachment, style = nil
style ||= attachment[:default_style]
file = attachment[:instance]["#{attachment[:name]}_file_name"]
return nil unless file && attachment[:instance].id
prefix = interpolate attachment, "#{attachment[:path_prefix]}/#{attachment[:path]}", style
File.join( prefix.split("/") )
end
def url_for attachment, style = nil
style ||= attachment[:default_style]
file = attachment[:instance]["#{attachment[:name]}_file_name"]
return nil unless file && attachment[:instance].id
interpolate attachment, "#{attachment[:url_prefix]}/#{attachment[:path]}", style
end
def ensure_directories_for attachment
attachment[:files].each do |style, file|
dirname = File.dirname(path_for(attachment, style))
FileUtils.mkdir_p dirname
end
end
def write_attachment attachment
return if attachment[:files].blank?
ensure_directories_for attachment
attachment[:files].each do |style, atch|
atch.rewind
data = atch.read
File.open( path_for(attachment, style), "w" ) do |file|
file.rewind
file.write(data)
end
end
attachment[:files] = nil
attachment[:dirty] = false
end
def delete_attachment attachment, complain = false
(attachment[:thumbnails].keys + [:original]).each do |style|
file_path = path_for(attachment, style)
begin
FileUtils.rm file_path if file_path
rescue SystemCallError => e
raise PaperclipError.new(attachment), "Could not delete thumbnail." if ::Thoughtbot::Paperclip.options[:whiny_deletes] || complain
end
end
end
def attachment_valid? attachment
attachment[:thumbnails].merge(:original => nil).all? do |style, geometry|
if attachment[:instance]["#{attachment[:name]}_file_name"]
if attachment[:dirty]
!attachment[:files][style].blank? && attachment[:errors].empty?
else
File.file?( path_for(attachment, style) )
end
else
false
end
end
end
end
end
end
end
\ No newline at end of file
module Thoughtbot
module Paperclip
module ClassMethods
def has_attached_file_with_s3 *attachment_names
attachments, options = has_attached_file_without_s3 *attachment_names
access_key = secret_key = ""
if file_name = s3_credentials_file
creds = YAML.load_file(file_name)
creds = creds[RAILS_ENV] || creds if Object.const_defined?("RAILS_ENV")
access_key = creds['access_key_id']
secret_key = creds['secret_access_key']
else
access_key = Thoughtbot::Paperclip.options[:s3_access_key_id]
secret_key = Thoughtbot::Paperclip.options[:s3_secret_access_key]
end
if options[:storage].to_s.downcase == "s3"
require 'aws/s3'
AWS::S3::Base.establish_connection!(
:access_key_id => access_key,
:secret_access_key => secret_key,
:persistent => Thoughtbot::Paperclip.options[:s3_persistent] || true
)
end
end
alias_method_chain :has_attached_file, :s3
private
def s3_credentials_file
[ Thoughtbot::Paperclip.options[:s3_credentials_file], File.join(RAILS_ROOT, "config", "s3.yml") ].compact.each do |f|
return f if File.exists?(f)
end
nil
end
end
class Storage
class S3 < Storage
def path_for attachment, style = nil
style ||= attachment[:default_style]
file = attachment[:instance]["#{attachment[:name]}_file_name"]
return nil unless file && attachment[:instance].id
interpolate attachment, attachment[:path], style
end
def url_for attachment, style = nil
"http://s3.amazonaws.com/#{bucket_for(attachment)}/#{path_for(attachment, style)}"
end
def bucket_for attachment
bucket_name = interpolate attachment, attachment[:url_prefix], nil
end
def ensure_bucket_for attachment, style = nil
begin
bucket_name = bucket_for attachment
AWS::S3::Bucket.create(bucket_name)
bucket_name
rescue AWS::S3::S3Exception => e
raise Thoughtbot::Paperclip::PaperclipError.new(attachment), "You are not allowed access to the bucket '#{bucket_name}'."
end
end
def write_attachment attachment
return if attachment[:files].blank?
bucket = ensure_bucket_for attachment
attachment[:files].each do |style, atch|
atch.rewind
AWS::S3::S3Object.store( path_for(attachment, style), atch, bucket, :access => attachment[:access] || :public_read )
end
attachment[:files] = nil
attachment[:dirty] = false
end
def delete_attachment attachment, complain = false
(attachment[:thumbnails].keys + [:original]).each do |style|
file_path = path_for(attachment, style)
AWS::S3::S3Object.delete( file_path, bucket_for(attachment) )
end
end
def attachment_valid? attachment
attachment[:thumbnails].merge(:original => nil).all? do |style, geometry|
if attachment[:instance]["#{attachment[:name]}_file_name"]
if attachment[:dirty]
!attachment[:files][style].blank? && attachment[:errors].empty?
else
AWS::S3::S3Object.exists?( path_for(attachment, style), bucket_for(attachment) )
end
else
false
end
end
end
end
end
end
end
\ No newline at end of file
......@@ -13,6 +13,12 @@ begin
table.column :avatar_file_name, :string
table.column :avatar_content_type, :string
end
ActiveRecord::Base.connection.create_table :ess_threes, :force => true do |table|
table.column :resume_file_name, :string
table.column :resume_content_type, :string
table.column :avatar_file_name, :string
table.column :avatar_content_type, :string
end
ActiveRecord::Base.connection.create_table :negatives, :force => true do |table|
table.column :this_is_the_wrong_name_file_name, :string
end
......@@ -47,5 +53,23 @@ class NonStandard < ActiveRecord::Base
:missing_url => "/:class/:style/:attachment/404.png"
end
# class EssThree < ActiveRecord::Base
# has_attached_file :resume, :attachment_type => :document,
# :path_prefix => "paperclip/test",
# :path => ":attachment_:id_:name",
# :missing_url => "/:class/:style/:attachment/404.txt",
# :storage => :S3
# has_attached_file :avatar, :attachment_type => :image,
# :thumbnails => { :cropped => "200x10#",
# :bigger => "1000x1000",
# :smaller => "200x200>",
# :square => "150x150#" },
# :path_prefix => "paperclip/test/images",
# :path => ":class/:attachment/:id/:style_:name",
# :default_style => :square,
# :missing_url => "/:class/:style/:attachment/404.png",
# :storage => :S3
# end
class Negative < ActiveRecord::Base
end
\ No newline at end of file
require 'test/unit'
require File.dirname(__FILE__) + "/test_helper.rb"
require File.dirname(__FILE__) + "/../init.rb"
require File.join(File.dirname(__FILE__), "models.rb")
class PaperclipS3Test < Test::Unit::TestCase
def setup
end
def test_truth
assert true
end
end
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment