dm-paperclip 2.4.1 → 2.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. data/Gemfile +29 -0
  2. data/Gemfile.lock +100 -0
  3. data/README.md +145 -0
  4. data/Rakefile +37 -71
  5. data/VERSION +1 -0
  6. data/dm-paperclip.gemspec +103 -0
  7. data/lib/dm-paperclip.rb +88 -74
  8. data/lib/dm-paperclip/attachment.rb +139 -102
  9. data/lib/dm-paperclip/callbacks.rb +55 -0
  10. data/lib/dm-paperclip/command_line.rb +86 -0
  11. data/lib/dm-paperclip/ext/blank.rb +24 -0
  12. data/lib/dm-paperclip/ext/class.rb +50 -0
  13. data/lib/dm-paperclip/ext/compatibility.rb +11 -0
  14. data/lib/dm-paperclip/ext/try_dup.rb +12 -0
  15. data/lib/dm-paperclip/geometry.rb +3 -5
  16. data/lib/dm-paperclip/interpolations.rb +57 -32
  17. data/lib/dm-paperclip/iostream.rb +12 -26
  18. data/lib/dm-paperclip/processor.rb +14 -4
  19. data/lib/dm-paperclip/storage.rb +2 -257
  20. data/lib/dm-paperclip/storage/filesystem.rb +73 -0
  21. data/lib/dm-paperclip/storage/s3.rb +209 -0
  22. data/lib/dm-paperclip/storage/s3/aws_library.rb +41 -0
  23. data/lib/dm-paperclip/storage/s3/aws_s3_library.rb +60 -0
  24. data/lib/dm-paperclip/style.rb +90 -0
  25. data/lib/dm-paperclip/thumbnail.rb +33 -24
  26. data/lib/dm-paperclip/upfile.rb +13 -5
  27. data/lib/dm-paperclip/validations.rb +40 -37
  28. data/lib/dm-paperclip/version.rb +4 -0
  29. data/test/attachment_test.rb +510 -67
  30. data/test/command_line_test.rb +138 -0
  31. data/test/fixtures/s3.yml +8 -0
  32. data/test/fixtures/twopage.pdf +0 -0
  33. data/test/fixtures/uppercase.PNG +0 -0
  34. data/test/geometry_test.rb +54 -19
  35. data/test/helper.rb +91 -28
  36. data/test/integration_test.rb +252 -79
  37. data/test/interpolations_test.rb +150 -0
  38. data/test/iostream_test.rb +8 -15
  39. data/test/paperclip_test.rb +222 -69
  40. data/test/processor_test.rb +10 -0
  41. data/test/storage_test.rb +102 -23
  42. data/test/style_test.rb +141 -0
  43. data/test/thumbnail_test.rb +106 -18
  44. data/test/upfile_test.rb +36 -0
  45. metadata +136 -121
  46. data/README.rdoc +0 -116
  47. data/init.rb +0 -1
  48. data/lib/dm-paperclip/callback_compatability.rb +0 -33
@@ -0,0 +1,41 @@
1
+ module Paperclip
2
+ module Storage
3
+ module S3
4
+ # Mixin which interfaces with the 'aws' and 'right_aws' libraries.
5
+ module AwsLibrary
6
+ protected
7
+
8
+ def s3_connect!
9
+ @s3 = Aws::S3.new(
10
+ @s3_credentials[:access_key_id],
11
+ @s3_credentials[:secret_access_key]
12
+ )
13
+ @s3_bucket = @s3.bucket(bucket_name)
14
+ end
15
+
16
+ def s3_expiring_url(key,time)
17
+ @s3.interface.get_link(bucket_name,key,time)
18
+ end
19
+
20
+ def s3_exists?(key)
21
+ @s3_bucket.keys(:prefix => key).any? { |s3_key| s3_key.name == key }
22
+ end
23
+
24
+ def s3_download(key,file)
25
+ @s3_bucket.key(key).get { |chunk| file.write(chunk) }
26
+ end
27
+
28
+ def s3_store(key,file)
29
+ @s3_bucket.key(key).put(
30
+ file,
31
+ @s3_permissions.to_s.gsub('_','-')
32
+ )
33
+ end
34
+
35
+ def s3_delete(key)
36
+ @s3_bucket.key(key).delete
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,60 @@
1
+ module Paperclip
2
+ module Storage
3
+ module S3
4
+ # Mixin which interfaces with the 'aws-s3' library.
5
+ module AwsS3Library
6
+ protected
7
+
8
+ def s3_connect!
9
+ AWS::S3::Base.establish_connection!(@s3_options.merge(
10
+ :access_key_id => @s3_credentials[:access_key_id],
11
+ :secret_access_key => @s3_credentials[:secret_access_key]
12
+ ))
13
+ end
14
+
15
+ def s3_expiring_url(key,time)
16
+ AWS::S3::S3Object.url_for(key, bucket_name, :expires_in => time)
17
+ end
18
+
19
+ def s3_exists?(key)
20
+ AWS::S3::S3Object.exists?(key, bucket_name)
21
+ end
22
+
23
+ def s3_download(key,file)
24
+ file.write(AWS::S3::S3Object.value(key, bucket_name))
25
+ end
26
+
27
+ def s3_create_bucket
28
+ AWS::S3::Bucket.create(bucket_name)
29
+ end
30
+
31
+ def s3_store(key,file)
32
+ begin
33
+ AWS::S3::S3Object.store(
34
+ key,
35
+ file,
36
+ bucket_name,
37
+ {
38
+ :content_type => instance_read(:content_type),
39
+ :access => @s3_permissions,
40
+ }.merge(@s3_headers)
41
+ )
42
+ rescue AWS::S3::NoSuchBucket => e
43
+ s3_create_bucket
44
+ retry
45
+ rescue AWS::S3::ResponseError => e
46
+ raise
47
+ end
48
+ end
49
+
50
+ def s3_delete(key)
51
+ begin
52
+ AWS::S3::S3Object.delete(key, bucket_name)
53
+ rescue AWS::S3::ResponseError
54
+ # Ignore this.
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,90 @@
1
+ # encoding: utf-8
2
+ module Paperclip
3
+ # The Style class holds the definition of a thumbnail style, applying
4
+ # whatever processing is required to normalize the definition and delaying
5
+ # the evaluation of block parameters until useful context is available.
6
+
7
+ class Style
8
+
9
+ attr_reader :name, :attachment, :format
10
+
11
+ # Creates a Style object. +name+ is the name of the attachment,
12
+ # +definition+ is the style definition from has_attached_file, which
13
+ # can be string, array or hash
14
+ def initialize name, definition, attachment
15
+ @name = name
16
+ @attachment = attachment
17
+ if definition.is_a? Hash
18
+ @geometry = definition.delete(:geometry)
19
+ @format = definition.delete(:format)
20
+ @processors = definition.delete(:processors)
21
+ @other_args = definition
22
+ else
23
+ @geometry, @format = [definition, nil].flatten[0..1]
24
+ @other_args = {}
25
+ end
26
+ @format = nil if Paperclip::Ext.blank?(@format)
27
+ end
28
+
29
+ # retrieves from the attachment the processors defined in the has_attached_file call
30
+ # (which method (in the attachment) will call any supplied procs)
31
+ # There is an important change of interface here: a style rule can set its own processors
32
+ # by default we behave as before, though.
33
+ def processors
34
+ @processors || attachment.processors
35
+ end
36
+
37
+ # retrieves from the attachment the whiny setting
38
+ def whiny
39
+ attachment.whiny
40
+ end
41
+
42
+ # returns true if we're inclined to grumble
43
+ def whiny?
44
+ !!whiny
45
+ end
46
+
47
+ def convert_options
48
+ attachment.send(:extra_options_for, name)
49
+ end
50
+
51
+ # returns the geometry string for this style
52
+ # if a proc has been supplied, we call it here
53
+ def geometry
54
+ @geometry.respond_to?(:call) ? @geometry.call(attachment.instance) : @geometry
55
+ end
56
+
57
+ # Supplies the hash of options that processors expect to receive as their second argument
58
+ # Arguments other than the standard geometry, format etc are just passed through from
59
+ # initialization and any procs are called here, just before post-processing.
60
+ def processor_options
61
+ args = {}
62
+ @other_args.each do |k,v|
63
+ args[k] = v.respond_to?(:call) ? v.call(attachment) : v
64
+ end
65
+ [:processors, :geometry, :format, :whiny, :convert_options].each do |k|
66
+ (arg = send(k)) && args[k] = arg
67
+ end
68
+ args
69
+ end
70
+
71
+ # Supports getting and setting style properties with hash notation to ensure backwards-compatibility
72
+ # eg. @attachment.styles[:large][:geometry]@ will still work
73
+ def [](key)
74
+ if [:name, :convert_options, :whiny, :processors, :geometry, :format].include?(key)
75
+ send(key)
76
+ elsif defined? @other_args[key]
77
+ @other_args[key]
78
+ end
79
+ end
80
+
81
+ def []=(key, value)
82
+ if [:name, :convert_options, :whiny, :processors, :geometry, :format].include?(key)
83
+ send("#{key}=".intern, value)
84
+ else
85
+ @other_args[key] = value
86
+ end
87
+ end
88
+
89
+ end
90
+ end
@@ -2,7 +2,7 @@ module Paperclip
2
2
  # Handles thumbnailing images that are uploaded.
3
3
  class Thumbnail < Processor
4
4
 
5
- attr_accessor :current_geometry, :target_geometry, :format, :whiny, :convert_options
5
+ attr_accessor :current_geometry, :target_geometry, :format, :whiny, :convert_options, :source_file_options
6
6
 
7
7
  # Creates a Thumbnail object set to work on the +file+ given. It
8
8
  # will attempt to transform the image into one defined by +target_geometry+
@@ -12,17 +12,23 @@ module Paperclip
12
12
  # set, the options will be appended to the convert command upon image conversion
13
13
  def initialize file, options = {}, attachment = nil
14
14
  super
15
- geometry = options[:geometry]
16
- @file = file
17
- @crop = geometry[-1,1] == '#'
18
- @target_geometry = Geometry.parse geometry
19
- @current_geometry = Geometry.from_file @file
20
- @convert_options = options[:convert_options]
21
- @whiny = options[:whiny].nil? ? true : options[:whiny]
22
- @format = options[:format]
23
15
 
24
- @current_format = File.extname(@file.path)
25
- @basename = File.basename(@file.path, @current_format)
16
+ geometry = options[:geometry]
17
+ @file = file
18
+ @crop = geometry[-1,1] == '#'
19
+ @target_geometry = Geometry.parse geometry
20
+ @current_geometry = Geometry.from_file @file
21
+ @source_file_options = options[:source_file_options]
22
+ @convert_options = options[:convert_options]
23
+ @whiny = options[:whiny].nil? ? true : options[:whiny]
24
+ @format = options[:format]
25
+
26
+ @source_file_options = @source_file_options.split(/\s+/) if @source_file_options.respond_to?(:split)
27
+ @convert_options = @convert_options.split(/\s+/) if @convert_options.respond_to?(:split)
28
+
29
+ @current_format = File.extname(@file.path)
30
+ @basename = File.basename(@file.path, @current_format)
31
+
26
32
  end
27
33
 
28
34
  # Returns true if the +target_geometry+ is meant to crop.
@@ -32,25 +38,28 @@ module Paperclip
32
38
 
33
39
  # Returns true if the image is meant to make use of additional convert options.
34
40
  def convert_options?
35
- not @convert_options.blank?
41
+ !@convert_options.nil? && !@convert_options.empty?
36
42
  end
37
43
 
38
44
  # Performs the conversion of the +file+ into a thumbnail. Returns the Tempfile
39
45
  # that contains the new image.
40
46
  def make
41
47
  src = @file
42
- dst = Tempfile.new([@basename, @format].compact.join("."))
48
+ dst = Tempfile.new([@basename, @format ? ".#{@format}" : ''])
43
49
  dst.binmode
44
50
 
45
- command = <<-end_command
46
- "#{ File.expand_path(src.path) }[0]"
47
- #{ transformation_command }
48
- "#{ File.expand_path(dst.path) }"
49
- end_command
50
-
51
51
  begin
52
- success = Paperclip.run("convert", command.gsub(/\s+/, " "))
53
- rescue PaperclipCommandLineError
52
+ parameters = []
53
+ parameters << source_file_options
54
+ parameters << ":source"
55
+ parameters << transformation_command
56
+ parameters << convert_options
57
+ parameters << ":dest"
58
+
59
+ parameters = parameters.flatten.compact.join(" ").strip.squeeze(" ")
60
+
61
+ success = Paperclip.run("convert", parameters, :source => "#{File.expand_path(src.path)}[0]", :dest => File.expand_path(dst.path))
62
+ rescue PaperclipCommandLineError => e
54
63
  raise PaperclipError, "There was an error processing the thumbnail for #{@basename}" if @whiny
55
64
  end
56
65
 
@@ -61,9 +70,9 @@ module Paperclip
61
70
  # into the thumbnail.
62
71
  def transformation_command
63
72
  scale, crop = @current_geometry.transformation_to(@target_geometry, crop?)
64
- trans = "-resize \"#{scale}\""
65
- trans << " -crop \"#{crop}\" +repage" if crop
66
- trans << " #{convert_options}" if convert_options?
73
+ trans = []
74
+ trans << "-resize" << %["#{scale}"] unless scale.nil? || scale.empty?
75
+ trans << "-crop" << %["#{crop}"] << "+repage" if crop
67
76
  trans
68
77
  end
69
78
  end
@@ -8,13 +8,18 @@ module Paperclip
8
8
  def content_type
9
9
  type = (self.path.match(/\.(\w+)$/)[1] rescue "octet-stream").downcase
10
10
  case type
11
- when %r"jpe?g" then "image/jpeg"
11
+ when %r"jp(e|g|eg)" then "image/jpeg"
12
12
  when %r"tiff?" then "image/tiff"
13
13
  when %r"png", "gif", "bmp" then "image/#{type}"
14
14
  when "txt" then "text/plain"
15
15
  when %r"html?" then "text/html"
16
- when "csv", "xml", "css", "js" then "text/#{type}"
17
- else "application/x-#{type}"
16
+ when "js" then "application/js"
17
+ when "csv", "xml", "css" then "text/#{type}"
18
+ else
19
+ # On BSDs, `file` doesn't give a result code of 1 if the file doesn't exist.
20
+ content_type = (Paperclip.run("file", "-b --mime-type :file", :file => self.path).split(':').last.strip rescue "application/x-#{type}")
21
+ content_type = "application/x-#{type}" if content_type.match(/\(.*?\)/)
22
+ content_type
18
23
  end
19
24
  end
20
25
 
@@ -32,16 +37,19 @@ end
32
37
 
33
38
  if defined? StringIO
34
39
  class StringIO
35
- attr_accessor :original_filename, :content_type
40
+ attr_accessor :original_filename, :content_type, :fingerprint
36
41
  def original_filename
37
42
  @original_filename ||= "stringio.txt"
38
43
  end
39
44
  def content_type
40
45
  @content_type ||= "text/plain"
41
46
  end
47
+ def fingerprint
48
+ @fingerprint ||= Digest::MD5.hexdigest(self.string)
49
+ end
42
50
  end
43
51
  end
44
52
 
45
53
  class File #:nodoc:
46
54
  include Paperclip::Upfile
47
- end
55
+ end
@@ -10,8 +10,7 @@ module Paperclip
10
10
  # * +greater_than+: equivalent to :in => options[:greater_than]..Infinity
11
11
  # * +message+: error message to display, use :min and :max as replacements
12
12
  def validates_attachment_size(*fields)
13
- opts = opts_from_validator_args(fields)
14
- add_validator_to_context(opts, fields, Paperclip::Validate::SizeValidator)
13
+ validators.add(Paperclip::Validate::SizeValidator, *fields)
15
14
  end
16
15
 
17
16
  # Adds errors if thumbnail creation fails. The same as specifying :whiny_thumbnails => true.
@@ -21,8 +20,7 @@ module Paperclip
21
20
 
22
21
  # Places ActiveRecord-style validations on the presence of a file.
23
22
  def validates_attachment_presence(*fields)
24
- opts = opts_from_validator_args(fields)
25
- add_validator_to_context(opts, fields, Paperclip::Validate::RequiredFieldValidator)
23
+ validators.add(Paperclip::Validate::RequiredFieldValidator, *fields)
26
24
  end
27
25
 
28
26
  # Places ActiveRecord-style validations on the content type of the file assigned. The
@@ -30,45 +28,41 @@ module Paperclip
30
28
  # * +content_type+: Allowed content types. Can be a single content type or an array. Allows all by default.
31
29
  # * +message+: The message to display when the uploaded file has an invalid content type.
32
30
  def validates_attachment_content_type(*fields)
33
- opts = opts_from_validator_args(fields)
34
- add_validator_to_context(opts, fields, Paperclip::Validate::ContentTypeValidator)
31
+ validators.add(Paperclip::Validate::ContentTypeValidator, *fields)
35
32
  end
36
33
 
34
+ # Places ActiveRecord-style validations on the geometry of the file assigned. The
35
+ # required options are:
36
+ # * +height+: a Range of pixels (i.e. 100..300+),
37
+ # * +width+: a Range of pixels (i.e. 100..300+)
38
+ def validates_attachment_geometry(*fields)
39
+ validators.add(Paperclip::Validate::GeometryValidator, *fields)
40
+ end
37
41
  end
38
42
 
39
43
  class SizeValidator < DataMapper::Validate::GenericValidator #:nodoc:
40
- def initialize(field_name, options={})
41
- super
42
- @field_name, @options = field_name, options
43
- end
44
-
45
44
  def call(target)
46
45
  field_value = target.validation_property_value(:"#{@field_name}_file_size")
47
46
  return true if field_value.nil?
48
47
 
49
- @options[:in] = (@options[:greater_than]..(1/0)) unless @options[:greater_than].nil?
48
+ @options[:in] = (@options[:greater_than]..(1.0/0)) unless @options[:greater_than].nil?
50
49
  @options[:in] = (0..@options[:less_than]) unless @options[:less_than].nil?
51
50
  return true if @options[:in].include? field_value.to_i
52
51
 
53
52
  error_message ||= @options[:message] unless @options[:message].nil?
54
- error_message ||= "%s must be less than %s bytes".t(Extlib::Inflection.humanize(@field_name), @options[:less_than]) unless @options[:less_than].nil?
55
- error_message ||= "%s must be greater than %s bytes".t(Extlib::Inflection.humanize(@field_name), @options[:greater_than]) unless @options[:greater_than].nil?
56
- error_message ||= "%s must be between %s and %s bytes".t(Extlib::Inflection.humanize(@field_name), @options[:in].first, @options[:in].last)
53
+ error_message ||= sprintf("%s must be less than %s bytes",DataMapper::Inflector.humanize(@field_name), @options[:less_than]) unless @options[:less_than].nil?
54
+ error_message ||= sprintf("%s must be greater than %s bytes",DataMapper::Inflector.humanize(@field_name), @options[:greater_than]) unless @options[:greater_than].nil?
55
+ error_message ||= sprintf("%s must be between %s and %s bytes",DataMapper::Inflector.humanize(@field_name), @options[:in].first, @options[:in].last)
57
56
  add_error(target, error_message , @field_name)
58
57
  return false
59
58
  end
60
59
  end
61
60
 
62
61
  class RequiredFieldValidator < DataMapper::Validate::GenericValidator #:nodoc:
63
- def initialize(field_name, options={})
64
- super
65
- @field_name, @options = field_name, options
66
- end
67
-
68
62
  def call(target)
69
63
  field_value = target.validation_property_value(@field_name)
70
- if field_value.nil? || field_value.original_filename.blank?
71
- error_message = @options[:message] || "%s must be set".t(Extlib::Inflection.humanize(@field_name))
64
+ if field_value.nil? || Paperclip::Ext.blank?(field_value.original_filename)
65
+ error_message = @options[:message] || sprintf("%s must be set",DataMapper::Inflector.humanize(@field_name))
72
66
  add_error(target, error_message , @field_name)
73
67
  return false
74
68
  end
@@ -77,21 +71,16 @@ module Paperclip
77
71
  end
78
72
 
79
73
  class ContentTypeValidator < DataMapper::Validate::GenericValidator #:nodoc:
80
- def initialize(field_name, options={})
81
- super
82
- @field_name, @options = field_name, options
83
- end
84
-
85
74
  def call(target)
86
75
  valid_types = [@options[:content_type]].flatten
87
76
  field_value = target.validation_property_value(@field_name)
88
77
 
89
- unless field_value.nil? || field_value.original_filename.blank?
90
- unless @options[:content_type].blank?
78
+ unless field_value.nil? || Paperclip::Ext.blank?(field_value.original_filename)
79
+ unless Paperclip::Ext.blank?(@options[:content_type])
91
80
  content_type = target.validation_property_value(:"#{@field_name}_content_type")
92
81
  unless valid_types.any?{|t| t === content_type }
93
82
  error_message ||= @options[:message] unless @options[:message].nil?
94
- error_message ||= "%s's content type of '%s' is not a valid content type".t(Extlib::Inflection.humanize(@field_name), content_type)
83
+ error_message ||= sprintf("%s's content type of '%s' is not a valid content type",DataMapper::Inflector.humanize(@field_name), content_type)
95
84
  add_error(target, error_message , @field_name)
96
85
  return false
97
86
  end
@@ -103,22 +92,36 @@ module Paperclip
103
92
  end
104
93
 
105
94
  class CopyAttachmentErrors < DataMapper::Validate::GenericValidator #:nodoc:
106
- def initialize(field_name, options={})
107
- super
108
- @field_name, @options = field_name, options
109
- end
110
-
111
95
  def call(target)
112
96
  field_value = target.validation_property_value(@field_name)
113
- unless field_value.nil? || field_value.original_filename.blank?
97
+ unless field_value.nil? || Paperclip::Ext.blank?(field_value.original_filename)
114
98
  return true if field_value.errors.length == 0
115
- field_value.errors.each { |message| add_error(target, message, @field_name) }
99
+ field_value.errors.each do |error, message|
100
+ [message].flatten.each { |m| add_error(target, m, @field_name) }
101
+ end
116
102
  return false
117
103
  end
118
104
  return true
119
105
  end
120
106
  end
121
107
 
108
+ class GeometryValidator < DataMapper::Validate::GenericValidator #:nodoc:
109
+ def call(target)
110
+ field_value = target.validation_property_value(@field_name)
111
+ return true if field_value.queued_for_write[:original].nil?
112
+
113
+ geometry = Paperclip::Geometry.from_file(field_value.queued_for_write[:original].path)
114
+
115
+ return true if @options[:width].include?(geometry.width) && @options[:height].include?(geometry.height)
116
+
117
+ error_message ||= sprintf("%s width must be between %s and %s px", DataMapper::Inflector.humanize(@field_name), @options[:width].begin, @options[:width].end) unless @options[:width].include?(geometry.width)
118
+ error_message ||= sprintf("%s height must be between %s and %s px", DataMapper::Inflector.humanize(@field_name), @options[:height].begin, @options[:height].end) unless@options[:height].include?(geometry.height)
119
+
120
+ add_error(target, error_message , @field_name)
121
+ return false
122
+ end
123
+ end
124
+
122
125
  end
123
126
  end
124
127