active_storage_validations 0.9.7 → 2.0.2

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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +737 -229
  3. data/config/locales/da.yml +53 -0
  4. data/config/locales/de.yml +50 -19
  5. data/config/locales/en.yml +50 -19
  6. data/config/locales/es.yml +50 -19
  7. data/config/locales/fr.yml +50 -19
  8. data/config/locales/it.yml +50 -19
  9. data/config/locales/ja.yml +50 -19
  10. data/config/locales/nl.yml +50 -19
  11. data/config/locales/pl.yml +50 -19
  12. data/config/locales/pt-BR.yml +50 -19
  13. data/config/locales/ru.yml +50 -19
  14. data/config/locales/sv.yml +53 -0
  15. data/config/locales/tr.yml +50 -19
  16. data/config/locales/uk.yml +50 -19
  17. data/config/locales/vi.yml +50 -19
  18. data/config/locales/zh-CN.yml +53 -0
  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 +47 -0
  22. data/lib/active_storage_validations/analyzer/image_analyzer/vips.rb +57 -0
  23. data/lib/active_storage_validations/analyzer/image_analyzer.rb +49 -0
  24. data/lib/active_storage_validations/analyzer/null_analyzer.rb +18 -0
  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 +87 -0
  28. data/lib/active_storage_validations/aspect_ratio_validator.rb +154 -99
  29. data/lib/active_storage_validations/attached_validator.rb +22 -5
  30. data/lib/active_storage_validations/base_comparison_validator.rb +71 -0
  31. data/lib/active_storage_validations/content_type_validator.rb +206 -25
  32. data/lib/active_storage_validations/dimension_validator.rb +105 -82
  33. data/lib/active_storage_validations/duration_validator.rb +55 -0
  34. data/lib/active_storage_validations/extensors/asv_blob_metadatable.rb +49 -0
  35. data/lib/active_storage_validations/extensors/asv_marcelable.rb +12 -0
  36. data/lib/active_storage_validations/limit_validator.rb +75 -16
  37. data/lib/active_storage_validations/matchers/aspect_ratio_validator_matcher.rb +119 -0
  38. data/lib/active_storage_validations/matchers/attached_validator_matcher.rb +48 -25
  39. data/lib/active_storage_validations/matchers/base_comparison_validator_matcher.rb +140 -0
  40. data/lib/active_storage_validations/matchers/content_type_validator_matcher.rb +94 -59
  41. data/lib/active_storage_validations/matchers/dimension_validator_matcher.rb +97 -55
  42. data/lib/active_storage_validations/matchers/duration_validator_matcher.rb +39 -0
  43. data/lib/active_storage_validations/matchers/limit_validator_matcher.rb +127 -0
  44. data/lib/active_storage_validations/matchers/processable_file_validator_matcher.rb +78 -0
  45. data/lib/active_storage_validations/matchers/shared/asv_active_storageable.rb +19 -0
  46. data/lib/active_storage_validations/matchers/shared/asv_allow_blankable.rb +28 -0
  47. data/lib/active_storage_validations/matchers/shared/asv_attachable.rb +72 -0
  48. data/lib/active_storage_validations/matchers/shared/asv_contextable.rb +49 -0
  49. data/lib/active_storage_validations/matchers/shared/asv_messageable.rb +28 -0
  50. data/lib/active_storage_validations/matchers/shared/asv_rspecable.rb +27 -0
  51. data/lib/active_storage_validations/matchers/shared/asv_validatable.rb +56 -0
  52. data/lib/active_storage_validations/matchers/size_validator_matcher.rb +17 -71
  53. data/lib/active_storage_validations/matchers/total_size_validator_matcher.rb +47 -0
  54. data/lib/active_storage_validations/matchers.rb +11 -16
  55. data/lib/active_storage_validations/processable_file_validator.rb +37 -0
  56. data/lib/active_storage_validations/railtie.rb +11 -0
  57. data/lib/active_storage_validations/shared/asv_active_storageable.rb +30 -0
  58. data/lib/active_storage_validations/shared/asv_analyzable.rb +80 -0
  59. data/lib/active_storage_validations/shared/asv_attachable.rb +204 -0
  60. data/lib/active_storage_validations/shared/asv_errorable.rb +40 -0
  61. data/lib/active_storage_validations/shared/asv_loggable.rb +11 -0
  62. data/lib/active_storage_validations/shared/asv_optionable.rb +29 -0
  63. data/lib/active_storage_validations/shared/asv_symbolizable.rb +14 -0
  64. data/lib/active_storage_validations/size_validator.rb +24 -40
  65. data/lib/active_storage_validations/total_size_validator.rb +51 -0
  66. data/lib/active_storage_validations/version.rb +1 -1
  67. data/lib/active_storage_validations.rb +20 -6
  68. metadata +127 -21
  69. data/lib/active_storage_validations/metadata.rb +0 -123
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/concern'
4
+
5
+ module ActiveStorageValidations
6
+ module Matchers
7
+ module ASVRspecable
8
+ extend ActiveSupport::Concern
9
+
10
+ def initialize_rspecable
11
+ @failure_message_artefacts = []
12
+ end
13
+
14
+ def description
15
+ raise NotImplementedError, "#{self.class} did not define #{__method__}"
16
+ end
17
+
18
+ def failure_message
19
+ raise NotImplementedError, "#{self.class} did not define #{__method__}"
20
+ end
21
+
22
+ def failure_message_when_negated
23
+ failure_message.sub(/is expected to validate/, 'is expected not to validate')
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/concern'
4
+
5
+ module ActiveStorageValidations
6
+ module Matchers
7
+ module ASVValidatable
8
+ extend ActiveSupport::Concern
9
+
10
+ private
11
+
12
+ def validate
13
+ @subject.validate(@context)
14
+ end
15
+
16
+ def validator_errors_for_attribute
17
+ @subject.errors.details[@attribute_name].select do |error|
18
+ error[:validator_type] == validator_class.to_sym
19
+ end
20
+ end
21
+
22
+ def is_valid?
23
+ validator_errors_for_attribute.none? do |error|
24
+ error[:error].in?(available_errors)
25
+ end
26
+ end
27
+
28
+ def available_errors
29
+ [
30
+ *validator_class::ERROR_TYPES,
31
+ *errors_from_custom_messages
32
+ ].compact
33
+ end
34
+
35
+ def validator_class
36
+ self.class.name.gsub(/::Matchers|Matcher/, '').constantize
37
+ end
38
+
39
+ def attribute_validator
40
+ @subject.class.validators_on(@attribute_name).find do |validator|
41
+ validator.class == validator_class
42
+ end
43
+ end
44
+
45
+ def attribute_validators
46
+ @subject.class.validators_on(@attribute_name).select do |validator|
47
+ validator.class == validator_class
48
+ end
49
+ end
50
+
51
+ def errors_from_custom_messages
52
+ attribute_validators.map { |validator| validator.options[:message] }
53
+ end
54
+ end
55
+ end
56
+ end
@@ -1,91 +1,37 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Big thank you to the paperclip validation matchers:
4
- # https://github.com/thoughtbot/paperclip/blob/v6.1.0/lib/paperclip/matchers/validate_attachment_size_matcher.rb
3
+ require_relative 'base_comparison_validator_matcher'
4
+
5
5
  module ActiveStorageValidations
6
6
  module Matchers
7
- def validate_size_of(name)
8
- SizeValidatorMatcher.new(name)
7
+ def validate_size_of(attribute_name)
8
+ SizeValidatorMatcher.new(attribute_name)
9
9
  end
10
10
 
11
- class SizeValidatorMatcher
12
- def initialize(attribute_name)
13
- @attribute_name = attribute_name
14
- @low = @high = nil
15
- end
16
-
11
+ class SizeValidatorMatcher < BaseComparisonValidatorMatcher
17
12
  def description
18
- "validate file size of #{@attribute_name}"
19
- end
20
-
21
- def less_than(size)
22
- @high = size - 1.byte
23
- self
24
- end
25
-
26
- def less_than_or_equal_to(size)
27
- @high = size
28
- self
29
- end
30
-
31
- def greater_than(size)
32
- @low = size + 1.byte
33
- self
34
- end
35
-
36
- def greater_than_or_equal_to(size)
37
- @low = size
38
- self
39
- end
40
-
41
- def between(range)
42
- @low, @high = range.first, range.last
43
- self
44
- end
45
-
46
- def matches?(subject)
47
- @subject = subject.is_a?(Class) ? subject.new : subject
48
- responds_to_methods && lower_than_low? && higher_than_low? && lower_than_high? && higher_than_high?
13
+ "validate file size of :#{@attribute_name}"
49
14
  end
50
15
 
51
16
  def failure_message
52
- "is expected to validate file size of #{@attribute_name} to be between #{@low} and #{@high} bytes"
17
+ message = ["is expected to validate file size of :#{@attribute_name}"]
18
+ build_failure_message(message)
19
+ message.join("\n")
53
20
  end
54
21
 
55
- def failure_message_when_negated
56
- "is expected to not validate file size of #{@attribute_name} to be between #{@low} and #{@high} bytes"
57
- end
58
-
59
- protected
60
-
61
- def responds_to_methods
62
- @subject.respond_to?(@attribute_name) &&
63
- @subject.public_send(@attribute_name).respond_to?(:attach) &&
64
- @subject.public_send(@attribute_name).respond_to?(:detach)
65
- end
66
-
67
- def lower_than_low?
68
- @low.nil? || !passes_validation_with_size(@low - 1)
69
- end
70
-
71
- def higher_than_low?
72
- @low.nil? || passes_validation_with_size(@low + 1)
73
- end
22
+ private
74
23
 
75
- def lower_than_high?
76
- @high.nil? || @high == Float::INFINITY || passes_validation_with_size(@high - 1)
24
+ def failure_message_unit
25
+ "bytes"
77
26
  end
78
27
 
79
- def higher_than_high?
80
- @high.nil? || @high == Float::INFINITY || !passes_validation_with_size(@high + 1)
28
+ def smallest_measurement
29
+ 1.byte
81
30
  end
82
31
 
83
- def passes_validation_with_size(new_size)
84
- io = Tempfile.new('Hello world!')
85
- Matchers.stub_method(io, :size, new_size) do
86
- @subject.public_send(@attribute_name).attach(io: io, filename: 'test.png', content_type: 'image/pg')
87
- @subject.validate
88
- @subject.errors.details[@attribute_name].all? { |error| error[:error] != :file_size_out_of_range }
32
+ def mock_value_for(io, size)
33
+ Matchers.stub_method(io, :size, size) do
34
+ yield
89
35
  end
90
36
  end
91
37
  end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_comparison_validator_matcher'
4
+
5
+ module ActiveStorageValidations
6
+ module Matchers
7
+ def validate_total_size_of(attribute_name)
8
+ TotalSizeValidatorMatcher.new(attribute_name)
9
+ end
10
+
11
+ class TotalSizeValidatorMatcher < BaseComparisonValidatorMatcher
12
+ def description
13
+ "validate total file size of :#{@attribute_name}"
14
+ end
15
+
16
+ def failure_message
17
+ message = ["is expected to validate total file size of :#{@attribute_name}"]
18
+ build_failure_message(message)
19
+ message.join("\n")
20
+ end
21
+
22
+ protected
23
+
24
+ def attach_file
25
+ # has_many_attached relation
26
+ @subject.public_send(@attribute_name).attach([dummy_blob])
27
+ @subject.public_send(@attribute_name)
28
+ end
29
+
30
+ private
31
+
32
+ def failure_message_unit
33
+ "bytes"
34
+ end
35
+
36
+ def smallest_measurement
37
+ 1.byte
38
+ end
39
+
40
+ def mock_value_for(io, size)
41
+ Matchers.stub_method(io, :size, size) do
42
+ yield
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -1,9 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'active_storage_validations/matchers/aspect_ratio_validator_matcher'
3
4
  require 'active_storage_validations/matchers/attached_validator_matcher'
5
+ require 'active_storage_validations/matchers/processable_file_validator_matcher'
6
+ require 'active_storage_validations/matchers/limit_validator_matcher'
4
7
  require 'active_storage_validations/matchers/content_type_validator_matcher'
5
8
  require 'active_storage_validations/matchers/dimension_validator_matcher'
9
+ require 'active_storage_validations/matchers/duration_validator_matcher'
6
10
  require 'active_storage_validations/matchers/size_validator_matcher'
11
+ require 'active_storage_validations/matchers/total_size_validator_matcher'
7
12
 
8
13
  module ActiveStorageValidations
9
14
  module Matchers
@@ -21,22 +26,12 @@ module ActiveStorageValidations
21
26
  end
22
27
  end
23
28
 
24
- def self.mock_metadata(attachment, width, height)
25
- if Rails.gem_version >= Gem::Version.new('6.0.0')
26
- # Mock the Metadata class for rails 6
27
- mock = OpenStruct.new(metadata: { width: width, height: height })
28
- stub_method(ActiveStorageValidations::Metadata, :new, mock) do
29
- yield
30
- end
31
- else
32
- # Stub the metadata analysis for rails 5
33
- stub_method(attachment, :analyze, true) do
34
- stub_method(attachment, :analyzed?, true) do
35
- stub_method(attachment, :metadata, { width: width, height: height }) do
36
- yield
37
- end
38
- end
39
- end
29
+ def self.mock_metadata(attachment, metadata = {})
30
+ asv_metadata_available_keys = { width: nil, height: nil, duration: nil, content_type: nil }
31
+ mock = Struct.new(:metadata).new(asv_metadata_available_keys.merge(metadata)) # ensure all keys are present, and it does not raise while trying to access them
32
+
33
+ stub_method(ActiveStorageValidations::Analyzer, :new, mock) do
34
+ yield
40
35
  end
41
36
  end
42
37
  end
@@ -0,0 +1,37 @@
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_symbolizable'
8
+
9
+ module ActiveStorageValidations
10
+ class ProcessableFileValidator < ActiveModel::EachValidator # :nodoc
11
+ include ASVActiveStorageable
12
+ include ASVAnalyzable
13
+ include ASVAttachable
14
+ include ASVErrorable
15
+ include ASVSymbolizable
16
+
17
+ ERROR_TYPES = %i[
18
+ file_not_processable
19
+ ].freeze
20
+ METADATA_KEYS = %i[].freeze
21
+
22
+ def validate_each(record, attribute, _value)
23
+ return if no_attachments?(record, attribute)
24
+
25
+ validate_changed_files_from_metadata(record, attribute, METADATA_KEYS)
26
+ end
27
+
28
+ private
29
+
30
+ def is_valid?(record, attribute, attachable, metadata)
31
+ return if !metadata.empty?
32
+
33
+ errors_options = initialize_error_options(options, attachable)
34
+ add_error(record, attribute, ERROR_TYPES.first , **errors_options)
35
+ end
36
+ end
37
+ end
@@ -2,5 +2,16 @@
2
2
 
3
3
  module ActiveStorageValidations
4
4
  class Railtie < ::Rails::Railtie
5
+ initializer 'active_storage_validations.configure', after: :load_config_initializers do
6
+ ActiveSupport.on_load(:active_record) do
7
+ send :include, ActiveStorageValidations
8
+ end
9
+ end
10
+
11
+ initializer 'active_storage_validations.extend_active_storage_blob' do
12
+ ActiveSupport.on_load(:active_storage_blob) do
13
+ include(ActiveStorageValidations::ASVBlobMetadatable)
14
+ end
15
+ end
5
16
  end
6
17
  end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorageValidations
4
+ # ActiveStorageValidations::ASVActiveStorageable
5
+ #
6
+ # Validator helper methods to make our code more explicit.
7
+ module ASVActiveStorageable
8
+ extend ActiveSupport::Concern
9
+
10
+ private
11
+
12
+ # Retrieve either an `ActiveStorage::Attached::One` or an
13
+ # `ActiveStorage::Attached::Many` instance depending on attribute definition
14
+ def attached_files(record, attribute)
15
+ Array.wrap(record.send(attribute))
16
+ end
17
+
18
+ def attachments_present?(record, attribute)
19
+ record.send(attribute).attached?
20
+ end
21
+
22
+ def no_attachments?(record, attribute)
23
+ !attachments_present?(record, attribute)
24
+ end
25
+
26
+ def will_have_attachments_after_save?(record, attribute)
27
+ !Array.wrap(record.send(attribute)).all?(&:marked_for_destruction?)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,80 @@
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
+ # 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
38
+ end
39
+
40
+ def metadata_analyzer_for(attachable)
41
+ case attachable_media_type(attachable)
42
+ when "image" then image_analyzer_for(attachable)
43
+ when "video" then video_analyzer_for(attachable)
44
+ when "audio" then audio_analyzer_for(attachable)
45
+ else fallback_analyzer_for(attachable)
46
+ end
47
+ end
48
+
49
+ def image_analyzer_for(attachable)
50
+ case image_processor
51
+ when :mini_magick
52
+ ActiveStorageValidations::Analyzer::ImageAnalyzer::ImageMagick.new(attachable)
53
+ when :vips
54
+ ActiveStorageValidations::Analyzer::ImageAnalyzer::Vips.new(attachable)
55
+ end
56
+ end
57
+
58
+ def image_processor
59
+ # Rails returns nil for default image processor, because it is set in an after initialize callback
60
+ # https://github.com/rails/rails/blob/main/activestorage/lib/active_storage/engine.rb
61
+ ActiveStorage.variant_processor || DEFAULT_IMAGE_PROCESSOR
62
+ end
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
+
72
+ def fallback_analyzer_for(attachable)
73
+ ActiveStorageValidations::Analyzer::NullAnalyzer.new(attachable)
74
+ end
75
+
76
+ def content_type_analyzer_for(attachable)
77
+ ActiveStorageValidations::Analyzer::ContentTypeAnalyzer.new(attachable)
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,204 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorageValidations
4
+ # ActiveStorageValidations::ASVAttachable
5
+ #
6
+ # Validator methods for analyzing attachable.
7
+ #
8
+ # An attachable is a file representation such as ActiveStorage::Blob,
9
+ # ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile, Hash, String,
10
+ # File or Pathname
11
+ module ASVAttachable
12
+ extend ActiveSupport::Concern
13
+
14
+ private
15
+
16
+ # Loop through the newly submitted attachables to validate them. Using
17
+ # attachables is the only way to get the attached file io that is necessary
18
+ # to perform file analyses.
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
+ end
23
+ end
24
+
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
40
+
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
50
+ end
51
+
52
+ # Retrieve the full declared content_type from attachable.
53
+ def full_attachable_content_type(attachable)
54
+ case attachable
55
+ when ActiveStorage::Blob
56
+ attachable.content_type
57
+ when ActionDispatch::Http::UploadedFile
58
+ attachable.content_type
59
+ when Rack::Test::UploadedFile
60
+ attachable.content_type
61
+ when String
62
+ blob = ActiveStorage::Blob.find_signed!(attachable)
63
+ blob.content_type
64
+ when Hash
65
+ attachable[:content_type]
66
+ when File
67
+ supports_file_attachment? ? marcel_content_type_from_filename(attachable) : raise_rails_like_error(attachable)
68
+ when Pathname
69
+ supports_pathname_attachment? ? marcel_content_type_from_filename(attachable) : raise_rails_like_error(attachable)
70
+ else
71
+ raise_rails_like_error(attachable)
72
+ end
73
+ end
74
+
75
+ # Retrieve the declared content_type from attachable without potential mime
76
+ # type parameters (e.g. 'application/x-rar-compressed;version=5')
77
+ def attachable_content_type(attachable)
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
85
+ end
86
+
87
+ # Retrieve the content_type from attachable using the same logic as Rails
88
+ # ActiveStorage::Blob::Identifiable#identify_content_type
89
+ def attachable_content_type_rails_like(attachable)
90
+ Marcel::MimeType.for(
91
+ attachable_io(attachable, max_byte_size: 4.kilobytes),
92
+ name: attachable_filename(attachable).to_s,
93
+ declared_type: full_attachable_content_type(attachable)
94
+ )
95
+ end
96
+
97
+ # Retrieve the media type of the attachable, which is the first part of the
98
+ # content type (or mime type).
99
+ # Possible values are: application/audio/example/font/image/model/text/video
100
+ def attachable_media_type(attachable)
101
+ (full_attachable_content_type(attachable) || marcel_content_type_from_filename(attachable)).split("/").first
102
+ end
103
+
104
+ # Retrieve the io from attachable.
105
+ def attachable_io(attachable, max_byte_size: nil)
106
+ io = case attachable
107
+ when ActiveStorage::Blob
108
+ (max_byte_size && supports_blob_download_chunk?) ? attachable.download_chunk(0...max_byte_size) : attachable.download
109
+ when ActionDispatch::Http::UploadedFile
110
+ max_byte_size ? attachable.read(max_byte_size) : attachable.read
111
+ when Rack::Test::UploadedFile
112
+ max_byte_size ? attachable.read(max_byte_size) : attachable.read
113
+ when String
114
+ blob = ActiveStorage::Blob.find_signed!(attachable)
115
+ (max_byte_size && supports_blob_download_chunk?) ? blob.download_chunk(0...max_byte_size) : blob.download
116
+ when Hash
117
+ max_byte_size ? attachable[:io].read(max_byte_size) : attachable[:io].read
118
+ when File
119
+ raise_rails_like_error(attachable) unless supports_file_attachment?
120
+ max_byte_size ? attachable.read(max_byte_size) : attachable.read
121
+ when Pathname
122
+ raise_rails_like_error(attachable) unless supports_pathname_attachment?
123
+ max_byte_size ? attachable.read(max_byte_size) : attachable.read
124
+ else
125
+ raise_rails_like_error(attachable)
126
+ end
127
+
128
+ rewind_attachable_io(attachable)
129
+ io
130
+ end
131
+
132
+ # Rewind the io attachable.
133
+ def rewind_attachable_io(attachable)
134
+ case attachable
135
+ when ActiveStorage::Blob, String
136
+ # nothing to do
137
+ when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile
138
+ attachable.rewind
139
+ when Hash
140
+ attachable[:io].rewind
141
+ when File
142
+ raise_rails_like_error(attachable) unless supports_file_attachment?
143
+ attachable.rewind
144
+ when Pathname
145
+ raise_rails_like_error(attachable) unless supports_pathname_attachment?
146
+ File.open(attachable) { |f| f.rewind }
147
+ else
148
+ raise_rails_like_error(attachable)
149
+ end
150
+ end
151
+
152
+ # Retrieve the declared filename from attachable.
153
+ def attachable_filename(attachable)
154
+ case attachable
155
+ when ActiveStorage::Blob
156
+ attachable.filename
157
+ when ActionDispatch::Http::UploadedFile
158
+ attachable.original_filename
159
+ when Rack::Test::UploadedFile
160
+ attachable.original_filename
161
+ when String
162
+ blob = ActiveStorage::Blob.find_signed!(attachable)
163
+ blob.filename
164
+ when Hash
165
+ attachable[:filename]
166
+ when File
167
+ supports_file_attachment? ? File.basename(attachable) : raise_rails_like_error(attachable)
168
+ when Pathname
169
+ supports_pathname_attachment? ? File.basename(attachable) : raise_rails_like_error(attachable)
170
+ else
171
+ raise_rails_like_error(attachable)
172
+ end
173
+ end
174
+
175
+ # Raise the same Rails error for not-implemented file representations.
176
+ def raise_rails_like_error(attachable)
177
+ raise(
178
+ ArgumentError,
179
+ "Could not find or build blob: expected attachable, " \
180
+ "got #{attachable.inspect}"
181
+ )
182
+ end
183
+
184
+ # Check if the current Rails version supports File or Pathname attachment
185
+ #
186
+ # https://github.com/rails/rails/blob/7-1-stable/activestorage/CHANGELOG.md#rails-710rc1-september-27-2023
187
+ def supports_file_attachment?
188
+ Rails.gem_version >= Gem::Version.new('7.1.0.rc1')
189
+ end
190
+ alias :supports_pathname_attachment? :supports_file_attachment?
191
+
192
+ # Check if the current Rails version supports ActiveStorage::Blob#download_chunk
193
+ #
194
+ # https://github.com/rails/rails/blob/7-0-stable/activestorage/CHANGELOG.md#rails-700alpha1-september-15-2021
195
+ def supports_blob_download_chunk?
196
+ Rails.gem_version >= Gem::Version.new('7.0.0.alpha1')
197
+ end
198
+
199
+ # Retrieve the content_type from the file name only
200
+ def marcel_content_type_from_filename(attachable)
201
+ Marcel::MimeType.for(name: attachable_filename(attachable).to_s)
202
+ end
203
+ end
204
+ end