active_storage_validations 0.9.8 → 1.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +26 -4
- data/config/locales/en.yml +1 -0
- data/config/locales/zh-CN.yml +22 -0
- data/lib/active_storage_validations/aspect_ratio_validator.rb +15 -17
- data/lib/active_storage_validations/content_type_validator.rb +15 -9
- data/lib/active_storage_validations/dimension_validator.rb +38 -32
- data/lib/active_storage_validations/limit_validator.rb +12 -10
- data/lib/active_storage_validations/matchers/content_type_validator_matcher.rb +12 -34
- data/lib/active_storage_validations/metadata.rb +18 -7
- data/lib/active_storage_validations/option_proc_unfolding.rb +16 -0
- data/lib/active_storage_validations/processable_image_validator.rb +43 -0
- data/lib/active_storage_validations/size_validator.rb +22 -20
- data/lib/active_storage_validations/version.rb +1 -1
- data/lib/active_storage_validations.rb +2 -0
- metadata +19 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 267c083e1e37ff53853d928bf3df0a69b8c9cf0662c3cf744f99cde3d5809125
|
4
|
+
data.tar.gz: 36554892e5c2df7ad812119b6acfae985e4adbe1dd8f7c29699f8f9ba69f0506
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6cbe5d7521afc8680a9df584968c4e3da7c5e1602efc30f60dc6890a54bb9d609daf7e7eaa0924d6d7971f13e9c763e4063c228d14645b8c5261f1702c4a21c9
|
7
|
+
data.tar.gz: 989e058c095e07790f647af2582355dddd715101b6dce10d4b521865362c5857f8df869270ae88e5eb58d20fa698ed470f4b181517ac016439d45f0e30049e21
|
data/README.md
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
+
[<img src="https://github.com/igorkasyanchuk/rails_time_travel/blob/main/docs/more_gems.png?raw=true"
|
2
|
+
/>](https://www.railsjazz.com/?utm_source=github&utm_medium=top&utm_campaign=active_storage_validations)
|
3
|
+
|
1
4
|
# Active Storage Validations
|
2
5
|
|
3
6
|
[![MiniTest](https://github.com/igorkasyanchuk/active_storage_validations/workflows/MiniTest/badge.svg)](https://github.com/igorkasyanchuk/active_storage_validations/actions)
|
@@ -16,7 +19,9 @@ This gems doing it for you. Just use `attached: true` or `content_type: 'image/p
|
|
16
19
|
* validates dimension of images/videos
|
17
20
|
* validates number of uploaded files (min/max required)
|
18
21
|
* validates aspect ratio (if square, portrait, landscape, is_16_9, ...)
|
22
|
+
* validates if file can be processed by MiniMagick or Vips
|
19
23
|
* custom error messages
|
24
|
+
* allow procs for dynamic determination of values
|
20
25
|
|
21
26
|
## Usage
|
22
27
|
|
@@ -36,6 +41,7 @@ class User < ApplicationRecord
|
|
36
41
|
dimension: { width: { min: 800, max: 2400 },
|
37
42
|
height: { min: 600, max: 1800 }, message: 'is not given between dimension' }
|
38
43
|
validates :image, attached: true,
|
44
|
+
processable_image: true,
|
39
45
|
content_type: ['image/png', 'image/jpeg'],
|
40
46
|
aspect_ratio: :landscape
|
41
47
|
end
|
@@ -121,6 +127,18 @@ class User < ApplicationRecord
|
|
121
127
|
end
|
122
128
|
```
|
123
129
|
|
130
|
+
- Proc Usage:
|
131
|
+
|
132
|
+
Procs can be used instead of values in all the above examples. They will be called on every validation.
|
133
|
+
```ruby
|
134
|
+
class User < ApplicationRecord
|
135
|
+
has_many_attached :proc_files
|
136
|
+
|
137
|
+
validates :proc_files, limit: { max: -> (record) { record.admin? ? 100 : 10 } }
|
138
|
+
end
|
139
|
+
|
140
|
+
```
|
141
|
+
|
124
142
|
## Internationalization (I18n)
|
125
143
|
|
126
144
|
Active Storage Validations uses I18n for error messages. For this, add these keys in your translation file:
|
@@ -148,6 +166,7 @@ en:
|
|
148
166
|
aspect_ratio_not_landscape: "must be a landscape image"
|
149
167
|
aspect_ratio_is_not: "must have an aspect ratio of %{aspect_ratio}"
|
150
168
|
aspect_ratio_unknown: "has an unknown aspect ratio"
|
169
|
+
image_not_processable: "is not a valid image"
|
151
170
|
```
|
152
171
|
|
153
172
|
In some cases, Active Storage Validations provides variables to help you customize messages:
|
@@ -295,20 +314,18 @@ end
|
|
295
314
|
|
296
315
|
To run tests in root folder of gem:
|
297
316
|
|
298
|
-
* `BUNDLE_GEMFILE=gemfiles/rails_5_2.gemfile bundle exec rake test` to run for Rails 5.2
|
299
317
|
* `BUNDLE_GEMFILE=gemfiles/rails_6_0.gemfile bundle exec rake test` to run for Rails 6.0
|
300
318
|
* `BUNDLE_GEMFILE=gemfiles/rails_6_1.gemfile bundle exec rake test` to run for Rails 6.1
|
319
|
+
* `BUNDLE_GEMFILE=gemfiles/rails_7_0.gemfile bundle exec rake test` to run for Rails 7.0
|
301
320
|
* `BUNDLE_GEMFILE=gemfiles/rails_next.gemfile bundle exec rake test` to run for Rails main branch
|
302
321
|
|
303
322
|
Snippet to run in console:
|
304
323
|
|
305
324
|
```
|
306
|
-
BUNDLE_GEMFILE=gemfiles/rails_5_2.gemfile bundle
|
307
325
|
BUNDLE_GEMFILE=gemfiles/rails_6_0.gemfile bundle
|
308
326
|
BUNDLE_GEMFILE=gemfiles/rails_6_1.gemfile bundle
|
309
327
|
BUNDLE_GEMFILE=gemfiles/rails_7_0.gemfile bundle
|
310
328
|
BUNDLE_GEMFILE=gemfiles/rails_next.gemfile bundle
|
311
|
-
BUNDLE_GEMFILE=gemfiles/rails_5_2.gemfile bundle exec rake test
|
312
329
|
BUNDLE_GEMFILE=gemfiles/rails_6_0.gemfile bundle exec rake test
|
313
330
|
BUNDLE_GEMFILE=gemfiles/rails_6_1.gemfile bundle exec rake test
|
314
331
|
BUNDLE_GEMFILE=gemfiles/rails_7_0.gemfile bundle exec rake test
|
@@ -378,10 +395,15 @@ You are welcome to contribute.
|
|
378
395
|
- https://github.com/stephensolis
|
379
396
|
- https://github.com/kwent
|
380
397
|
= https://github.com/Animesh-Ghosh
|
398
|
+
= https://github.com/gr8bit
|
399
|
+
= https://github.com/codegeek319
|
400
|
+
= https://github.com/clwy-cn
|
401
|
+
= https://github.com/kukicola
|
402
|
+
= https://github.com/sobrinho
|
381
403
|
|
382
404
|
## License
|
383
405
|
|
384
406
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
385
407
|
|
386
408
|
[<img src="https://github.com/igorkasyanchuk/rails_time_travel/blob/main/docs/more_gems.png?raw=true"
|
387
|
-
/>](https://www.railsjazz.com
|
409
|
+
/>](https://www.railsjazz.com/?utm_source=github&utm_medium=bottom&utm_campaign=active_storage_validations)
|
data/config/locales/en.yml
CHANGED
@@ -0,0 +1,22 @@
|
|
1
|
+
zh-CN:
|
2
|
+
errors:
|
3
|
+
messages:
|
4
|
+
content_type_invalid: "文件类型错误"
|
5
|
+
file_size_out_of_range: "文件大小 %{file_size} 超出限定范围"
|
6
|
+
limit_out_of_range: "文件数超出限定范围"
|
7
|
+
image_metadata_missing: "不是有效的图像"
|
8
|
+
dimension_min_inclusion: "必须大于或等于 %{width} x %{height} 像素"
|
9
|
+
dimension_max_inclusion: "必须小于或等于 %{width} x %{height} 像素"
|
10
|
+
dimension_width_inclusion: "宽度不在 %{min} 和 %{max} 像素之间"
|
11
|
+
dimension_height_inclusion: "高度不在 %{min} 和 %{max} 像素之间"
|
12
|
+
dimension_width_greater_than_or_equal_to: "宽度必须大于或等于 %{length} 像素"
|
13
|
+
dimension_height_greater_than_or_equal_to: "高度必须大于或等于 %{length} 像素"
|
14
|
+
dimension_width_less_than_or_equal_to: "宽度必须小于或等于 %{length} 像素"
|
15
|
+
dimension_height_less_than_or_equal_to: "高度必须小于或等于 %{length} 像素"
|
16
|
+
dimension_width_equal_to: "宽度必须等于 %{length} 像素"
|
17
|
+
dimension_height_equal_to: "高度必须等于 %{length} 像素"
|
18
|
+
aspect_ratio_not_square: "必须是方形图片"
|
19
|
+
aspect_ratio_not_portrait: "必须是竖屏图片"
|
20
|
+
aspect_ratio_not_landscape: "必须是横屏图片"
|
21
|
+
aspect_ratio_is_not: "纵横比必须是 %{aspect_ratio}"
|
22
|
+
aspect_ratio_unknown: "未知的纵横比"
|
@@ -4,17 +4,14 @@ require_relative 'metadata.rb'
|
|
4
4
|
|
5
5
|
module ActiveStorageValidations
|
6
6
|
class AspectRatioValidator < ActiveModel::EachValidator # :nodoc
|
7
|
+
include OptionProcUnfolding
|
8
|
+
|
7
9
|
AVAILABLE_CHECKS = %i[with].freeze
|
8
10
|
PRECISION = 3
|
9
11
|
|
10
|
-
def initialize(options)
|
11
|
-
super(options)
|
12
|
-
end
|
13
|
-
|
14
|
-
|
15
12
|
def check_validity!
|
16
13
|
return true if AVAILABLE_CHECKS.any? { |argument| options.key?(argument) }
|
17
|
-
raise ArgumentError, 'You must pass
|
14
|
+
raise ArgumentError, 'You must pass :with to the validator'
|
18
15
|
end
|
19
16
|
|
20
17
|
if Rails.gem_version >= Gem::Version.new('6.0.0')
|
@@ -55,26 +52,27 @@ module ActiveStorageValidations
|
|
55
52
|
|
56
53
|
|
57
54
|
def is_valid?(record, attribute, metadata)
|
55
|
+
flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS)
|
58
56
|
if metadata[:width].to_i <= 0 || metadata[:height].to_i <= 0
|
59
|
-
add_error(record, attribute,
|
57
|
+
add_error(record, attribute, :image_metadata_missing, flat_options[:with])
|
60
58
|
return false
|
61
59
|
end
|
62
60
|
|
63
|
-
case
|
61
|
+
case flat_options[:with]
|
64
62
|
when :square
|
65
63
|
return true if metadata[:width] == metadata[:height]
|
66
|
-
add_error(record, attribute, :aspect_ratio_not_square)
|
64
|
+
add_error(record, attribute, :aspect_ratio_not_square, flat_options[:with])
|
67
65
|
|
68
66
|
when :portrait
|
69
67
|
return true if metadata[:height] > metadata[:width]
|
70
|
-
add_error(record, attribute, :aspect_ratio_not_portrait)
|
68
|
+
add_error(record, attribute, :aspect_ratio_not_portrait, flat_options[:with])
|
71
69
|
|
72
70
|
when :landscape
|
73
71
|
return true if metadata[:width] > metadata[:height]
|
74
|
-
add_error(record, attribute, :aspect_ratio_not_landscape)
|
72
|
+
add_error(record, attribute, :aspect_ratio_not_landscape, flat_options[:with])
|
75
73
|
|
76
74
|
else
|
77
|
-
if
|
75
|
+
if flat_options[:with] =~ /is_(\d*)_(\d*)/
|
78
76
|
x = $1.to_i
|
79
77
|
y = $2.to_i
|
80
78
|
|
@@ -82,17 +80,17 @@ module ActiveStorageValidations
|
|
82
80
|
|
83
81
|
add_error(record, attribute, :aspect_ratio_is_not, "#{x}x#{y}")
|
84
82
|
else
|
85
|
-
add_error(record, attribute, :aspect_ratio_unknown)
|
83
|
+
add_error(record, attribute, :aspect_ratio_unknown, flat_options[:with])
|
86
84
|
end
|
87
85
|
end
|
88
86
|
false
|
89
87
|
end
|
90
88
|
|
91
89
|
|
92
|
-
def add_error(record, attribute,
|
93
|
-
|
94
|
-
return if record.errors.added?(attribute,
|
95
|
-
record.errors.add(attribute,
|
90
|
+
def add_error(record, attribute, default_message, interpolate)
|
91
|
+
message = options[:message].presence || default_message
|
92
|
+
return if record.errors.added?(attribute, message)
|
93
|
+
record.errors.add(attribute, message, aspect_ratio: interpolate)
|
96
94
|
end
|
97
95
|
|
98
96
|
end
|
@@ -2,16 +2,23 @@
|
|
2
2
|
|
3
3
|
module ActiveStorageValidations
|
4
4
|
class ContentTypeValidator < ActiveModel::EachValidator # :nodoc:
|
5
|
+
include OptionProcUnfolding
|
6
|
+
|
7
|
+
AVAILABLE_CHECKS = %i[with in].freeze
|
8
|
+
|
5
9
|
def validate_each(record, attribute, _value)
|
6
|
-
return true
|
10
|
+
return true unless record.send(attribute).attached?
|
7
11
|
|
12
|
+
types = authorized_types(record)
|
13
|
+
return true if types.empty?
|
14
|
+
|
8
15
|
files = Array.wrap(record.send(attribute))
|
9
16
|
|
10
|
-
errors_options = { authorized_types: types_to_human_format }
|
17
|
+
errors_options = { authorized_types: types_to_human_format(types) }
|
11
18
|
errors_options[:message] = options[:message] if options[:message].present?
|
12
19
|
|
13
20
|
files.each do |file|
|
14
|
-
next if is_valid?(file)
|
21
|
+
next if is_valid?(file, types)
|
15
22
|
|
16
23
|
errors_options[:content_type] = content_type(file)
|
17
24
|
record.errors.add(attribute, :content_type_invalid, **errors_options)
|
@@ -19,10 +26,9 @@ module ActiveStorageValidations
|
|
19
26
|
end
|
20
27
|
end
|
21
28
|
|
22
|
-
def
|
23
|
-
|
24
|
-
|
25
|
-
@types = (Array.wrap(options[:with]) + Array.wrap(options[:in])).compact.map do |type|
|
29
|
+
def authorized_types(record)
|
30
|
+
flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS)
|
31
|
+
(Array.wrap(flat_options[:with]) + Array.wrap(flat_options[:in])).compact.map do |type|
|
26
32
|
if type.is_a?(Regexp)
|
27
33
|
type
|
28
34
|
else
|
@@ -31,7 +37,7 @@ module ActiveStorageValidations
|
|
31
37
|
end
|
32
38
|
end
|
33
39
|
|
34
|
-
def types_to_human_format
|
40
|
+
def types_to_human_format(types)
|
35
41
|
types
|
36
42
|
.map { |type| type.to_s.split('/').last.upcase }
|
37
43
|
.join(', ')
|
@@ -41,7 +47,7 @@ module ActiveStorageValidations
|
|
41
47
|
file.blob.present? && file.blob.content_type
|
42
48
|
end
|
43
49
|
|
44
|
-
def is_valid?(file)
|
50
|
+
def is_valid?(file, types)
|
45
51
|
file_type = content_type(file)
|
46
52
|
types.any? do |type|
|
47
53
|
type == file_type || (type.is_a?(Regexp) && type.match?(file_type.to_s))
|
@@ -4,25 +4,30 @@ require_relative 'metadata.rb'
|
|
4
4
|
|
5
5
|
module ActiveStorageValidations
|
6
6
|
class DimensionValidator < ActiveModel::EachValidator # :nodoc
|
7
|
+
include OptionProcUnfolding
|
8
|
+
|
7
9
|
AVAILABLE_CHECKS = %i[width height min max].freeze
|
8
10
|
|
9
|
-
def
|
11
|
+
def process_options(record)
|
12
|
+
flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS)
|
13
|
+
|
10
14
|
[:width, :height].each do |length|
|
11
|
-
if
|
12
|
-
if range =
|
15
|
+
if flat_options[length] and flat_options[length].is_a?(Hash)
|
16
|
+
if (range = flat_options[length][:in])
|
13
17
|
raise ArgumentError, ":in must be a Range" unless range.is_a?(Range)
|
14
|
-
|
18
|
+
flat_options[length][:min], flat_options[length][:max] = range.min, range.max
|
15
19
|
end
|
16
20
|
end
|
17
21
|
end
|
18
22
|
[:min, :max].each do |dim|
|
19
|
-
if range =
|
23
|
+
if (range = flat_options[dim])
|
20
24
|
raise ArgumentError, ":#{dim} must be a Range (width..height)" unless range.is_a?(Range)
|
21
|
-
|
22
|
-
|
25
|
+
flat_options[:width] = { dim => range.first }
|
26
|
+
flat_options[:height] = { dim => range.last }
|
23
27
|
end
|
24
28
|
end
|
25
|
-
|
29
|
+
|
30
|
+
flat_options
|
26
31
|
end
|
27
32
|
|
28
33
|
|
@@ -64,26 +69,27 @@ module ActiveStorageValidations
|
|
64
69
|
|
65
70
|
|
66
71
|
def is_valid?(record, attribute, file_metadata)
|
72
|
+
flat_options = process_options(record)
|
67
73
|
# Validation fails unless file metadata contains valid width and height.
|
68
74
|
if file_metadata[:width].to_i <= 0 || file_metadata[:height].to_i <= 0
|
69
|
-
add_error(record, attribute,
|
75
|
+
add_error(record, attribute, :image_metadata_missing)
|
70
76
|
return false
|
71
77
|
end
|
72
78
|
|
73
79
|
# Validation based on checks :min and :max (:min, :max has higher priority to :width, :height).
|
74
|
-
if
|
75
|
-
if
|
76
|
-
(
|
77
|
-
(
|
80
|
+
if flat_options[:min] || flat_options[:max]
|
81
|
+
if flat_options[:min] && (
|
82
|
+
(flat_options[:width][:min] && file_metadata[:width] < flat_options[:width][:min]) ||
|
83
|
+
(flat_options[:height][:min] && file_metadata[:height] < flat_options[:height][:min])
|
78
84
|
)
|
79
|
-
add_error(record, attribute,
|
85
|
+
add_error(record, attribute, :dimension_min_inclusion, width: flat_options[:width][:min], height: flat_options[:height][:min])
|
80
86
|
return false
|
81
87
|
end
|
82
|
-
if
|
83
|
-
(
|
84
|
-
(
|
88
|
+
if flat_options[:max] && (
|
89
|
+
(flat_options[:width][:max] && file_metadata[:width] > flat_options[:width][:max]) ||
|
90
|
+
(flat_options[:height][:max] && file_metadata[:height] > flat_options[:height][:max])
|
85
91
|
)
|
86
|
-
add_error(record, attribute,
|
92
|
+
add_error(record, attribute, :dimension_max_inclusion, width: flat_options[:width][:max], height: flat_options[:height][:max])
|
87
93
|
return false
|
88
94
|
end
|
89
95
|
|
@@ -91,24 +97,24 @@ module ActiveStorageValidations
|
|
91
97
|
else
|
92
98
|
width_or_height_invalid = false
|
93
99
|
[:width, :height].each do |length|
|
94
|
-
next unless
|
95
|
-
if
|
96
|
-
if
|
97
|
-
add_error(record, attribute,
|
100
|
+
next unless flat_options[length]
|
101
|
+
if flat_options[length].is_a?(Hash)
|
102
|
+
if flat_options[length][:in] && (file_metadata[length] < flat_options[length][:min] || file_metadata[length] > flat_options[length][:max])
|
103
|
+
add_error(record, attribute, :"dimension_#{length}_inclusion", min: flat_options[length][:min], max: flat_options[length][:max])
|
98
104
|
width_or_height_invalid = true
|
99
105
|
else
|
100
|
-
if
|
101
|
-
add_error(record, attribute,
|
106
|
+
if flat_options[length][:min] && file_metadata[length] < flat_options[length][:min]
|
107
|
+
add_error(record, attribute, :"dimension_#{length}_greater_than_or_equal_to", length: flat_options[length][:min])
|
102
108
|
width_or_height_invalid = true
|
103
109
|
end
|
104
|
-
if
|
105
|
-
add_error(record, attribute,
|
110
|
+
if flat_options[length][:max] && file_metadata[length] > flat_options[length][:max]
|
111
|
+
add_error(record, attribute, :"dimension_#{length}_less_than_or_equal_to", length: flat_options[length][:max])
|
106
112
|
width_or_height_invalid = true
|
107
113
|
end
|
108
114
|
end
|
109
115
|
else
|
110
|
-
if file_metadata[length] !=
|
111
|
-
add_error(record, attribute,
|
116
|
+
if file_metadata[length] != flat_options[length]
|
117
|
+
add_error(record, attribute, :"dimension_#{length}_equal_to", length: flat_options[length])
|
112
118
|
width_or_height_invalid = true
|
113
119
|
end
|
114
120
|
end
|
@@ -120,10 +126,10 @@ module ActiveStorageValidations
|
|
120
126
|
true # valid file
|
121
127
|
end
|
122
128
|
|
123
|
-
def add_error(record, attribute,
|
124
|
-
|
125
|
-
return if record.errors.added?(attribute,
|
126
|
-
record.errors.add(attribute,
|
129
|
+
def add_error(record, attribute, default_message, **attrs)
|
130
|
+
message = options[:message].presence || default_message
|
131
|
+
return if record.errors.added?(attribute, message)
|
132
|
+
record.errors.add(attribute, message, **attrs)
|
127
133
|
end
|
128
134
|
|
129
135
|
end
|
@@ -2,11 +2,12 @@
|
|
2
2
|
|
3
3
|
module ActiveStorageValidations
|
4
4
|
class LimitValidator < ActiveModel::EachValidator # :nodoc:
|
5
|
+
include OptionProcUnfolding
|
6
|
+
|
5
7
|
AVAILABLE_CHECKS = %i[max min].freeze
|
6
8
|
|
7
9
|
def check_validity!
|
8
10
|
return true if AVAILABLE_CHECKS.any? { |argument| options.key?(argument) }
|
9
|
-
|
10
11
|
raise ArgumentError, 'You must pass either :max or :min to the validator'
|
11
12
|
end
|
12
13
|
|
@@ -14,19 +15,20 @@ module ActiveStorageValidations
|
|
14
15
|
return true unless record.send(attribute).attached?
|
15
16
|
|
16
17
|
files = Array.wrap(record.send(attribute)).compact.uniq
|
17
|
-
|
18
|
+
flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS)
|
19
|
+
errors_options = { min: flat_options[:min], max: flat_options[:max] }
|
18
20
|
|
19
|
-
return true if files_count_valid?(files.count)
|
21
|
+
return true if files_count_valid?(files.count, flat_options)
|
20
22
|
record.errors.add(attribute, options[:message].presence || :limit_out_of_range, **errors_options)
|
21
23
|
end
|
22
24
|
|
23
|
-
def files_count_valid?(count)
|
24
|
-
if
|
25
|
-
count >=
|
26
|
-
elsif
|
27
|
-
count <=
|
28
|
-
elsif
|
29
|
-
count >=
|
25
|
+
def files_count_valid?(count, flat_options)
|
26
|
+
if flat_options[:max].present? && flat_options[:min].present?
|
27
|
+
count >= flat_options[:min] && count <= flat_options[:max]
|
28
|
+
elsif flat_options[:max].present?
|
29
|
+
count <= flat_options[:max]
|
30
|
+
elsif flat_options[:min].present?
|
31
|
+
count >= flat_options[:min]
|
30
32
|
end
|
31
33
|
end
|
32
34
|
end
|
@@ -33,15 +33,19 @@ module ActiveStorageValidations
|
|
33
33
|
end
|
34
34
|
|
35
35
|
def failure_message
|
36
|
-
|
37
|
-
Expected #{@attribute_name}
|
36
|
+
message = ["Expected #{@attribute_name}"]
|
38
37
|
|
39
|
-
|
40
|
-
|
38
|
+
if @allowed_types
|
39
|
+
message << "Accept content types: #{allowed_types.join(", ")}"
|
40
|
+
message << "#{@missing_allowed_types.join(", ")} were rejected"
|
41
|
+
end
|
42
|
+
|
43
|
+
if @rejected_types
|
44
|
+
message << "Reject content types: #{rejected_types.join(", ")}"
|
45
|
+
message << "#{@missing_rejected_types.join(", ")} were accepted"
|
46
|
+
end
|
41
47
|
|
42
|
-
|
43
|
-
#{rejected_types_and_failures}
|
44
|
-
MESSAGE
|
48
|
+
message.join("\n")
|
45
49
|
end
|
46
50
|
|
47
51
|
protected
|
@@ -57,7 +61,7 @@ module ActiveStorageValidations
|
|
57
61
|
end
|
58
62
|
|
59
63
|
def rejected_types
|
60
|
-
@rejected_types ||
|
64
|
+
@rejected_types || []
|
61
65
|
end
|
62
66
|
|
63
67
|
def allowed_types_allowed?
|
@@ -70,22 +74,6 @@ module ActiveStorageValidations
|
|
70
74
|
@missing_rejected_types.none?
|
71
75
|
end
|
72
76
|
|
73
|
-
def accepted_types_and_failures
|
74
|
-
if @missing_allowed_types.present?
|
75
|
-
"#{@missing_allowed_types.join(", ")} were rejected."
|
76
|
-
else
|
77
|
-
"All were accepted successfully."
|
78
|
-
end
|
79
|
-
end
|
80
|
-
|
81
|
-
def rejected_types_and_failures
|
82
|
-
if @missing_rejected_types.present?
|
83
|
-
"#{@missing_rejected_types.join(", ")} were accepted."
|
84
|
-
else
|
85
|
-
"All were rejected successfully."
|
86
|
-
end
|
87
|
-
end
|
88
|
-
|
89
77
|
def type_allowed?(type)
|
90
78
|
@subject.public_send(@attribute_name).attach(attachment_for(type))
|
91
79
|
@subject.validate
|
@@ -96,16 +84,6 @@ module ActiveStorageValidations
|
|
96
84
|
suffix = type.to_s.split('/').last
|
97
85
|
{ io: Tempfile.new('.'), filename: "test.#{suffix}", content_type: type }
|
98
86
|
end
|
99
|
-
|
100
|
-
private
|
101
|
-
|
102
|
-
def content_type_keys
|
103
|
-
if Rails.gem_version < Gem::Version.new('6.1.0')
|
104
|
-
Mime::LOOKUP.keys
|
105
|
-
else
|
106
|
-
Marcel::TYPES.keys
|
107
|
-
end
|
108
|
-
end
|
109
87
|
end
|
110
88
|
end
|
111
89
|
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
module ActiveStorageValidations
|
2
2
|
class Metadata
|
3
|
+
class InvalidImageError < StandardError; end
|
4
|
+
|
3
5
|
attr_reader :file
|
4
6
|
|
5
7
|
def initialize(file)
|
@@ -10,7 +12,7 @@ module ActiveStorageValidations
|
|
10
12
|
def image_processor
|
11
13
|
Rails.application.config.active_storage.variant_processor
|
12
14
|
end
|
13
|
-
|
15
|
+
|
14
16
|
def exception_class
|
15
17
|
if image_processor == :vips && defined?(Vips)
|
16
18
|
Vips::Error
|
@@ -35,6 +37,16 @@ module ActiveStorageValidations
|
|
35
37
|
{ width: image.width, height: image.height }
|
36
38
|
end
|
37
39
|
end
|
40
|
+
rescue InvalidImageError
|
41
|
+
logger.info "Skipping image analysis because ImageMagick or Vips doesn't support the file"
|
42
|
+
{}
|
43
|
+
end
|
44
|
+
|
45
|
+
def valid?
|
46
|
+
read_image
|
47
|
+
true
|
48
|
+
rescue InvalidImageError
|
49
|
+
false
|
38
50
|
end
|
39
51
|
|
40
52
|
private
|
@@ -76,12 +88,9 @@ module ActiveStorageValidations
|
|
76
88
|
end
|
77
89
|
end
|
78
90
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
logger.info "Skipping image analysis because ImageMagick or Vips doesn't support the file"
|
83
|
-
{}
|
84
|
-
end
|
91
|
+
|
92
|
+
raise InvalidImageError unless valid_image?(image)
|
93
|
+
yield image if block_given?
|
85
94
|
rescue LoadError, NameError
|
86
95
|
logger.info "Skipping image analysis because the mini_magick or ruby-vips gem isn't installed"
|
87
96
|
{}
|
@@ -93,6 +102,8 @@ module ActiveStorageValidations
|
|
93
102
|
end
|
94
103
|
|
95
104
|
def valid_image?(image)
|
105
|
+
return false unless image
|
106
|
+
|
96
107
|
image_processor == :vips && image.is_a?(Vips::Image) ? image.avg : image.valid?
|
97
108
|
rescue exception_class
|
98
109
|
false
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module ActiveStorageValidations
|
2
|
+
module OptionProcUnfolding
|
3
|
+
|
4
|
+
def unfold_procs(record, object, only_keys)
|
5
|
+
case object
|
6
|
+
when Hash
|
7
|
+
object.merge(object) { |key, value| only_keys&.exclude?(key) ? {} : unfold_procs(record, value, nil) }
|
8
|
+
when Array
|
9
|
+
object.map { |o| unfold_procs(record, o, only_keys) }
|
10
|
+
else
|
11
|
+
object.is_a?(Proc) ? object.call(record) : object
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'metadata.rb'
|
4
|
+
|
5
|
+
module ActiveStorageValidations
|
6
|
+
class ProcessableImageValidator < ActiveModel::EachValidator # :nodoc
|
7
|
+
include OptionProcUnfolding
|
8
|
+
|
9
|
+
if Rails.gem_version >= Gem::Version.new('6.0.0')
|
10
|
+
def validate_each(record, attribute, _value)
|
11
|
+
return true unless record.send(attribute).attached?
|
12
|
+
|
13
|
+
changes = record.attachment_changes[attribute.to_s]
|
14
|
+
return true if changes.blank?
|
15
|
+
|
16
|
+
files = Array.wrap(changes.is_a?(ActiveStorage::Attached::Changes::CreateMany) ? changes.attachables : changes.attachable)
|
17
|
+
|
18
|
+
files.each do |file|
|
19
|
+
add_error(record, attribute, :image_not_processable) unless Metadata.new(file).valid?
|
20
|
+
end
|
21
|
+
end
|
22
|
+
else
|
23
|
+
# Rails 5
|
24
|
+
def validate_each(record, attribute, _value)
|
25
|
+
return true unless record.send(attribute).attached?
|
26
|
+
|
27
|
+
files = Array.wrap(record.send(attribute))
|
28
|
+
|
29
|
+
files.each do |file|
|
30
|
+
add_error(record, attribute, :image_not_processable) unless Metadata.new(file).valid?
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def add_error(record, attribute, default_message)
|
38
|
+
message = options[:message].presence || default_message
|
39
|
+
return if record.errors.added?(attribute, message)
|
40
|
+
record.errors.add(attribute, message)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -2,14 +2,15 @@
|
|
2
2
|
|
3
3
|
module ActiveStorageValidations
|
4
4
|
class SizeValidator < ActiveModel::EachValidator # :nodoc:
|
5
|
+
include OptionProcUnfolding
|
6
|
+
|
5
7
|
delegate :number_to_human_size, to: ActiveSupport::NumberHelper
|
6
8
|
|
7
9
|
AVAILABLE_CHECKS = %i[less_than less_than_or_equal_to greater_than greater_than_or_equal_to between].freeze
|
8
10
|
|
9
11
|
def check_validity!
|
10
12
|
return true if AVAILABLE_CHECKS.any? { |argument| options.key?(argument) }
|
11
|
-
|
12
|
-
raise ArgumentError, 'You must pass either :less_than, :greater_than, or :between to the validator'
|
13
|
+
raise ArgumentError, 'You must pass either :less_than(_or_equal_to), :greater_than(_or_equal_to), or :between to the validator'
|
13
14
|
end
|
14
15
|
|
15
16
|
def validate_each(record, attribute, _value)
|
@@ -20,39 +21,40 @@ module ActiveStorageValidations
|
|
20
21
|
|
21
22
|
errors_options = {}
|
22
23
|
errors_options[:message] = options[:message] if options[:message].present?
|
24
|
+
flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS)
|
23
25
|
|
24
26
|
files.each do |file|
|
25
|
-
next if content_size_valid?(file.blob.byte_size)
|
27
|
+
next if content_size_valid?(file.blob.byte_size, flat_options)
|
26
28
|
|
27
29
|
errors_options[:file_size] = number_to_human_size(file.blob.byte_size)
|
28
|
-
errors_options[:min_size] = number_to_human_size(min_size)
|
29
|
-
errors_options[:max_size] = number_to_human_size(max_size)
|
30
|
+
errors_options[:min_size] = number_to_human_size(min_size(flat_options))
|
31
|
+
errors_options[:max_size] = number_to_human_size(max_size(flat_options))
|
30
32
|
|
31
33
|
record.errors.add(attribute, :file_size_out_of_range, **errors_options)
|
32
34
|
break
|
33
35
|
end
|
34
36
|
end
|
35
37
|
|
36
|
-
def content_size_valid?(file_size)
|
37
|
-
if
|
38
|
-
|
39
|
-
elsif
|
40
|
-
file_size <
|
41
|
-
elsif
|
42
|
-
file_size <=
|
43
|
-
elsif
|
44
|
-
file_size >
|
45
|
-
elsif
|
46
|
-
file_size >=
|
38
|
+
def content_size_valid?(file_size, flat_options)
|
39
|
+
if flat_options[:between].present?
|
40
|
+
flat_options[:between].include?(file_size)
|
41
|
+
elsif flat_options[:less_than].present?
|
42
|
+
file_size < flat_options[:less_than]
|
43
|
+
elsif flat_options[:less_than_or_equal_to].present?
|
44
|
+
file_size <= flat_options[:less_than_or_equal_to]
|
45
|
+
elsif flat_options[:greater_than].present?
|
46
|
+
file_size > flat_options[:greater_than]
|
47
|
+
elsif flat_options[:greater_than_or_equal_to].present?
|
48
|
+
file_size >= flat_options[:greater_than_or_equal_to]
|
47
49
|
end
|
48
50
|
end
|
49
51
|
|
50
|
-
def min_size
|
51
|
-
|
52
|
+
def min_size(flat_options)
|
53
|
+
flat_options[:between]&.min || flat_options[:greater_than] || flat_options[:greater_than_or_equal_to]
|
52
54
|
end
|
53
55
|
|
54
|
-
def max_size
|
55
|
-
|
56
|
+
def max_size(flat_options)
|
57
|
+
flat_options[:between]&.max || flat_options[:less_than] || flat_options[:less_than_or_equal_to]
|
56
58
|
end
|
57
59
|
end
|
58
60
|
end
|
@@ -2,12 +2,14 @@
|
|
2
2
|
|
3
3
|
require 'active_storage_validations/railtie'
|
4
4
|
require 'active_storage_validations/engine'
|
5
|
+
require 'active_storage_validations/option_proc_unfolding'
|
5
6
|
require 'active_storage_validations/attached_validator'
|
6
7
|
require 'active_storage_validations/content_type_validator'
|
7
8
|
require 'active_storage_validations/size_validator'
|
8
9
|
require 'active_storage_validations/limit_validator'
|
9
10
|
require 'active_storage_validations/dimension_validator'
|
10
11
|
require 'active_storage_validations/aspect_ratio_validator'
|
12
|
+
require 'active_storage_validations/processable_image_validator'
|
11
13
|
|
12
14
|
ActiveSupport.on_load(:active_record) do
|
13
15
|
send :include, ActiveStorageValidations
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active_storage_validations
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Igor Kasyanchuk
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-10-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activejob
|
@@ -164,6 +164,20 @@ dependencies:
|
|
164
164
|
- - ">="
|
165
165
|
- !ruby/object:Gem::Version
|
166
166
|
version: '0'
|
167
|
+
- !ruby/object:Gem::Dependency
|
168
|
+
name: globalid
|
169
|
+
requirement: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - ">="
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '0'
|
174
|
+
type: :development
|
175
|
+
prerelease: false
|
176
|
+
version_requirements: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - ">="
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: '0'
|
167
181
|
description: Validations for Active Storage (presence)
|
168
182
|
email:
|
169
183
|
- igorkasyanchuk@gmail.com
|
@@ -187,6 +201,7 @@ files:
|
|
187
201
|
- config/locales/tr.yml
|
188
202
|
- config/locales/uk.yml
|
189
203
|
- config/locales/vi.yml
|
204
|
+
- config/locales/zh-CN.yml
|
190
205
|
- lib/active_storage_validations.rb
|
191
206
|
- lib/active_storage_validations/aspect_ratio_validator.rb
|
192
207
|
- lib/active_storage_validations/attached_validator.rb
|
@@ -200,6 +215,8 @@ files:
|
|
200
215
|
- lib/active_storage_validations/matchers/dimension_validator_matcher.rb
|
201
216
|
- lib/active_storage_validations/matchers/size_validator_matcher.rb
|
202
217
|
- lib/active_storage_validations/metadata.rb
|
218
|
+
- lib/active_storage_validations/option_proc_unfolding.rb
|
219
|
+
- lib/active_storage_validations/processable_image_validator.rb
|
203
220
|
- lib/active_storage_validations/railtie.rb
|
204
221
|
- lib/active_storage_validations/size_validator.rb
|
205
222
|
- lib/active_storage_validations/version.rb
|