active_storage_validations 1.3.4 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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