Commit da74efdd by jyurek

Fixed some bugs, refactored file iterator, changed :thumbnails to :styles


git-svn-id: https://svn.thoughtbot.com/plugins/paperclip/trunk@256 7bbfaf0e-4d1d-0410-9690-a8bb5f8ef2aa
parent dfe6629a
...@@ -79,8 +79,7 @@ module Paperclip ...@@ -79,8 +79,7 @@ module Paperclip
# is interpolated just as the url is. The default value is "/:class/:attachment/missing_:style.png" # is interpolated just as the url is. The default value is "/:class/:attachment/missing_:style.png"
# has_attached_file :avatar, :missing_url => "/images/default_:style_avatar.png" # has_attached_file :avatar, :missing_url => "/images/default_:style_avatar.png"
# User.new.avatar_url(:small) # => "/images/default_small_avatar.png" # User.new.avatar_url(:small) # => "/images/default_small_avatar.png"
# * +attachment_type+: If this is set to :image (which it is, by default), Paperclip will attempt to make # * +content_type+: The valid content types that this attachment may be.
# thumbnails if they are specified.
# * +thumbnails+: A hash of thumbnail styles and their geometries. You can find more about geometry strings # * +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 # at the ImageMagick website (http://www.imagemagick.org/script/command-line-options.php#resize). Paperclip
# also adds the "#" option (e.g. "50x50#"), which will resize the image to fit maximally inside # also adds the "#" option (e.g. "50x50#"), which will resize the image to fit maximally inside
......
...@@ -24,6 +24,7 @@ module Paperclip ...@@ -24,6 +24,7 @@ module Paperclip
self.extend(storage_module) self.extend(storage_module)
end end
# Sets the file managed by this instance. It also creates the thumbnails if the attachment is an image.
def assign uploaded_file def assign uploaded_file
return destroy if uploaded_file.nil? return destroy if uploaded_file.nil?
return unless is_a_file? uploaded_file return unless is_a_file? uploaded_file
...@@ -34,37 +35,45 @@ module Paperclip ...@@ -34,37 +35,45 @@ module Paperclip
self[:original] = uploaded_file.read self[:original] = uploaded_file.read
@dirty = true @dirty = true
if definition.attachment_type == :image if definition.content_type == :image
make_thumbnails_from(self[:original]) convert( self[:original] )
end end
end end
def [](style) def [](style) #:nodoc:
@files[style] @files[style]
end end
def []=(style, data) def []=(style, data) #:nodoc:
@files[style] = data @files[style] = data
end end
def clear_files def clear_files #:nodoc:
@files = {} @files = {}
definition.styles.each{|style, geo| self[style] = nil } definition.styles.each{|style, geo| self[style] = nil }
@dirty = false @dirty = false
end end
def for_attached_files # Iterates over the files that are stored in memory and hands them to the
# supplied block. If no assignment has happened since either the object
# was instantiated or the last time it was saved, +nil+ will be passed as
# the data argument.
def each_unsaved
@files.each do |style, data| @files.each do |style, data|
yield style, data yield( style, data ) if data
end end
end end
def styles
@files.keys
end
# Returns true if the attachment has been assigned and not saved.
def dirty? def dirty?
@dirty @dirty
end end
# Validations # Runs any validations that have been defined on the attachment.
def valid? def valid?
definition.validations.each do |validation, constraints| definition.validations.each do |validation, constraints|
send("validate_#{validation}", *constraints) send("validate_#{validation}", *constraints)
...@@ -72,8 +81,8 @@ module Paperclip ...@@ -72,8 +81,8 @@ module Paperclip
errors.uniq!.empty? errors.uniq!.empty?
end end
# ActiveRecord Callbacks # Writes (or deletes, if +nil+) the attachment. This is called automatically
# when the active record is saved; you do not need to call this yourself.
def save def save
write_attachment if dirty? write_attachment if dirty?
delete_attachment if @delete_on_save delete_attachment if @delete_on_save
...@@ -81,6 +90,8 @@ module Paperclip ...@@ -81,6 +90,8 @@ module Paperclip
clear_files clear_files
end end
# Queues up the attachment for destruction, but does not actually delete.
# The attachment will be deleted when the record is saved.
def destroy(complain = false) def destroy(complain = false)
returning true do returning true do
@delete_on_save = true @delete_on_save = true
...@@ -92,10 +103,16 @@ module Paperclip ...@@ -92,10 +103,16 @@ module Paperclip
end end
end end
# Immediately destroys the attachment. Typically called as an ActiveRecord
# callback on destroy. You shold not need to call this.
def destroy! def destroy!
delete_attachment if definition.delete_on_destroy delete_attachment if definition.delete_on_destroy
end end
# Returns the public-facing URL of the attachment. If this record has not
# been saved or does not have an attachment, this method will return the
# "missing" url, which can be used to supply a default. This is what should
# be supplied to the +image_tag+ helper.
def url style = nil def url style = nil
style ||= definition.default_style style ||= definition.default_style
pattern = if original_filename && instance.id pattern = if original_filename && instance.id
...@@ -106,30 +123,37 @@ module Paperclip ...@@ -106,30 +123,37 @@ module Paperclip
interpolate( style, pattern ) interpolate( style, pattern )
end end
# Returns the data contained by the attachment of a particular style. This
# should be used if you need to restrict permissions internally to the app.
def read style = nil def read style = nil
style ||= definition.default_style style ||= definition.default_style
self[style] ? self[style] : read_attachment(style) self[style] ? self[style] : read_attachment(style)
end end
# Sets errors if there must be an attachment but isn't.
def validate_existence *constraints def validate_existence *constraints
definition.styles.keys.each do |style| definition.styles.keys.each do |style|
errors << "requires a valid #{style} file." unless attachment_exists?(style) errors << "requires a valid #{style} file." unless attachment_exists?(style)
end end
end end
# Sets errors if the file does not meet the file size constraints.
def validate_size *constraints def validate_size *constraints
errors << "file too large. Must be under #{constraints.last} bytes." if original_file_size > constraints.last errors << "file too large. Must be under #{constraints.last} bytes." if original_file_size > constraints.last
errors << "file too small. Must be over #{constraints.first} bytes." if original_file_size <= constraints.first errors << "file too small. Must be over #{constraints.first} bytes." if original_file_size <= constraints.first
end end
# Returns true if all the files exist.
def exists?(style) def exists?(style)
style ||= definition.default_style style ||= definition.default_style
attachment_exists?(style) attachment_exists?(style)
end end
def make_thumbnails_from data # Generates the thumbnails from the data supplied. Following this call, the data will
# be available from for_attached_files.
def convert data
begin begin
definition.thumbnails.each do |style, geometry| definition.styles.each do |style, geometry|
self[style] = Thumbnail.make(geometry, data) self[style] = Thumbnail.make(geometry, data)
end end
rescue PaperclipError => e rescue PaperclipError => e
...@@ -139,6 +163,13 @@ module Paperclip ...@@ -139,6 +163,13 @@ module Paperclip
end end
end end
# Returns a hash of procs that will perform the various interpolations for
# the path, url, and missing_url attachment options. The procs are used as
# arguments to gsub!, so the used will be replaced with the return value
# of the proc. You can add to this list by assigning to the hash:
# Paperclip::Attachment.interpolations[:content_type] = lambda{|style, attachment| attachment.content_type }
# attchment.interpolate("none", ":content_type")
# # => "image/jpeg"
def self.interpolations def self.interpolations
@interpolations ||= { @interpolations ||= {
:rails_root => lambda{|style, atch| RAILS_ROOT }, :rails_root => lambda{|style, atch| RAILS_ROOT },
...@@ -152,22 +183,29 @@ module Paperclip ...@@ -152,22 +183,29 @@ module Paperclip
} }
end end
# Searches for patterns in +source+ string supplied and replaces them with values
# returned by the procs in the interpolations hash.
def interpolate style, source def interpolate style, source
returning source.dup do |s| returning source.dup do |s|
Attachment.interpolations.each do |key, proc| Attachment.interpolations.each do |key, proc|
s.gsub!(/:#{key}/){ proc.call(style, self) } s.gsub!(/:#{key}/) do
proc.call(style, self) rescue ":#{key}"
end
end end
end end
end end
# Sets the *_file_name column on the activerecord for this attachment
def original_filename= new_name def original_filename= new_name
instance["#{name}_file_name"] = @original_filename = new_name instance["#{name}_file_name"] = @original_filename = new_name
end end
# Sets the *_content_type column on the activerecord for this attachment
def content_type= new_type def content_type= new_type
instance["#{name}_content_type"] = @content_type = new_type instance["#{name}_content_type"] = @content_type = new_type
end end
# Sets the *_file_size column on the activerecord for this attachment
def original_file_size= new_size def original_file_size= new_size
instance["#{name}_file_size"] = @original_file_size = new_size instance["#{name}_file_size"] = @original_file_size = new_size
end end
...@@ -178,13 +216,13 @@ module Paperclip ...@@ -178,13 +216,13 @@ module Paperclip
protected protected
def is_a_file? data def is_a_file? data #:nodoc:
[:content_type, :original_filename, :read].map do |meth| [:content_type, :original_filename, :read].map do |meth|
data.respond_to? meth data.respond_to? meth
end.all? end.all?
end end
def sanitize_filename filename def sanitize_filename filename #:nodoc:
File.basename(filename).gsub(/[^\w\.\_]/,'_') File.basename(filename).gsub(/[^\w\.\_]/,'_')
end end
end end
......
...@@ -10,8 +10,8 @@ module Paperclip ...@@ -10,8 +10,8 @@ module Paperclip
:path => ":rails_root/public/:class/:attachment/:id/:style_:filename", :path => ":rails_root/public/:class/:attachment/:id/:style_:filename",
:url => "/:class/:attachment/:id/:style_:filename", :url => "/:class/:attachment/:id/:style_:filename",
:missing_url => "/:class/:attachment/:style_missing.png", :missing_url => "/:class/:attachment/:style_missing.png",
:attachment_type => :image, :content_type => :image,
:thumbnails => {}, :styles => {},
:delete_on_destroy => true, :delete_on_destroy => true,
:default_style => :original :default_style => :original
} }
...@@ -26,15 +26,10 @@ module Paperclip ...@@ -26,15 +26,10 @@ module Paperclip
@name @name
end end
# A hash of all styles of the attachment. Essentially all the thumbnails # A hash of all styles of the attachment, plus :original. If :original is specified
# plus the original. # in the styles option, it will be overwritten.
def styles def styles
@styles ||= thumbnails.merge(:original => nil) @styles ||= @options[:styles].merge(:original => nil)
end
# A hash of all defined thumbnails for this attachment.
def thumbnails
@thumbnails ||= @options[:thumbnails] || {}
end end
# A convenience method to insert validation options into the options hash # A convenience method to insert validation options into the options hash
......
...@@ -3,10 +3,10 @@ module Paperclip ...@@ -3,10 +3,10 @@ module Paperclip
module Filesystem module Filesystem
def write_attachment def write_attachment
ensure_directories ensure_directories
for_attached_files do |style, data| each_unsaved do |style, data|
File.open( file_name(style), "w" ) do |file| File.open( file_name(style), "w" ) do |file|
file.rewind file.rewind
file.write(data) if data file.write(data)
end end
end end
end end
...@@ -16,7 +16,7 @@ module Paperclip ...@@ -16,7 +16,7 @@ module Paperclip
end end
def delete_attachment complain = false def delete_attachment complain = false
for_attached_files do |style, data| styles.each do |style|
file_path = file_name(style) file_path = file_name(style)
begin begin
FileUtils.rm file_path if file_path FileUtils.rm file_path if file_path
...@@ -36,7 +36,7 @@ module Paperclip ...@@ -36,7 +36,7 @@ module Paperclip
end end
def ensure_directories def ensure_directories
for_attached_files do |style, file| each_unsaved do |style, file|
dirname = File.dirname( file_name(style) ) dirname = File.dirname( file_name(style) )
FileUtils.mkdir_p dirname FileUtils.mkdir_p dirname
end end
......
...@@ -44,10 +44,6 @@ module Paperclip ...@@ -44,10 +44,6 @@ module Paperclip
interpolate( style, definition.url ) interpolate( style, definition.url )
end end
def attachment_exists? style = nil
AWS::S3::S3Object.exists?( file_name(style), bucket )
end
def bucket def bucket
definition.bucket definition.bucket
end end
...@@ -61,9 +57,20 @@ module Paperclip ...@@ -61,9 +57,20 @@ module Paperclip
end end
end end
def stream style = nil, &block
AWS::S3::S3Object.stream( file_name(style), bucket, &block )
end
# These four methods are the primary interface for the storage module.
# Everything above this is support for these methods.
def attachment_exists? style = nil
AWS::S3::S3Object.exists?( file_name(style), bucket )
end
def write_attachment def write_attachment
ensure_bucket ensure_bucket
for_attached_files do |style, data| each_unsaved do |style, data|
AWS::S3::S3Object.store( file_name(style), data, bucket, :access => definition.s3_access || :public_read ) AWS::S3::S3Object.store( file_name(style), data, bucket, :access => definition.s3_access || :public_read )
end end
end end
...@@ -72,12 +79,8 @@ module Paperclip ...@@ -72,12 +79,8 @@ module Paperclip
AWS::S3::S3Object.value( file_name(style), bucket ) AWS::S3::S3Object.value( file_name(style), bucket )
end end
def stream style = nil, &block
AWS::S3::S3Object.stream( file_name(style), bucket, &block )
end
def delete_attachment complain = false def delete_attachment complain = false
for_attached_files do |style| styles.each do |style, data|
AWS::S3::S3Object.delete( file_name(style), bucket ) AWS::S3::S3Object.delete( file_name(style), bucket )
end end
end end
......
...@@ -96,7 +96,7 @@ class TestAttachment < Test::Unit::TestCase ...@@ -96,7 +96,7 @@ class TestAttachment < Test::Unit::TestCase
context "with an image with thumbnails attached to :image and saved" do context "with an image with thumbnails attached to :image and saved" do
setup do setup do
assert Foo.has_attached_file(:image, :thumbnails => {:small => "16x16", :medium => "100x100", :large => "250x250", :square => "32x32#"}) assert Foo.has_attached_file(:image, :styles => {:small => "16x16", :medium => "100x100", :large => "250x250", :square => "32x32#"})
@foo = Foo.new @foo = Foo.new
@file = File.new(File.join(File.dirname(__FILE__), "fixtures", "test_image.jpg")) @file = File.new(File.join(File.dirname(__FILE__), "fixtures", "test_image.jpg"))
assert_nothing_raised{ @foo.image = @file } assert_nothing_raised{ @foo.image = @file }
...@@ -135,7 +135,7 @@ class TestAttachment < Test::Unit::TestCase ...@@ -135,7 +135,7 @@ class TestAttachment < Test::Unit::TestCase
context "with an invalid image with a square thumbnail attached to :image" do context "with an invalid image with a square thumbnail attached to :image" do
setup do setup do
assert Foo.has_attached_file(:image, :thumbnails => {:square => "32x32#"}) assert Foo.has_attached_file(:image, :styles => {:square => "32x32#"})
assert Foo.validates_attached_file(:image) assert Foo.validates_attached_file(:image)
@foo = Foo.new @foo = Foo.new
@file = File.new(File.join(File.dirname(__FILE__), "fixtures", "test_invalid_image.jpg")) @file = File.new(File.join(File.dirname(__FILE__), "fixtures", "test_invalid_image.jpg"))
...@@ -151,7 +151,7 @@ class TestAttachment < Test::Unit::TestCase ...@@ -151,7 +151,7 @@ class TestAttachment < Test::Unit::TestCase
context "with an invalid image attached to :image" do context "with an invalid image attached to :image" do
setup do setup do
assert Foo.has_attached_file(:image, :thumbnails => {:sorta_square => "32x32"}) assert Foo.has_attached_file(:image, :styles => {:sorta_square => "32x32"})
assert Foo.validates_attached_file(:image) assert Foo.validates_attached_file(:image)
@foo = Foo.new @foo = Foo.new
@file = File.new(File.join(File.dirname(__FILE__), "fixtures", "test_invalid_image.jpg")) @file = File.new(File.join(File.dirname(__FILE__), "fixtures", "test_invalid_image.jpg"))
......
...@@ -25,7 +25,7 @@ class TestAttachmentDefinition < Test::Unit::TestCase ...@@ -25,7 +25,7 @@ class TestAttachmentDefinition < Test::Unit::TestCase
:path => "/home/stuff/place", :path => "/home/stuff/place",
:url => "/attachments/:attachment/:name", :url => "/attachments/:attachment/:name",
:custom_definition => :boogie!, :custom_definition => :boogie!,
:thumbnails => {:thumb => "100x100", :large => "300x300>"}, :styles => {:thumb => "100x100", :large => "300x300>"},
:validates_existance => true, :validates_existance => true,
:validates_size => [0, 2048] :validates_size => [0, 2048]
} }
...@@ -38,13 +38,13 @@ class TestAttachmentDefinition < Test::Unit::TestCase ...@@ -38,13 +38,13 @@ class TestAttachmentDefinition < Test::Unit::TestCase
end end
should "be able to read options using attribute readers" do should "be able to read options using attribute readers" do
@options.keys.each do |key| (@options.keys - [:styles]).each do |key|
assert_equal @options[key], @def.send(key) assert_equal @options[key], @def.send(key)
end end
end end
should "return styles as thumbnails plus the original" do should "return styles as the styles option plus the original" do
assert( (@def.thumbnails.keys + [:original]).map(&:to_s).sort == @def.styles.keys.map(&:to_s).sort ) assert_equal( (@options[:styles].keys + [:original]).map(&:to_s).sort.uniq, @def.styles.keys.map(&:to_s).sort )
end end
should "return all validations when sent :validations" do should "return all validations when sent :validations" do
......
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