active_storage_validations 1.3.4 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -67
  3. data/config/locales/da.yml +1 -0
  4. data/config/locales/de.yml +1 -0
  5. data/config/locales/en.yml +1 -0
  6. data/config/locales/es.yml +1 -0
  7. data/config/locales/fr.yml +1 -0
  8. data/config/locales/it.yml +1 -0
  9. data/config/locales/ja.yml +1 -0
  10. data/config/locales/nl.yml +1 -0
  11. data/config/locales/pl.yml +1 -0
  12. data/config/locales/pt-BR.yml +1 -0
  13. data/config/locales/ru.yml +1 -0
  14. data/config/locales/sv.yml +1 -0
  15. data/config/locales/tr.yml +1 -0
  16. data/config/locales/uk.yml +1 -0
  17. data/config/locales/vi.yml +1 -0
  18. data/config/locales/zh-CN.yml +1 -0
  19. data/lib/active_storage_validations/analyzer/image_analyzer/image_magick.rb +47 -0
  20. data/lib/active_storage_validations/analyzer/image_analyzer/vips.rb +58 -0
  21. data/lib/active_storage_validations/analyzer/image_analyzer.rb +93 -0
  22. data/lib/active_storage_validations/analyzer/null_analyzer.rb +18 -0
  23. data/lib/active_storage_validations/analyzer.rb +34 -0
  24. data/lib/active_storage_validations/aspect_ratio_validator.rb +150 -118
  25. data/lib/active_storage_validations/content_type_spoof_detector.rb +3 -1
  26. data/lib/active_storage_validations/content_type_validator.rb +13 -5
  27. data/lib/active_storage_validations/dimension_validator.rb +2 -0
  28. data/lib/active_storage_validations/marcel_extensor.rb +2 -0
  29. data/lib/active_storage_validations/matchers/processable_image_validator_matcher.rb +4 -4
  30. data/lib/active_storage_validations/matchers.rb +2 -1
  31. data/lib/active_storage_validations/processable_image_validator.rb +2 -0
  32. data/lib/active_storage_validations/shared/asv_active_storageable.rb +2 -2
  33. data/lib/active_storage_validations/shared/asv_analyzable.rb +45 -0
  34. data/lib/active_storage_validations/shared/asv_attachable.rb +63 -16
  35. data/lib/active_storage_validations/version.rb +1 -1
  36. data/lib/active_storage_validations.rb +6 -0
  37. metadata +8 -3
  38. data/lib/active_storage_validations/metadata.rb +0 -179
@@ -1,118 +1,150 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'shared/asv_active_storageable'
4
- require_relative 'shared/asv_attachable'
5
- require_relative 'shared/asv_errorable'
6
- require_relative 'shared/asv_optionable'
7
- require_relative 'shared/asv_symbolizable'
8
-
9
- module ActiveStorageValidations
10
- class AspectRatioValidator < ActiveModel::EachValidator # :nodoc
11
- include ASVActiveStorageable
12
- include ASVAttachable
13
- include ASVErrorable
14
- include ASVOptionable
15
- include ASVSymbolizable
16
-
17
- AVAILABLE_CHECKS = %i[with].freeze
18
- NAMED_ASPECT_RATIOS = %i[square portrait landscape].freeze
19
- ASPECT_RATIO_REGEX = /is_([1-9]\d*)_([1-9]\d*)/.freeze
20
- ERROR_TYPES = %i[
21
- image_metadata_missing
22
- aspect_ratio_not_square
23
- aspect_ratio_not_portrait
24
- aspect_ratio_not_landscape
25
- aspect_ratio_is_not
26
- ].freeze
27
- PRECISION = 3.freeze
28
-
29
- def check_validity!
30
- ensure_at_least_one_validator_option
31
- ensure_aspect_ratio_validity
32
- end
33
-
34
- def validate_each(record, attribute, _value)
35
- return if no_attachments?(record, attribute)
36
-
37
- validate_changed_files_from_metadata(record, attribute)
38
- end
39
-
40
- private
41
-
42
- def is_valid?(record, attribute, attachable, metadata)
43
- flat_options = set_flat_options(record)
44
-
45
- return if image_metadata_missing?(record, attribute, attachable, flat_options, metadata)
46
-
47
- case flat_options[:with]
48
- when :square then validate_square_aspect_ratio(record, attribute, attachable, flat_options, metadata)
49
- when :portrait then validate_portrait_aspect_ratio(record, attribute, attachable, flat_options, metadata)
50
- when :landscape then validate_landscape_aspect_ratio(record, attribute, attachable, flat_options, metadata)
51
- when ASPECT_RATIO_REGEX then validate_regex_aspect_ratio(record, attribute, attachable, flat_options, metadata)
52
- end
53
- end
54
-
55
- def image_metadata_missing?(record, attribute, attachable, flat_options, metadata)
56
- return false if metadata[:width].to_i > 0 && metadata[:height].to_i > 0
57
-
58
- errors_options = initialize_error_options(options, attachable)
59
- errors_options[:aspect_ratio] = flat_options[:with]
60
- add_error(record, attribute, :image_metadata_missing, **errors_options)
61
- true
62
- end
63
-
64
- def validate_square_aspect_ratio(record, attribute, attachable, flat_options, metadata)
65
- return if metadata[:width] == metadata[:height]
66
-
67
- errors_options = initialize_error_options(options, attachable)
68
- errors_options[:aspect_ratio] = flat_options[:with]
69
- add_error(record, attribute, :aspect_ratio_not_square, **errors_options)
70
- end
71
-
72
- def validate_portrait_aspect_ratio(record, attribute, attachable, flat_options, metadata)
73
- return if metadata[:width] < metadata[:height]
74
-
75
- errors_options = initialize_error_options(options, attachable)
76
- errors_options[:aspect_ratio] = flat_options[:with]
77
- add_error(record, attribute, :aspect_ratio_not_portrait, **errors_options)
78
- end
79
-
80
- def validate_landscape_aspect_ratio(record, attribute, attachable, flat_options, metadata)
81
- return if metadata[:width] > metadata[:height]
82
-
83
- errors_options = initialize_error_options(options, attachable)
84
- errors_options[:aspect_ratio] = flat_options[:with]
85
- add_error(record, attribute, :aspect_ratio_not_landscape, **errors_options)
86
- end
87
-
88
- def validate_regex_aspect_ratio(record, attribute, attachable, flat_options, metadata)
89
- flat_options[:with] =~ ASPECT_RATIO_REGEX
90
- x = $1.to_i
91
- y = $2.to_i
92
-
93
- return if x > 0 && y > 0 && (x.to_f / y).round(PRECISION) == (metadata[:width].to_f / metadata[:height]).round(PRECISION)
94
-
95
- errors_options = initialize_error_options(options, attachable)
96
- errors_options[:aspect_ratio] = "#{x}:#{y}"
97
- add_error(record, attribute, :aspect_ratio_is_not, **errors_options)
98
- end
99
-
100
- def ensure_at_least_one_validator_option
101
- unless AVAILABLE_CHECKS.any? { |argument| options.key?(argument) }
102
- raise ArgumentError, 'You must pass :with to the validator'
103
- end
104
- end
105
-
106
- def ensure_aspect_ratio_validity
107
- return true if options[:with]&.is_a?(Proc)
108
-
109
- unless NAMED_ASPECT_RATIOS.include?(options[:with]) || options[:with] =~ ASPECT_RATIO_REGEX
110
- raise ArgumentError, <<~ERROR_MESSAGE
111
- You must pass a valid aspect ratio to the validator
112
- It should either be a named aspect ratio (#{NAMED_ASPECT_RATIOS.join(', ')})
113
- Or an aspect ratio like 'is_16_9' (matching /#{ASPECT_RATIO_REGEX.source}/)
114
- ERROR_MESSAGE
115
- end
116
- end
117
- end
118
- end
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'shared/asv_active_storageable'
4
+ require_relative 'shared/asv_analyzable'
5
+ require_relative 'shared/asv_attachable'
6
+ require_relative 'shared/asv_errorable'
7
+ require_relative 'shared/asv_optionable'
8
+ require_relative 'shared/asv_symbolizable'
9
+
10
+ module ActiveStorageValidations
11
+ class AspectRatioValidator < ActiveModel::EachValidator # :nodoc
12
+ include ASVActiveStorageable
13
+ include ASVAnalyzable
14
+ include ASVAttachable
15
+ include ASVErrorable
16
+ include ASVOptionable
17
+ include ASVSymbolizable
18
+
19
+ AVAILABLE_CHECKS = %i[with in].freeze
20
+ NAMED_ASPECT_RATIOS = %i[square portrait landscape].freeze
21
+ ASPECT_RATIO_REGEX = /is_([1-9]\d*)_([1-9]\d*)/.freeze
22
+ ERROR_TYPES = %i[
23
+ aspect_ratio_not_square
24
+ aspect_ratio_not_portrait
25
+ aspect_ratio_not_landscape
26
+ aspect_ratio_is_not
27
+ aspect_ratio_invalid
28
+ image_metadata_missing
29
+ ].freeze
30
+ PRECISION = 3.freeze
31
+
32
+ def check_validity!
33
+ ensure_at_least_one_validator_option
34
+ ensure_aspect_ratio_validity
35
+ end
36
+
37
+ def validate_each(record, attribute, _value)
38
+ return if no_attachments?(record, attribute)
39
+
40
+ flat_options = set_flat_options(record)
41
+ @authorized_aspect_ratios = authorized_aspect_ratios_from_options(flat_options).compact
42
+ return if @authorized_aspect_ratios.empty?
43
+
44
+ validate_changed_files_from_metadata(record, attribute)
45
+ end
46
+
47
+ private
48
+
49
+ def is_valid?(record, attribute, attachable, metadata)
50
+ !image_metadata_missing?(record, attribute, attachable, metadata) &&
51
+ authorized_aspect_ratio?(record, attribute, attachable, metadata)
52
+ end
53
+
54
+ def authorized_aspect_ratio?(record, attribute, attachable, metadata)
55
+ attachable_aspect_ratio_is_authorized = @authorized_aspect_ratios.any? do |authorized_aspect_ratio|
56
+ case authorized_aspect_ratio
57
+ when :square then valid_square_aspect_ratio?(metadata)
58
+ when :portrait then valid_portrait_aspect_ratio?(metadata)
59
+ when :landscape then valid_landscape_aspect_ratio?(metadata)
60
+ when ASPECT_RATIO_REGEX then valid_regex_aspect_ratio?(authorized_aspect_ratio, metadata)
61
+ end
62
+ end
63
+
64
+ return true if attachable_aspect_ratio_is_authorized
65
+
66
+ errors_options = initialize_error_options(options, attachable)
67
+ errors_options[:aspect_ratio] = string_aspect_ratios
68
+ add_error(record, attribute, aspect_ratio_error_mapping, **errors_options)
69
+ false
70
+ end
71
+
72
+ def aspect_ratio_error_mapping
73
+ return :aspect_ratio_invalid unless @authorized_aspect_ratios.one?
74
+
75
+ aspect_ratio = @authorized_aspect_ratios.first
76
+ NAMED_ASPECT_RATIOS.include?(aspect_ratio) ? :"aspect_ratio_not_#{aspect_ratio}" : :aspect_ratio_is_not
77
+ end
78
+
79
+ def image_metadata_missing?(record, attribute, attachable, metadata)
80
+ return false if metadata[:width].to_i > 0 && metadata[:height].to_i > 0
81
+
82
+ errors_options = initialize_error_options(options, attachable)
83
+ errors_options[:aspect_ratio] = string_aspect_ratios
84
+ add_error(record, attribute, :image_metadata_missing, **errors_options)
85
+ true
86
+ end
87
+
88
+ def valid_square_aspect_ratio?(metadata)
89
+ metadata[:width] == metadata[:height]
90
+ end
91
+
92
+ def valid_portrait_aspect_ratio?(metadata)
93
+ metadata[:width] < metadata[:height]
94
+ end
95
+
96
+ def valid_landscape_aspect_ratio?(metadata)
97
+ metadata[:width] > metadata[:height]
98
+ end
99
+
100
+ def valid_regex_aspect_ratio?(aspect_ratio, metadata)
101
+ aspect_ratio =~ ASPECT_RATIO_REGEX
102
+ x = ::Regexp.last_match(1).to_i
103
+ y = ::Regexp.last_match(2).to_i
104
+
105
+ x > 0 && y > 0 && (x.to_f / y).round(PRECISION) == (metadata[:width].to_f / metadata[:height]).round(PRECISION)
106
+ end
107
+
108
+ def ensure_at_least_one_validator_option
109
+ return if AVAILABLE_CHECKS.any? { |argument| options.key?(argument) }
110
+
111
+ raise ArgumentError, 'You must pass either :with or :in to the validator'
112
+ end
113
+
114
+ def ensure_aspect_ratio_validity
115
+ return true if options[:with]&.is_a?(Proc) || options[:in]&.is_a?(Proc)
116
+
117
+ authorized_aspect_ratios_from_options(options).each do |aspect_ratio|
118
+ unless NAMED_ASPECT_RATIOS.include?(aspect_ratio) || aspect_ratio =~ ASPECT_RATIO_REGEX
119
+ raise ArgumentError, invalid_aspect_ratio_message
120
+ end
121
+ end
122
+ end
123
+
124
+ def invalid_aspect_ratio_message
125
+ <<~ERROR_MESSAGE
126
+ You must pass a valid aspect ratio to the validator
127
+ It should either be a named aspect ratio (#{NAMED_ASPECT_RATIOS.join(', ')})
128
+ Or an aspect ratio like 'is_16_9' (matching /#{ASPECT_RATIO_REGEX.source}/)
129
+ ERROR_MESSAGE
130
+ end
131
+
132
+ def authorized_aspect_ratios_from_options(flat_options)
133
+ (Array.wrap(flat_options[:with]) + Array.wrap(flat_options[:in]))
134
+ end
135
+
136
+ def string_aspect_ratios
137
+ @authorized_aspect_ratios.map do |aspect_ratio|
138
+ if NAMED_ASPECT_RATIOS.include?(aspect_ratio)
139
+ aspect_ratio
140
+ else
141
+ aspect_ratio =~ ASPECT_RATIO_REGEX
142
+ x = ::Regexp.last_match(1).to_i
143
+ y = ::Regexp.last_match(2).to_i
144
+
145
+ "#{x}:#{y}"
146
+ end
147
+ end.join(', ')
148
+ end
149
+ end
150
+ end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'shared/asv_analyzable'
3
4
  require_relative 'shared/asv_attachable'
4
5
  require_relative 'shared/asv_loggable'
5
6
  require 'open3'
@@ -8,6 +9,7 @@ module ActiveStorageValidations
8
9
  class ContentTypeSpoofDetector
9
10
  class FileCommandLineToolNotInstalledError < StandardError; end
10
11
 
12
+ include ASVAnalyzable
11
13
  include ASVAttachable
12
14
  include ASVLoggable
13
15
 
@@ -49,7 +51,7 @@ module ActiveStorageValidations
49
51
  end
50
52
 
51
53
  def open3_mime_type_for_io
52
- return nil if io.blank?
54
+ return nil if io.bytesize == 0
53
55
 
54
56
  Tempfile.create do |tempfile|
55
57
  tempfile.binmode
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'shared/asv_active_storageable'
4
+ require_relative 'shared/asv_analyzable'
4
5
  require_relative 'shared/asv_attachable'
5
6
  require_relative 'shared/asv_errorable'
6
7
  require_relative 'shared/asv_optionable'
@@ -10,6 +11,7 @@ require_relative 'content_type_spoof_detector'
10
11
  module ActiveStorageValidations
11
12
  class ContentTypeValidator < ActiveModel::EachValidator # :nodoc:
12
13
  include ASVActiveStorageable
14
+ include ASVAnalyzable
13
15
  include ASVAttachable
14
16
  include ASVErrorable
15
17
  include ASVOptionable
@@ -32,9 +34,11 @@ module ActiveStorageValidations
32
34
  @authorized_content_types = authorized_content_types_from_options(record)
33
35
  return if @authorized_content_types.empty?
34
36
 
35
- attachables_from_changes(record, attribute).each do |attachable|
36
- set_attachable_cached_values(attachable)
37
- is_valid?(record, attribute, attachable)
37
+ checked_files = disable_spoofing_protection? ? attached_files(record, attribute) : attachables_from_changes(record, attribute)
38
+
39
+ checked_files.each do |file|
40
+ set_attachable_cached_values(file)
41
+ is_valid?(record, attribute, file)
38
42
  end
39
43
  end
40
44
 
@@ -52,8 +56,8 @@ module ActiveStorageValidations
52
56
  end
53
57
 
54
58
  def set_attachable_cached_values(attachable)
55
- @attachable_content_type = attachable_content_type(attachable)
56
- @attachable_filename = attachable_filename(attachable).to_s
59
+ @attachable_content_type = disable_spoofing_protection? ? attachable.blob.content_type : attachable_content_type_rails_like(attachable)
60
+ @attachable_filename = disable_spoofing_protection? ? attachable.blob.filename.to_s : attachable_filename(attachable).to_s
57
61
  end
58
62
 
59
63
  # Check if the provided content_type is authorized and not spoofed against
@@ -114,6 +118,10 @@ module ActiveStorageValidations
114
118
  Marcel::MimeType.for(declared_type: @attachable_content_type, name: @attachable_filename)
115
119
  end
116
120
 
121
+ def disable_spoofing_protection?
122
+ !enable_spoofing_protection?
123
+ end
124
+
117
125
  def enable_spoofing_protection?
118
126
  options[:spoofing_protection] == true
119
127
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'shared/asv_active_storageable'
4
+ require_relative 'shared/asv_analyzable'
4
5
  require_relative 'shared/asv_attachable'
5
6
  require_relative 'shared/asv_errorable'
6
7
  require_relative 'shared/asv_optionable'
@@ -9,6 +10,7 @@ require_relative 'shared/asv_symbolizable'
9
10
  module ActiveStorageValidations
10
11
  class DimensionValidator < ActiveModel::EachValidator # :nodoc
11
12
  include ASVActiveStorageable
13
+ include ASVAnalyzable
12
14
  include ASVAttachable
13
15
  include ASVErrorable
14
16
  include ASVOptionable
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "marcel"
4
+
3
5
  Marcel::MimeType.extend "application/x-rar-compressed", parents: %(application/x-rar)
4
6
  Marcel::MimeType.extend "audio/x-hx-aac-adts", parents: %(audio/x-aac)
5
7
  Marcel::MimeType.extend "audio/x-m4a", parents: %(audio/mp4)
@@ -43,10 +43,10 @@ module ActiveStorageValidations
43
43
  @subject = subject.is_a?(Class) ? subject.new : subject
44
44
 
45
45
  is_a_valid_active_storage_attribute? &&
46
- is_context_valid? &&
47
- is_custom_message_valid? &&
48
- is_valid_when_image_processable? &&
49
- is_invalid_when_image_not_processable?
46
+ is_context_valid? &&
47
+ is_custom_message_valid? &&
48
+ is_valid_when_image_processable? &&
49
+ is_invalid_when_image_not_processable?
50
50
  end
51
51
 
52
52
  private
@@ -27,7 +27,8 @@ module ActiveStorageValidations
27
27
 
28
28
  def self.mock_metadata(attachment, width, height)
29
29
  mock = Struct.new(:metadata).new({ width: width, height: height })
30
- stub_method(ActiveStorageValidations::Metadata, :new, mock) do
30
+
31
+ stub_method(ActiveStorageValidations::Analyzer, :new, mock) do
31
32
  yield
32
33
  end
33
34
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'shared/asv_active_storageable'
4
+ require_relative 'shared/asv_analyzable'
4
5
  require_relative 'shared/asv_attachable'
5
6
  require_relative 'shared/asv_errorable'
6
7
  require_relative 'shared/asv_symbolizable'
@@ -8,6 +9,7 @@ require_relative 'shared/asv_symbolizable'
8
9
  module ActiveStorageValidations
9
10
  class ProcessableImageValidator < ActiveModel::EachValidator # :nodoc
10
11
  include ASVActiveStorageable
12
+ include ASVAnalyzable
11
13
  include ASVAttachable
12
14
  include ASVErrorable
13
15
  include ASVSymbolizable
@@ -9,8 +9,8 @@ module ActiveStorageValidations
9
9
 
10
10
  private
11
11
 
12
- # Retrieve either an ActiveStorage::Attached::One or an
13
- # ActiveStorage::Attached::Many instance depending on attribute definition
12
+ # Retrieve either an `ActiveStorage::Attached::One` or an
13
+ # `ActiveStorage::Attached::Many` instance depending on attribute definition
14
14
  def attached_files(record, attribute)
15
15
  Array.wrap(record.send(attribute))
16
16
  end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorageValidations
4
+ # ActiveStorageValidations::ASVAnalyzable
5
+ #
6
+ # Validator methods for choosing the right analyzer depending on the file
7
+ # media type and available third-party analyzers.
8
+ module ASVAnalyzable
9
+ extend ActiveSupport::Concern
10
+
11
+ DEFAULT_IMAGE_PROCESSOR = :mini_magick.freeze
12
+
13
+ private
14
+
15
+ def metadata_for(attachable)
16
+ analyzer_for(attachable).metadata
17
+ end
18
+
19
+ def analyzer_for(attachable)
20
+ case attachable_media_type(attachable)
21
+ when "image" then image_analyzer_for(attachable)
22
+ else fallback_analyzer_for(attachable)
23
+ end
24
+ end
25
+
26
+ def image_analyzer_for(attachable)
27
+ case image_processor
28
+ when :mini_magick
29
+ ActiveStorageValidations::Analyzer::ImageAnalyzer::ImageMagick.new(attachable)
30
+ when :vips
31
+ ActiveStorageValidations::Analyzer::ImageAnalyzer::Vips.new(attachable)
32
+ end
33
+ end
34
+
35
+ def image_processor
36
+ # Rails returns nil for default image processor, because it is set in an after initialize callback
37
+ # https://github.com/rails/rails/blob/main/activestorage/lib/active_storage/engine.rb
38
+ ActiveStorage.variant_processor || DEFAULT_IMAGE_PROCESSOR
39
+ end
40
+
41
+ def fallback_analyzer_for(attachable)
42
+ ActiveStorageValidations::Analyzer::NullAnalyzer.new(attachable)
43
+ end
44
+ end
45
+ end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../metadata"
4
-
5
3
  module ActiveStorageValidations
6
4
  # ActiveStorageValidations::ASVAttachable
7
5
  #
@@ -20,7 +18,7 @@ module ActiveStorageValidations
20
18
  # to perform file analyses.
21
19
  def validate_changed_files_from_metadata(record, attribute)
22
20
  attachables_from_changes(record, attribute).each do |attachable|
23
- is_valid?(record, attribute, attachable, Metadata.new(attachable).metadata)
21
+ is_valid?(record, attribute, attachable, metadata_for(attachable))
24
22
  end
25
23
  end
26
24
 
@@ -64,25 +62,67 @@ module ActiveStorageValidations
64
62
  def attachable_content_type(attachable)
65
63
  full_attachable_content_type(attachable) && full_attachable_content_type(attachable).downcase.split(/[;,\s]/, 2).first
66
64
  end
65
+
66
+ # Retrieve the content_type from attachable using the same logic as Rails
67
+ # ActiveStorage::Blob::Identifiable#identify_content_type
68
+ def attachable_content_type_rails_like(attachable)
69
+ Marcel::MimeType.for(
70
+ attachable_io(attachable, max_byte_size: 4.kilobytes),
71
+ name: attachable_filename(attachable).to_s,
72
+ declared_type: full_attachable_content_type(attachable)
73
+ )
74
+ end
67
75
 
76
+ # Retrieve the media type of the attachable, which is the first part of the
77
+ # content type (or mime type).
78
+ # Possible values are: application/audio/example/font/image/model/text/video
79
+ def attachable_media_type(attachable)
80
+ (full_attachable_content_type(attachable) || marcel_content_type_from_filename(attachable)).split("/").first
81
+ end
82
+
68
83
  # Retrieve the io from attachable.
69
- def attachable_io(attachable)
84
+ def attachable_io(attachable, max_byte_size: nil)
85
+ io = case attachable
86
+ when ActiveStorage::Blob
87
+ (max_byte_size && supports_blob_download_chunk?) ? attachable.download_chunk(0...max_byte_size) : attachable.download
88
+ when ActionDispatch::Http::UploadedFile
89
+ max_byte_size ? attachable.read(max_byte_size) : attachable.read
90
+ when Rack::Test::UploadedFile
91
+ max_byte_size ? attachable.read(max_byte_size) : attachable.read
92
+ when String
93
+ blob = ActiveStorage::Blob.find_signed!(attachable)
94
+ (max_byte_size && supports_blob_download_chunk?) ? blob.download_chunk(0...max_byte_size) : blob.download
95
+ when Hash
96
+ max_byte_size ? attachable[:io].read(max_byte_size) : attachable[:io].read
97
+ when File
98
+ raise_rails_like_error(attachable) unless supports_file_attachment?
99
+ max_byte_size ? attachable.read(max_byte_size) : attachable.read
100
+ when Pathname
101
+ raise_rails_like_error(attachable) unless supports_pathname_attachment?
102
+ max_byte_size ? attachable.read(max_byte_size) : attachable.read
103
+ else
104
+ raise_rails_like_error(attachable)
105
+ end
106
+
107
+ rewind_attachable_io(attachable)
108
+ io
109
+ end
110
+
111
+ # Rewind the io attachable.
112
+ def rewind_attachable_io(attachable)
70
113
  case attachable
71
- when ActiveStorage::Blob
72
- attachable.download
73
- when ActionDispatch::Http::UploadedFile
74
- attachable.read
75
- when Rack::Test::UploadedFile
76
- attachable.read
77
- when String
78
- blob = ActiveStorage::Blob.find_signed!(attachable)
79
- blob.download
114
+ when ActiveStorage::Blob, String
115
+ # nothing to do
116
+ when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile
117
+ attachable.rewind
80
118
  when Hash
81
- attachable[:io].read
119
+ attachable[:io].rewind
82
120
  when File
83
- supports_file_attachment? ? attachable : raise_rails_like_error(attachable)
121
+ raise_rails_like_error(attachable) unless supports_file_attachment?
122
+ attachable.rewind
84
123
  when Pathname
85
- supports_pathname_attachment? ? attachable.read : raise_rails_like_error(attachable)
124
+ raise_rails_like_error(attachable) unless supports_pathname_attachment?
125
+ File.open(attachable) { |f| f.rewind }
86
126
  else
87
127
  raise_rails_like_error(attachable)
88
128
  end
@@ -128,6 +168,13 @@ module ActiveStorageValidations
128
168
  end
129
169
  alias :supports_pathname_attachment? :supports_file_attachment?
130
170
 
171
+ # Check if the current Rails version supports ActiveStorage::Blob#download_chunk
172
+ #
173
+ # https://github.com/rails/rails/blob/7-0-stable/activestorage/CHANGELOG.md#rails-700alpha1-september-15-2021
174
+ def supports_blob_download_chunk?
175
+ Rails.gem_version >= Gem::Version.new('7.0.0.alpha1')
176
+ end
177
+
131
178
  # Retrieve the content_type from the file name only
132
179
  def marcel_content_type_from_filename(attachable)
133
180
  Marcel::MimeType.for(name: attachable_filename(attachable).to_s)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveStorageValidations
4
- VERSION = '1.3.4'
4
+ VERSION = '1.4.0'
5
5
  end
@@ -3,6 +3,12 @@
3
3
  require 'active_model'
4
4
  require 'active_support/concern'
5
5
 
6
+ require 'active_storage_validations/analyzer'
7
+ require 'active_storage_validations/analyzer/image_analyzer'
8
+ require 'active_storage_validations/analyzer/image_analyzer/image_magick'
9
+ require 'active_storage_validations/analyzer/image_analyzer/vips'
10
+ require 'active_storage_validations/analyzer/null_analyzer'
11
+
6
12
  require 'active_storage_validations/railtie'
7
13
  require 'active_storage_validations/engine'
8
14
  require 'active_storage_validations/attached_validator'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_storage_validations
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.4
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Igor Kasyanchuk
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-11-14 00:00:00.000000000 Z
11
+ date: 2024-12-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activejob
@@ -259,6 +259,11 @@ files:
259
259
  - config/locales/vi.yml
260
260
  - config/locales/zh-CN.yml
261
261
  - lib/active_storage_validations.rb
262
+ - lib/active_storage_validations/analyzer.rb
263
+ - lib/active_storage_validations/analyzer/image_analyzer.rb
264
+ - lib/active_storage_validations/analyzer/image_analyzer/image_magick.rb
265
+ - lib/active_storage_validations/analyzer/image_analyzer/vips.rb
266
+ - lib/active_storage_validations/analyzer/null_analyzer.rb
262
267
  - lib/active_storage_validations/aspect_ratio_validator.rb
263
268
  - lib/active_storage_validations/attached_validator.rb
264
269
  - lib/active_storage_validations/base_size_validator.rb
@@ -285,10 +290,10 @@ files:
285
290
  - lib/active_storage_validations/matchers/shared/asv_validatable.rb
286
291
  - lib/active_storage_validations/matchers/size_validator_matcher.rb
287
292
  - lib/active_storage_validations/matchers/total_size_validator_matcher.rb
288
- - lib/active_storage_validations/metadata.rb
289
293
  - lib/active_storage_validations/processable_image_validator.rb
290
294
  - lib/active_storage_validations/railtie.rb
291
295
  - lib/active_storage_validations/shared/asv_active_storageable.rb
296
+ - lib/active_storage_validations/shared/asv_analyzable.rb
292
297
  - lib/active_storage_validations/shared/asv_attachable.rb
293
298
  - lib/active_storage_validations/shared/asv_errorable.rb
294
299
  - lib/active_storage_validations/shared/asv_loggable.rb