Commit f8fff43d by Henrik N

Merge commit 'tb/master'

parents 27727359 e483854d
...@@ -12,7 +12,7 @@ task :default => [:clean, :test] ...@@ -12,7 +12,7 @@ task :default => [:clean, :test]
desc 'Test the paperclip plugin.' desc 'Test the paperclip plugin.'
Rake::TestTask.new(:test) do |t| Rake::TestTask.new(:test) do |t|
t.libs << 'lib' << 'profile' t.libs << 'lib' << 'profile'
t.pattern = 'test/**/test_*.rb' t.pattern = 'test/**/*_test.rb'
t.verbose = true t.verbose = true
end end
...@@ -46,6 +46,7 @@ task :clean do |t| ...@@ -46,6 +46,7 @@ task :clean do |t|
end end
spec = Gem::Specification.new do |s| spec = Gem::Specification.new do |s|
s.rubygems_version = "1.2.0"
s.name = "paperclip" s.name = "paperclip"
s.version = Paperclip::VERSION s.version = Paperclip::VERSION
s.author = "Jon Yurek" s.author = "Jon Yurek"
...@@ -65,6 +66,9 @@ spec = Gem::Specification.new do |s| ...@@ -65,6 +66,9 @@ spec = Gem::Specification.new do |s|
s.extra_rdoc_files = ["README"] s.extra_rdoc_files = ["README"]
s.rdoc_options << '--line-numbers' << '--inline-source' s.rdoc_options << '--line-numbers' << '--inline-source'
s.requirements << "ImageMagick" s.requirements << "ImageMagick"
s.add_runtime_dependency 'right_aws'
s.add_development_dependency 'Shoulda'
s.add_development_dependency 'mocha'
end end
Rake::GemPackageTask.new(spec) do |pkg| Rake::GemPackageTask.new(spec) do |pkg|
......
Usage: Usage:
script/generate attachment Class attachment1 attachment2 script/generate paperclip Class attachment1 (attachment2 ...)
This will create a migration that will add the proper columns to your class's table. This will create a migration that will add the proper columns to your class's table.
\ No newline at end of file
...@@ -113,6 +113,12 @@ module Paperclip ...@@ -113,6 +113,12 @@ module Paperclip
def has_attached_file name, options = {} def has_attached_file name, options = {}
include InstanceMethods include InstanceMethods
%w(file_name content_type).each do |field|
unless column_names.include?("#{name}_#{field}")
raise PaperclipError.new("#{self} model does not have required column '#{name}_#{field}'")
end
end
write_inheritable_attribute(:attachment_definitions, {}) if attachment_definitions.nil? write_inheritable_attribute(:attachment_definitions, {}) if attachment_definitions.nil?
attachment_definitions[name] = {:validations => []}.merge(options) attachment_definitions[name] = {:validations => []}.merge(options)
...@@ -133,7 +139,7 @@ module Paperclip ...@@ -133,7 +139,7 @@ module Paperclip
end end
validates_each(name) do |record, attr, value| validates_each(name) do |record, attr, value|
value.send(:flush_errors) value.send(:flush_errors) unless value.valid?
end end
end end
...@@ -180,7 +186,11 @@ module Paperclip ...@@ -180,7 +186,11 @@ module Paperclip
# Places ActiveRecord-style validations on the content type of the file assigned. The # Places ActiveRecord-style validations on the content type of the file assigned. The
# possible options are: # possible options are:
# * +content_type+: Allowed content types. Can be a single content type or an array. Allows all by default. # * +content_type+: Allowed content types. Can be a single content type or an array.
# Each type can be a String or a Regexp. It should be noted that Internet Explorer uploads
# files with content_types that you may not expect. For example, JPEG images are given
# image/pjpeg and PNGs are image/x-png, so keep that in mind when determining how you match.
# Allows all by default.
# * +message+: The message to display when the uploaded file has an invalid content type. # * +message+: The message to display when the uploaded file has an invalid content type.
def validates_attachment_content_type name, options = {} def validates_attachment_content_type name, options = {}
attachment_definitions[name][:validations] << lambda do |attachment, instance| attachment_definitions[name][:validations] << lambda do |attachment, instance|
...@@ -190,7 +200,7 @@ module Paperclip ...@@ -190,7 +200,7 @@ module Paperclip
unless options[:content_type].blank? unless options[:content_type].blank?
content_type = instance[:"#{name}_content_type"] content_type = instance[:"#{name}_content_type"]
unless valid_types.any?{|t| t === content_type } unless valid_types.any?{|t| t === content_type }
options[:message] || ActiveRecord::Errors.default_error_messages[:inclusion] options[:message] || "is not one of the allowed file types."
end end
end end
end end
......
...@@ -43,6 +43,8 @@ module Paperclip ...@@ -43,6 +43,8 @@ module Paperclip
normalize_style_definition normalize_style_definition
initialize_storage initialize_storage
logger.info("[paperclip] Paperclip attachment #{name} on #{instance.class} initialized.")
end end
# What gets called when you call instance.attachment = File. It clears errors, # What gets called when you call instance.attachment = File. It clears errors,
...@@ -50,6 +52,7 @@ module Paperclip ...@@ -50,6 +52,7 @@ module Paperclip
# the previous file for deletion, to be flushed away on #save of its host. # the previous file for deletion, to be flushed away on #save of its host.
def assign uploaded_file def assign uploaded_file
return nil unless valid_assignment?(uploaded_file) return nil unless valid_assignment?(uploaded_file)
logger.info("[paperclip] Assigning #{uploaded_file} to #{name}")
queue_existing_for_delete queue_existing_for_delete
@errors = [] @errors = []
...@@ -57,6 +60,7 @@ module Paperclip ...@@ -57,6 +60,7 @@ module Paperclip
return nil if uploaded_file.nil? return nil if uploaded_file.nil?
logger.info("[paperclip] Writing attributes for #{name}")
@queued_for_write[:original] = uploaded_file.to_tempfile @queued_for_write[:original] = uploaded_file.to_tempfile
@instance[:"#{@name}_file_name"] = uploaded_file.original_filename.strip.gsub /[^\w\d\.\-]+/, '_' @instance[:"#{@name}_file_name"] = uploaded_file.original_filename.strip.gsub /[^\w\d\.\-]+/, '_'
@instance[:"#{@name}_content_type"] = uploaded_file.content_type.strip @instance[:"#{@name}_content_type"] = uploaded_file.content_type.strip
...@@ -95,6 +99,7 @@ module Paperclip ...@@ -95,6 +99,7 @@ module Paperclip
# Returns true if there are any errors on this attachment. # Returns true if there are any errors on this attachment.
def valid? def valid?
validate
errors.length == 0 errors.length == 0
end end
...@@ -112,11 +117,13 @@ module Paperclip ...@@ -112,11 +117,13 @@ module Paperclip
# the instance's errors and returns false, cancelling the save. # the instance's errors and returns false, cancelling the save.
def save def save
if valid? if valid?
logger.info("[paperclip] Saving files for #{name}")
flush_deletes flush_deletes
flush_writes flush_writes
@dirty = false @dirty = false
true true
else else
logger.info("[paperclip] Errors on #{name}. Not saving.")
flush_errors flush_errors
false false
end end
...@@ -166,18 +173,27 @@ module Paperclip ...@@ -166,18 +173,27 @@ module Paperclip
# again. # again.
def reprocess! def reprocess!
new_original = Tempfile.new("paperclip-reprocess") new_original = Tempfile.new("paperclip-reprocess")
old_original = to_file(:original) if old_original = to_file(:original)
new_original.write( old_original.read ) new_original.write( old_original.read )
new_original.rewind new_original.rewind
@queued_for_write = { :original => new_original } @queued_for_write = { :original => new_original }
post_process post_process
old_original.close if old_original.respond_to?(:close) old_original.close if old_original.respond_to?(:close)
save
else
true
end
end end
private private
def logger
instance.logger
end
def valid_assignment? file #:nodoc: def valid_assignment? file #:nodoc:
file.nil? || (file.respond_to?(:original_filename) && file.respond_to?(:content_type)) file.nil? || (file.respond_to?(:original_filename) && file.respond_to?(:content_type))
end end
...@@ -189,6 +205,7 @@ module Paperclip ...@@ -189,6 +205,7 @@ module Paperclip
end.flatten.compact.uniq end.flatten.compact.uniq
@errors += @validation_errors @errors += @validation_errors
end end
@validation_errors
end end
def normalize_style_definition def normalize_style_definition
...@@ -206,9 +223,11 @@ module Paperclip ...@@ -206,9 +223,11 @@ module Paperclip
def post_process #:nodoc: def post_process #:nodoc:
return if @queued_for_write[:original].nil? return if @queued_for_write[:original].nil?
logger.info("[paperclip] Post-processing #{name}")
@styles.each do |name, args| @styles.each do |name, args|
begin begin
dimensions, format = args dimensions, format = args
dimensions = dimensions.call(instance) if dimensions.respond_to? :call
@queued_for_write[name] = Thumbnail.make(@queued_for_write[:original], @queued_for_write[name] = Thumbnail.make(@queued_for_write[:original],
dimensions, dimensions,
format, format,
...@@ -231,6 +250,7 @@ module Paperclip ...@@ -231,6 +250,7 @@ module Paperclip
def queue_existing_for_delete #:nodoc: def queue_existing_for_delete #:nodoc:
return if original_filename.blank? return if original_filename.blank?
logger.info("[paperclip] Queueing the existing files for #{name} for deletion.")
@queued_for_delete += [:original, *@styles.keys].uniq.map do |style| @queued_for_delete += [:original, *@styles.keys].uniq.map do |style|
path(style) if exists?(style) path(style) if exists?(style)
end.compact end.compact
......
...@@ -76,9 +76,9 @@ module Paperclip ...@@ -76,9 +76,9 @@ module Paperclip
# overhanging image would be cropped. Useful for square thumbnail images. The cropping # overhanging image would be cropped. Useful for square thumbnail images. The cropping
# is weighted at the center of the Geometry. # is weighted at the center of the Geometry.
def transformation_to dst, crop = false def transformation_to dst, crop = false
ratio = Geometry.new( dst.width / self.width, dst.height / self.height )
if crop if crop
ratio = Geometry.new( dst.width / self.width, dst.height / self.height )
scale_geometry, scale = scaling(dst, ratio) scale_geometry, scale = scaling(dst, ratio)
crop_geometry = cropping(dst, ratio, scale) crop_geometry = cropping(dst, ratio, scale)
else else
......
...@@ -9,7 +9,7 @@ module Paperclip ...@@ -9,7 +9,7 @@ module Paperclip
# almost all cases, should) be coordinated with the value of the +url+ option to # 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 # allow files to be saved into a place where Apache can serve them without
# hitting your app. Defaults to # hitting your app. Defaults to
# ":rails_root/public/:class/:attachment/:id/:style_:filename". # ":rails_root/public/:attachment/:id/:style/:basename.:extension"
# By default this places the files in the app's public directory which can be served # 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 # 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 # make a symlink to the capistrano-created system directory from inside your app's
...@@ -36,8 +36,10 @@ module Paperclip ...@@ -36,8 +36,10 @@ module Paperclip
alias_method :to_io, :to_file alias_method :to_io, :to_file
def flush_writes #:nodoc: def flush_writes #:nodoc:
logger.info("[paperclip] Writing files for #{name}")
@queued_for_write.each do |style, file| @queued_for_write.each do |style, file|
FileUtils.mkdir_p(File.dirname(path(style))) FileUtils.mkdir_p(File.dirname(path(style)))
logger.info("[paperclip] -> #{path(style)}")
result = file.stream_to(path(style)) result = file.stream_to(path(style))
file.close file.close
result.close result.close
...@@ -46,8 +48,10 @@ module Paperclip ...@@ -46,8 +48,10 @@ module Paperclip
end end
def flush_deletes #:nodoc: def flush_deletes #:nodoc:
logger.info("[paperclip] Deleting files for #{name}")
@queued_for_delete.each do |path| @queued_for_delete.each do |path|
begin begin
logger.info("[paperclip] -> #{path}")
FileUtils.rm(path) if File.exist?(path) FileUtils.rm(path) if File.exist?(path)
rescue Errno::ENOENT => e rescue Errno::ENOENT => e
# ignore file-not-found, let everything else pass # ignore file-not-found, let everything else pass
...@@ -102,6 +106,7 @@ module Paperclip ...@@ -102,6 +106,7 @@ module Paperclip
base.class.interpolations[:s3_url] = lambda do |attachment, style| base.class.interpolations[:s3_url] = lambda do |attachment, style|
"https://s3.amazonaws.com/#{attachment.bucket_name}/#{attachment.path(style).gsub(%r{^/}, "")}" "https://s3.amazonaws.com/#{attachment.bucket_name}/#{attachment.path(style).gsub(%r{^/}, "")}"
end end
ActiveRecord::Base.logger.info("[paperclip] S3 Storage Initalized.")
end end
def s3 def s3
...@@ -135,8 +140,10 @@ module Paperclip ...@@ -135,8 +140,10 @@ module Paperclip
alias_method :to_io, :to_file alias_method :to_io, :to_file
def flush_writes #:nodoc: def flush_writes #:nodoc:
logger.info("[paperclip] Writing files for #{name}")
@queued_for_write.each do |style, file| @queued_for_write.each do |style, file|
begin begin
logger.info("[paperclip] -> #{path(style)}")
key = s3_bucket.key(path(style)) key = s3_bucket.key(path(style))
key.data = file key.data = file
key.put(nil, @s3_permissions) key.put(nil, @s3_permissions)
...@@ -148,8 +155,10 @@ module Paperclip ...@@ -148,8 +155,10 @@ module Paperclip
end end
def flush_deletes #:nodoc: def flush_deletes #:nodoc:
logger.info("[paperclip] Writing files for #{name}")
@queued_for_delete.each do |path| @queued_for_delete.each do |path|
begin begin
logger.info("[paperclip] -> #{path(style)}")
if file = s3_bucket.key(path) if file = s3_bucket.key(path)
file.delete file.delete
end end
......
...@@ -6,12 +6,15 @@ module Paperclip ...@@ -6,12 +6,15 @@ module Paperclip
# Infer the MIME-type of the file from the extension. # Infer the MIME-type of the file from the extension.
def content_type def content_type
type = self.path.match(/\.(\w+)$/)[1] rescue "octet-stream" type = (self.path.match(/\.(\w+)$/)[1] rescue "octet-stream").downcase
case type case type
when "jpg", "png", "gif" then "image/#{type}" when %r"jpe?g" then "image/jpeg"
when "txt" then "text/plain" when %r"tiff?" then "image/tiff"
when "csv", "xml", "html", "htm", "css", "js" then "text/#{type}" when %r"png", "gif", "bmp" then "image/#{type}"
else "x-application/#{type}" when "txt" then "text/plain"
when %r"html?" then "text/html"
when "csv", "xml", "css", "js" then "text/#{type}"
else "application/x-#{type}"
end end
end end
...@@ -31,3 +34,4 @@ end ...@@ -31,3 +34,4 @@ end
class File #:nodoc: class File #:nodoc:
include Paperclip::Upfile include Paperclip::Upfile
end end
...@@ -14,25 +14,66 @@ def obtain_attachments ...@@ -14,25 +14,66 @@ def obtain_attachments
end end
end end
def for_all_attachments
klass = obtain_class
names = obtain_attachments
ids = klass.connection.select_values("SELECT id FROM #{klass.table_name}")
ids.each do |id|
instance = klass.find(id)
names.each do |name|
result = if instance.send("#{ name }?")
yield(instance, name)
else
true
end
print result ? "." : "x"; $stdout.flush
end
end
puts " Done."
end
namespace :paperclip do namespace :paperclip do
desc "Regenerates thumbnails for a given CLASS (and optional ATTACHMENT)" desc "Refreshes both metadata and thumbnails."
task :refresh => :environment do task :refresh => ["paperclip:refresh:metadata", "paperclip:refresh:thumbnails"]
klass = obtain_class
names = obtain_attachments namespace :refresh do
instances = klass.find(:all) desc "Regenerates thumbnails for a given CLASS (and optional ATTACHMENT)."
task :thumbnails => :environment do
puts "Regenerating thumbnails for #{instances.length} instances of #{klass.name}:" errors = []
instances.each do |instance| for_all_attachments do |instance, name|
names.each do |name| result = instance.send(name).reprocess!
result = if instance.send("#{ name }?") errors << [instance.id, instance.errors] unless instance.errors.blank?
instance.send(name).reprocess! result
instance.send(name).save end
errors.each{|e| puts "#{e.first}: #{e.last.full_messages.inspect}" }
end
desc "Regenerates content_type/size metadata for a given CLASS (and optional ATTACHMENT)."
task :metadata => :environment do
for_all_attachments do |instance, name|
if file = instance.send(name).to_file
instance.send("#{name}_file_name=", instance.send("#{name}_file_name").strip)
instance.send("#{name}_content_type=", file.content_type.strip)
instance.send("#{name}_file_size=", file.size) if instance.respond_to?("#{name}_file_size")
instance.save(false)
else else
true true
end end
print result ? "." : "x"; $stdout.flush
end end
end end
puts " Done." end
desc "Cleans out invalid attachments. Useful after you've added new validations."
task :clean => :environment do
for_all_attachments do |instance, name|
instance.send(name).send(:validate)
if instance.send(name).valid?
true
else
instance.send("#{name}=", nil)
instance.save
end
end
end end
end end
...@@ -155,6 +155,7 @@ class AttachmentTest < Test::Unit::TestCase ...@@ -155,6 +155,7 @@ class AttachmentTest < Test::Unit::TestCase
@instance.stubs(:[]).with(:test_content_type).returns(nil) @instance.stubs(:[]).with(:test_content_type).returns(nil)
@instance.stubs(:[]).with(:test_file_size).returns(nil) @instance.stubs(:[]).with(:test_file_size).returns(nil)
@instance.stubs(:[]).with(:test_updated_at).returns(nil) @instance.stubs(:[]).with(:test_updated_at).returns(nil)
@instance.stubs(:logger).returns(ActiveRecord::Base.logger)
@attachment = Paperclip::Attachment.new(:test, @attachment = Paperclip::Attachment.new(:test,
@instance) @instance)
@file = File.new(File.join(File.dirname(__FILE__), @file = File.new(File.join(File.dirname(__FILE__),
......
...@@ -7,6 +7,14 @@ class PaperclipTest < Test::Unit::TestCase ...@@ -7,6 +7,14 @@ class PaperclipTest < Test::Unit::TestCase
@file = File.new(File.join(FIXTURES_DIR, "5k.png")) @file = File.new(File.join(FIXTURES_DIR, "5k.png"))
end end
should "error when trying to also create a 'blah' attachment" do
assert_raises(Paperclip::PaperclipError) do
Dummy.class_eval do
has_attached_file :blah
end
end
end
context "that is attr_protected" do context "that is attr_protected" do
setup do setup do
Dummy.class_eval do Dummy.class_eval do
...@@ -64,6 +72,38 @@ class PaperclipTest < Test::Unit::TestCase ...@@ -64,6 +72,38 @@ class PaperclipTest < Test::Unit::TestCase
assert Dummy.new.respond_to?(:avatar=) assert Dummy.new.respond_to?(:avatar=)
end end
context "that is valid" do
setup do
@dummy = Dummy.new
@dummy.avatar = @file
end
should "be valid" do
assert @dummy.valid?
end
context "then has a validation added that makes it invalid" do
setup do
assert @dummy.save
Dummy.class_eval do
validates_attachment_content_type :avatar, :content_type => ["text/plain"]
end
@dummy2 = Dummy.find(@dummy.id)
end
should "be invalid when reloaded" do
assert ! @dummy2.valid?, @dummy2.errors.inspect
end
should "be able to call #valid? twice without having duplicate errors" do
@dummy2.avatar.valid?
first_errors = @dummy2.avatar.errors
@dummy2.avatar.valid?
assert_equal first_errors, @dummy2.avatar.errors
end
end
end
[[:presence, nil, "5k.png", nil], [[:presence, nil, "5k.png", nil],
[:size, {:in => 1..10240}, "5k.png", "12k.png"], [:size, {:in => 1..10240}, "5k.png", "12k.png"],
[:size2, {:in => 1..10240}, nil, "12k.png"], [:size2, {:in => 1..10240}, nil, "12k.png"],
...@@ -102,21 +142,6 @@ class PaperclipTest < Test::Unit::TestCase ...@@ -102,21 +142,6 @@ class PaperclipTest < Test::Unit::TestCase
assert_equal 1, @dummy.avatar.errors.length assert_equal 1, @dummy.avatar.errors.length
end end
end end
# context "and an invalid file with :message" do
# setup do
# @file = args[3] && File.new(File.join(FIXTURES_DIR, args[3]))
# end
#
# should "have errors" do
# if args[1] && args[1][:message] && args[4]
# @dummy.avatar = @file
# assert ! @dummy.avatar.valid?
# assert_equal 1, @dummy.avatar.errors.length
# assert_equal args[4], @dummy.avatar.errors[0]
# end
# end
# end
end end
end end
end end
......
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