active_storage_validations 0.7.1 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 77830eb3fb31d94775a69c7c76aceecc968c359359c2652221d5bfb75469ed4c
4
- data.tar.gz: ae4fb05883cf2fc96f8f01a6a893949ca1c567a502cbfbb98c53b37b6809287e
3
+ metadata.gz: 90da6d94caed4f46393b01b35be7b3d0015304c991c4a04919122ec0256a2692
4
+ data.tar.gz: 3ea9462025ac280ac8201f46448c721e4d6633fcb13403c0fce75a5e89b841ad
5
5
  SHA512:
6
- metadata.gz: 1993163590b8360ff78542812a3e69f842de7f2b9868351a9c4bddf60a51c8cbc243d1abd676c0b767581ab3a9063d8fa112739170a8d6b721252d8e0fae9456
7
- data.tar.gz: abf00aa0ac44061f51a884b9362104b1d660311d6f4ac8f6cc3d464ff3cc695da0fe7dbb8eba549aa11d51ef09384ec1e486a89c8fe27497b614b8de9f962020
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 `rake test`.
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
 
@@ -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"
@@ -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
- module ActiveStorageValidations
4
- class DimensionValidator < ActiveModel::EachValidator # :nodoc
5
- AVAILABLE_CHECKS = %i[width height min max].freeze
6
-
7
- def initialize(options)
8
- require 'mini_magick'
9
-
10
- [:width, :height].each do |length|
11
- if options[length] and options[length].is_a?(Hash)
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
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
- 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 }
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
- def check_validity!
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'
32
- end
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
- files = Array.wrap(record.send(attribute))
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
- files.each do |file|
41
- # Analyze file first if not analyzed to get all required metadata.
42
- file.analyze; file.reload unless file.analyzed?
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
- # File has no dimension and no width and height in metadata.
45
- raise StandardError, 'File has no dimension and no width and height in metadata' unless (['width', 'height'] - file.metadata.keys).empty?
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
- next if dimension_valid?(record, attribute, file.metadata)
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
- def dimension_valid?(record, attribute, file_metadata)
54
- valid = true
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
- # Validation based on checks :min and :max (:min, :max has higher priority to :width, :height).
57
- if options[:min] || options[:max]
58
- if options[:min] && (
59
- (options[:width][:min] && file_metadata[:width] < options[:width][:min]) ||
60
- (options[:height][:min] && file_metadata[:height] < options[:height][:min])
61
- )
62
- valid = record.errors.add(attribute, options[:message].presence || :"dimension_min_inclusion", width: options[:width][:min], height: options[:height][:min]).empty?
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
- if options[:max] && (
65
- (options[:width][:max] && file_metadata[:width] > options[:width][:max]) ||
66
- (options[:height][:max] && file_metadata[:height] > options[:height][:max])
67
- )
68
- valid = record.errors.add(attribute, options[:message].presence || :"dimension_max_inclusion", width: options[:width][:max], height: options[:height][:max]).empty?
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
- # Validation based on checks :width and :height.
72
- else
73
- [:width, :height].each do |length|
74
- next unless options[length]
75
- if options[length].is_a?(Hash)
76
- if options[length][:in] && (file_metadata[length] < options[length][:min] || file_metadata[length] > options[length][:max])
77
- valid = record.errors.add(attribute, options[:message].presence || :"dimension_#{length}_inclusion", min: options[length][:min], max: options[length][:max]).empty?
78
- else
79
- if options[length][:min] && file_metadata[length] < options[length][:min]
80
- valid = record.errors.add(attribute, options[:message].presence || :"dimension_#{length}_greater_than_or_equal_to", length: options[length][:min]).empty?
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
- if options[length][:max] && file_metadata[length] > options[length][:max]
83
- valid = record.errors.add(attribute, options[:message].presence || :"dimension_#{length}_less_than_or_equal_to", length: options[length][:max]).empty?
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
- valid
95
- end
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
- end
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveStorageValidations
4
- VERSION = '0.7.1'
4
+ VERSION = '0.8.0'
5
5
  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.7.1
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-04-22 00:00:00.000000000 Z
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: '4.9'
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: '4.9'
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: {}