active_storage_validations 1.4.0 → 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
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