active_storage_validations 1.1.4 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +84 -33
  3. data/config/locales/da.yml +33 -0
  4. data/config/locales/de.yml +6 -0
  5. data/config/locales/en.yml +6 -0
  6. data/config/locales/es.yml +6 -0
  7. data/config/locales/fr.yml +6 -0
  8. data/config/locales/it.yml +6 -0
  9. data/config/locales/ja.yml +6 -0
  10. data/config/locales/nl.yml +6 -0
  11. data/config/locales/pl.yml +6 -0
  12. data/config/locales/pt-BR.yml +6 -0
  13. data/config/locales/ru.yml +6 -0
  14. data/config/locales/sv.yml +11 -1
  15. data/config/locales/tr.yml +6 -0
  16. data/config/locales/uk.yml +6 -0
  17. data/config/locales/vi.yml +6 -0
  18. data/config/locales/zh-CN.yml +6 -0
  19. data/lib/active_storage_validations/aspect_ratio_validator.rb +10 -34
  20. data/lib/active_storage_validations/attached_validator.rb +6 -4
  21. data/lib/active_storage_validations/base_size_validator.rb +68 -0
  22. data/lib/active_storage_validations/concerns/active_storageable.rb +28 -0
  23. data/lib/active_storage_validations/concerns/errorable.rb +4 -5
  24. data/lib/active_storage_validations/concerns/loggable.rb +9 -0
  25. data/lib/active_storage_validations/concerns/metadatable.rb +31 -0
  26. data/lib/active_storage_validations/content_type_spoof_detector.rb +130 -0
  27. data/lib/active_storage_validations/content_type_validator.rb +56 -22
  28. data/lib/active_storage_validations/dimension_validator.rb +31 -52
  29. data/lib/active_storage_validations/limit_validator.rb +5 -3
  30. data/lib/active_storage_validations/marcel_extensor.rb +5 -0
  31. data/lib/active_storage_validations/matchers/aspect_ratio_validator_matcher.rb +6 -15
  32. data/lib/active_storage_validations/matchers/attached_validator_matcher.rb +5 -13
  33. data/lib/active_storage_validations/matchers/base_size_validator_matcher.rb +134 -0
  34. data/lib/active_storage_validations/matchers/concerns/attachable.rb +66 -0
  35. data/lib/active_storage_validations/matchers/concerns/contextable.rb +20 -8
  36. data/lib/active_storage_validations/matchers/concerns/messageable.rb +1 -1
  37. data/lib/active_storage_validations/matchers/concerns/validatable.rb +9 -3
  38. data/lib/active_storage_validations/matchers/content_type_validator_matcher.rb +12 -2
  39. data/lib/active_storage_validations/matchers/dimension_validator_matcher.rb +6 -15
  40. data/lib/active_storage_validations/matchers/limit_validator_matcher.rb +127 -0
  41. data/lib/active_storage_validations/matchers/processable_image_validator_matcher.rb +78 -0
  42. data/lib/active_storage_validations/matchers/size_validator_matcher.rb +4 -139
  43. data/lib/active_storage_validations/matchers/total_size_validator_matcher.rb +31 -0
  44. data/lib/active_storage_validations/matchers.rb +6 -15
  45. data/lib/active_storage_validations/metadata.rb +27 -12
  46. data/lib/active_storage_validations/processable_image_validator.rb +17 -32
  47. data/lib/active_storage_validations/size_validator.rb +6 -55
  48. data/lib/active_storage_validations/total_size_validator.rb +45 -0
  49. data/lib/active_storage_validations/version.rb +1 -1
  50. data/lib/active_storage_validations.rb +4 -1
  51. metadata +45 -46
@@ -1,38 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Big thank you to the paperclip validation matchers:
4
- # https://github.com/thoughtbot/paperclip/blob/v6.1.0/lib/paperclip/matchers/validate_attachment_size_matcher.rb
5
-
6
- require_relative 'concerns/active_storageable.rb'
7
- require_relative 'concerns/allow_blankable.rb'
8
- require_relative 'concerns/contextable.rb'
9
- require_relative 'concerns/messageable.rb'
10
- require_relative 'concerns/rspecable.rb'
11
- require_relative 'concerns/validatable.rb'
3
+ require_relative 'base_size_validator_matcher'
12
4
 
13
5
  module ActiveStorageValidations
14
6
  module Matchers
15
- def validate_size_of(name)
16
- SizeValidatorMatcher.new(name)
7
+ def validate_size_of(attribute_name)
8
+ SizeValidatorMatcher.new(attribute_name)
17
9
  end
18
10
 
19
- class SizeValidatorMatcher
20
- include ActiveStorageable
21
- include AllowBlankable
22
- include Contextable
23
- include Messageable
24
- include Rspecable
25
- include Validatable
26
-
27
- def initialize(attribute_name)
28
- initialize_allow_blankable
29
- initialize_contextable
30
- initialize_messageable
31
- initialize_rspecable
32
- @attribute_name = attribute_name
33
- @min = @max = nil
34
- end
35
-
11
+ class SizeValidatorMatcher < BaseSizeValidatorMatcher
36
12
  def description
37
13
  "validate file size of :#{@attribute_name}"
38
14
  end
@@ -42,117 +18,6 @@ module ActiveStorageValidations
42
18
  build_failure_message(message)
43
19
  message.join("\n")
44
20
  end
45
-
46
- def less_than(size)
47
- @max = size - 1.byte
48
- self
49
- end
50
-
51
- def less_than_or_equal_to(size)
52
- @max = size
53
- self
54
- end
55
-
56
- def greater_than(size)
57
- @min = size + 1.byte
58
- self
59
- end
60
-
61
- def greater_than_or_equal_to(size)
62
- @min = size
63
- self
64
- end
65
-
66
- def between(range)
67
- @min, @max = range.first, range.last
68
- self
69
- end
70
-
71
- def matches?(subject)
72
- @subject = subject.is_a?(Class) ? subject.new : subject
73
-
74
- is_a_valid_active_storage_attribute? &&
75
- is_context_valid? &&
76
- is_allowing_blank? &&
77
- is_custom_message_valid? &&
78
- not_lower_than_min? &&
79
- higher_than_min? &&
80
- lower_than_max? &&
81
- not_higher_than_max?
82
- end
83
-
84
- protected
85
-
86
- def build_failure_message(message)
87
- return unless @failure_message_artefacts.present?
88
-
89
- message << " but there seem to have issues with the matcher methods you used, since:"
90
- @failure_message_artefacts.each do |error_case|
91
- message << " validation failed when provided with a #{error_case[:size]} bytes test file"
92
- end
93
- message << " whereas it should have passed"
94
- end
95
-
96
- def not_lower_than_min?
97
- @min.nil? || !passes_validation_with_size(@min - 1)
98
- end
99
-
100
- def higher_than_min?
101
- @min.nil? || passes_validation_with_size(@min + 1)
102
- end
103
-
104
- def lower_than_max?
105
- @max.nil? || @max == Float::INFINITY || passes_validation_with_size(@max - 1)
106
- end
107
-
108
- def not_higher_than_max?
109
- @max.nil? || @max == Float::INFINITY || !passes_validation_with_size(@max + 1)
110
- end
111
-
112
- def passes_validation_with_size(size)
113
- mock_size_for(io, size) do
114
- attach_file
115
- validate
116
- is_valid? || add_failure_message_artefact(size)
117
- end
118
- end
119
-
120
- def add_failure_message_artefact(size)
121
- @failure_message_artefacts << { size: size }
122
- false
123
- end
124
-
125
- def is_custom_message_valid?
126
- return true unless @custom_message
127
-
128
- mock_size_for(io, -1.kilobytes) do
129
- attach_file
130
- validate
131
- has_an_error_message_which_is_custom_message?
132
- end
133
- end
134
-
135
- def mock_size_for(io, size)
136
- Matchers.stub_method(io, :size, size) do
137
- yield
138
- end
139
- end
140
-
141
- def attach_file
142
- @subject.public_send(@attribute_name).attach(dummy_file)
143
- end
144
-
145
- def dummy_file
146
- {
147
- io: io,
148
- filename: 'test.png',
149
- content_type: 'image/png'
150
- }
151
- end
152
-
153
- def io
154
- @io ||= Tempfile.new('Hello world!')
155
- end
156
21
  end
157
22
  end
158
23
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_size_validator_matcher'
4
+
5
+ module ActiveStorageValidations
6
+ module Matchers
7
+ def validate_total_size_of(attribute_name)
8
+ TotalSizeValidatorMatcher.new(attribute_name)
9
+ end
10
+
11
+ class TotalSizeValidatorMatcher < BaseSizeValidatorMatcher
12
+ def description
13
+ "validate total file size of :#{@attribute_name}"
14
+ end
15
+
16
+ def failure_message
17
+ message = ["is expected to validate total file size of :#{@attribute_name}"]
18
+ build_failure_message(message)
19
+ message.join("\n")
20
+ end
21
+
22
+ protected
23
+
24
+ def attach_file
25
+ # has_many_attached relation
26
+ @subject.public_send(@attribute_name).attach([dummy_blob])
27
+ @subject.public_send(@attribute_name)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -2,9 +2,12 @@
2
2
 
3
3
  require 'active_storage_validations/matchers/aspect_ratio_validator_matcher'
4
4
  require 'active_storage_validations/matchers/attached_validator_matcher'
5
+ require 'active_storage_validations/matchers/processable_image_validator_matcher'
6
+ require 'active_storage_validations/matchers/limit_validator_matcher'
5
7
  require 'active_storage_validations/matchers/content_type_validator_matcher'
6
8
  require 'active_storage_validations/matchers/dimension_validator_matcher'
7
9
  require 'active_storage_validations/matchers/size_validator_matcher'
10
+ require 'active_storage_validations/matchers/total_size_validator_matcher'
8
11
 
9
12
  module ActiveStorageValidations
10
13
  module Matchers
@@ -23,21 +26,9 @@ module ActiveStorageValidations
23
26
  end
24
27
 
25
28
  def self.mock_metadata(attachment, width, height)
26
- if Rails.gem_version >= Gem::Version.new('6.0.0')
27
- # Mock the Metadata class for rails 6
28
- mock = OpenStruct.new(metadata: { width: width, height: height })
29
- stub_method(ActiveStorageValidations::Metadata, :new, mock) do
30
- yield
31
- end
32
- else
33
- # Stub the metadata analysis for rails 5
34
- stub_method(attachment, :analyze, true) do
35
- stub_method(attachment, :analyzed?, true) do
36
- stub_method(attachment, :metadata, { width: width, height: height }) do
37
- yield
38
- end
39
- end
40
- end
29
+ mock = Struct.new(:metadata).new({ width: width, height: height })
30
+ stub_method(ActiveStorageValidations::Metadata, :new, mock) do
31
+ yield
41
32
  end
42
33
  end
43
34
  end
@@ -1,5 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'concerns/loggable'
4
+
1
5
  module ActiveStorageValidations
2
6
  class Metadata
7
+ include Loggable
8
+
3
9
  class InvalidImageError < StandardError; end
4
10
 
5
11
  attr_reader :file
@@ -34,7 +40,7 @@ module ActiveStorageValidations
34
40
  private
35
41
 
36
42
  def image_processor
37
- # Rails returns nil for default image processor, because it is set in an after initiliaze callback
43
+ # Rails returns nil for default image processor, because it is set in an after initialize callback
38
44
  # https://github.com/rails/rails/blob/89d8569abe2564c8187debf32dd3b4e33d6ad983/activestorage/lib/active_storage/engine.rb
39
45
  Rails.application.config.active_storage.variant_processor || DEFAULT_IMAGE_PROCESSOR
40
46
  end
@@ -62,11 +68,7 @@ module ActiveStorageValidations
62
68
  if is_string || file.is_a?(ActiveStorage::Blob)
63
69
  blob =
64
70
  if is_string
65
- if Rails.gem_version < Gem::Version.new('6.1.0')
66
- ActiveStorage::Blob.find_signed(file)
67
- else
68
- ActiveStorage::Blob.find_signed!(file)
69
- end
71
+ ActiveStorage::Blob.find_signed!(file)
70
72
  else
71
73
  file
72
74
  end
@@ -101,7 +103,7 @@ module ActiveStorageValidations
101
103
  end
102
104
 
103
105
  def new_image_from_path(path)
104
- if vips_image_processor? && (Vips::get_suffixes.include?(File.extname(path).downcase) || !Vips::respond_to?(:vips_foreign_get_suffixes))
106
+ if vips_image_processor? && (supported_vips_suffix?(path) || vips_version_below_8_8? || open_uri_tempfile?(path))
105
107
  begin
106
108
  Vips::Image.new_from_file(path)
107
109
  rescue exception_class
@@ -116,6 +118,24 @@ module ActiveStorageValidations
116
118
  end
117
119
  end
118
120
 
121
+ def supported_vips_suffix?(path)
122
+ Vips::get_suffixes.include?(File.extname(path).downcase)
123
+ end
124
+
125
+ def vips_version_below_8_8?
126
+ # FYI, Vips 8.8 was released in 2019
127
+ # https://github.com/libvips/libvips/releases/tag/v8.8.0
128
+ !Vips::respond_to?(:vips_foreign_get_suffixes)
129
+ end
130
+
131
+ def open_uri_tempfile?(path)
132
+ # When trying to open urls for 'large' images, OpenURI will return a
133
+ # tempfile. That tempfile does not have an extension indicating the type
134
+ # of file. However, Vips will be able to process it anyway.
135
+ # The 'large' file value is derived from OpenUri::Buffer class (> 10ko)
136
+ path.split('/').last.starts_with?("open-uri")
137
+ end
138
+
119
139
  def valid_image?(image)
120
140
  return false unless image
121
141
 
@@ -156,10 +176,5 @@ module ActiveStorageValidations
156
176
  raise "Something wrong with params."
157
177
  end
158
178
  end
159
-
160
- def logger
161
- Rails.logger
162
- end
163
-
164
179
  end
165
180
  end
@@ -1,49 +1,34 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'concerns/active_storageable.rb'
3
4
  require_relative 'concerns/errorable.rb'
5
+ require_relative 'concerns/metadatable.rb'
4
6
  require_relative 'concerns/symbolizable.rb'
5
- require_relative 'metadata.rb'
6
7
 
7
8
  module ActiveStorageValidations
8
9
  class ProcessableImageValidator < ActiveModel::EachValidator # :nodoc
9
- include OptionProcUnfolding
10
+ include ActiveStorageable
10
11
  include Errorable
12
+ include Metadatable
11
13
  include Symbolizable
12
14
 
13
15
  ERROR_TYPES = %i[
14
16
  image_not_processable
15
17
  ].freeze
16
18
 
17
- if Rails.gem_version >= Gem::Version.new('6.0.0')
18
- def validate_each(record, attribute, _value)
19
- return true unless record.send(attribute).attached?
20
-
21
- changes = record.attachment_changes[attribute.to_s]
22
- return true if changes.blank?
23
-
24
- files = Array.wrap(changes.is_a?(ActiveStorage::Attached::Changes::CreateMany) ? changes.attachables : changes.attachable)
25
-
26
- files.each do |file|
27
- if !Metadata.new(file).valid?
28
- errors_options = initialize_error_options(options, file)
29
- add_error(record, attribute, ERROR_TYPES.first , **errors_options) unless Metadata.new(file).valid?
30
- end
31
- end
32
- end
33
- else
34
- # Rails 5
35
- def validate_each(record, attribute, _value)
36
- return true unless record.send(attribute).attached?
37
-
38
- files = Array.wrap(record.send(attribute))
39
-
40
- files.each do |file|
41
- if !Metadata.new(file).valid?
42
- errors_options = initialize_error_options(options, file)
43
- add_error(record, attribute, ERROR_TYPES.first , **errors_options) unless Metadata.new(file).valid?
44
- end
45
- end
46
- end
19
+ def validate_each(record, attribute, _value)
20
+ return if no_attachments?(record, attribute)
21
+
22
+ validate_changed_files_from_metadata(record, attribute)
23
+ end
24
+
25
+ private
26
+
27
+ def is_valid?(record, attribute, attachable, metadata)
28
+ return if !metadata.empty?
29
+
30
+ errors_options = initialize_error_options(options, attachable)
31
+ add_error(record, attribute, ERROR_TYPES.first , **errors_options)
47
32
  end
48
33
  end
49
34
  end
@@ -1,23 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'concerns/errorable.rb'
4
- require_relative 'concerns/symbolizable.rb'
3
+ require_relative 'base_size_validator.rb'
5
4
 
6
5
  module ActiveStorageValidations
7
- class SizeValidator < ActiveModel::EachValidator # :nodoc:
8
- include OptionProcUnfolding
9
- include Errorable
10
- include Symbolizable
11
-
12
- delegate :number_to_human_size, to: ActiveSupport::NumberHelper
13
-
14
- AVAILABLE_CHECKS = %i[
15
- less_than
16
- less_than_or_equal_to
17
- greater_than
18
- greater_than_or_equal_to
19
- between
20
- ].freeze
6
+ class SizeValidator < BaseSizeValidator
21
7
  ERROR_TYPES = %i[
22
8
  file_size_not_less_than
23
9
  file_size_not_less_than_or_equal_to
@@ -26,58 +12,23 @@ module ActiveStorageValidations
26
12
  file_size_not_between
27
13
  ].freeze
28
14
 
29
- def check_validity!
30
- unless AVAILABLE_CHECKS.one? { |argument| options.key?(argument) }
31
- raise ArgumentError, 'You must pass either :less_than(_or_equal_to), :greater_than(_or_equal_to), or :between to the validator'
32
- end
33
- end
34
-
35
15
  def validate_each(record, attribute, _value)
36
- # only attached
37
- return true unless record.send(attribute).attached?
16
+ return if no_attachments?(record, attribute)
38
17
 
39
- files = Array.wrap(record.send(attribute))
40
18
  flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS)
41
19
 
42
- files.each do |file|
20
+ attached_files(record, attribute).each do |file|
43
21
  next if is_valid?(file.blob.byte_size, flat_options)
44
22
 
45
23
  errors_options = initialize_error_options(options, file)
24
+ populate_error_options(errors_options, flat_options)
46
25
  errors_options[:file_size] = number_to_human_size(file.blob.byte_size)
47
- errors_options[:min_size] = number_to_human_size(min_size(flat_options))
48
- errors_options[:max_size] = number_to_human_size(max_size(flat_options))
26
+
49
27
  keys = AVAILABLE_CHECKS & flat_options.keys
50
28
  error_type = "file_size_not_#{keys.first}".to_sym
51
29
 
52
30
  add_error(record, attribute, error_type, **errors_options)
53
- break
54
- end
55
- end
56
-
57
- private
58
-
59
- def is_valid?(file_size, flat_options)
60
- return false if file_size < 0
61
-
62
- if flat_options[:between].present?
63
- flat_options[:between].include?(file_size)
64
- elsif flat_options[:less_than].present?
65
- file_size < flat_options[:less_than]
66
- elsif flat_options[:less_than_or_equal_to].present?
67
- file_size <= flat_options[:less_than_or_equal_to]
68
- elsif flat_options[:greater_than].present?
69
- file_size > flat_options[:greater_than]
70
- elsif flat_options[:greater_than_or_equal_to].present?
71
- file_size >= flat_options[:greater_than_or_equal_to]
72
31
  end
73
32
  end
74
-
75
- def min_size(flat_options)
76
- flat_options[:between]&.min || flat_options[:greater_than] || flat_options[:greater_than_or_equal_to]
77
- end
78
-
79
- def max_size(flat_options)
80
- flat_options[:between]&.max || flat_options[:less_than] || flat_options[:less_than_or_equal_to]
81
- end
82
33
  end
83
34
  end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_size_validator.rb'
4
+
5
+ module ActiveStorageValidations
6
+ class TotalSizeValidator < BaseSizeValidator
7
+ ERROR_TYPES = %i[
8
+ total_file_size_not_less_than
9
+ total_file_size_not_less_than_or_equal_to
10
+ total_file_size_not_greater_than
11
+ total_file_size_not_greater_than_or_equal_to
12
+ total_file_size_not_between
13
+ ].freeze
14
+
15
+ def validate_each(record, attribute, _value)
16
+ custom_check_validity!(record, attribute)
17
+
18
+ return if no_attachments?(record, attribute)
19
+
20
+ total_file_size = attached_files(record, attribute).sum { |file| file.blob.byte_size }
21
+ flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS)
22
+
23
+ return if is_valid?(total_file_size, flat_options)
24
+
25
+ errors_options = initialize_error_options(options, nil)
26
+ populate_error_options(errors_options, flat_options)
27
+ errors_options[:total_file_size] = number_to_human_size(total_file_size)
28
+
29
+ keys = AVAILABLE_CHECKS & flat_options.keys
30
+ error_type = "total_file_size_not_#{keys.first}".to_sym
31
+
32
+ add_error(record, attribute, error_type, **errors_options)
33
+ end
34
+
35
+ private
36
+
37
+ def custom_check_validity!(record, attribute)
38
+ # We can't perform this check in the #check_validity! hook because we do not
39
+ # have enough data (only options & attributes are accessible)
40
+ unless record.send(attribute).is_a?(ActiveStorage::Attached::Many)
41
+ raise ArgumentError, 'This validator is only available for has_many_attached relations'
42
+ end
43
+ end
44
+ end
45
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveStorageValidations
4
- VERSION = '1.1.4'
4
+ VERSION = '1.3.0'
5
5
  end
@@ -8,11 +8,14 @@ require 'active_storage_validations/engine'
8
8
  require 'active_storage_validations/option_proc_unfolding'
9
9
  require 'active_storage_validations/attached_validator'
10
10
  require 'active_storage_validations/content_type_validator'
11
- require 'active_storage_validations/size_validator'
12
11
  require 'active_storage_validations/limit_validator'
13
12
  require 'active_storage_validations/dimension_validator'
14
13
  require 'active_storage_validations/aspect_ratio_validator'
15
14
  require 'active_storage_validations/processable_image_validator'
15
+ require 'active_storage_validations/size_validator'
16
+ require 'active_storage_validations/total_size_validator'
17
+
18
+ require 'active_storage_validations/marcel_extensor'
16
19
 
17
20
  ActiveSupport.on_load(:active_record) do
18
21
  send :include, ActiveStorageValidations