paperclip 3.4.2 → 3.5.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of paperclip might be problematic. Click here for more details.

Files changed (50) hide show
  1. checksums.yaml +15 -0
  2. data/.travis.yml +0 -1
  3. data/Appraisals +4 -4
  4. data/NEWS +16 -0
  5. data/README.md +21 -2
  6. data/features/step_definitions/rails_steps.rb +9 -0
  7. data/features/step_definitions/s3_steps.rb +1 -1
  8. data/features/support/fakeweb.rb +4 -1
  9. data/lib/paperclip.rb +6 -42
  10. data/lib/paperclip/attachment.rb +25 -27
  11. data/lib/paperclip/filename_cleaner.rb +16 -0
  12. data/lib/paperclip/glue.rb +0 -1
  13. data/lib/paperclip/has_attached_file.rb +86 -0
  14. data/lib/paperclip/io_adapters/abstract_adapter.rb +8 -0
  15. data/lib/paperclip/io_adapters/data_uri_adapter.rb +27 -0
  16. data/lib/paperclip/io_adapters/empty_string_adapter.rb +18 -0
  17. data/lib/paperclip/io_adapters/stringio_adapter.rb +3 -3
  18. data/lib/paperclip/matchers/have_attached_file_matcher.rb +1 -5
  19. data/lib/paperclip/missing_attachment_styles.rb +11 -16
  20. data/lib/paperclip/storage/filesystem.rb +7 -3
  21. data/lib/paperclip/storage/s3.rb +2 -0
  22. data/lib/paperclip/tasks/attachments.rb +59 -0
  23. data/lib/paperclip/validators.rb +21 -2
  24. data/lib/paperclip/validators/attachment_content_type_validator.rb +9 -2
  25. data/lib/paperclip/validators/attachment_presence_validator.rb +8 -8
  26. data/lib/paperclip/validators/attachment_size_validator.rb +12 -7
  27. data/lib/paperclip/version.rb +1 -1
  28. data/lib/tasks/paperclip.rake +24 -6
  29. data/paperclip.gemspec +5 -3
  30. data/test/attachment_processing_test.rb +69 -15
  31. data/test/attachment_test.rb +49 -0
  32. data/test/filename_cleaner_test.rb +14 -0
  33. data/test/has_attached_file_test.rb +109 -0
  34. data/test/helper.rb +1 -1
  35. data/test/integration_test.rb +1 -65
  36. data/test/io_adapters/abstract_adapter_test.rb +8 -0
  37. data/test/io_adapters/data_uri_adapter_test.rb +60 -0
  38. data/test/io_adapters/empty_string_adapter_test.rb +17 -0
  39. data/test/paperclip_missing_attachment_styles_test.rb +4 -8
  40. data/test/paperclip_test.rb +0 -5
  41. data/test/rake_test.rb +103 -0
  42. data/test/storage/s3_test.rb +13 -1
  43. data/test/tasks/attachments_test.rb +77 -0
  44. data/test/validators/attachment_content_type_validator_test.rb +26 -0
  45. data/test/validators/attachment_size_validator_test.rb +10 -0
  46. data/test/validators_test.rb +8 -1
  47. metadata +56 -77
  48. data/lib/paperclip/attachment_options.rb +0 -9
  49. data/lib/paperclip/instance_methods.rb +0 -35
  50. data/test/attachment_options_test.rb +0 -27
@@ -23,6 +23,14 @@ module Paperclip
23
23
  @original_filename = new_filename.gsub(OS_RESTRICTED_CHARACTERS, "_")
24
24
  end
25
25
 
26
+ def nil?
27
+ false
28
+ end
29
+
30
+ def assignment?
31
+ true
32
+ end
33
+
26
34
  private
27
35
 
28
36
  def destination
@@ -0,0 +1,27 @@
1
+ module Paperclip
2
+ class DataUriAdapter < StringioAdapter
3
+
4
+ REGEXP = /^data:([-\w]+\/[-\w\+]+);base64,(.*)/
5
+
6
+ def initialize(target_uri)
7
+ @target_uri = target_uri
8
+ cache_current_values
9
+ @tempfile = copy_to_tempfile
10
+ end
11
+
12
+ private
13
+
14
+ def cache_current_values
15
+ self.original_filename = 'base64.txt'
16
+ data_uri_parts ||= @target_uri.match(REGEXP) || []
17
+ @content_type = data_uri_parts[1] || 'text/plain'
18
+ @target = StringIO.new(Base64.decode64(data_uri_parts[2] || ''))
19
+ @size = @target.size
20
+ end
21
+
22
+ end
23
+ end
24
+
25
+ Paperclip.io_adapters.register Paperclip::DataUriAdapter do |target|
26
+ String === target && target =~ Paperclip::DataUriAdapter::REGEXP
27
+ end
@@ -0,0 +1,18 @@
1
+ module Paperclip
2
+ class EmptyStringAdapter < AbstractAdapter
3
+ def initialize(target)
4
+ end
5
+
6
+ def nil?
7
+ false
8
+ end
9
+
10
+ def assignment?
11
+ false
12
+ end
13
+ end
14
+ end
15
+
16
+ Paperclip.io_adapters.register Paperclip::EmptyStringAdapter do |target|
17
+ target.is_a?(String) && target.empty?
18
+ end
@@ -3,7 +3,7 @@ module Paperclip
3
3
  def initialize(target)
4
4
  @target = target
5
5
  cache_current_values
6
- @tempfile = copy_to_tempfile(@target)
6
+ @tempfile = copy_to_tempfile
7
7
  end
8
8
 
9
9
  attr_writer :content_type
@@ -21,8 +21,8 @@ module Paperclip
21
21
  @size = @target.size
22
22
  end
23
23
 
24
- def copy_to_tempfile(src)
25
- while data = src.read(16*1024)
24
+ def copy_to_tempfile
25
+ while data = @target.read(16*1024)
26
26
  destination.write(data)
27
27
  end
28
28
  destination.rewind
@@ -20,7 +20,7 @@ module Paperclip
20
20
  def matches? subject
21
21
  @subject = subject
22
22
  @subject = @subject.class unless Class === @subject
23
- responds? && has_column? && included?
23
+ responds? && has_column?
24
24
  end
25
25
 
26
26
  def failure_message
@@ -47,10 +47,6 @@ module Paperclip
47
47
  def has_column?
48
48
  @subject.column_names.include?("#{@attachment_name}_file_name")
49
49
  end
50
-
51
- def included?
52
- @subject.ancestors.include?(Paperclip::InstanceMethods)
53
- end
54
50
  end
55
51
  end
56
52
  end
@@ -1,16 +1,14 @@
1
-
1
+ require 'paperclip/tasks/attachments'
2
2
  require 'set'
3
+
3
4
  module Paperclip
4
5
  class << self
5
- attr_accessor :classes_with_attachments
6
6
  attr_writer :registered_attachments_styles_path
7
7
  def registered_attachments_styles_path
8
8
  @registered_attachments_styles_path ||= Rails.root.join('public/system/paperclip_attachments.yml').to_s
9
9
  end
10
10
  end
11
11
 
12
- self.classes_with_attachments = Set.new
13
-
14
12
  # Get list of styles saved on previous deploy (running rake paperclip:refresh:missing_styles)
15
13
  def self.get_registered_attachments_styles
16
14
  YAML.load_file(Paperclip.registered_attachments_styles_path)
@@ -36,18 +34,15 @@ module Paperclip
36
34
  # }
37
35
  def self.current_attachments_styles
38
36
  Hash.new.tap do |current_styles|
39
- Paperclip.classes_with_attachments.each do |klass_name|
40
- klass = Paperclip.class_for(klass_name)
41
- klass.attachment_definitions.each do |attachment_name, attachment_attributes|
42
- # TODO: is it even possible to take into account Procs?
43
- next if attachment_attributes[:styles].kind_of?(Proc)
44
- attachment_attributes[:styles].try(:keys).try(:each) do |style_name|
45
- klass_sym = klass.to_s.to_sym
46
- current_styles[klass_sym] ||= Hash.new
47
- current_styles[klass_sym][attachment_name.to_sym] ||= Array.new
48
- current_styles[klass_sym][attachment_name.to_sym] << style_name.to_sym
49
- current_styles[klass_sym][attachment_name.to_sym].map!(&:to_s).sort!.map!(&:to_sym).uniq!
50
- end
37
+ Paperclip::Tasks::Attachments.each_definition do |klass, attachment_name, attachment_attributes|
38
+ # TODO: is it even possible to take into account Procs?
39
+ next if attachment_attributes[:styles].kind_of?(Proc)
40
+ attachment_attributes[:styles].try(:keys).try(:each) do |style_name|
41
+ klass_sym = klass.to_s.to_sym
42
+ current_styles[klass_sym] ||= Hash.new
43
+ current_styles[klass_sym][attachment_name.to_sym] ||= Array.new
44
+ current_styles[klass_sym][attachment_name.to_sym] << style_name.to_sym
45
+ current_styles[klass_sym][attachment_name.to_sym].map!(&:to_s).sort!.map!(&:to_sym).uniq!
51
46
  end
52
47
  end
53
48
  end
@@ -36,9 +36,13 @@ module Paperclip
36
36
  def flush_writes #:nodoc:
37
37
  @queued_for_write.each do |style_name, file|
38
38
  FileUtils.mkdir_p(File.dirname(path(style_name)))
39
- File.open(path(style_name), "wb") do |new_file|
40
- while chunk = file.read(16 * 1024)
41
- new_file.write(chunk)
39
+ begin
40
+ FileUtils.mv(file.path, path(style_name))
41
+ rescue SystemCallError
42
+ File.open(path(style_name), "wb") do |new_file|
43
+ while chunk = file.read(16 * 1024)
44
+ new_file.write(chunk)
45
+ end
42
46
  end
43
47
  end
44
48
  unless @options[:override_file_permissions] == false
@@ -164,6 +164,8 @@ module Paperclip
164
164
  if path
165
165
  base_options = { :expires => time, :secure => use_secure_protocol?(style_name) }
166
166
  s3_object(style_name).url_for(:read, base_options.merge(s3_url_options)).to_s
167
+ else
168
+ url
167
169
  end
168
170
  end
169
171
 
@@ -0,0 +1,59 @@
1
+ require 'singleton'
2
+
3
+ module Paperclip
4
+ module Tasks
5
+ class Attachments
6
+ include Singleton
7
+
8
+ def self.add(klass, attachment_name, attachment_options)
9
+ instance.add(klass, attachment_name, attachment_options)
10
+ end
11
+
12
+ def self.clear
13
+ instance.clear
14
+ end
15
+
16
+ def self.names_for(klass)
17
+ instance.names_for(klass)
18
+ end
19
+
20
+ def self.each_definition(&block)
21
+ instance.each_definition(&block)
22
+ end
23
+
24
+ def self.definitions_for(klass)
25
+ instance.definitions_for(klass)
26
+ end
27
+
28
+ def initialize
29
+ clear
30
+ end
31
+
32
+ def add(klass, attachment_name, attachment_options)
33
+ @attachments ||= {}
34
+ @attachments[klass] ||= {}
35
+ @attachments[klass][attachment_name] = attachment_options
36
+ end
37
+
38
+ def clear
39
+ @attachments = Hash.new { |h,k| h[k] = {} }
40
+ end
41
+
42
+ def names_for(klass)
43
+ @attachments[klass].keys
44
+ end
45
+
46
+ def each_definition
47
+ @attachments.each do |klass, attachments|
48
+ attachments.each do |name, options|
49
+ yield klass, name, options
50
+ end
51
+ end
52
+ end
53
+
54
+ def definitions_for(klass)
55
+ @attachments[klass]
56
+ end
57
+ end
58
+ end
59
+ end
@@ -34,13 +34,32 @@ module Paperclip
34
34
  validator_kind = $1.underscore.to_sym
35
35
 
36
36
  if options.has_key?(validator_kind)
37
- options[:"attachment_#{validator_kind}"] = options.delete(validator_kind)
37
+ validator_options = options.delete(validator_kind)
38
+ validator_options = {} if validator_options == true
39
+ local_options = attributes + [validator_options]
40
+ send(:"validates_attachment_#{validator_kind}", *local_options)
38
41
  end
39
42
  end
40
43
  end
44
+ end
45
+
46
+ def validate_before_processing(validator_class, options)
47
+ options = options.dup
48
+ attributes = options.delete(:attributes)
49
+ attributes.each do |attribute|
50
+ options[:attributes] = [attribute]
51
+ create_validating_before_filter(attribute, validator_class, options)
52
+ end
53
+ end
41
54
 
42
- validates(*attributes + [options])
55
+ def create_validating_before_filter(attribute, validator_class, options)
56
+ if_clause = options.delete(:if)
57
+ unless_clause = options.delete(:unless)
58
+ send(:"before_#{attribute}_post_process", :if => if_clause, :unless => unless_clause) do |*args|
59
+ validator_class.new(options.dup).validate(self)
60
+ end
43
61
  end
62
+
44
63
  end
45
64
  end
46
65
  end
@@ -7,6 +7,7 @@ module Paperclip
7
7
  end
8
8
 
9
9
  def validate_each(record, attribute, value)
10
+ base_attribute = attribute.to_sym
10
11
  attribute = "#{attribute}_content_type".to_sym
11
12
  value = record.send :read_attribute_for_validation, attribute
12
13
 
@@ -14,6 +15,10 @@ module Paperclip
14
15
 
15
16
  validate_whitelist(record, attribute, value)
16
17
  validate_blacklist(record, attribute, value)
18
+
19
+ if record.errors.include? attribute
20
+ record.errors.add base_attribute, record.errors[attribute]
21
+ end
17
22
  end
18
23
 
19
24
  def validate_whitelist(record, attribute, value)
@@ -48,7 +53,7 @@ module Paperclip
48
53
  end
49
54
 
50
55
  module HelperMethods
51
- # Places ActiveRecord-style validations on the content type of the file
56
+ # Places ActiveModel validations on the content type of the file
52
57
  # assigned. The possible options are:
53
58
  # * +content_type+: Allowed content types. Can be a single content type
54
59
  # or an array. Each type can be a String or a Regexp. It should be
@@ -68,7 +73,9 @@ module Paperclip
68
73
  # You'll still need to have a virtual attribute (created by +attr_accessor+)
69
74
  # name +[attachment]_content_type+ to be able to use this validator.
70
75
  def validates_attachment_content_type(*attr_names)
71
- validates_with AttachmentContentTypeValidator, _merge_attributes(attr_names)
76
+ options = _merge_attributes(attr_names)
77
+ validates_with AttachmentContentTypeValidator, options.dup
78
+ validate_before_processing AttachmentContentTypeValidator, options.dup
72
79
  end
73
80
  end
74
81
  end
@@ -2,24 +2,24 @@ require 'active_model/validations/presence'
2
2
 
3
3
  module Paperclip
4
4
  module Validators
5
- class AttachmentPresenceValidator < ActiveModel::Validations::PresenceValidator
6
- def validate(record)
7
- [attributes].flatten.map do |attribute|
8
- if record.send(:read_attribute_for_validation, "#{attribute}_file_name").blank?
9
- record.errors.add(attribute, :blank, options)
10
- end
5
+ class AttachmentPresenceValidator < ActiveModel::EachValidator
6
+ def validate_each(record, attribute, value)
7
+ if record.send("#{attribute}_file_name").blank?
8
+ record.errors.add(attribute, :blank, options)
11
9
  end
12
10
  end
13
11
  end
14
12
 
15
13
  module HelperMethods
16
- # Places ActiveRecord-style validations on the presence of a file.
14
+ # Places ActiveModel validations on the presence of a file.
17
15
  # Options:
18
16
  # * +if+: A lambda or name of an instance method. Validation will only
19
17
  # be run if this lambda or method returns true.
20
18
  # * +unless+: Same as +if+ but validates if lambda or method returns false.
21
19
  def validates_attachment_presence(*attr_names)
22
- validates_with AttachmentPresenceValidator, _merge_attributes(attr_names)
20
+ options = _merge_attributes(attr_names)
21
+ validates_with AttachmentPresenceValidator, options.dup
22
+ validate_before_processing AttachmentPresenceValidator, options.dup
23
23
  end
24
24
  end
25
25
  end
@@ -11,6 +11,7 @@ module Paperclip
11
11
  end
12
12
 
13
13
  def validate_each(record, attr_name, value)
14
+ base_attr_name = attr_name
14
15
  attr_name = "#{attr_name}_file_size".to_sym
15
16
  value = record.send(:read_attribute_for_validation, attr_name)
16
17
 
@@ -21,11 +22,13 @@ module Paperclip
21
22
 
22
23
  unless value.send(CHECKS[option], option_value)
23
24
  error_message_key = options[:in] ? :in_between : option
24
- record.errors.add(attr_name, error_message_key, filtered_options(value).merge(
25
- :min => min_value_in_human_size(record),
26
- :max => max_value_in_human_size(record),
27
- :count => human_size(option_value)
28
- ))
25
+ [ attr_name, base_attr_name ].each do |error_attr_name|
26
+ record.errors.add(error_attr_name, error_message_key, filtered_options(value).merge(
27
+ :min => min_value_in_human_size(record),
28
+ :max => max_value_in_human_size(record),
29
+ :count => human_size(option_value)
30
+ ))
31
+ end
29
32
  end
30
33
  end
31
34
  end
@@ -85,7 +88,7 @@ module Paperclip
85
88
  end
86
89
 
87
90
  module HelperMethods
88
- # Places ActiveRecord-style validations on the size of the file assigned. The
91
+ # Places ActiveModel validations on the size of the file assigned. The
89
92
  # possible options are:
90
93
  # * +in+: a Range of bytes (i.e. +1..1.megabyte+),
91
94
  # * +less_than+: equivalent to :in => 0..options[:less_than]
@@ -95,7 +98,9 @@ module Paperclip
95
98
  # be run if this lambda or method returns true.
96
99
  # * +unless+: Same as +if+ but validates if lambda or method returns false.
97
100
  def validates_attachment_size(*attr_names)
98
- validates_with AttachmentSizeValidator, _merge_attributes(attr_names)
101
+ options = _merge_attributes(attr_names)
102
+ validates_with AttachmentSizeValidator, options.dup
103
+ validate_before_processing AttachmentSizeValidator, options.dup
99
104
  end
100
105
  end
101
106
  end
@@ -1,3 +1,3 @@
1
1
  module Paperclip
2
- VERSION = "3.4.2" unless defined? Paperclip::VERSION
2
+ VERSION = "3.5.0" unless defined? Paperclip::VERSION
3
3
  end
@@ -1,3 +1,5 @@
1
+ require 'paperclip/tasks/attachments'
2
+
1
3
  module Paperclip
2
4
  module Task
3
5
  def self.obtain_class
@@ -9,13 +11,23 @@ module Paperclip
9
11
  def self.obtain_attachments(klass)
10
12
  klass = Paperclip.class_for(klass.to_s)
11
13
  name = ENV['ATTACHMENT'] || ENV['attachment']
12
- raise "Class #{klass.name} has no attachments specified" unless klass.respond_to?(:attachment_definitions)
13
- if !name.blank? && klass.attachment_definitions.keys.map(&:to_s).include?(name.to_s)
14
+
15
+ attachment_names = Paperclip::Tasks::Attachments.names_for(klass)
16
+
17
+ if attachment_names.empty?
18
+ raise "Class #{klass.name} has no attachments specified"
19
+ end
20
+
21
+ if !name.blank? && attachment_names.map(&:to_s).include?(name.to_s)
14
22
  [ name ]
15
23
  else
16
- klass.attachment_definitions.keys
24
+ attachment_names
17
25
  end
18
26
  end
27
+
28
+ def self.log_error(error)
29
+ $stderr.puts error
30
+ end
19
31
  end
20
32
  end
21
33
 
@@ -31,10 +43,16 @@ namespace :paperclip do
31
43
  styles = (ENV['STYLES'] || ENV['styles'] || '').split(',').map(&:to_sym)
32
44
  names.each do |name|
33
45
  Paperclip.each_instance_with_attachment(klass, name) do |instance|
34
- instance.send(name).reprocess!(*styles)
46
+ attachment = instance.send(name)
47
+ begin
48
+ attachment.reprocess!(*styles)
49
+ rescue Exception => e
50
+ Paperclip::Task.log_error("exception while processing #{klass} ID #{instance.id}:")
51
+ Paperclip::Task.log_error(" " + e.message + "\n")
52
+ end
35
53
  unless instance.errors.blank?
36
- puts "errors while processing #{klass} ID #{instance.id}:"
37
- puts " " + instance.errors.full_messages.join("\n ") + "\n"
54
+ Paperclip::Task.log_error("errors while processing #{klass} ID #{instance.id}:")
55
+ Paperclip::Task.log_error(" " + instance.errors.full_messages.join("\n ") + "\n")
38
56
  end
39
57
  end
40
58
  end