active_storage_validations 0.7.1 → 0.8.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 +18 -3
- data/config/locales/de.yml +1 -0
- data/config/locales/en.yml +6 -0
- data/lib/active_storage_validations.rb +1 -0
- data/lib/active_storage_validations/aspect_ratio_validator.rb +167 -0
- data/lib/active_storage_validations/dimension_validator.rb +198 -67
- data/lib/active_storage_validations/metadata.rb +76 -0
- data/lib/active_storage_validations/version.rb +1 -1
- metadata +21 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 90da6d94caed4f46393b01b35be7b3d0015304c991c4a04919122ec0256a2692
|
4
|
+
data.tar.gz: 3ea9462025ac280ac8201f46448c721e4d6633fcb13403c0fce75a5e89b841ad
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: efdb486d80faeb8fd8a0d629f8c666cd34d7936d43d6b15a4806c169258f4c203fcd25618988af5e50c93a8b6020e64f4fabd30d6b16560cbda3e5553ce2699b
|
7
|
+
data.tar.gz: 7b588e741bb0dbb44c0fa5c1506b37711d13eb605098ed4e04e27ec02450ab902c76364e9d72fb4b1a246c6ed641c611640c632ebbfb44835a82bc0e6e74a6ed
|
data/README.md
CHANGED
@@ -13,6 +13,7 @@ This gems doing it for you. Just use `attached: true` or `content_type: 'image/p
|
|
13
13
|
* validates size of files
|
14
14
|
* validates dimension of images/videos
|
15
15
|
* validates number of uploaded files (min/max required)
|
16
|
+
* validates aspect ratio (if square, portrait, landscape, is_16_9, ...)
|
16
17
|
* custom error messages
|
17
18
|
|
18
19
|
## Usage
|
@@ -31,6 +32,9 @@ class User < ApplicationRecord
|
|
31
32
|
validates :photos, attached: true, content_type: ['image/png', 'image/jpg', 'image/jpeg'],
|
32
33
|
dimension: { width: { min: 800, max: 2400 },
|
33
34
|
height: { min: 600, max: 1800 }, message: 'is not given between dimension' }
|
35
|
+
validates :image, attached: true,
|
36
|
+
content_type: ['image/png', 'image/jpg'],
|
37
|
+
aspect_ratio: :landscape
|
34
38
|
end
|
35
39
|
```
|
36
40
|
|
@@ -91,6 +95,7 @@ en:
|
|
91
95
|
content_type_invalid: "has an invalid content type"
|
92
96
|
file_size_out_of_range: "size %{file_size} is not between required range"
|
93
97
|
limit_out_of_range: "total number is out of range"
|
98
|
+
image_metadata_missing: "is not a valid image"
|
94
99
|
dimension_min_inclusion: "must be greater than or equal to %{width} x %{height} pixel."
|
95
100
|
dimension_max_inclusion: "must be less than or equal to %{width} x %{height} pixel."
|
96
101
|
dimension_width_inclusion: "width is not included between %{min} and %{max} pixel."
|
@@ -101,6 +106,12 @@ en:
|
|
101
106
|
dimension_height_less_than_or_equal_to: "height must be less than or equal to %{length} pixel."
|
102
107
|
dimension_width_equal_to: "width must be equal to %{length} pixel."
|
103
108
|
dimension_height_equal_to: "height must be equal to %{length} pixel."
|
109
|
+
aspect_ratio_not_square: "doesn't a square image"
|
110
|
+
aspect_ratio_not_portrait: "doesn't contain a portrait image"
|
111
|
+
aspect_ratio_not_landscape: "doesn't contain a landscape image"
|
112
|
+
aspect_ratio_is_not: "doesn't contain aspect ratio of %{aspect_ratio}"
|
113
|
+
aspect_ratio_unknown: "has an unknown aspect ratio"
|
114
|
+
|
104
115
|
```
|
105
116
|
|
106
117
|
In some cases Active Storage Validations provides variables to help you customize messages:
|
@@ -128,10 +139,11 @@ limit_out_of_range: "total number is out of range. range: [%{min}, %{max}]"
|
|
128
139
|
Add this line to your application's Gemfile:
|
129
140
|
|
130
141
|
```ruby
|
142
|
+
# Rails 5.2 and Rails 6
|
131
143
|
gem 'active_storage_validations'
|
132
144
|
|
133
|
-
# Optional, to use :dimension validator
|
134
|
-
gem 'mini_magick'
|
145
|
+
# Optional, to use :dimension validator or :aspect_ratio validator
|
146
|
+
gem 'mini_magick', '>= 4.9.5'
|
135
147
|
```
|
136
148
|
|
137
149
|
And then execute:
|
@@ -156,7 +168,10 @@ Very simple example of validation with file attached, content type check and cus
|
|
156
168
|
|
157
169
|
## Tests & Contributing
|
158
170
|
|
159
|
-
To run tests in root folder of gem
|
171
|
+
To run tests in root folder of gem:
|
172
|
+
|
173
|
+
* `BUNDLE_GEMFILE=gemfiles/rails_5_2.gemfile bundle exec rake test` to run for Rails 5.2
|
174
|
+
* `BUNDLE_GEMFILE=gemfiles/rails_6.0.gemfile bundle exec rake test` to run for Rails 6.0
|
160
175
|
|
161
176
|
To play with app `cd test/dummy` and `rails s -b 0.0.0.0` (before `rails db:migrate`).
|
162
177
|
|
data/config/locales/de.yml
CHANGED
@@ -4,6 +4,7 @@ de:
|
|
4
4
|
content_type_invalid: "hat einen ungültigen Dateityp"
|
5
5
|
file_size_out_of_range: "Dateigröße von %{file_size} ist außerhalb des erlaubten Bereichs"
|
6
6
|
limit_out_of_range: "Anzahl ist außerhalb des gültigen Bereichs"
|
7
|
+
image_metadata_missing: "ist nicht ein gültigen Bild"
|
7
8
|
dimension_min_inclusion: "muss größer oder gleich %{width} x %{height} Pixel sein"
|
8
9
|
dimension_max_inclusion: "muss kleiner oder gleich %{width} x %{height} Pixel sein"
|
9
10
|
dimension_width_inclusion: "Breite ist nicht zwischen %{min} und %{max} Pixel enthalten"
|
data/config/locales/en.yml
CHANGED
@@ -4,6 +4,7 @@ en:
|
|
4
4
|
content_type_invalid: "has an invalid content type"
|
5
5
|
file_size_out_of_range: "size %{file_size} is not between required range"
|
6
6
|
limit_out_of_range: "total number is out of range"
|
7
|
+
image_metadata_missing: "is not a valid image"
|
7
8
|
dimension_min_inclusion: "must be greater than or equal to %{width} x %{height} pixel"
|
8
9
|
dimension_max_inclusion: "must be less than or equal to %{width} x %{height} pixel"
|
9
10
|
dimension_width_inclusion: "width is not included between %{min} and %{max} pixel"
|
@@ -14,3 +15,8 @@ en:
|
|
14
15
|
dimension_height_less_than_or_equal_to: "height must be less than or equal to %{length} pixel"
|
15
16
|
dimension_width_equal_to: "width must be equal to %{length} pixel"
|
16
17
|
dimension_height_equal_to: "height must be equal to %{length} pixel"
|
18
|
+
aspect_ratio_not_square: "doesn't a square image"
|
19
|
+
aspect_ratio_not_portrait: "doesn't contain a portrait image"
|
20
|
+
aspect_ratio_not_landscape: "doesn't contain a landscape image"
|
21
|
+
aspect_ratio_is_not: "doesn't contain aspect ratio of %{aspect_ratio}"
|
22
|
+
aspect_ratio_unknown: "has an unknown aspect ratio"
|
@@ -7,6 +7,7 @@ require 'active_storage_validations/content_type_validator'
|
|
7
7
|
require 'active_storage_validations/size_validator'
|
8
8
|
require 'active_storage_validations/limit_validator'
|
9
9
|
require 'active_storage_validations/dimension_validator'
|
10
|
+
require 'active_storage_validations/aspect_ratio_validator'
|
10
11
|
|
11
12
|
ActiveSupport.on_load(:active_record) do
|
12
13
|
send :include, ActiveStorageValidations
|
@@ -0,0 +1,167 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'metadata.rb'
|
4
|
+
|
5
|
+
if Rails::VERSION::MAJOR >= 6
|
6
|
+
module ActiveStorageValidations
|
7
|
+
class AspectRatioValidator < ActiveModel::EachValidator # :nodoc
|
8
|
+
AVAILABLE_CHECKS = %i[with].freeze
|
9
|
+
PRECISION = 3
|
10
|
+
|
11
|
+
def initialize(options)
|
12
|
+
require 'mini_magick' unless defined?(MiniMagick)
|
13
|
+
super(options)
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
def check_validity!
|
18
|
+
return true if AVAILABLE_CHECKS.any? { |argument| options.key?(argument) }
|
19
|
+
raise ArgumentError, 'You must pass "aspect_ratio: :OPTION" option to the validator'
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
def validate_each(record, attribute, _value)
|
24
|
+
return true unless record.send(attribute).attached?
|
25
|
+
|
26
|
+
changes = record.attachment_changes[attribute.to_s]
|
27
|
+
return true if changes.blank?
|
28
|
+
|
29
|
+
files = Array.wrap(changes.is_a?(ActiveStorage::Attached::Changes::CreateMany) ? changes.attachables : changes.attachable)
|
30
|
+
|
31
|
+
files.each do |file|
|
32
|
+
metadata = Metadata.new(file).metadata
|
33
|
+
next if is_valid?(record, attribute, metadata)
|
34
|
+
break
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
|
42
|
+
def is_valid?(record, attribute, metadata)
|
43
|
+
if metadata[:width].to_i <= 0 || metadata[:height].to_i <= 0
|
44
|
+
add_error(record, attribute, options[:message].presence || :image_metadata_missing)
|
45
|
+
return false
|
46
|
+
end
|
47
|
+
|
48
|
+
case options[:with]
|
49
|
+
when :square
|
50
|
+
return true if metadata[:width] == metadata[:height]
|
51
|
+
add_error(record, attribute, :aspect_ratio_not_square)
|
52
|
+
|
53
|
+
when :portrait
|
54
|
+
return true if metadata[:height] > metadata[:width]
|
55
|
+
add_error(record, attribute, :aspect_ratio_not_portrait)
|
56
|
+
|
57
|
+
when :landscape
|
58
|
+
return true if metadata[:width] > metadata[:height]
|
59
|
+
add_error(record, attribute, :aspect_ratio_not_landscape)
|
60
|
+
|
61
|
+
else
|
62
|
+
if options[:with] =~ /is\_(\d*)\_(\d*)/
|
63
|
+
x = $1.to_i
|
64
|
+
y = $2.to_i
|
65
|
+
|
66
|
+
return true if (x.to_f / y).round(PRECISION) == (metadata[:width].to_f / metadata[:height]).round(PRECISION)
|
67
|
+
|
68
|
+
add_error(record, attribute, :aspect_ratio_is_not, "#{x}x#{y}")
|
69
|
+
else
|
70
|
+
add_error(record, attribute, :aspect_ratio_unknown)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
false
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
def add_error(record, attribute, type, interpolate = options[:with])
|
78
|
+
key = options[:message].presence || type
|
79
|
+
return if record.errors.added?(attribute, key)
|
80
|
+
record.errors.add(attribute, key, aspect_ratio: interpolate)
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
else
|
87
|
+
# Rails 5
|
88
|
+
module ActiveStorageValidations
|
89
|
+
class AspectRatioValidator < ActiveModel::EachValidator # :nodoc
|
90
|
+
AVAILABLE_CHECKS = %i[with].freeze
|
91
|
+
PRECISION = 3
|
92
|
+
|
93
|
+
def initialize(options)
|
94
|
+
require 'mini_magick' unless defined?(MiniMagick)
|
95
|
+
super(options)
|
96
|
+
end
|
97
|
+
|
98
|
+
|
99
|
+
def check_validity!
|
100
|
+
return true if AVAILABLE_CHECKS.any? { |argument| options.key?(argument) }
|
101
|
+
raise ArgumentError, 'You must pass "aspect_ratio: :OPTION" option to the validator'
|
102
|
+
end
|
103
|
+
|
104
|
+
|
105
|
+
def validate_each(record, attribute, _value)
|
106
|
+
return true unless record.send(attribute).attached?
|
107
|
+
|
108
|
+
files = Array.wrap(record.send(attribute))
|
109
|
+
|
110
|
+
files.each do |file|
|
111
|
+
# Analyze file first if not analyzed to get all required metadata.
|
112
|
+
file.analyze; file.reload unless file.analyzed?
|
113
|
+
metadata = file.metadata
|
114
|
+
|
115
|
+
next if is_valid?(record, attribute, metadata)
|
116
|
+
break
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
|
124
|
+
def is_valid?(record, attribute, metadata)
|
125
|
+
if metadata[:width].to_i <= 0 || metadata[:height].to_i <= 0
|
126
|
+
add_error(record, attribute, options[:message].presence || :image_metadata_missing)
|
127
|
+
return false
|
128
|
+
end
|
129
|
+
|
130
|
+
case options[:with]
|
131
|
+
when :square
|
132
|
+
return true if metadata[:width] == metadata[:height]
|
133
|
+
add_error(record, attribute, :aspect_ratio_not_square)
|
134
|
+
|
135
|
+
when :portrait
|
136
|
+
return true if metadata[:height] > metadata[:width]
|
137
|
+
add_error(record, attribute, :aspect_ratio_not_portrait)
|
138
|
+
|
139
|
+
when :landscape
|
140
|
+
return true if metadata[:width] > metadata[:height]
|
141
|
+
add_error(record, attribute, :aspect_ratio_not_landscape)
|
142
|
+
|
143
|
+
else
|
144
|
+
if options[:with] =~ /is\_(\d*)\_(\d*)/
|
145
|
+
x = $1.to_i
|
146
|
+
y = $2.to_i
|
147
|
+
|
148
|
+
return true if (x.to_f / y).round(PRECISION) == (metadata[:width].to_f / metadata[:height]).round(PRECISION)
|
149
|
+
|
150
|
+
add_error(record, attribute, :aspect_ratio_is_not, "#{x}x#{y}")
|
151
|
+
else
|
152
|
+
add_error(record, attribute, :aspect_ratio_unknown)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
false
|
156
|
+
end
|
157
|
+
|
158
|
+
|
159
|
+
def add_error(record, attribute, type, interpolate = options[:with])
|
160
|
+
key = options[:message].presence || type
|
161
|
+
return if record.errors.added?(attribute, key)
|
162
|
+
record.errors.add(attribute, key, aspect_ratio: interpolate)
|
163
|
+
end
|
164
|
+
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
@@ -1,98 +1,229 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
3
|
+
require_relative 'metadata.rb'
|
4
|
+
|
5
|
+
if Rails::VERSION::MAJOR >= 6
|
6
|
+
module ActiveStorageValidations
|
7
|
+
class DimensionValidator < ActiveModel::EachValidator # :nodoc
|
8
|
+
AVAILABLE_CHECKS = %i[width height min max].freeze
|
9
|
+
|
10
|
+
def initialize(options)
|
11
|
+
require 'mini_magick' unless defined?(MiniMagick)
|
12
|
+
|
13
|
+
[:width, :height].each do |length|
|
14
|
+
if options[length] and options[length].is_a?(Hash)
|
15
|
+
if range = options[length][:in]
|
16
|
+
raise ArgumentError, ":in must be a Range" unless range.is_a?(Range)
|
17
|
+
options[length][:min], options[length][:max] = range.min, range.max
|
18
|
+
end
|
15
19
|
end
|
16
20
|
end
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
21
|
+
[:min, :max].each do |dim|
|
22
|
+
if range = options[dim]
|
23
|
+
raise ArgumentError, ":#{dim} must be a Range (width..height)" unless range.is_a?(Range)
|
24
|
+
options[:width] = { dim => range.first }
|
25
|
+
options[:height] = { dim => range.last }
|
26
|
+
end
|
23
27
|
end
|
28
|
+
super
|
24
29
|
end
|
25
|
-
super
|
26
|
-
end
|
27
30
|
|
28
31
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
32
|
+
def check_validity!
|
33
|
+
return true if AVAILABLE_CHECKS.any? { |argument| options.key?(argument) }
|
34
|
+
raise ArgumentError, 'You must pass either :width, :height, :min or :max to the validator'
|
35
|
+
end
|
36
|
+
|
33
37
|
|
38
|
+
def validate_each(record, attribute, _value)
|
39
|
+
return true unless record.send(attribute).attached?
|
40
|
+
|
41
|
+
changes = record.attachment_changes[attribute.to_s]
|
42
|
+
return true if changes.blank?
|
43
|
+
|
44
|
+
files = Array.wrap(changes.is_a?(ActiveStorage::Attached::Changes::CreateMany) ? changes.attachables : changes.attachable)
|
45
|
+
files.each do |file|
|
46
|
+
metadata = Metadata.new(file).metadata
|
47
|
+
next if is_valid?(record, attribute, metadata)
|
48
|
+
break
|
49
|
+
end
|
50
|
+
end
|
34
51
|
|
35
|
-
def validate_each(record, attribute, _value)
|
36
|
-
return true unless record.send(attribute).attached?
|
37
52
|
|
38
|
-
|
53
|
+
def is_valid?(record, attribute, file_metadata)
|
54
|
+
# Validation fails unless file metadata contains valid width and height.
|
55
|
+
if file_metadata[:width].to_i <= 0 || file_metadata[:height].to_i <= 0
|
56
|
+
add_error(record, attribute, options[:message].presence || :image_metadata_missing)
|
57
|
+
return false
|
58
|
+
end
|
39
59
|
|
40
|
-
|
41
|
-
|
42
|
-
|
60
|
+
# Validation based on checks :min and :max (:min, :max has higher priority to :width, :height).
|
61
|
+
if options[:min] || options[:max]
|
62
|
+
if options[:min] && (
|
63
|
+
(options[:width][:min] && file_metadata[:width] < options[:width][:min]) ||
|
64
|
+
(options[:height][:min] && file_metadata[:height] < options[:height][:min])
|
65
|
+
)
|
66
|
+
add_error(record, attribute, options[:message].presence || :"dimension_min_inclusion", width: options[:width][:min], height: options[:height][:min])
|
67
|
+
return false
|
68
|
+
end
|
69
|
+
if options[:max] && (
|
70
|
+
(options[:width][:max] && file_metadata[:width] > options[:width][:max]) ||
|
71
|
+
(options[:height][:max] && file_metadata[:height] > options[:height][:max])
|
72
|
+
)
|
73
|
+
add_error(record, attribute, options[:message].presence || :"dimension_max_inclusion", width: options[:width][:max], height: options[:height][:max])
|
74
|
+
return false
|
75
|
+
end
|
43
76
|
|
44
|
-
#
|
45
|
-
|
77
|
+
# Validation based on checks :width and :height.
|
78
|
+
else
|
79
|
+
[:width, :height].each do |length|
|
80
|
+
next unless options[length]
|
81
|
+
if options[length].is_a?(Hash)
|
82
|
+
if options[length][:in] && (file_metadata[length] < options[length][:min] || file_metadata[length] > options[length][:max])
|
83
|
+
add_error(record, attribute, options[:message].presence || :"dimension_#{length}_inclusion", min: options[length][:min], max: options[length][:max])
|
84
|
+
return false
|
85
|
+
else
|
86
|
+
if options[length][:min] && file_metadata[length] < options[length][:min]
|
87
|
+
add_error(record, attribute, options[:message].presence || :"dimension_#{length}_greater_than_or_equal_to", length: options[length][:min])
|
88
|
+
return false
|
89
|
+
end
|
90
|
+
if options[length][:max] && file_metadata[length] > options[length][:max]
|
91
|
+
add_error(record, attribute, options[:message].presence || :"dimension_#{length}_less_than_or_equal_to", length: options[length][:max])
|
92
|
+
return false
|
93
|
+
end
|
94
|
+
end
|
95
|
+
else
|
96
|
+
if file_metadata[length] != options[length]
|
97
|
+
add_error(record, attribute, options[:message].presence || :"dimension_#{length}_equal_to", length: options[length])
|
98
|
+
return false
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
46
103
|
|
47
|
-
|
48
|
-
break
|
104
|
+
true # valid file
|
49
105
|
end
|
106
|
+
|
107
|
+
def add_error(record, attribute, type, *attrs)
|
108
|
+
key = options[:message].presence || type
|
109
|
+
return if record.errors.added?(attribute, key)
|
110
|
+
record.errors.add(attribute, key, *attrs)
|
111
|
+
end
|
112
|
+
|
50
113
|
end
|
114
|
+
end
|
115
|
+
|
51
116
|
|
52
117
|
|
53
|
-
|
54
|
-
|
118
|
+
else
|
119
|
+
# Rails 5
|
120
|
+
module ActiveStorageValidations
|
121
|
+
class DimensionValidator < ActiveModel::EachValidator # :nodoc
|
122
|
+
AVAILABLE_CHECKS = %i[width height min max].freeze
|
55
123
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
124
|
+
def initialize(options)
|
125
|
+
require 'mini_magick' unless defined?(MiniMagick)
|
126
|
+
|
127
|
+
[:width, :height].each do |length|
|
128
|
+
if options[length] and options[length].is_a?(Hash)
|
129
|
+
if range = options[length][:in]
|
130
|
+
raise ArgumentError, ":in must be a Range" unless range.is_a?(Range)
|
131
|
+
options[length][:min], options[length][:max] = range.min, range.max
|
132
|
+
end
|
133
|
+
end
|
63
134
|
end
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
135
|
+
[:min, :max].each do |dim|
|
136
|
+
if range = options[dim]
|
137
|
+
raise ArgumentError, ":#{dim} must be a Range (width..height)" unless range.is_a?(Range)
|
138
|
+
options[:width] = { dim => range.first }
|
139
|
+
options[:height] = { dim => range.last }
|
140
|
+
end
|
69
141
|
end
|
142
|
+
super
|
143
|
+
end
|
70
144
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
145
|
+
|
146
|
+
def check_validity!
|
147
|
+
return true if AVAILABLE_CHECKS.any? { |argument| options.key?(argument) }
|
148
|
+
raise ArgumentError, 'You must pass either :width, :height, :min or :max to the validator'
|
149
|
+
end
|
150
|
+
|
151
|
+
|
152
|
+
def validate_each(record, attribute, _value)
|
153
|
+
return true unless record.send(attribute).attached?
|
154
|
+
|
155
|
+
files = Array.wrap(record.send(attribute))
|
156
|
+
files.each do |file|
|
157
|
+
# Analyze file first if not analyzed to get all required metadata.
|
158
|
+
file.analyze; file.reload unless file.analyzed?
|
159
|
+
metadata = file.metadata rescue {}
|
160
|
+
next if is_valid?(record, attribute, metadata)
|
161
|
+
break
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
|
166
|
+
def is_valid?(record, attribute, file_metadata)
|
167
|
+
# Validation fails unless file metadata contains valid width and height.
|
168
|
+
if file_metadata[:width].to_i <= 0 || file_metadata[:height].to_i <= 0
|
169
|
+
add_error(record, attribute, options[:message].presence || :image_metadata_missing)
|
170
|
+
return false
|
171
|
+
end
|
172
|
+
|
173
|
+
# Validation based on checks :min and :max (:min, :max has higher priority to :width, :height).
|
174
|
+
if options[:min] || options[:max]
|
175
|
+
if options[:min] && (
|
176
|
+
(options[:width][:min] && file_metadata[:width] < options[:width][:min]) ||
|
177
|
+
(options[:height][:min] && file_metadata[:height] < options[:height][:min])
|
178
|
+
)
|
179
|
+
add_error(record, attribute, options[:message].presence || :"dimension_min_inclusion", width: options[:width][:min], height: options[:height][:min])
|
180
|
+
return false
|
181
|
+
end
|
182
|
+
if options[:max] && (
|
183
|
+
(options[:width][:max] && file_metadata[:width] > options[:width][:max]) ||
|
184
|
+
(options[:height][:max] && file_metadata[:height] > options[:height][:max])
|
185
|
+
)
|
186
|
+
add_error(record, attribute, options[:message].presence || :"dimension_max_inclusion", width: options[:width][:max], height: options[:height][:max])
|
187
|
+
return false
|
188
|
+
end
|
189
|
+
|
190
|
+
# Validation based on checks :width and :height.
|
191
|
+
else
|
192
|
+
[:width, :height].each do |length|
|
193
|
+
next unless options[length]
|
194
|
+
if options[length].is_a?(Hash)
|
195
|
+
if options[length][:in] && (file_metadata[length] < options[length][:min] || file_metadata[length] > options[length][:max])
|
196
|
+
add_error(record, attribute, options[:message].presence || :"dimension_#{length}_inclusion", min: options[length][:min], max: options[length][:max])
|
197
|
+
return false
|
198
|
+
else
|
199
|
+
if options[length][:min] && file_metadata[length] < options[length][:min]
|
200
|
+
add_error(record, attribute, options[:message].presence || :"dimension_#{length}_greater_than_or_equal_to", length: options[length][:min])
|
201
|
+
return false
|
202
|
+
end
|
203
|
+
if options[length][:max] && file_metadata[length] > options[length][:max]
|
204
|
+
add_error(record, attribute, options[:message].presence || :"dimension_#{length}_less_than_or_equal_to", length: options[length][:max])
|
205
|
+
return false
|
206
|
+
end
|
81
207
|
end
|
82
|
-
|
83
|
-
|
208
|
+
else
|
209
|
+
if file_metadata[length] != options[length]
|
210
|
+
add_error(record, attribute, options[:message].presence || :"dimension_#{length}_equal_to", length: options[length])
|
211
|
+
return false
|
84
212
|
end
|
85
213
|
end
|
86
|
-
else
|
87
|
-
if file_metadata[length] != options[length]
|
88
|
-
valid = record.errors.add(attribute, options[:message].presence || :"dimension_#{length}_equal_to", length: options[length]).empty?
|
89
|
-
end
|
90
214
|
end
|
91
215
|
end
|
216
|
+
|
217
|
+
true # valid file
|
92
218
|
end
|
93
219
|
|
94
|
-
|
95
|
-
|
220
|
+
def add_error(record, attribute, type, *attrs)
|
221
|
+
key = options[:message].presence || type
|
222
|
+
return if record.errors.added?(attribute, key)
|
223
|
+
record.errors.add(attribute, key, *attrs)
|
224
|
+
end
|
96
225
|
|
226
|
+
end
|
97
227
|
end
|
98
|
-
|
228
|
+
|
229
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module ActiveStorageValidations
|
2
|
+
class Metadata
|
3
|
+
attr_reader :file
|
4
|
+
|
5
|
+
def initialize(file)
|
6
|
+
@file = file
|
7
|
+
end
|
8
|
+
|
9
|
+
def metadata
|
10
|
+
read_image do |image|
|
11
|
+
if rotated_image?(image)
|
12
|
+
{ width: image.height, height: image.width }
|
13
|
+
else
|
14
|
+
{ width: image.width, height: image.height }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def read_image
|
22
|
+
if file.is_a?(String)
|
23
|
+
blob = ActiveStorage::Blob.find_signed(file)
|
24
|
+
|
25
|
+
tempfile = Tempfile.new(["ActiveStorage-#{blob.id}-", blob.filename.extension_with_delimiter])
|
26
|
+
tempfile.binmode
|
27
|
+
|
28
|
+
blob.download do |chunk|
|
29
|
+
tempfile.write(chunk)
|
30
|
+
end
|
31
|
+
|
32
|
+
tempfile.flush
|
33
|
+
tempfile.rewind
|
34
|
+
|
35
|
+
image = MiniMagick::Image.new(tempfile.path)
|
36
|
+
else
|
37
|
+
image = MiniMagick::Image.new(read_file_path)
|
38
|
+
end
|
39
|
+
|
40
|
+
if image.valid?
|
41
|
+
yield image
|
42
|
+
else
|
43
|
+
logger.info "Skipping image analysis because ImageMagick doesn't support the file"
|
44
|
+
{}
|
45
|
+
end
|
46
|
+
rescue LoadError
|
47
|
+
logger.info "Skipping image analysis because the mini_magick gem isn't installed"
|
48
|
+
{}
|
49
|
+
rescue MiniMagick::Error => error
|
50
|
+
logger.error "Skipping image analysis due to an ImageMagick error: #{error.message}"
|
51
|
+
{}
|
52
|
+
ensure
|
53
|
+
image = nil
|
54
|
+
end
|
55
|
+
|
56
|
+
def rotated_image?(image)
|
57
|
+
%w[ RightTop LeftBottom ].include?(image["%[orientation]"])
|
58
|
+
end
|
59
|
+
|
60
|
+
def read_file_path
|
61
|
+
case file
|
62
|
+
when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile
|
63
|
+
file.path
|
64
|
+
when Hash
|
65
|
+
File.open(file.fetch(:io)).path
|
66
|
+
else
|
67
|
+
raise "Something wrong with params."
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def logger
|
72
|
+
Rails.logger
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
end
|
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: 0.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Igor Kasyanchuk
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-08-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -30,14 +30,14 @@ dependencies:
|
|
30
30
|
requirements:
|
31
31
|
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
33
|
+
version: 4.9.5
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version:
|
40
|
+
version: 4.9.5
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: pry
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -80,6 +80,20 @@ dependencies:
|
|
80
80
|
- - ">="
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: coffee-rails
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
83
97
|
description: Validations for Active Storage (presence)
|
84
98
|
email:
|
85
99
|
- igorkasyanchuk@gmail.com
|
@@ -93,16 +107,18 @@ files:
|
|
93
107
|
- config/locales/de.yml
|
94
108
|
- config/locales/en.yml
|
95
109
|
- lib/active_storage_validations.rb
|
110
|
+
- lib/active_storage_validations/aspect_ratio_validator.rb
|
96
111
|
- lib/active_storage_validations/attached_validator.rb
|
97
112
|
- lib/active_storage_validations/content_type_validator.rb
|
98
113
|
- lib/active_storage_validations/dimension_validator.rb
|
99
114
|
- lib/active_storage_validations/engine.rb
|
100
115
|
- lib/active_storage_validations/limit_validator.rb
|
116
|
+
- lib/active_storage_validations/metadata.rb
|
101
117
|
- lib/active_storage_validations/railtie.rb
|
102
118
|
- lib/active_storage_validations/size_validator.rb
|
103
119
|
- lib/active_storage_validations/version.rb
|
104
120
|
- lib/tasks/active_storage_validations_tasks.rake
|
105
|
-
homepage: https://github.com/igorkasyanchuk
|
121
|
+
homepage: https://github.com/igorkasyanchuk/active_storage_validations
|
106
122
|
licenses:
|
107
123
|
- MIT
|
108
124
|
metadata: {}
|