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 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: {}