active_storage_validations 1.4.0 → 2.0.1

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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +619 -213
  3. data/config/locales/da.yml +50 -30
  4. data/config/locales/de.yml +50 -30
  5. data/config/locales/en.yml +50 -30
  6. data/config/locales/es.yml +50 -30
  7. data/config/locales/fr.yml +50 -30
  8. data/config/locales/it.yml +50 -30
  9. data/config/locales/ja.yml +50 -30
  10. data/config/locales/nl.yml +50 -30
  11. data/config/locales/pl.yml +50 -30
  12. data/config/locales/pt-BR.yml +50 -30
  13. data/config/locales/ru.yml +50 -30
  14. data/config/locales/sv.yml +50 -30
  15. data/config/locales/tr.yml +50 -30
  16. data/config/locales/uk.yml +50 -30
  17. data/config/locales/vi.yml +50 -30
  18. data/config/locales/zh-CN.yml +50 -30
  19. data/lib/active_storage_validations/analyzer/audio_analyzer.rb +58 -0
  20. data/lib/active_storage_validations/analyzer/content_type_analyzer.rb +60 -0
  21. data/lib/active_storage_validations/analyzer/image_analyzer/image_magick.rb +4 -4
  22. data/lib/active_storage_validations/analyzer/image_analyzer/vips.rb +11 -12
  23. data/lib/active_storage_validations/analyzer/image_analyzer.rb +9 -53
  24. data/lib/active_storage_validations/analyzer/null_analyzer.rb +2 -2
  25. data/lib/active_storage_validations/analyzer/shared/asv_ff_probable.rb +61 -0
  26. data/lib/active_storage_validations/analyzer/video_analyzer.rb +130 -0
  27. data/lib/active_storage_validations/analyzer.rb +54 -1
  28. data/lib/active_storage_validations/aspect_ratio_validator.rb +15 -11
  29. data/lib/active_storage_validations/{base_size_validator.rb → base_comparison_validator.rb} +18 -16
  30. data/lib/active_storage_validations/content_type_validator.rb +56 -23
  31. data/lib/active_storage_validations/dimension_validator.rb +20 -19
  32. data/lib/active_storage_validations/duration_validator.rb +55 -0
  33. data/lib/active_storage_validations/extensors/asv_blob_metadatable.rb +49 -0
  34. data/lib/active_storage_validations/{marcel_extensor.rb → extensors/asv_marcelable.rb} +3 -0
  35. data/lib/active_storage_validations/limit_validator.rb +14 -2
  36. data/lib/active_storage_validations/matchers/aspect_ratio_validator_matcher.rb +1 -1
  37. data/lib/active_storage_validations/matchers/{base_size_validator_matcher.rb → base_comparison_validator_matcher.rb} +31 -25
  38. data/lib/active_storage_validations/matchers/content_type_validator_matcher.rb +7 -3
  39. data/lib/active_storage_validations/matchers/dimension_validator_matcher.rb +1 -1
  40. data/lib/active_storage_validations/matchers/duration_validator_matcher.rb +39 -0
  41. data/lib/active_storage_validations/matchers/{processable_image_validator_matcher.rb → processable_file_validator_matcher.rb} +5 -5
  42. data/lib/active_storage_validations/matchers/size_validator_matcher.rb +18 -2
  43. data/lib/active_storage_validations/matchers/total_size_validator_matcher.rb +18 -2
  44. data/lib/active_storage_validations/matchers.rb +5 -3
  45. data/lib/active_storage_validations/{processable_image_validator.rb → processable_file_validator.rb} +4 -3
  46. data/lib/active_storage_validations/railtie.rb +5 -0
  47. data/lib/active_storage_validations/shared/asv_analyzable.rb +38 -3
  48. data/lib/active_storage_validations/shared/asv_attachable.rb +36 -15
  49. data/lib/active_storage_validations/size_validator.rb +11 -3
  50. data/lib/active_storage_validations/total_size_validator.rb +9 -3
  51. data/lib/active_storage_validations/version.rb +1 -1
  52. data/lib/active_storage_validations.rb +7 -3
  53. metadata +14 -8
  54. data/lib/active_storage_validations/content_type_spoof_detector.rb +0 -96
@@ -12,13 +12,36 @@ module ActiveStorageValidations
12
12
 
13
13
  private
14
14
 
15
- def metadata_for(attachable)
16
- analyzer_for(attachable).metadata
15
+ # Retrieve the ASV metadata from the blob.
16
+ # If the blob has not been analyzed by our gem yet, the gem will analyze the
17
+ # attachable with the corresponding analyzer and set the metadata in the
18
+ # blob.
19
+ def metadata_for(blob, attachable, metadata_keys)
20
+ return blob.active_storage_validations_metadata if blob_has_asv_metadata?(blob, metadata_keys)
21
+
22
+ new_metadata = generate_metadata_for(attachable, metadata_keys)
23
+ blob.merge_into_active_storage_validations_metadata(new_metadata)
24
+ end
25
+
26
+ def blob_has_asv_metadata?(blob, metadata_keys)
27
+ return false unless blob.active_storage_validations_metadata.present?
28
+
29
+ metadata_keys.all? { |key| blob.active_storage_validations_metadata.key?(key) }
30
+ end
31
+
32
+ def generate_metadata_for(attachable, metadata_keys)
33
+ if metadata_keys == ActiveStorageValidations::ContentTypeValidator::METADATA_KEYS
34
+ content_type_analyzer_for(attachable).content_type
35
+ else
36
+ metadata_analyzer_for(attachable).metadata
37
+ end
17
38
  end
18
39
 
19
- def analyzer_for(attachable)
40
+ def metadata_analyzer_for(attachable)
20
41
  case attachable_media_type(attachable)
21
42
  when "image" then image_analyzer_for(attachable)
43
+ when "video" then video_analyzer_for(attachable)
44
+ when "audio" then audio_analyzer_for(attachable)
22
45
  else fallback_analyzer_for(attachable)
23
46
  end
24
47
  end
@@ -38,8 +61,20 @@ module ActiveStorageValidations
38
61
  ActiveStorage.variant_processor || DEFAULT_IMAGE_PROCESSOR
39
62
  end
40
63
 
64
+ def video_analyzer_for(attachable)
65
+ ActiveStorageValidations::Analyzer::VideoAnalyzer.new(attachable)
66
+ end
67
+
68
+ def audio_analyzer_for(attachable)
69
+ ActiveStorageValidations::Analyzer::AudioAnalyzer.new(attachable)
70
+ end
71
+
41
72
  def fallback_analyzer_for(attachable)
42
73
  ActiveStorageValidations::Analyzer::NullAnalyzer.new(attachable)
43
74
  end
75
+
76
+ def content_type_analyzer_for(attachable)
77
+ ActiveStorageValidations::Analyzer::ContentTypeAnalyzer.new(attachable)
78
+ end
44
79
  end
45
80
  end
@@ -16,22 +16,37 @@ module ActiveStorageValidations
16
16
  # Loop through the newly submitted attachables to validate them. Using
17
17
  # attachables is the only way to get the attached file io that is necessary
18
18
  # to perform file analyses.
19
- def validate_changed_files_from_metadata(record, attribute)
20
- attachables_from_changes(record, attribute).each do |attachable|
21
- is_valid?(record, attribute, attachable, metadata_for(attachable))
19
+ def validate_changed_files_from_metadata(record, attribute, metadata_keys)
20
+ attachables_and_blobs(record, attribute).each do |attachable, blob|
21
+ is_valid?(record, attribute, attachable, metadata_for(blob, attachable, metadata_keys))
22
22
  end
23
23
  end
24
24
 
25
- # Retrieve an array of newly submitted attachables. Some file could be passed
26
- # several times, we just need to perform the analysis once on the file,
27
- # therefore the use of #uniq.
28
- def attachables_from_changes(record, attribute)
29
- changes = record.attachment_changes[attribute.to_s]
30
- return [] if changes.blank?
25
+ # Retrieve an array-like of attachables and blobs. Unlike its name suggests,
26
+ # getting attachables from attachment_changes is not getting the changed
27
+ # attachables but all attachables from the `has_many_attached` relation.
28
+ # For the `has_one_attached` relation, it only yields the new attachable,
29
+ # but if we are validating previously attached file, we need to use the blob
30
+ # See #attach at: https://github.com/rails/rails/blob/main/activestorage/lib/active_storage/attached/many.rb
31
+ #
32
+ # Some file could be passed several times, we just need to perform the
33
+ # analysis once on the file, therefore the use of #uniq.
34
+ def attachables_and_blobs(record, attribute)
35
+ changes = if record.public_send(attribute).is_a?(ActiveStorage::Attached::One)
36
+ record.attachment_changes[attribute.to_s].presence || record.public_send(attribute)
37
+ else
38
+ record.attachment_changes[attribute.to_s]
39
+ end
31
40
 
32
- Array.wrap(
33
- changes.is_a?(ActiveStorage::Attached::Changes::CreateMany) ? changes.attachables : changes.attachable
34
- ).uniq
41
+ return to_enum(:attachables_and_blobs, record, attribute) if changes.blank? || !block_given?
42
+
43
+ if changes.is_a?(ActiveStorage::Attached::Changes::CreateMany)
44
+ changes.attachables.uniq.zip(changes.blobs.uniq).each do |attachable, blob|
45
+ yield attachable, blob
46
+ end
47
+ else
48
+ yield changes.is_a?(ActiveStorage::Attached::Changes::CreateOne) ? changes.attachable : changes.blob, changes.blob
49
+ end
35
50
  end
36
51
 
37
52
  # Retrieve the full declared content_type from attachable.
@@ -60,9 +75,15 @@ module ActiveStorageValidations
60
75
  # Retrieve the declared content_type from attachable without potential mime
61
76
  # type parameters (e.g. 'application/x-rar-compressed;version=5')
62
77
  def attachable_content_type(attachable)
63
- full_attachable_content_type(attachable) && full_attachable_content_type(attachable).downcase.split(/[;,\s]/, 2).first
78
+ full_attachable_content_type(attachable) && content_type_without_parameters(full_attachable_content_type(attachable))
79
+ end
80
+
81
+ # Remove the potential mime type parameters from the content_type (e.g.
82
+ # 'application/x-rar-compressed;version=5')
83
+ def content_type_without_parameters(content_type)
84
+ content_type && content_type.downcase.split(/[;,\s]/, 2).first
64
85
  end
65
-
86
+
66
87
  # Retrieve the content_type from attachable using the same logic as Rails
67
88
  # ActiveStorage::Blob::Identifiable#identify_content_type
68
89
  def attachable_content_type_rails_like(attachable)
@@ -79,7 +100,7 @@ module ActiveStorageValidations
79
100
  def attachable_media_type(attachable)
80
101
  (full_attachable_content_type(attachable) || marcel_content_type_from_filename(attachable)).split("/").first
81
102
  end
82
-
103
+
83
104
  # Retrieve the io from attachable.
84
105
  def attachable_io(attachable, max_byte_size: nil)
85
106
  io = case attachable
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'base_size_validator'
3
+ require_relative 'base_comparison_validator'
4
4
 
5
5
  module ActiveStorageValidations
6
- class SizeValidator < BaseSizeValidator
6
+ class SizeValidator < BaseComparisonValidator
7
7
  ERROR_TYPES = %i[
8
8
  file_size_not_less_than
9
9
  file_size_not_less_than_or_equal_to
@@ -12,6 +12,8 @@ module ActiveStorageValidations
12
12
  file_size_not_between
13
13
  ].freeze
14
14
 
15
+ delegate :number_to_human_size, to: ActiveSupport::NumberHelper
16
+
15
17
  def validate_each(record, attribute, _value)
16
18
  return if no_attachments?(record, attribute)
17
19
 
@@ -22,7 +24,7 @@ module ActiveStorageValidations
22
24
 
23
25
  errors_options = initialize_error_options(options, file)
24
26
  populate_error_options(errors_options, flat_options)
25
- errors_options[:file_size] = number_to_human_size(file.blob.byte_size)
27
+ errors_options[:file_size] = format_bound_value(file.blob.byte_size)
26
28
 
27
29
  keys = AVAILABLE_CHECKS & flat_options.keys
28
30
  error_type = "file_size_not_#{keys.first}".to_sym
@@ -30,5 +32,11 @@ module ActiveStorageValidations
30
32
  add_error(record, attribute, error_type, **errors_options)
31
33
  end
32
34
  end
35
+
36
+ private
37
+
38
+ def format_bound_value(value)
39
+ number_to_human_size(value)
40
+ end
33
41
  end
34
42
  end
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'base_size_validator'
3
+ require_relative 'base_comparison_validator'
4
4
 
5
5
  module ActiveStorageValidations
6
- class TotalSizeValidator < BaseSizeValidator
6
+ class TotalSizeValidator < BaseComparisonValidator
7
7
  ERROR_TYPES = %i[
8
8
  total_file_size_not_less_than
9
9
  total_file_size_not_less_than_or_equal_to
@@ -12,6 +12,8 @@ module ActiveStorageValidations
12
12
  total_file_size_not_between
13
13
  ].freeze
14
14
 
15
+ delegate :number_to_human_size, to: ActiveSupport::NumberHelper
16
+
15
17
  def validate_each(record, attribute, _value)
16
18
  custom_check_validity!(record, attribute)
17
19
 
@@ -24,7 +26,7 @@ module ActiveStorageValidations
24
26
 
25
27
  errors_options = initialize_error_options(options, nil)
26
28
  populate_error_options(errors_options, flat_options)
27
- errors_options[:total_file_size] = number_to_human_size(total_file_size)
29
+ errors_options[:total_file_size] = format_bound_value(total_file_size)
28
30
 
29
31
  keys = AVAILABLE_CHECKS & flat_options.keys
30
32
  error_type = "total_file_size_not_#{keys.first}".to_sym
@@ -41,5 +43,9 @@ module ActiveStorageValidations
41
43
  raise ArgumentError, 'This validator is only available for has_many_attached relations'
42
44
  end
43
45
  end
46
+
47
+ def format_bound_value(value)
48
+ number_to_human_size(value)
49
+ end
44
50
  end
45
51
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveStorageValidations
4
- VERSION = '1.4.0'
4
+ VERSION = '2.0.1'
5
5
  end
@@ -8,6 +8,11 @@ require 'active_storage_validations/analyzer/image_analyzer'
8
8
  require 'active_storage_validations/analyzer/image_analyzer/image_magick'
9
9
  require 'active_storage_validations/analyzer/image_analyzer/vips'
10
10
  require 'active_storage_validations/analyzer/null_analyzer'
11
+ require 'active_storage_validations/analyzer/video_analyzer'
12
+ require 'active_storage_validations/analyzer/audio_analyzer'
13
+
14
+ require 'active_storage_validations/extensors/asv_blob_metadatable'
15
+ require 'active_storage_validations/extensors/asv_marcelable'
11
16
 
12
17
  require 'active_storage_validations/railtie'
13
18
  require 'active_storage_validations/engine'
@@ -15,13 +20,12 @@ require 'active_storage_validations/attached_validator'
15
20
  require 'active_storage_validations/content_type_validator'
16
21
  require 'active_storage_validations/limit_validator'
17
22
  require 'active_storage_validations/dimension_validator'
23
+ require 'active_storage_validations/duration_validator'
18
24
  require 'active_storage_validations/aspect_ratio_validator'
19
- require 'active_storage_validations/processable_image_validator'
25
+ require 'active_storage_validations/processable_file_validator'
20
26
  require 'active_storage_validations/size_validator'
21
27
  require 'active_storage_validations/total_size_validator'
22
28
 
23
- require 'active_storage_validations/marcel_extensor'
24
-
25
29
  ActiveSupport.on_load(:active_record) do
26
30
  send :include, ActiveStorageValidations
27
31
  end
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.4.0
4
+ version: 2.0.1
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-12-21 00:00:00.000000000 Z
11
+ date: 2025-01-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activejob
@@ -260,27 +260,33 @@ files:
260
260
  - config/locales/zh-CN.yml
261
261
  - lib/active_storage_validations.rb
262
262
  - lib/active_storage_validations/analyzer.rb
263
+ - lib/active_storage_validations/analyzer/audio_analyzer.rb
264
+ - lib/active_storage_validations/analyzer/content_type_analyzer.rb
263
265
  - lib/active_storage_validations/analyzer/image_analyzer.rb
264
266
  - lib/active_storage_validations/analyzer/image_analyzer/image_magick.rb
265
267
  - lib/active_storage_validations/analyzer/image_analyzer/vips.rb
266
268
  - lib/active_storage_validations/analyzer/null_analyzer.rb
269
+ - lib/active_storage_validations/analyzer/shared/asv_ff_probable.rb
270
+ - lib/active_storage_validations/analyzer/video_analyzer.rb
267
271
  - lib/active_storage_validations/aspect_ratio_validator.rb
268
272
  - lib/active_storage_validations/attached_validator.rb
269
- - lib/active_storage_validations/base_size_validator.rb
270
- - lib/active_storage_validations/content_type_spoof_detector.rb
273
+ - lib/active_storage_validations/base_comparison_validator.rb
271
274
  - lib/active_storage_validations/content_type_validator.rb
272
275
  - lib/active_storage_validations/dimension_validator.rb
276
+ - lib/active_storage_validations/duration_validator.rb
273
277
  - lib/active_storage_validations/engine.rb
278
+ - lib/active_storage_validations/extensors/asv_blob_metadatable.rb
279
+ - lib/active_storage_validations/extensors/asv_marcelable.rb
274
280
  - lib/active_storage_validations/limit_validator.rb
275
- - lib/active_storage_validations/marcel_extensor.rb
276
281
  - lib/active_storage_validations/matchers.rb
277
282
  - lib/active_storage_validations/matchers/aspect_ratio_validator_matcher.rb
278
283
  - lib/active_storage_validations/matchers/attached_validator_matcher.rb
279
- - lib/active_storage_validations/matchers/base_size_validator_matcher.rb
284
+ - lib/active_storage_validations/matchers/base_comparison_validator_matcher.rb
280
285
  - lib/active_storage_validations/matchers/content_type_validator_matcher.rb
281
286
  - lib/active_storage_validations/matchers/dimension_validator_matcher.rb
287
+ - lib/active_storage_validations/matchers/duration_validator_matcher.rb
282
288
  - lib/active_storage_validations/matchers/limit_validator_matcher.rb
283
- - lib/active_storage_validations/matchers/processable_image_validator_matcher.rb
289
+ - lib/active_storage_validations/matchers/processable_file_validator_matcher.rb
284
290
  - lib/active_storage_validations/matchers/shared/asv_active_storageable.rb
285
291
  - lib/active_storage_validations/matchers/shared/asv_allow_blankable.rb
286
292
  - lib/active_storage_validations/matchers/shared/asv_attachable.rb
@@ -290,7 +296,7 @@ files:
290
296
  - lib/active_storage_validations/matchers/shared/asv_validatable.rb
291
297
  - lib/active_storage_validations/matchers/size_validator_matcher.rb
292
298
  - lib/active_storage_validations/matchers/total_size_validator_matcher.rb
293
- - lib/active_storage_validations/processable_image_validator.rb
299
+ - lib/active_storage_validations/processable_file_validator.rb
294
300
  - lib/active_storage_validations/railtie.rb
295
301
  - lib/active_storage_validations/shared/asv_active_storageable.rb
296
302
  - lib/active_storage_validations/shared/asv_analyzable.rb
@@ -1,96 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'shared/asv_analyzable'
4
- require_relative 'shared/asv_attachable'
5
- require_relative 'shared/asv_loggable'
6
- require 'open3'
7
-
8
- module ActiveStorageValidations
9
- class ContentTypeSpoofDetector
10
- class FileCommandLineToolNotInstalledError < StandardError; end
11
-
12
- include ASVAnalyzable
13
- include ASVAttachable
14
- include ASVLoggable
15
-
16
- def initialize(record, attribute, attachable)
17
- @record = record
18
- @attribute = attribute
19
- @attachable = attachable
20
- end
21
-
22
- def spoofed?
23
- if supplied_content_type_vs_open3_analizer_mismatch?
24
- logger.info "Content Type Spoofing detected for file '#{filename}'. The supplied content type is '#{supplied_content_type}' but the content type discovered using open3 is '#{content_type_from_analyzer}'."
25
- true
26
- else
27
- false
28
- end
29
- end
30
-
31
- private
32
-
33
- def filename
34
- @filename ||= attachable_filename(@attachable).to_s
35
- end
36
-
37
- def supplied_content_type
38
- @supplied_content_type ||= attachable_content_type(@attachable)
39
- end
40
-
41
- def io
42
- @io ||= attachable_io(@attachable)
43
- end
44
-
45
- # Return the content_type found by Open3 analysis.
46
- #
47
- # Using Open3 is a better alternative than Marcel (Marcel::MimeType.for(io))
48
- # for analyzing content type solely based on the file io.
49
- def content_type_from_analyzer
50
- @content_type_from_analyzer ||= open3_mime_type_for_io
51
- end
52
-
53
- def open3_mime_type_for_io
54
- return nil if io.bytesize == 0
55
-
56
- Tempfile.create do |tempfile|
57
- tempfile.binmode
58
- tempfile.write(io)
59
- tempfile.rewind
60
-
61
- command = "file -b --mime-type #{tempfile.path}"
62
- output, status = Open3.capture2(command)
63
-
64
- if status.success?
65
- mime_type = output.strip
66
- return mime_type
67
- else
68
- raise "Error determining MIME type: #{output}"
69
- end
70
-
71
- rescue Errno::ENOENT
72
- raise FileCommandLineToolNotInstalledError, 'file command-line tool is not installed'
73
- end
74
- end
75
-
76
- def supplied_content_type_vs_open3_analizer_mismatch?
77
- supplied_content_type.present? &&
78
- !supplied_content_type_intersects_content_type_from_analyzer?
79
- end
80
-
81
- def supplied_content_type_intersects_content_type_from_analyzer?
82
- # Ruby intersects? method is only available from 3.1
83
- enlarged_content_type(supplied_content_type).any? do |item|
84
- enlarged_content_type(content_type_from_analyzer).include?(item)
85
- end
86
- end
87
-
88
- def enlarged_content_type(content_type)
89
- [content_type, *parent_content_types(content_type)].compact.uniq
90
- end
91
-
92
- def parent_content_types(content_type)
93
- Marcel::TYPE_PARENTS[content_type] || []
94
- end
95
- end
96
- end