active_storage_validations 1.2.0 → 1.3.0
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 +30 -17
- data/config/locales/da.yml +1 -1
- data/config/locales/de.yml +1 -0
- data/config/locales/en.yml +1 -0
- data/config/locales/es.yml +1 -0
- data/config/locales/fr.yml +1 -0
- data/config/locales/it.yml +1 -0
- data/config/locales/ja.yml +1 -0
- data/config/locales/nl.yml +1 -0
- data/config/locales/pl.yml +1 -0
- data/config/locales/pt-BR.yml +1 -0
- data/config/locales/ru.yml +1 -0
- data/config/locales/sv.yml +1 -0
- data/config/locales/tr.yml +1 -0
- data/config/locales/uk.yml +1 -0
- data/config/locales/vi.yml +1 -0
- data/config/locales/zh-CN.yml +1 -0
- data/lib/active_storage_validations/aspect_ratio_validator.rb +10 -34
- data/lib/active_storage_validations/attached_validator.rb +5 -3
- data/lib/active_storage_validations/base_size_validator.rb +3 -1
- data/lib/active_storage_validations/concerns/active_storageable.rb +28 -0
- data/lib/active_storage_validations/concerns/errorable.rb +2 -3
- data/lib/active_storage_validations/concerns/loggable.rb +9 -0
- data/lib/active_storage_validations/concerns/metadatable.rb +31 -0
- data/lib/active_storage_validations/content_type_spoof_detector.rb +130 -0
- data/lib/active_storage_validations/content_type_validator.rb +56 -22
- data/lib/active_storage_validations/dimension_validator.rb +31 -52
- data/lib/active_storage_validations/limit_validator.rb +5 -3
- data/lib/active_storage_validations/marcel_extensor.rb +5 -0
- data/lib/active_storage_validations/matchers/concerns/attachable.rb +27 -9
- data/lib/active_storage_validations/matchers/concerns/messageable.rb +1 -1
- data/lib/active_storage_validations/matchers/content_type_validator_matcher.rb +7 -0
- data/lib/active_storage_validations/matchers/limit_validator_matcher.rb +127 -0
- data/lib/active_storage_validations/matchers/total_size_validator_matcher.rb +1 -10
- data/lib/active_storage_validations/matchers.rb +4 -15
- data/lib/active_storage_validations/metadata.rb +7 -10
- data/lib/active_storage_validations/processable_image_validator.rb +17 -32
- data/lib/active_storage_validations/size_validator.rb +2 -6
- data/lib/active_storage_validations/total_size_validator.rb +3 -7
- data/lib/active_storage_validations/version.rb +1 -1
- data/lib/active_storage_validations.rb +2 -0
- metadata +18 -12
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'concerns/active_storageable.rb'
|
|
3
4
|
require_relative 'concerns/errorable.rb'
|
|
4
5
|
require_relative 'concerns/symbolizable.rb'
|
|
6
|
+
require_relative 'content_type_spoof_detector.rb'
|
|
5
7
|
|
|
6
8
|
module ActiveStorageValidations
|
|
7
9
|
class ContentTypeValidator < ActiveModel::EachValidator # :nodoc:
|
|
10
|
+
include ActiveStorageable
|
|
8
11
|
include OptionProcUnfolding
|
|
9
12
|
include Errorable
|
|
10
13
|
include Symbolizable
|
|
11
14
|
|
|
12
15
|
AVAILABLE_CHECKS = %i[with in].freeze
|
|
13
|
-
ERROR_TYPES = %i[
|
|
16
|
+
ERROR_TYPES = %i[
|
|
17
|
+
content_type_invalid
|
|
18
|
+
spoofed_content_type
|
|
19
|
+
].freeze
|
|
14
20
|
|
|
15
21
|
def check_validity!
|
|
16
22
|
ensure_exactly_one_validator_option
|
|
@@ -18,31 +24,24 @@ module ActiveStorageValidations
|
|
|
18
24
|
end
|
|
19
25
|
|
|
20
26
|
def validate_each(record, attribute, _value)
|
|
21
|
-
return
|
|
27
|
+
return if no_attachments?(record, attribute)
|
|
22
28
|
|
|
23
29
|
types = authorized_types(record)
|
|
24
|
-
return
|
|
30
|
+
return if types.empty?
|
|
25
31
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
files.each do |file|
|
|
29
|
-
next if is_valid?(file, types)
|
|
30
|
-
|
|
31
|
-
errors_options = initialize_error_options(options, file)
|
|
32
|
-
errors_options[:authorized_types] = types_to_human_format(types)
|
|
33
|
-
errors_options[:content_type] = content_type(file)
|
|
34
|
-
add_error(record, attribute, ERROR_TYPES.first, **errors_options)
|
|
35
|
-
break
|
|
32
|
+
attached_files(record, attribute).each do |file|
|
|
33
|
+
is_valid?(record, attribute, file, types)
|
|
36
34
|
end
|
|
37
35
|
end
|
|
38
36
|
|
|
37
|
+
private
|
|
38
|
+
|
|
39
39
|
def authorized_types(record)
|
|
40
40
|
flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS)
|
|
41
41
|
(Array.wrap(flat_options[:with]) + Array.wrap(flat_options[:in])).compact.map do |type|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
Marcel::MimeType.for(declared_type: type.to_s, extension: type.to_s)
|
|
42
|
+
case type
|
|
43
|
+
when String, Symbol then Marcel::MimeType.for(declared_type: type.to_s, extension: type.to_s)
|
|
44
|
+
when Regexp then type
|
|
46
45
|
end
|
|
47
46
|
end
|
|
48
47
|
end
|
|
@@ -54,13 +53,44 @@ module ActiveStorageValidations
|
|
|
54
53
|
end
|
|
55
54
|
|
|
56
55
|
def content_type(file)
|
|
57
|
-
|
|
56
|
+
# We remove potential mime type parameters
|
|
57
|
+
file.blob.present? && file.blob.content_type.downcase.split(/[;,\s]/, 2).first
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def is_valid?(record, attribute, file, types)
|
|
61
|
+
file_type_in_authorized_types?(record, attribute, file, types) &&
|
|
62
|
+
not_spoofing_content_type?(record, attribute, file)
|
|
58
63
|
end
|
|
59
64
|
|
|
60
|
-
def
|
|
65
|
+
def file_type_in_authorized_types?(record, attribute, file, types)
|
|
61
66
|
file_type = content_type(file)
|
|
62
|
-
types.any? do |type|
|
|
63
|
-
|
|
67
|
+
file_type_is_authorized = types.any? do |type|
|
|
68
|
+
case type
|
|
69
|
+
when String then type == file_type
|
|
70
|
+
when Regexp then type.match?(file_type.to_s)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
if file_type_is_authorized
|
|
75
|
+
true
|
|
76
|
+
else
|
|
77
|
+
errors_options = initialize_error_options(options, file)
|
|
78
|
+
errors_options[:authorized_types] = types_to_human_format(types)
|
|
79
|
+
errors_options[:content_type] = content_type(file)
|
|
80
|
+
add_error(record, attribute, ERROR_TYPES.first, **errors_options)
|
|
81
|
+
false
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def not_spoofing_content_type?(record, attribute, file)
|
|
86
|
+
return true unless enable_spoofing_protection?
|
|
87
|
+
|
|
88
|
+
if ContentTypeSpoofDetector.new(record, attribute, file).spoofed?
|
|
89
|
+
errors_options = initialize_error_options(options, file)
|
|
90
|
+
add_error(record, attribute, ERROR_TYPES.second, **errors_options)
|
|
91
|
+
false
|
|
92
|
+
else
|
|
93
|
+
true
|
|
64
94
|
end
|
|
65
95
|
end
|
|
66
96
|
|
|
@@ -81,7 +111,7 @@ module ActiveStorageValidations
|
|
|
81
111
|
def invalid_content_type_message(content_type)
|
|
82
112
|
<<~ERROR_MESSAGE
|
|
83
113
|
You must pass valid content types to the validator
|
|
84
|
-
'#{content_type}' is not
|
|
114
|
+
'#{content_type}' is not found in Marcel::EXTENSIONS mimes
|
|
85
115
|
ERROR_MESSAGE
|
|
86
116
|
end
|
|
87
117
|
|
|
@@ -93,5 +123,9 @@ module ActiveStorageValidations
|
|
|
93
123
|
false # We always validate regexes
|
|
94
124
|
end
|
|
95
125
|
end
|
|
126
|
+
|
|
127
|
+
def enable_spoofing_protection?
|
|
128
|
+
options[:spoofing_protection] == true
|
|
129
|
+
end
|
|
96
130
|
end
|
|
97
131
|
end
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'concerns/active_storageable.rb'
|
|
3
4
|
require_relative 'concerns/errorable.rb'
|
|
5
|
+
require_relative 'concerns/metadatable.rb'
|
|
4
6
|
require_relative 'concerns/symbolizable.rb'
|
|
5
|
-
require_relative 'metadata.rb'
|
|
6
7
|
|
|
7
8
|
module ActiveStorageValidations
|
|
8
9
|
class DimensionValidator < ActiveModel::EachValidator # :nodoc
|
|
9
|
-
include
|
|
10
|
+
include ActiveStorageable
|
|
10
11
|
include Errorable
|
|
12
|
+
include OptionProcUnfolding
|
|
13
|
+
include Metadatable
|
|
11
14
|
include Symbolizable
|
|
12
15
|
|
|
13
16
|
AVAILABLE_CHECKS = %i[width height min max].freeze
|
|
@@ -25,65 +28,19 @@ module ActiveStorageValidations
|
|
|
25
28
|
dimension_height_equal_to
|
|
26
29
|
].freeze
|
|
27
30
|
|
|
28
|
-
def process_options(record)
|
|
29
|
-
flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS)
|
|
30
|
-
|
|
31
|
-
[:width, :height].each do |length|
|
|
32
|
-
if flat_options[length] and flat_options[length].is_a?(Hash)
|
|
33
|
-
if (range = flat_options[length][:in])
|
|
34
|
-
raise ArgumentError, ":in must be a Range" unless range.is_a?(Range)
|
|
35
|
-
flat_options[length][:min], flat_options[length][:max] = range.min, range.max
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
[:min, :max].each do |dim|
|
|
40
|
-
if (range = flat_options[dim])
|
|
41
|
-
raise ArgumentError, ":#{dim} must be a Range (width..height)" unless range.is_a?(Range)
|
|
42
|
-
flat_options[:width] = { dim => range.first }
|
|
43
|
-
flat_options[:height] = { dim => range.last }
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
flat_options
|
|
48
|
-
end
|
|
49
|
-
|
|
50
31
|
def check_validity!
|
|
51
32
|
unless AVAILABLE_CHECKS.any? { |argument| options.key?(argument) }
|
|
52
33
|
raise ArgumentError, 'You must pass either :width, :height, :min or :max to the validator'
|
|
53
34
|
end
|
|
54
35
|
end
|
|
55
36
|
|
|
37
|
+
def validate_each(record, attribute, _value)
|
|
38
|
+
return if no_attachments?(record, attribute)
|
|
56
39
|
|
|
57
|
-
|
|
58
|
-
def validate_each(record, attribute, _value)
|
|
59
|
-
return true unless record.send(attribute).attached?
|
|
60
|
-
|
|
61
|
-
changes = record.attachment_changes[attribute.to_s]
|
|
62
|
-
return true if changes.blank?
|
|
63
|
-
|
|
64
|
-
files = Array.wrap(changes.is_a?(ActiveStorage::Attached::Changes::CreateMany) ? changes.attachables : changes.attachable)
|
|
65
|
-
files.each do |file|
|
|
66
|
-
metadata = Metadata.new(file).metadata
|
|
67
|
-
next if is_valid?(record, attribute, file, metadata)
|
|
68
|
-
break
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
else
|
|
72
|
-
# Rails 5
|
|
73
|
-
def validate_each(record, attribute, _value)
|
|
74
|
-
return true unless record.send(attribute).attached?
|
|
75
|
-
|
|
76
|
-
files = Array.wrap(record.send(attribute))
|
|
77
|
-
files.each do |file|
|
|
78
|
-
# Analyze file first if not analyzed to get all required metadata.
|
|
79
|
-
file.analyze; file.reload unless file.analyzed?
|
|
80
|
-
metadata = file.metadata rescue {}
|
|
81
|
-
next if is_valid?(record, attribute, file, metadata)
|
|
82
|
-
break
|
|
83
|
-
end
|
|
84
|
-
end
|
|
40
|
+
validate_changed_files_from_metadata(record, attribute)
|
|
85
41
|
end
|
|
86
42
|
|
|
43
|
+
private
|
|
87
44
|
|
|
88
45
|
def is_valid?(record, attribute, file, metadata)
|
|
89
46
|
flat_options = process_options(record)
|
|
@@ -163,5 +120,27 @@ module ActiveStorageValidations
|
|
|
163
120
|
|
|
164
121
|
true # valid file
|
|
165
122
|
end
|
|
123
|
+
|
|
124
|
+
def process_options(record)
|
|
125
|
+
flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS)
|
|
126
|
+
|
|
127
|
+
[:width, :height].each do |length|
|
|
128
|
+
if flat_options[length] and flat_options[length].is_a?(Hash)
|
|
129
|
+
if (range = flat_options[length][:in])
|
|
130
|
+
raise ArgumentError, ":in must be a Range" unless range.is_a?(Range)
|
|
131
|
+
flat_options[length][:min], flat_options[length][:max] = range.min, range.max
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
[:min, :max].each do |dim|
|
|
136
|
+
if (range = flat_options[dim])
|
|
137
|
+
raise ArgumentError, ":#{dim} must be a Range (width..height)" unless range.is_a?(Range)
|
|
138
|
+
flat_options[:width] = { dim => range.first }
|
|
139
|
+
flat_options[:height] = { dim => range.last }
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
flat_options
|
|
144
|
+
end
|
|
166
145
|
end
|
|
167
146
|
end
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'concerns/active_storageable.rb'
|
|
3
4
|
require_relative 'concerns/errorable.rb'
|
|
4
5
|
require_relative 'concerns/symbolizable.rb'
|
|
5
6
|
|
|
6
7
|
module ActiveStorageValidations
|
|
7
8
|
class LimitValidator < ActiveModel::EachValidator # :nodoc:
|
|
9
|
+
include ActiveStorageable
|
|
8
10
|
include OptionProcUnfolding
|
|
9
11
|
include Errorable
|
|
10
12
|
include Symbolizable
|
|
@@ -19,11 +21,11 @@ module ActiveStorageValidations
|
|
|
19
21
|
ensure_arguments_validity
|
|
20
22
|
end
|
|
21
23
|
|
|
22
|
-
def validate_each(record, attribute,
|
|
23
|
-
files =
|
|
24
|
+
def validate_each(record, attribute, _value)
|
|
25
|
+
files = attached_files(record, attribute).reject(&:blank?)
|
|
24
26
|
flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS)
|
|
25
27
|
|
|
26
|
-
return
|
|
28
|
+
return if files_count_valid?(files.count, flat_options)
|
|
27
29
|
|
|
28
30
|
errors_options = initialize_error_options(options)
|
|
29
31
|
errors_options[:min] = flat_options[:min]
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
Marcel::MimeType.extend "application/x-rar-compressed", parents: %(application/x-rar)
|
|
2
|
+
Marcel::MimeType.extend "audio/x-hx-aac-adts", parents: %(audio/x-aac)
|
|
3
|
+
Marcel::MimeType.extend "audio/x-m4a", parents: %(audio/mp4)
|
|
4
|
+
Marcel::MimeType.extend "text/xml", parents: %(application/xml) # alias
|
|
5
|
+
Marcel::MimeType.extend "video/theora", parents: %(video/ogg)
|
|
@@ -8,6 +8,27 @@ module ActiveStorageValidations
|
|
|
8
8
|
@subject.public_send(@attribute_name)
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
+
def attach_files(count)
|
|
12
|
+
return unless count.positive?
|
|
13
|
+
|
|
14
|
+
file_array = Array.new(count, dummy_file)
|
|
15
|
+
|
|
16
|
+
@subject.public_send(@attribute_name).attach(file_array)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def detach_file
|
|
20
|
+
@subject.attachment_changes.delete(@attribute_name.to_s)
|
|
21
|
+
end
|
|
22
|
+
alias :detach_files :detach_file
|
|
23
|
+
|
|
24
|
+
def file_attached?
|
|
25
|
+
@subject.public_send(@attribute_name).attached?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def dummy_blob
|
|
29
|
+
ActiveStorage::Blob.create_and_upload!(**dummy_file)
|
|
30
|
+
end
|
|
31
|
+
|
|
11
32
|
def dummy_file
|
|
12
33
|
{
|
|
13
34
|
io: io,
|
|
@@ -18,8 +39,8 @@ module ActiveStorageValidations
|
|
|
18
39
|
|
|
19
40
|
def processable_image
|
|
20
41
|
{
|
|
21
|
-
io:
|
|
22
|
-
filename: '
|
|
42
|
+
io: StringIO.new(image_data),
|
|
43
|
+
filename: 'processable_image.png',
|
|
23
44
|
content_type: 'image/png'
|
|
24
45
|
}
|
|
25
46
|
end
|
|
@@ -27,7 +48,7 @@ module ActiveStorageValidations
|
|
|
27
48
|
def not_processable_image
|
|
28
49
|
{
|
|
29
50
|
io: Tempfile.new('.'),
|
|
30
|
-
filename: '
|
|
51
|
+
filename: 'not_processable_image.txt',
|
|
31
52
|
content_type: 'text/plain'
|
|
32
53
|
}
|
|
33
54
|
end
|
|
@@ -36,12 +57,9 @@ module ActiveStorageValidations
|
|
|
36
57
|
@io ||= Tempfile.new('Hello world!')
|
|
37
58
|
end
|
|
38
59
|
|
|
39
|
-
def
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def file_attached?
|
|
44
|
-
@subject.public_send(@attribute_name).attached?
|
|
60
|
+
def image_data
|
|
61
|
+
# Binary data for a 1x1 transparent PNG image
|
|
62
|
+
"\x89PNG\r\n\x1A\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1F\x15\xC4\x89\x00\x00\x00\nIDATx\x9Cc\x00\x01\x00\x00\x05\x00\x01\r\n\x2D\xB4\x00\x00\x00\x00IEND\xAE\x42\x60\x82"
|
|
45
63
|
end
|
|
46
64
|
end
|
|
47
65
|
end
|
|
@@ -130,6 +130,13 @@ module ActiveStorageValidations
|
|
|
130
130
|
content_type: type
|
|
131
131
|
}
|
|
132
132
|
end
|
|
133
|
+
|
|
134
|
+
# Due to the way we build test attachments in #attachment_for
|
|
135
|
+
# (ie spoofed file basically), we need to ignore the error related to
|
|
136
|
+
# content type spoofing in our matcher to pass the tests
|
|
137
|
+
def validator_errors_for_attribute
|
|
138
|
+
super.reject { |hash| hash[:error] == :spoofed_content_type }
|
|
139
|
+
end
|
|
133
140
|
end
|
|
134
141
|
end
|
|
135
142
|
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'concerns/active_storageable'
|
|
4
|
+
require_relative 'concerns/allow_blankable'
|
|
5
|
+
require_relative 'concerns/attachable'
|
|
6
|
+
require_relative 'concerns/contextable'
|
|
7
|
+
require_relative 'concerns/messageable'
|
|
8
|
+
require_relative 'concerns/rspecable'
|
|
9
|
+
require_relative 'concerns/validatable'
|
|
10
|
+
|
|
11
|
+
module ActiveStorageValidations
|
|
12
|
+
module Matchers
|
|
13
|
+
def validate_limits_of(name)
|
|
14
|
+
LimitValidatorMatcher.new(name)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class LimitValidatorMatcher
|
|
18
|
+
include ActiveStorageable
|
|
19
|
+
include AllowBlankable
|
|
20
|
+
include Attachable
|
|
21
|
+
include Contextable
|
|
22
|
+
include Messageable
|
|
23
|
+
include Rspecable
|
|
24
|
+
include Validatable
|
|
25
|
+
|
|
26
|
+
def initialize(attribute_name)
|
|
27
|
+
initialize_allow_blankable
|
|
28
|
+
initialize_contextable
|
|
29
|
+
initialize_messageable
|
|
30
|
+
initialize_rspecable
|
|
31
|
+
@attribute_name = attribute_name
|
|
32
|
+
@min = @max = nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def description
|
|
36
|
+
"validate the limit files of :#{@attribute_name}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def failure_message
|
|
40
|
+
message = ["is expected to validate limit file of :#{@attribute_name}"]
|
|
41
|
+
build_failure_message(message)
|
|
42
|
+
message.join("\n")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def min(count)
|
|
46
|
+
@min = count
|
|
47
|
+
self
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def max(count)
|
|
51
|
+
@max = count
|
|
52
|
+
self
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def matches?(subject)
|
|
56
|
+
@subject = subject.is_a?(Class) ? subject.new : subject
|
|
57
|
+
|
|
58
|
+
is_a_valid_active_storage_attribute? &&
|
|
59
|
+
is_context_valid? &&
|
|
60
|
+
is_custom_message_valid? &&
|
|
61
|
+
file_count_not_smaller_than_min? &&
|
|
62
|
+
file_count_equal_min? &&
|
|
63
|
+
file_count_larger_than_min? &&
|
|
64
|
+
file_count_smaller_than_max? &&
|
|
65
|
+
file_count_equal_max? &&
|
|
66
|
+
file_count_not_larger_than_max?
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def build_failure_message(message)
|
|
72
|
+
return unless @failure_message_artefacts.present?
|
|
73
|
+
|
|
74
|
+
message << " but there seem to have issues with the matcher methods you used, since:"
|
|
75
|
+
@failure_message_artefacts.each do |error_case|
|
|
76
|
+
message << " validation failed when provided with #{error_case[:count]} file(s)"
|
|
77
|
+
end
|
|
78
|
+
message << " whereas it should have passed"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def file_count_not_smaller_than_min?
|
|
82
|
+
@min.nil? || @min.zero? || !passes_validation_with_limits(@min - 1)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def file_count_equal_min?
|
|
86
|
+
@min.nil? || @min.zero? || passes_validation_with_limits(@min)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def file_count_larger_than_min?
|
|
90
|
+
@min.nil? || @min.zero? || @min == @max || passes_validation_with_limits(@min + 1)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def file_count_smaller_than_max?
|
|
94
|
+
@max.nil? || @min == @max || passes_validation_with_limits(@max - 1)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def file_count_equal_max?
|
|
98
|
+
@max.nil? || passes_validation_with_limits(@max)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def file_count_not_larger_than_max?
|
|
102
|
+
@max.nil? || !passes_validation_with_limits(@max + 1)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def passes_validation_with_limits(count)
|
|
106
|
+
attach_files(count)
|
|
107
|
+
validate
|
|
108
|
+
detach_files
|
|
109
|
+
is_valid? || add_failure_message_artefact(count)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def is_custom_message_valid?
|
|
113
|
+
return true if !@custom_message || (@min&.zero? && @max.nil?)
|
|
114
|
+
|
|
115
|
+
@min.nil? ? attach_files(@max + 1) : attach_files(@min - 1)
|
|
116
|
+
validate
|
|
117
|
+
detach_files
|
|
118
|
+
has_an_error_message_which_is_custom_message?
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def add_failure_message_artefact(count)
|
|
122
|
+
@failure_message_artefacts << { count: count }
|
|
123
|
+
false
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -22,19 +22,10 @@ module ActiveStorageValidations
|
|
|
22
22
|
protected
|
|
23
23
|
|
|
24
24
|
def attach_file
|
|
25
|
-
#
|
|
25
|
+
# has_many_attached relation
|
|
26
26
|
@subject.public_send(@attribute_name).attach([dummy_blob])
|
|
27
27
|
@subject.public_send(@attribute_name)
|
|
28
28
|
end
|
|
29
|
-
|
|
30
|
-
def dummy_blob
|
|
31
|
-
ActiveStorage::Blob.create_and_upload!(
|
|
32
|
-
io: io,
|
|
33
|
-
filename: 'test.png',
|
|
34
|
-
content_type: 'image/png',
|
|
35
|
-
service_name: 'test'
|
|
36
|
-
)
|
|
37
|
-
end
|
|
38
29
|
end
|
|
39
30
|
end
|
|
40
31
|
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require 'active_storage_validations/matchers/aspect_ratio_validator_matcher'
|
|
4
4
|
require 'active_storage_validations/matchers/attached_validator_matcher'
|
|
5
5
|
require 'active_storage_validations/matchers/processable_image_validator_matcher'
|
|
6
|
+
require 'active_storage_validations/matchers/limit_validator_matcher'
|
|
6
7
|
require 'active_storage_validations/matchers/content_type_validator_matcher'
|
|
7
8
|
require 'active_storage_validations/matchers/dimension_validator_matcher'
|
|
8
9
|
require 'active_storage_validations/matchers/size_validator_matcher'
|
|
@@ -25,21 +26,9 @@ module ActiveStorageValidations
|
|
|
25
26
|
end
|
|
26
27
|
|
|
27
28
|
def self.mock_metadata(attachment, width, height)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
stub_method(ActiveStorageValidations::Metadata, :new, mock) do
|
|
32
|
-
yield
|
|
33
|
-
end
|
|
34
|
-
else
|
|
35
|
-
# Stub the metadata analysis for rails 5
|
|
36
|
-
stub_method(attachment, :analyze, true) do
|
|
37
|
-
stub_method(attachment, :analyzed?, true) do
|
|
38
|
-
stub_method(attachment, :metadata, { width: width, height: height }) do
|
|
39
|
-
yield
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
end
|
|
29
|
+
mock = Struct.new(:metadata).new({ width: width, height: height })
|
|
30
|
+
stub_method(ActiveStorageValidations::Metadata, :new, mock) do
|
|
31
|
+
yield
|
|
43
32
|
end
|
|
44
33
|
end
|
|
45
34
|
end
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'concerns/loggable'
|
|
4
|
+
|
|
1
5
|
module ActiveStorageValidations
|
|
2
6
|
class Metadata
|
|
7
|
+
include Loggable
|
|
8
|
+
|
|
3
9
|
class InvalidImageError < StandardError; end
|
|
4
10
|
|
|
5
11
|
attr_reader :file
|
|
@@ -62,11 +68,7 @@ module ActiveStorageValidations
|
|
|
62
68
|
if is_string || file.is_a?(ActiveStorage::Blob)
|
|
63
69
|
blob =
|
|
64
70
|
if is_string
|
|
65
|
-
|
|
66
|
-
ActiveStorage::Blob.find_signed(file)
|
|
67
|
-
else
|
|
68
|
-
ActiveStorage::Blob.find_signed!(file)
|
|
69
|
-
end
|
|
71
|
+
ActiveStorage::Blob.find_signed!(file)
|
|
70
72
|
else
|
|
71
73
|
file
|
|
72
74
|
end
|
|
@@ -174,10 +176,5 @@ module ActiveStorageValidations
|
|
|
174
176
|
raise "Something wrong with params."
|
|
175
177
|
end
|
|
176
178
|
end
|
|
177
|
-
|
|
178
|
-
def logger
|
|
179
|
-
Rails.logger
|
|
180
|
-
end
|
|
181
|
-
|
|
182
179
|
end
|
|
183
180
|
end
|
|
@@ -1,49 +1,34 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'concerns/active_storageable.rb'
|
|
3
4
|
require_relative 'concerns/errorable.rb'
|
|
5
|
+
require_relative 'concerns/metadatable.rb'
|
|
4
6
|
require_relative 'concerns/symbolizable.rb'
|
|
5
|
-
require_relative 'metadata.rb'
|
|
6
7
|
|
|
7
8
|
module ActiveStorageValidations
|
|
8
9
|
class ProcessableImageValidator < ActiveModel::EachValidator # :nodoc
|
|
9
|
-
include
|
|
10
|
+
include ActiveStorageable
|
|
10
11
|
include Errorable
|
|
12
|
+
include Metadatable
|
|
11
13
|
include Symbolizable
|
|
12
14
|
|
|
13
15
|
ERROR_TYPES = %i[
|
|
14
16
|
image_not_processable
|
|
15
17
|
].freeze
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
else
|
|
34
|
-
# Rails 5
|
|
35
|
-
def validate_each(record, attribute, _value)
|
|
36
|
-
return true unless record.send(attribute).attached?
|
|
37
|
-
|
|
38
|
-
files = Array.wrap(record.send(attribute))
|
|
39
|
-
|
|
40
|
-
files.each do |file|
|
|
41
|
-
if !Metadata.new(file).valid?
|
|
42
|
-
errors_options = initialize_error_options(options, file)
|
|
43
|
-
add_error(record, attribute, ERROR_TYPES.first , **errors_options) unless Metadata.new(file).valid?
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
end
|
|
19
|
+
def validate_each(record, attribute, _value)
|
|
20
|
+
return if no_attachments?(record, attribute)
|
|
21
|
+
|
|
22
|
+
validate_changed_files_from_metadata(record, attribute)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def is_valid?(record, attribute, attachable, metadata)
|
|
28
|
+
return if !metadata.empty?
|
|
29
|
+
|
|
30
|
+
errors_options = initialize_error_options(options, attachable)
|
|
31
|
+
add_error(record, attribute, ERROR_TYPES.first , **errors_options)
|
|
47
32
|
end
|
|
48
33
|
end
|
|
49
34
|
end
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative 'concerns/errorable.rb'
|
|
4
|
-
require_relative 'concerns/symbolizable.rb'
|
|
5
3
|
require_relative 'base_size_validator.rb'
|
|
6
4
|
|
|
7
5
|
module ActiveStorageValidations
|
|
@@ -15,12 +13,11 @@ module ActiveStorageValidations
|
|
|
15
13
|
].freeze
|
|
16
14
|
|
|
17
15
|
def validate_each(record, attribute, _value)
|
|
18
|
-
return
|
|
16
|
+
return if no_attachments?(record, attribute)
|
|
19
17
|
|
|
20
|
-
files = Array.wrap(record.send(attribute))
|
|
21
18
|
flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS)
|
|
22
19
|
|
|
23
|
-
|
|
20
|
+
attached_files(record, attribute).each do |file|
|
|
24
21
|
next if is_valid?(file.blob.byte_size, flat_options)
|
|
25
22
|
|
|
26
23
|
errors_options = initialize_error_options(options, file)
|
|
@@ -31,7 +28,6 @@ module ActiveStorageValidations
|
|
|
31
28
|
error_type = "file_size_not_#{keys.first}".to_sym
|
|
32
29
|
|
|
33
30
|
add_error(record, attribute, error_type, **errors_options)
|
|
34
|
-
break
|
|
35
31
|
end
|
|
36
32
|
end
|
|
37
33
|
end
|