active_storage_validations 0.9.8 → 1.0.3
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 +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
|
[](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
|