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.
- checksums.yaml +4 -4
- data/README.md +737 -229
- data/config/locales/da.yml +53 -0
- data/config/locales/de.yml +50 -19
- data/config/locales/en.yml +50 -19
- data/config/locales/es.yml +50 -19
- data/config/locales/fr.yml +50 -19
- data/config/locales/it.yml +50 -19
- data/config/locales/ja.yml +50 -19
- data/config/locales/nl.yml +50 -19
- data/config/locales/pl.yml +50 -19
- data/config/locales/pt-BR.yml +50 -19
- data/config/locales/ru.yml +50 -19
- data/config/locales/sv.yml +53 -0
- data/config/locales/tr.yml +50 -19
- data/config/locales/uk.yml +50 -19
- data/config/locales/vi.yml +50 -19
- data/config/locales/zh-CN.yml +53 -0
- data/lib/active_storage_validations/analyzer/audio_analyzer.rb +58 -0
- data/lib/active_storage_validations/analyzer/content_type_analyzer.rb +60 -0
- data/lib/active_storage_validations/analyzer/image_analyzer/image_magick.rb +47 -0
- data/lib/active_storage_validations/analyzer/image_analyzer/vips.rb +57 -0
- data/lib/active_storage_validations/analyzer/image_analyzer.rb +49 -0
- data/lib/active_storage_validations/analyzer/null_analyzer.rb +18 -0
- data/lib/active_storage_validations/analyzer/shared/asv_ff_probable.rb +61 -0
- data/lib/active_storage_validations/analyzer/video_analyzer.rb +130 -0
- data/lib/active_storage_validations/analyzer.rb +87 -0
- data/lib/active_storage_validations/aspect_ratio_validator.rb +154 -99
- data/lib/active_storage_validations/attached_validator.rb +22 -5
- data/lib/active_storage_validations/base_comparison_validator.rb +71 -0
- data/lib/active_storage_validations/content_type_validator.rb +206 -25
- data/lib/active_storage_validations/dimension_validator.rb +105 -82
- data/lib/active_storage_validations/duration_validator.rb +55 -0
- data/lib/active_storage_validations/extensors/asv_blob_metadatable.rb +49 -0
- data/lib/active_storage_validations/extensors/asv_marcelable.rb +12 -0
- data/lib/active_storage_validations/limit_validator.rb +75 -16
- data/lib/active_storage_validations/matchers/aspect_ratio_validator_matcher.rb +119 -0
- data/lib/active_storage_validations/matchers/attached_validator_matcher.rb +48 -25
- data/lib/active_storage_validations/matchers/base_comparison_validator_matcher.rb +140 -0
- data/lib/active_storage_validations/matchers/content_type_validator_matcher.rb +94 -59
- data/lib/active_storage_validations/matchers/dimension_validator_matcher.rb +97 -55
- data/lib/active_storage_validations/matchers/duration_validator_matcher.rb +39 -0
- data/lib/active_storage_validations/matchers/limit_validator_matcher.rb +127 -0
- data/lib/active_storage_validations/matchers/processable_file_validator_matcher.rb +78 -0
- data/lib/active_storage_validations/matchers/shared/asv_active_storageable.rb +19 -0
- data/lib/active_storage_validations/matchers/shared/asv_allow_blankable.rb +28 -0
- data/lib/active_storage_validations/matchers/shared/asv_attachable.rb +72 -0
- data/lib/active_storage_validations/matchers/shared/asv_contextable.rb +49 -0
- data/lib/active_storage_validations/matchers/shared/asv_messageable.rb +28 -0
- data/lib/active_storage_validations/matchers/shared/asv_rspecable.rb +27 -0
- data/lib/active_storage_validations/matchers/shared/asv_validatable.rb +56 -0
- data/lib/active_storage_validations/matchers/size_validator_matcher.rb +17 -71
- data/lib/active_storage_validations/matchers/total_size_validator_matcher.rb +47 -0
- data/lib/active_storage_validations/matchers.rb +11 -16
- data/lib/active_storage_validations/processable_file_validator.rb +37 -0
- data/lib/active_storage_validations/railtie.rb +11 -0
- data/lib/active_storage_validations/shared/asv_active_storageable.rb +30 -0
- data/lib/active_storage_validations/shared/asv_analyzable.rb +80 -0
- data/lib/active_storage_validations/shared/asv_attachable.rb +204 -0
- data/lib/active_storage_validations/shared/asv_errorable.rb +40 -0
- data/lib/active_storage_validations/shared/asv_loggable.rb +11 -0
- data/lib/active_storage_validations/shared/asv_optionable.rb +29 -0
- data/lib/active_storage_validations/shared/asv_symbolizable.rb +14 -0
- data/lib/active_storage_validations/size_validator.rb +24 -40
- data/lib/active_storage_validations/total_size_validator.rb +51 -0
- data/lib/active_storage_validations/version.rb +1 -1
- data/lib/active_storage_validations.rb +20 -6
- metadata +127 -21
- 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
|
-
|
4
|
-
|
3
|
+
require_relative 'base_comparison_validator_matcher'
|
4
|
+
|
5
5
|
module ActiveStorageValidations
|
6
6
|
module Matchers
|
7
|
-
def validate_size_of(
|
8
|
-
SizeValidatorMatcher.new(
|
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
|
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
|
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
|
-
|
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
|
76
|
-
|
24
|
+
def failure_message_unit
|
25
|
+
"bytes"
|
77
26
|
end
|
78
27
|
|
79
|
-
def
|
80
|
-
|
28
|
+
def smallest_measurement
|
29
|
+
1.byte
|
81
30
|
end
|
82
31
|
|
83
|
-
def
|
84
|
-
io
|
85
|
-
|
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,
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|