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