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
@@ -1,51 +1,232 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
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_optionable'
|
8
|
+
require_relative 'shared/asv_symbolizable'
|
9
|
+
require_relative 'analyzer/content_type_analyzer'
|
10
|
+
|
3
11
|
module ActiveStorageValidations
|
4
12
|
class ContentTypeValidator < ActiveModel::EachValidator # :nodoc:
|
13
|
+
include ASVActiveStorageable
|
14
|
+
include ASVAnalyzable
|
15
|
+
include ASVAttachable
|
16
|
+
include ASVErrorable
|
17
|
+
include ASVOptionable
|
18
|
+
include ASVSymbolizable
|
19
|
+
|
20
|
+
AVAILABLE_CHECKS = %i[with in].freeze
|
21
|
+
ERROR_TYPES = %i[
|
22
|
+
content_type_invalid
|
23
|
+
content_type_spoofed
|
24
|
+
].freeze
|
25
|
+
METADATA_KEYS = %i[content_type].freeze
|
26
|
+
|
27
|
+
def check_validity!
|
28
|
+
ensure_exactly_one_validator_option
|
29
|
+
ensure_content_types_validity
|
30
|
+
end
|
31
|
+
|
5
32
|
def validate_each(record, attribute, _value)
|
6
|
-
return
|
33
|
+
return if no_attachments?(record, attribute)
|
34
|
+
|
35
|
+
@authorized_content_types = authorized_content_types_from_options(record)
|
36
|
+
return if @authorized_content_types.empty?
|
7
37
|
|
8
|
-
|
38
|
+
attachables_and_blobs(record, attribute).each do |attachable, blob|
|
39
|
+
set_attachable_cached_values(blob)
|
40
|
+
is_valid?(record, attribute, attachable, blob)
|
41
|
+
end
|
42
|
+
end
|
9
43
|
|
10
|
-
|
11
|
-
errors_options[:message] = options[:message] if options[:message].present?
|
44
|
+
private
|
12
45
|
|
13
|
-
|
14
|
-
|
46
|
+
def authorized_content_types_from_options(record)
|
47
|
+
flat_options = set_flat_options(record)
|
15
48
|
|
16
|
-
|
17
|
-
|
18
|
-
|
49
|
+
(Array.wrap(flat_options[:with]) + Array.wrap(flat_options[:in])).compact.map do |type|
|
50
|
+
case type
|
51
|
+
when String, Symbol then Marcel::MimeType.for(declared_type: type.to_s, extension: type.to_s)
|
52
|
+
when Regexp then type
|
53
|
+
end
|
19
54
|
end
|
20
55
|
end
|
21
56
|
|
22
|
-
def
|
23
|
-
|
57
|
+
def set_attachable_cached_values(blob)
|
58
|
+
@attachable_content_type = blob.content_type
|
59
|
+
@attachable_filename = blob.filename.to_s
|
60
|
+
end
|
61
|
+
|
62
|
+
# Check if the provided content_type is authorized and not spoofed against
|
63
|
+
# the file io.
|
64
|
+
def is_valid?(record, attribute, attachable, blob)
|
65
|
+
authorized_content_type?(record, attribute, attachable) &&
|
66
|
+
not_spoofing_content_type?(record, attribute, attachable, blob)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Dead code that we keep here for some time, maybe we will find a solution
|
70
|
+
# to this check later? (November 2024)
|
71
|
+
#
|
72
|
+
# We do not perform any validations against the extension because it is an
|
73
|
+
# unreliable source of truth. For example, a `.csv` file could have its
|
74
|
+
# `text/csv` content_type changed to `application/vnd.ms-excel` because
|
75
|
+
# it had been opened by Excel at some point, making the file extension vs
|
76
|
+
# file content_type check invalid.
|
77
|
+
# def extension_matches_content_type?(record, attribute, attachable)
|
78
|
+
# return true if !@attachable_filename || !@attachable_content_type
|
79
|
+
|
80
|
+
# extension = @attachable_filename.split('.').last
|
81
|
+
# possible_extensions = Marcel::TYPE_EXTS[@attachable_content_type]
|
82
|
+
# return true if possible_extensions && extension.downcase.in?(possible_extensions)
|
24
83
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
84
|
+
# errors_options = initialize_and_populate_error_options(options, attachable)
|
85
|
+
# add_error(record, attribute, ERROR_TYPES.first, **errors_options)
|
86
|
+
# false
|
87
|
+
# end
|
88
|
+
|
89
|
+
def authorized_content_type?(record, attribute, attachable)
|
90
|
+
attachable_content_type_is_authorized = @authorized_content_types.any? do |authorized_content_type|
|
91
|
+
case authorized_content_type
|
92
|
+
when String then authorized_content_type == marcel_attachable_content_type(attachable)
|
93
|
+
when Regexp then authorized_content_type.match?(marcel_attachable_content_type(attachable).to_s)
|
30
94
|
end
|
31
95
|
end
|
96
|
+
|
97
|
+
return true if attachable_content_type_is_authorized
|
98
|
+
|
99
|
+
errors_options = initialize_and_populate_error_options(options, attachable)
|
100
|
+
add_error(record, attribute, ERROR_TYPES.first, **errors_options)
|
101
|
+
false
|
102
|
+
end
|
103
|
+
|
104
|
+
def marcel_attachable_content_type(attachable)
|
105
|
+
Marcel::MimeType.for(declared_type: @attachable_content_type, name: @attachable_filename)
|
106
|
+
end
|
107
|
+
|
108
|
+
def not_spoofing_content_type?(record, attribute, attachable, blob)
|
109
|
+
return true unless enable_spoofing_protection?
|
110
|
+
|
111
|
+
@detected_content_type = metadata_for(blob, attachable, METADATA_KEYS)&.fetch(:content_type, nil)
|
112
|
+
|
113
|
+
if attachable_content_type_vs_detected_content_type_mismatch?
|
114
|
+
errors_options = initialize_and_populate_error_options(options, attachable)
|
115
|
+
errors_options[:detected_content_type] = @detected_content_type
|
116
|
+
errors_options[:detected_human_content_type] = content_type_to_human_format(@detected_content_type)
|
117
|
+
add_error(record, attribute, ERROR_TYPES.second, **errors_options)
|
118
|
+
false
|
119
|
+
else
|
120
|
+
true
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def disable_spoofing_protection?
|
125
|
+
!enable_spoofing_protection?
|
126
|
+
end
|
127
|
+
|
128
|
+
def enable_spoofing_protection?
|
129
|
+
options[:spoofing_protection] == true
|
130
|
+
end
|
131
|
+
|
132
|
+
def attachable_content_type_vs_detected_content_type_mismatch?
|
133
|
+
@attachable_content_type.present? &&
|
134
|
+
!attachable_content_type_intersects_detected_content_type?
|
135
|
+
end
|
136
|
+
|
137
|
+
def attachable_content_type_intersects_detected_content_type?
|
138
|
+
# Ruby intersects? method is only available from 3.1
|
139
|
+
enlarged_content_type(content_type_without_parameters(@attachable_content_type)).any? do |item|
|
140
|
+
enlarged_content_type(content_type_without_parameters(@detected_content_type)).include?(item)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def enlarged_content_type(content_type)
|
145
|
+
[content_type, *parent_content_types(content_type)].compact.uniq
|
32
146
|
end
|
33
147
|
|
34
|
-
def
|
35
|
-
|
36
|
-
|
148
|
+
def parent_content_types(content_type)
|
149
|
+
Marcel::TYPE_PARENTS[content_type] || []
|
150
|
+
end
|
151
|
+
|
152
|
+
def initialize_and_populate_error_options(options, attachable)
|
153
|
+
errors_options = initialize_error_options(options, attachable)
|
154
|
+
errors_options[:content_type] = @attachable_content_type
|
155
|
+
errors_options[:human_content_type] = content_type_to_human_format(@attachable_content_type)
|
156
|
+
errors_options[:authorized_human_content_types] = content_type_to_human_format(@authorized_content_types)
|
157
|
+
errors_options[:count] = @authorized_content_types.size
|
158
|
+
errors_options
|
159
|
+
end
|
160
|
+
|
161
|
+
def content_type_to_human_format(content_type)
|
162
|
+
Array(content_type)
|
163
|
+
.map do |content_type|
|
164
|
+
case content_type
|
165
|
+
when String, Symbol
|
166
|
+
content_type.to_s.match?(/\//) ? Marcel::TYPE_EXTS[content_type.to_s]&.first&.upcase : content_type.upcase
|
167
|
+
when Regexp
|
168
|
+
content_type.source
|
169
|
+
end
|
170
|
+
end
|
171
|
+
.flatten
|
172
|
+
.compact
|
37
173
|
.join(', ')
|
38
174
|
end
|
39
175
|
|
40
|
-
def
|
41
|
-
|
176
|
+
def ensure_exactly_one_validator_option
|
177
|
+
unless AVAILABLE_CHECKS.one? { |argument| options.key?(argument) }
|
178
|
+
raise ArgumentError, 'You must pass either :with or :in to the validator'
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def ensure_content_types_validity
|
183
|
+
return true if options[:with]&.is_a?(Proc) || options[:in]&.is_a?(Proc)
|
184
|
+
|
185
|
+
(Array(options[:with]) + Array(options[:in])).each do |content_type|
|
186
|
+
raise ArgumentError, invalid_content_type_option_message(content_type) if invalid_option?(content_type)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def invalid_content_type_option_message(content_type)
|
191
|
+
if content_type.to_s.match?(/\//)
|
192
|
+
<<~ERROR_MESSAGE
|
193
|
+
You must pass valid content types to the validator
|
194
|
+
'#{content_type}' is not found in Marcel content types (Marcel::TYPE_EXTS + Marcel::MAGIC)
|
195
|
+
ERROR_MESSAGE
|
196
|
+
else
|
197
|
+
<<~ERROR_MESSAGE
|
198
|
+
You must pass valid content types extensions to the validator
|
199
|
+
'#{content_type}' is not found in Marcel::EXTENSIONS
|
200
|
+
ERROR_MESSAGE
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def invalid_option?(content_type)
|
205
|
+
case content_type
|
206
|
+
when String, Symbol
|
207
|
+
content_type.to_s.match?(/\//) ? invalid_content_type?(content_type) : invalid_extension?(content_type)
|
208
|
+
when Regexp
|
209
|
+
false # We always validate regexes
|
210
|
+
end
|
42
211
|
end
|
43
212
|
|
44
|
-
def
|
45
|
-
|
46
|
-
|
47
|
-
type == file_type || (type.is_a?(Regexp) && type.match?(file_type.to_s))
|
213
|
+
def invalid_content_type?(content_type)
|
214
|
+
if content_type == 'image/jpg'
|
215
|
+
raise ArgumentError, "'image/jpg' is not a valid content type, you should use 'image/jpeg' instead"
|
48
216
|
end
|
217
|
+
|
218
|
+
all_available_marcel_content_types.exclude?(content_type.to_s)
|
219
|
+
end
|
220
|
+
|
221
|
+
def all_available_marcel_content_types
|
222
|
+
@all_available_marcel_content_types ||= Marcel::TYPE_EXTS
|
223
|
+
.keys
|
224
|
+
.push(*Marcel::MAGIC.map(&:first))
|
225
|
+
.tap(&:uniq!)
|
226
|
+
end
|
227
|
+
|
228
|
+
def invalid_extension?(content_type)
|
229
|
+
Marcel::MimeType.for(extension: content_type.to_s) == 'application/octet-stream'
|
49
230
|
end
|
50
231
|
end
|
51
232
|
end
|
@@ -1,127 +1,150 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative '
|
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_optionable'
|
8
|
+
require_relative 'shared/asv_symbolizable'
|
4
9
|
|
5
10
|
module ActiveStorageValidations
|
6
11
|
class DimensionValidator < ActiveModel::EachValidator # :nodoc
|
12
|
+
include ASVActiveStorageable
|
13
|
+
include ASVAnalyzable
|
14
|
+
include ASVAttachable
|
15
|
+
include ASVErrorable
|
16
|
+
include ASVOptionable
|
17
|
+
include ASVSymbolizable
|
18
|
+
|
7
19
|
AVAILABLE_CHECKS = %i[width height min max].freeze
|
20
|
+
ERROR_TYPES = %i[
|
21
|
+
dimension_min_not_included_in
|
22
|
+
dimension_max_not_included_in
|
23
|
+
dimension_width_not_included_in
|
24
|
+
dimension_height_not_included_in
|
25
|
+
dimension_width_not_greater_than_or_equal_to
|
26
|
+
dimension_height_not_greater_than_or_equal_to
|
27
|
+
dimension_width_not_less_than_or_equal_to
|
28
|
+
dimension_height_not_less_than_or_equal_to
|
29
|
+
dimension_width_not_equal_to
|
30
|
+
dimension_height_not_equal_to
|
31
|
+
media_metadata_missing
|
32
|
+
].freeze
|
33
|
+
METADATA_KEYS = %i[width height].freeze
|
8
34
|
|
9
|
-
def
|
10
|
-
|
11
|
-
|
12
|
-
if range = options[length][:in]
|
13
|
-
raise ArgumentError, ":in must be a Range" unless range.is_a?(Range)
|
14
|
-
options[length][:min], options[length][:max] = range.min, range.max
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|
18
|
-
[:min, :max].each do |dim|
|
19
|
-
if range = options[dim]
|
20
|
-
raise ArgumentError, ":#{dim} must be a Range (width..height)" unless range.is_a?(Range)
|
21
|
-
options[:width] = { dim => range.first }
|
22
|
-
options[:height] = { dim => range.last }
|
23
|
-
end
|
35
|
+
def check_validity!
|
36
|
+
unless AVAILABLE_CHECKS.any? { |argument| options.key?(argument) }
|
37
|
+
raise ArgumentError, 'You must pass either :width, :height, :min or :max to the validator'
|
24
38
|
end
|
25
|
-
super
|
26
39
|
end
|
27
40
|
|
41
|
+
def validate_each(record, attribute, _value)
|
42
|
+
return if no_attachments?(record, attribute)
|
28
43
|
|
29
|
-
|
30
|
-
return true if AVAILABLE_CHECKS.any? { |argument| options.key?(argument) }
|
31
|
-
raise ArgumentError, 'You must pass either :width, :height, :min or :max to the validator'
|
44
|
+
validate_changed_files_from_metadata(record, attribute, METADATA_KEYS)
|
32
45
|
end
|
33
46
|
|
47
|
+
private
|
34
48
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
changes = record.attachment_changes[attribute.to_s]
|
40
|
-
return true if changes.blank?
|
41
|
-
|
42
|
-
files = Array.wrap(changes.is_a?(ActiveStorage::Attached::Changes::CreateMany) ? changes.attachables : changes.attachable)
|
43
|
-
files.each do |file|
|
44
|
-
metadata = Metadata.new(file).metadata
|
45
|
-
next if is_valid?(record, attribute, metadata)
|
46
|
-
break
|
47
|
-
end
|
48
|
-
end
|
49
|
-
else
|
50
|
-
# Rails 5
|
51
|
-
def validate_each(record, attribute, _value)
|
52
|
-
return true unless record.send(attribute).attached?
|
53
|
-
|
54
|
-
files = Array.wrap(record.send(attribute))
|
55
|
-
files.each do |file|
|
56
|
-
# Analyze file first if not analyzed to get all required metadata.
|
57
|
-
file.analyze; file.reload unless file.analyzed?
|
58
|
-
metadata = file.metadata rescue {}
|
59
|
-
next if is_valid?(record, attribute, metadata)
|
60
|
-
break
|
61
|
-
end
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
49
|
+
def is_valid?(record, attribute, file, metadata)
|
50
|
+
flat_options = process_options(record)
|
51
|
+
errors_options = initialize_error_options(options, file)
|
65
52
|
|
66
|
-
def is_valid?(record, attribute, file_metadata)
|
67
53
|
# Validation fails unless file metadata contains valid width and height.
|
68
|
-
if
|
69
|
-
add_error(record, attribute,
|
54
|
+
if metadata[:width].to_i <= 0 || metadata[:height].to_i <= 0
|
55
|
+
add_error(record, attribute, :media_metadata_missing, **errors_options)
|
70
56
|
return false
|
71
57
|
end
|
72
58
|
|
73
59
|
# Validation based on checks :min and :max (:min, :max has higher priority to :width, :height).
|
74
|
-
if
|
75
|
-
if
|
76
|
-
(
|
77
|
-
(
|
60
|
+
if flat_options[:min] || flat_options[:max]
|
61
|
+
if flat_options[:min] && (
|
62
|
+
(flat_options[:width][:min] && metadata[:width] < flat_options[:width][:min]) ||
|
63
|
+
(flat_options[:height][:min] && metadata[:height] < flat_options[:height][:min])
|
78
64
|
)
|
79
|
-
|
65
|
+
errors_options[:width] = flat_options[:width][:min]
|
66
|
+
errors_options[:height] = flat_options[:height][:min]
|
67
|
+
|
68
|
+
add_error(record, attribute, :dimension_min_not_included_in, **errors_options)
|
80
69
|
return false
|
81
70
|
end
|
82
|
-
if
|
83
|
-
(
|
84
|
-
(
|
71
|
+
if flat_options[:max] && (
|
72
|
+
(flat_options[:width][:max] && metadata[:width] > flat_options[:width][:max]) ||
|
73
|
+
(flat_options[:height][:max] && metadata[:height] > flat_options[:height][:max])
|
85
74
|
)
|
86
|
-
|
75
|
+
errors_options[:width] = flat_options[:width][:max]
|
76
|
+
errors_options[:height] = flat_options[:height][:max]
|
77
|
+
|
78
|
+
add_error(record, attribute, :dimension_max_not_included_in, **errors_options)
|
87
79
|
return false
|
88
80
|
end
|
89
81
|
|
90
82
|
# Validation based on checks :width and :height.
|
91
83
|
else
|
84
|
+
width_or_height_invalid = false
|
85
|
+
|
92
86
|
[:width, :height].each do |length|
|
93
|
-
next unless
|
94
|
-
if
|
95
|
-
if
|
96
|
-
|
97
|
-
|
87
|
+
next unless flat_options[length]
|
88
|
+
if flat_options[length].is_a?(Hash)
|
89
|
+
if flat_options[length][:in] && (metadata[length] < flat_options[length][:min] || metadata[length] > flat_options[length][:max])
|
90
|
+
error_type = :"dimension_#{length}_not_included_in"
|
91
|
+
errors_options[:min] = flat_options[length][:min]
|
92
|
+
errors_options[:max] = flat_options[length][:max]
|
93
|
+
|
94
|
+
add_error(record, attribute, error_type, **errors_options)
|
95
|
+
width_or_height_invalid = true
|
98
96
|
else
|
99
|
-
if
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
97
|
+
if flat_options[length][:min] && metadata[length] < flat_options[length][:min]
|
98
|
+
error_type = :"dimension_#{length}_not_greater_than_or_equal_to"
|
99
|
+
errors_options[:length] = flat_options[length][:min]
|
100
|
+
|
101
|
+
add_error(record, attribute, error_type, **errors_options)
|
102
|
+
width_or_height_invalid = true
|
103
|
+
elsif flat_options[length][:max] && metadata[length] > flat_options[length][:max]
|
104
|
+
error_type = :"dimension_#{length}_not_less_than_or_equal_to"
|
105
|
+
errors_options[:length] = flat_options[length][:max]
|
106
|
+
|
107
|
+
add_error(record, attribute, error_type, **errors_options)
|
108
|
+
width_or_height_invalid = true
|
106
109
|
end
|
107
110
|
end
|
108
111
|
else
|
109
|
-
if
|
110
|
-
|
111
|
-
|
112
|
+
if metadata[length] != flat_options[length]
|
113
|
+
error_type = :"dimension_#{length}_not_equal_to"
|
114
|
+
errors_options[:length] = flat_options[length]
|
115
|
+
|
116
|
+
add_error(record, attribute, error_type, **errors_options)
|
117
|
+
width_or_height_invalid = true
|
112
118
|
end
|
113
119
|
end
|
114
120
|
end
|
121
|
+
|
122
|
+
return false if width_or_height_invalid
|
115
123
|
end
|
116
124
|
|
117
125
|
true # valid file
|
118
126
|
end
|
119
127
|
|
120
|
-
def
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
128
|
+
def process_options(record)
|
129
|
+
flat_options = set_flat_options(record)
|
130
|
+
|
131
|
+
[:width, :height].each do |length|
|
132
|
+
if flat_options[length] and flat_options[length].is_a?(Hash)
|
133
|
+
if (range = flat_options[length][:in])
|
134
|
+
raise ArgumentError, ":in must be a Range" unless range.is_a?(Range)
|
135
|
+
flat_options[length][:min], flat_options[length][:max] = range.min, range.max
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
[:min, :max].each do |dim|
|
140
|
+
if (range = flat_options[dim])
|
141
|
+
raise ArgumentError, ":#{dim} must be a Range (width..height)" unless range.is_a?(Range)
|
142
|
+
flat_options[:width] = { dim => range.first }
|
143
|
+
flat_options[:height] = { dim => range.last }
|
144
|
+
end
|
145
|
+
end
|
125
146
|
|
147
|
+
flat_options
|
148
|
+
end
|
126
149
|
end
|
127
150
|
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base_comparison_validator'
|
4
|
+
|
5
|
+
module ActiveStorageValidations
|
6
|
+
class DurationValidator < BaseComparisonValidator
|
7
|
+
include ASVAnalyzable
|
8
|
+
include ASVAttachable
|
9
|
+
|
10
|
+
ERROR_TYPES = %i[
|
11
|
+
duration_not_less_than
|
12
|
+
duration_not_less_than_or_equal_to
|
13
|
+
duration_not_greater_than
|
14
|
+
duration_not_greater_than_or_equal_to
|
15
|
+
duration_not_between
|
16
|
+
].freeze
|
17
|
+
METADATA_KEYS = %i[duration].freeze
|
18
|
+
|
19
|
+
def validate_each(record, attribute, _value)
|
20
|
+
return if no_attachments?(record, attribute)
|
21
|
+
|
22
|
+
flat_options = set_flat_options(record)
|
23
|
+
|
24
|
+
attachables_and_blobs(record, attribute).each do |attachable, blob|
|
25
|
+
duration = metadata_for(blob, attachable, METADATA_KEYS)&.fetch(:duration, nil)
|
26
|
+
|
27
|
+
if duration.to_i <= 0
|
28
|
+
errors_options = initialize_error_options(options, attachable)
|
29
|
+
add_error(record, attribute, :media_metadata_missing, **errors_options)
|
30
|
+
next
|
31
|
+
end
|
32
|
+
|
33
|
+
next if is_valid?(duration, flat_options)
|
34
|
+
|
35
|
+
errors_options = initialize_error_options(options, attachable)
|
36
|
+
populate_error_options(errors_options, flat_options)
|
37
|
+
errors_options[:duration] = format_bound_value(duration)
|
38
|
+
|
39
|
+
keys = AVAILABLE_CHECKS & flat_options.keys
|
40
|
+
error_type = "duration_not_#{keys.first}".to_sym
|
41
|
+
|
42
|
+
add_error(record, attribute, error_type, **errors_options)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def format_bound_value(value)
|
49
|
+
return nil unless value
|
50
|
+
|
51
|
+
custom_value = value == value.to_i ? value.to_i : value
|
52
|
+
ActiveSupport::Duration.build(custom_value).inspect
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveStorageValidations
|
4
|
+
module ASVBlobMetadatable
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
# This method returns the metadata that has been set by our gem.
|
9
|
+
# The metadata is stored in the blob's custom metadata. All keys are prefixed with 'asv_'
|
10
|
+
# to avoid conflicts with other metadata.
|
11
|
+
# It is not to set a active_storage_validation key equal to a a hash of our gem's metadata,
|
12
|
+
# because this would result in errors down the road with services such as S3.
|
13
|
+
#
|
14
|
+
# Because of how the metadata is stored, we need to convert the values from String
|
15
|
+
# to Integer or Boolean.
|
16
|
+
def active_storage_validations_metadata
|
17
|
+
metadata.dig('custom')
|
18
|
+
&.select { |key, _| key.to_s.start_with?('asv_') }
|
19
|
+
&.transform_keys { |key| key.to_s.delete_prefix('asv_') }
|
20
|
+
&.transform_values do |value|
|
21
|
+
case value
|
22
|
+
when /\A\d+\z/ then value.to_i
|
23
|
+
when /\A\d+\.\d+\z/ then value.to_f
|
24
|
+
when 'true' then true
|
25
|
+
when 'false' then false
|
26
|
+
else value
|
27
|
+
end
|
28
|
+
end || {}
|
29
|
+
end
|
30
|
+
|
31
|
+
# This method sets the metadata that has been detected by our gem.
|
32
|
+
# The metadata is stored in the blob's custom metadata. All keys are prefixed with 'asv_'.
|
33
|
+
# We need to store values as String, because services such as S3 will not accept other types.
|
34
|
+
def merge_into_active_storage_validations_metadata(hash)
|
35
|
+
aws_compatible_metadata = normalize_active_storage_validations_metadata_for_aws(hash)
|
36
|
+
|
37
|
+
metadata['custom'] ||= {}
|
38
|
+
metadata['custom'].merge!(aws_compatible_metadata)
|
39
|
+
|
40
|
+
active_storage_validations_metadata
|
41
|
+
end
|
42
|
+
|
43
|
+
def normalize_active_storage_validations_metadata_for_aws(hash)
|
44
|
+
hash.transform_keys { |key, _| key.to_s.start_with?('asv_') ? key : "asv_#{key}" }
|
45
|
+
.transform_values(&:to_s)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "marcel"
|
4
|
+
|
5
|
+
Marcel::MimeType.extend "application/x-rar-compressed", parents: %(application/x-rar)
|
6
|
+
Marcel::MimeType.extend "audio/x-hx-aac-adts", parents: %(audio/x-aac)
|
7
|
+
Marcel::MimeType.extend "audio/x-m4a", parents: %(audio/mp4)
|
8
|
+
Marcel::MimeType.extend "text/xml", parents: %(application/xml) # alias
|
9
|
+
Marcel::MimeType.extend "video/theora", parents: %(video/ogg)
|
10
|
+
|
11
|
+
# Add empty content type
|
12
|
+
Marcel::MimeType.extend "inode/x-empty", extensions: %w(empty)
|