active_storage_validations 1.3.5 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +620 -279
  3. data/config/locales/da.yml +50 -29
  4. data/config/locales/de.yml +50 -29
  5. data/config/locales/en.yml +50 -29
  6. data/config/locales/es.yml +50 -29
  7. data/config/locales/fr.yml +50 -29
  8. data/config/locales/it.yml +50 -29
  9. data/config/locales/ja.yml +50 -29
  10. data/config/locales/nl.yml +50 -29
  11. data/config/locales/pl.yml +50 -29
  12. data/config/locales/pt-BR.yml +50 -29
  13. data/config/locales/ru.yml +50 -29
  14. data/config/locales/sv.yml +50 -29
  15. data/config/locales/tr.yml +50 -29
  16. data/config/locales/uk.yml +50 -29
  17. data/config/locales/vi.yml +50 -29
  18. data/config/locales/zh-CN.yml +50 -29
  19. data/lib/active_storage_validations/analyzer/audio_analyzer.rb +58 -0
  20. data/lib/active_storage_validations/analyzer/content_type_analyzer.rb +60 -0
  21. data/lib/active_storage_validations/analyzer/image_analyzer/image_magick.rb +12 -11
  22. data/lib/active_storage_validations/analyzer/image_analyzer/vips.rb +12 -12
  23. data/lib/active_storage_validations/analyzer/image_analyzer.rb +18 -46
  24. data/lib/active_storage_validations/analyzer/null_analyzer.rb +2 -2
  25. data/lib/active_storage_validations/analyzer/shared/asv_ff_probable.rb +61 -0
  26. data/lib/active_storage_validations/analyzer/video_analyzer.rb +130 -0
  27. data/lib/active_storage_validations/analyzer.rb +54 -1
  28. data/lib/active_storage_validations/aspect_ratio_validator.rb +154 -120
  29. data/lib/active_storage_validations/{base_size_validator.rb → base_comparison_validator.rb} +18 -16
  30. data/lib/active_storage_validations/content_type_validator.rb +51 -17
  31. data/lib/active_storage_validations/dimension_validator.rb +20 -19
  32. data/lib/active_storage_validations/duration_validator.rb +55 -0
  33. data/lib/active_storage_validations/extensors/asv_blob_metadatable.rb +24 -0
  34. data/lib/active_storage_validations/{marcel_extensor.rb → extensors/asv_marcelable.rb} +5 -0
  35. data/lib/active_storage_validations/limit_validator.rb +14 -2
  36. data/lib/active_storage_validations/matchers/aspect_ratio_validator_matcher.rb +1 -1
  37. data/lib/active_storage_validations/matchers/{base_size_validator_matcher.rb → base_comparison_validator_matcher.rb} +31 -25
  38. data/lib/active_storage_validations/matchers/content_type_validator_matcher.rb +7 -3
  39. data/lib/active_storage_validations/matchers/dimension_validator_matcher.rb +1 -1
  40. data/lib/active_storage_validations/matchers/duration_validator_matcher.rb +39 -0
  41. data/lib/active_storage_validations/matchers/{processable_image_validator_matcher.rb → processable_file_validator_matcher.rb} +5 -5
  42. data/lib/active_storage_validations/matchers/size_validator_matcher.rb +18 -2
  43. data/lib/active_storage_validations/matchers/total_size_validator_matcher.rb +18 -2
  44. data/lib/active_storage_validations/matchers.rb +4 -3
  45. data/lib/active_storage_validations/{processable_image_validator.rb → processable_file_validator.rb} +4 -3
  46. data/lib/active_storage_validations/railtie.rb +5 -0
  47. data/lib/active_storage_validations/shared/asv_active_storageable.rb +2 -2
  48. data/lib/active_storage_validations/shared/asv_analyzable.rb +38 -3
  49. data/lib/active_storage_validations/shared/asv_attachable.rb +36 -15
  50. data/lib/active_storage_validations/size_validator.rb +11 -3
  51. data/lib/active_storage_validations/total_size_validator.rb +9 -3
  52. data/lib/active_storage_validations/version.rb +1 -1
  53. data/lib/active_storage_validations.rb +7 -3
  54. metadata +14 -8
  55. data/lib/active_storage_validations/content_type_spoof_detector.rb +0 -96
data/README.md CHANGED
@@ -6,330 +6,724 @@
6
6
  [![MiniTest](https://github.com/igorkasyanchuk/active_storage_validations/workflows/MiniTest/badge.svg)](https://github.com/igorkasyanchuk/active_storage_validations/actions)
7
7
  [![RailsJazz](https://github.com/igorkasyanchuk/rails_time_travel/blob/main/docs/my_other.svg?raw=true)](https://www.railsjazz.com)
8
8
  [![https://www.patreon.com/igorkasyanchuk](https://github.com/igorkasyanchuk/rails_time_travel/blob/main/docs/patron.svg?raw=true)](https://www.patreon.com/igorkasyanchuk)
9
- [![Listed on OpenSource-Heroes.com](https://opensource-heroes.com/badge-v1.svg)](https://opensource-heroes.com/r/igorkasyanchuk/active_storage_validations)
10
9
 
11
- If you are using `active_storage` gem and you want to add simple validations for it, like presence or content_type you need to write a custom validation method.
12
10
 
13
- This gems doing it for you. Just use `attached: true` or `content_type: 'image/png'` validation.
11
+ Active Storage Validations is a gem that allows you to add validations for Active Storage attributes.
14
12
 
15
- ## What it can do
13
+ This gems is doing it right for you! Just use `validates :avatar, attached: true, content_type: 'image/png'` and that's it!
16
14
 
17
- * validates if file(s) attached
18
- * validates content type
19
- * validates size of files
20
- * validates total size of files
21
- * validates dimension of images/videos
22
- * validates number of uploaded files (min/max required)
23
- * validates aspect ratio (if square, portrait, landscape, is_16_9, ...)
24
- * validates if file can be processed by MiniMagick or Vips
25
- * custom error messages
26
- * allow procs for dynamic determination of values
15
+ ## Table of Contents
27
16
 
28
- ## Usage
17
+ - [Getting started](#getting-started)
18
+ - [Installation](#installation)
19
+ - [Error messages (I18n)](#error-messages-i18n)
20
+ - [Using image metadata validators](#using-image-metadata-validators)
21
+ - [Using video and audio metadata validators](#using-video-and-audio-metadata-validators)
22
+ - [Validators](#validators)
23
+ - [Attached](#attached)
24
+ - [Limit](#limit)
25
+ - [Content type](#content-type)
26
+ - [Size](#size)
27
+ - [Total size](#total-size)
28
+ - [Dimension](#dimension)
29
+ - [Duration](#duration)
30
+ - [Aspect ratio](#aspect-ratio)
31
+ - [Processable file](#processable-file)
32
+ - [Upgrading from 1.x to 2.x](#upgrading-from-1x-to-2x)
33
+ - [Internationalization (I18n)](#internationalization-i18n)
34
+ - [Test matchers](#test-matchers)
35
+ - [Contributing](#contributing)
36
+ - [Additional information](#additional-information)
29
37
 
30
- For example you have a model like this and you want to add validation.
38
+ ## Getting started
39
+
40
+ ### Installation
41
+
42
+ Active Storage Validations work with Rails 6.1.4 onwards. Add this line to your application's Gemfile:
31
43
 
32
44
  ```ruby
33
- class User < ApplicationRecord
34
- has_one_attached :avatar
35
- has_many_attached :photos
36
- has_one_attached :image
37
-
38
- validates :name, presence: true
39
-
40
- validates :avatar, attached: true, content_type: 'image/png',
41
- dimension: { width: 200, height: 200 }
42
- validates :photos, attached: true, content_type: ['image/png', 'image/jpeg'],
43
- dimension: { width: { min: 800, max: 2400 },
44
- height: { min: 600, max: 1800 }, message: 'is not given between dimension' }
45
- validates :image, attached: true,
46
- processable_image: true,
47
- content_type: ['image/png', 'image/jpeg'],
48
- aspect_ratio: :landscape
49
- end
45
+ gem 'active_storage_validations'
46
+ ```
47
+
48
+ And then execute:
49
+
50
+ ```sh
51
+ $ bundle
50
52
  ```
51
53
 
52
- or
54
+ ### Error messages (I18n)
55
+
56
+ Once you have installed the gem, you need to add the gem I18n error messages to your app. See [Internationalization (I18n)](#internationalization-i18n) section for more details.
57
+
58
+ ### Using image metadata validators
59
+
60
+ Optionally, to use the image metadata validators (`dimension`, `aspect_ratio` and `processable_file`), you will have to add one of the corresponding gems:
61
+
62
+ ```ruby
63
+ gem 'mini_magick', '>= 4.9.5'
64
+ # Or
65
+ gem 'ruby-vips', '>= 2.1.0'
66
+ ```
67
+
68
+ Plus, you have to be sure to have the corresponding command-line tool installed on your system. For example, to use `mini_magick` gem, you need to have `imagemagick` installed on your system (both on your local and in your CI / production environments).
69
+
70
+ ### Using video and audio metadata validators
71
+
72
+ To use the video and audio metadata validators (`dimension`, `aspect_ratio`, `processable_file` and `duration`), you will not need to add any gems. However you will need to have the `ffmpeg` command-line tool installed on your system (once again, be sure to have it installed both on your local and in your CI / production environments).
73
+
74
+ ### Using content type spoofing protection validator option
75
+
76
+ To use the `spoofing_protection` option with the `content_type` validator, you only need to have the UNIX `file` command on your system.
77
+
78
+ ## Validators
79
+
80
+ **List of validators:**
81
+ - [Attached](#attached): validates if file(s) attached
82
+ - [Limit](#limit): validates number of uploaded files
83
+ - [Content type](#content-type): validates file content type
84
+ - [Size](#size): validates file size
85
+ - [Total size](#total-size): validates total file size for several files
86
+ - [Dimension](#dimension): validates image / video dimensions
87
+ - [Duration](#duration): validates video / audio duration
88
+ - [Aspect ratio](#aspect-ratio): validates image / video aspect ratio
89
+ - [Processable file](#processable-file): validates if a file can be processed
90
+ <br>
91
+ <br>
53
92
 
93
+ **Proc usage**<br>
94
+ Every validator can use procs instead of values in all the validator examples:
54
95
  ```ruby
55
- class Project < ApplicationRecord
56
- has_one_attached :logo
57
- has_one_attached :preview
58
- has_one_attached :attachment
59
- has_many_attached :documents
60
-
61
- validates :title, presence: true
62
-
63
- validates :logo, attached: true, size: { less_than: 100.megabytes , message: 'is too large' }
64
- validates :preview, attached: true, size: { between: 1.kilobyte..100.megabytes , message: 'is not given between size' }
65
- validates :attachment, attached: true, content_type: { in: 'application/pdf', message: 'is not a PDF' }
66
- validates :documents, limit: { min: 1, max: 3 }, total_size: { less_than: 5.megabytes }
96
+ class User < ApplicationRecord
97
+ has_many_attached :files
98
+
99
+ validates :files, limit: { max: -> (record) { record.admin? ? 100 : 10 } }
67
100
  end
68
101
  ```
69
102
 
70
- ### More examples
103
+ **Performance optimization**<br>
104
+ Some validators rely on an expensive operation (metadata analysis and content type analysis). To mitigate the performance cost, the gem leverages the `ActiveStorage::Blob.metadata` method to store retrieved metadata. Therefore, once the file has been analyzed by our gem, the expensive analysis operation will not be triggered again for new validations.
105
+
106
+ As stated in the Rails documentation: "Blobs are intended to be immutable in so far as their reference to a specific file goes". We based our performance optimization on the same assumption, so if you do not follow it, the gem will not work as expected.
107
+
108
+ ---
109
+
110
+ ### Attached
111
+
112
+ Validates if the attachment is present.
113
+
114
+ #### Options
71
115
 
72
- - Content type validation using symbols or regex.
116
+ The `attached` validator has no options.
73
117
 
118
+ #### Examples
119
+
120
+ Use it like this:
74
121
  ```ruby
75
122
  class User < ApplicationRecord
76
123
  has_one_attached :avatar
77
- has_many_attached :photos
78
124
 
79
- validates :avatar, attached: true, content_type: :png
80
- # or
81
- validates :photos, attached: true, content_type: [:png, :jpg, :jpeg]
82
- # or
83
- validates :avatar, content_type: /\Aimage\/.*\z/
125
+ validates :avatar, attached: true # ensures that avatar has an attached file
84
126
  end
85
127
  ```
86
- Please note that the symbol types must be registered by [`Marcel::EXTENSIONS`](https://github.com/rails/marcel/blob/main/lib/marcel/tables.rb) that's used by this gem to infer the full content type.
87
- Example code for adding a new content type to Marcel:
88
- ```ruby
89
- # config/initializers/mime_types.rb
90
- Marcel::MimeType.extend "application/ino", extensions: %w(ino), parents: "text/plain" # Registering arduino INO files
128
+
129
+ #### Error messages (I18n)
130
+
131
+ ```yml
132
+ en:
133
+ errors:
134
+ messages:
135
+ blank: "can't be blank"
91
136
  ```
92
137
 
93
- **Content type spoofing protection**
138
+ The error message for this validator relies on Rails own `blank` error message.
94
139
 
95
- File content type spoofing happens when an ill-intentioned user uploads a file which hides its true content type by faking its extension and its declared content type value. For example, a user may try to upload a `.exe` file (application/x-msdownload content type) dissimulated as a `.jpg` file (image/jpeg content type).
140
+ ---
96
141
 
97
- By default, the gem does not prevent content type spoofing (prevent it by default is a breaking change that will be implemented in v2). The spoofing protection relies on both the linux `file` command and `Marcel` gem. Be careful, since it needs to load the whole file io to perform the analysis, it will use a lot of RAM for very large files. Therefore it could be a wise decision not to enable it in this case.
142
+ ### Limit
98
143
 
99
- Take note that the `file` analyzer will not find the exactly same content type as the ActiveStorage blob (its content type detection relies on a different logic using content+filename+extension). To handle this issue, we consider a close parent content type to be a match. For example, for an ActiveStorage blob which content type is `video/x-ms-wmv`, the `file` analyzer will probably detect a `video/x-ms-asf` content type, this will be considered as a valid match because these 2 content types are closely related. The correlation mapping is based on `Marcel::TYPE_PARENTS`.
144
+ Validates the number of uploaded files.
100
145
 
101
- The difficulty to accurately predict a mime type may generate false positives, if so there are two solutions available:
102
- - If the ActiveStorage blob content type is closely related to the detected content type using the `file` analyzer, you can enhance `Marcel::TYPE_PARENTS` mapping using `Marcel::MimeType.extend "application/x-rar-compressed", parents: %(application/x-rar)` in the `config/initializers/mime_types.rb` file. (Please drop an issue so we can add it to the gem for everyone!)
103
- - If the ActiveStorage blob content type is not closely related, you still can disable the content type spoofing protection in the validator, if so, please drop us an issue so we can fix it for everyone!
146
+ #### Options
104
147
 
148
+ The `limit` validator has 2 possible options:
149
+ - `min`: defines the minimum allowed number of files
150
+ - `max`: defines the maximum allowed number of files
151
+
152
+ #### Examples
153
+
154
+ Use it like this:
105
155
  ```ruby
106
156
  class User < ApplicationRecord
107
- has_one_attached :avatar
157
+ has_many_attached :certificates
108
158
 
109
- validates :avatar, attached: true, content_type: :png # spoofing_protection not enabled, at your own risks!
110
- validates :avatar, attached: true, content_type: { with: :png, spoofing_protection: true } # spoofing_protection enabled
159
+ validates :certificates, limit: { min: 1, max: 10 } # restricts the number of files to between 1 and 10
111
160
  end
112
161
  ```
113
162
 
163
+ #### Error messages (I18n)
114
164
 
115
- - Dimension validation with `width`, `height` and `in`.
165
+ ```yml
166
+ en:
167
+ errors:
168
+ messages:
169
+ limit_out_of_range:
170
+ zero: "no files attached (must have between %{min} and %{max} files)"
171
+ one: "only 1 file attached (must have between %{min} and %{max} files)"
172
+ other: "total number of files must be between %{min} and %{max} files (there are %{count} files attached)"
173
+ limit_min_not_reached:
174
+ zero: "no files attached (must have at least %{min} files)"
175
+ one: "only 1 file attached (must have at least %{min} files)"
176
+ other: "%{count} files attached (must have at least %{min} files)"
177
+ limit_max_exceeded:
178
+ zero: "no files attached (maximum is %{max} files)"
179
+ one: "too many files attached (maximum is %{max} files, got %{count})"
180
+ other: "too many files attached (maximum is %{max} files, got %{count})"
181
+ ```
182
+
183
+ The `limit` validator error messages expose 3 values that you can use:
184
+ - `min` containing the minimum allowed number of files (e.g. `1`)
185
+ - `max` containing the maximum allowed number of files (e.g. `10`)
186
+ - `count` containing the current number of files (e.g. `5`)
187
+
188
+ ---
189
+
190
+ ### Content type
191
+
192
+ Validates if the attachment has an allowed content type.
116
193
 
194
+ #### Options
195
+
196
+ The `content_type` validator has 3 possible options:
197
+ - `with`: defines the exact allowed content type (string, symbol or regex)
198
+ - `in`: defines the allowed content types (array of strings or symbols)
199
+ - `spoofing_protection`: enables content type spoofing protection (boolean, defaults to `false`)
200
+
201
+ As mentioned above, this validator can define content types in several ways:
202
+ - String: `image/png` or `png`
203
+ - Symbol: `:png`
204
+ - Regex: `/\Avideo\/.*\z/`
205
+
206
+ #### Examples
207
+
208
+ Use it like this:
117
209
  ```ruby
118
210
  class User < ApplicationRecord
119
211
  has_one_attached :avatar
120
- has_many_attached :photos
121
212
 
122
- validates :avatar, dimension: { width: { in: 80..100 }, message: 'is not given between dimension' }
123
- validates :photos, dimension: { height: { in: 600..1800 } }
213
+ validates :avatar, content_type: 'image/png' # only allows PNG images
214
+ validates :avatar, content_type: :png # only allows PNG images, same as { with: :png }
215
+ validates :avatar, content_type: /\Avideo\/.*\z/ # only allows video files
216
+ validates :avatar, content_type: ['image/png', 'image/jpeg'] # only allows PNG and JPEG images
217
+ validates :avatar, content_type: { in: [:png, :jpeg], spoofing_protection: true } # only allows PNG, JPEG and their variants, with spoofing protection enabled
124
218
  end
125
219
  ```
126
220
 
127
- - Dimension validation with `min` and `max` range for width and height:
221
+ #### Best practices
222
+
223
+ When using the `content_type` validator, it is recommended to reflect the allowed content types in the html [`accept` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept) in the corresponding file field in your views. This will prevent users from trying to upload files with not allowed content types (however it is only an UX improvement, a malicious user can still try to upload files with not allowed content types therefore the backend validation).
128
224
 
225
+ For example, if you want to allow PNG and JPEG images only, you can do this:
129
226
  ```ruby
130
227
  class User < ApplicationRecord
228
+ ACCEPTED_CONTENT_TYPES = ['image/png', 'image/jpeg'].freeze
229
+
131
230
  has_one_attached :avatar
132
- has_many_attached :photos
133
-
134
- validates :avatar, dimension: { min: 200..100 }
135
- # Equivalent to:
136
- # validates :avatar, dimension: { width: { min: 200 }, height: { min: 100 } }
137
- validates :photos, dimension: { min: 200..100, max: 400..200 }
138
- # Equivalent to:
139
- # validates :avatar, dimension: { width: { min: 200, max: 400 }, height: { min: 100, max: 200 } }
231
+
232
+ validates :avatar, content_type: ACCEPTED_CONTENT_TYPES
140
233
  end
141
234
  ```
142
235
 
143
- - Aspect ratio validation:
236
+ ```erb
237
+ <%= form_with model: @user do |f| %>
238
+ <%= f.file_field :avatar,
239
+ accept: ACCEPTED_CONTENT_TYPES.join(',') %>
240
+ <% end %>
241
+ ```
242
+
243
+ #### Content type shorthands
144
244
 
245
+ If you choose to use a content_type 'shorthand' (like `png`), note that it will be converted to a full content type using `Marcel::MimeType.for` under the hood. Therefore, you should check if the content_type is registered by [`Marcel::EXTENSIONS`](https://github.com/rails/marcel/blob/main/lib/marcel/tables.rb). If it's not, you can register it by adding the following code to your `config/initializers/mime_types.rb` file:
145
246
  ```ruby
146
- class User < ApplicationRecord
147
- has_one_attached :avatar
148
- has_one_attached :photo
149
- has_many_attached :photos
247
+ Marcel::MimeType.extend "application/ino", extensions: %w(ino), parents: "text/plain" # Registering arduino INO files
248
+ ```
150
249
 
151
- validates :avatar, aspect_ratio: :square
152
- validates :photo, aspect_ratio: :landscape
250
+ #### Content type spoofing protection
153
251
 
154
- # you can also pass dynamic aspect ratio, like :is_4_3, :is_16_9, etc
155
- validates :photos, aspect_ratio: :is_4_3
156
- end
252
+ By default, the gem does not prevent content type spoofing. You can enable it by setting the `spoofing_protection` option to `true` in your validator options.
253
+
254
+ <details>
255
+ <summary>
256
+ ##### What is content type spoofing?
257
+ </summary>
258
+ File content type spoofing happens when an ill-intentioned user uploads a file which hides its true content type by faking its extension and its declared content type value. For example, a user may try to upload a `.exe` file (application/x-msdownload content type) dissimulated as a `.jpg` file (image/jpeg content type).
259
+ </details>
260
+
261
+ <details>
262
+ <summary>
263
+ ##### How do we prevent it?
264
+ </summary>
265
+ The spoofing protection relies on both the UNIX `file` command and `Marcel` gem. Be careful, since it needs to load the whole file io to perform the analysis, it will use a lot of RAM for very large files. Therefore it could be a wise decision not to enable it in this case.
266
+
267
+ Take note that the `file` analyzer will not find the exactly same content type as the ActiveStorage blob (ActiveStorage content type detection relies on a different logic using first 4kb of content + filename + extension). To handle this issue, we consider a close parent content type to be a match. For example, for an ActiveStorage blob which content type is `video/x-ms-wmv`, the `file` analyzer will probably detect a `video/x-ms-asf` content type, this will be considered as a valid match because these 2 content types are closely related. The correlation mapping is based on `Marcel::TYPE_PARENTS` table.
268
+ </details>
269
+
270
+ <details>
271
+ <summary>
272
+ ##### Edge cases
273
+ </summary>
274
+ The difficulty to accurately predict a mime type may generate false positives, if so there are two solutions available:
275
+ - If the ActiveStorage blob content type is closely related to the detected content type using the `file` analyzer, you can enhance `Marcel::TYPE_PARENTS` mapping using `Marcel::MimeType.extend "application/x-rar-compressed", parents: %(application/x-rar)` in the `config/initializers/mime_types.rb` file. (Please drop an issue so we can add it to the gem for everyone!)
276
+ - If the ActiveStorage blob content type is not closely related, you still can disable the content type spoofing protection in the validator, if so, please drop us an issue so we can fix it for everyone!
277
+ </details>
278
+
279
+
280
+ #### Error messages (I18n)
281
+
282
+ ```yml
283
+ en:
284
+ errors:
285
+ messages:
286
+ content_type_invalid:
287
+ one: "has an invalid content type (authorized content type is %{authorized_human_content_types})"
288
+ other: "has an invalid content type (authorized content types are %{authorized_human_content_types})"
289
+ content_type_spoofed:
290
+ one: "has a content type that is not equivalent to the one that is detected through its content (authorized content type is %{authorized_human_content_types})"
291
+ other: "has a content type that is not equivalent to the one that is detected through its content (authorized content types are %{authorized_human_content_types})"
157
292
  ```
158
293
 
159
- - Proc Usage:
294
+ The `content_type` validator error messages expose 7 values that you can use:
295
+ - `content_type` containing the content type of the sent file (e.g. `image/png`)
296
+ - `human_content_type` containing a more user-friendly version of the sent file content type (e.g. 'TXT' for 'text/plain')
297
+ - `detected_content_type` containing the detected content type of the sent file using `spoofing_protection` option (e.g. `image/png`)
298
+ - `detected_human_content_type` containing a more user-friendly version of the sent file detected content type using `spoofing_protection` option (e.g. 'TXT' for 'text/plain')
299
+ - `authorized_human_content_types` containing the list of authorized content types (e.g. 'PNG, JPEG' for `['image/png', 'image/jpeg']`)
300
+ - `count` containing the number of authorized content types (e.g. `2`)
301
+ - `filename` containing the filename
302
+
303
+ ---
304
+
305
+ ### Size
160
306
 
161
- Procs can be used instead of values in all the above examples. They will be called on every validation.
307
+ Validates each attached file size.
308
+
309
+ #### Options
310
+
311
+ The `size` validator has 5 possible options:
312
+ - `less_than`: defines the strict maximum allowed file size
313
+ - `less_than_or_equal_to`: defines the maximum allowed file size
314
+ - `greater_than`: defines the strict minimum allowed file size
315
+ - `greater_than_or_equal_to`: defines the minimum allowed file size
316
+ - `between`: defines the allowed file size range
317
+
318
+ #### Examples
319
+
320
+ Use it like this:
162
321
  ```ruby
163
322
  class User < ApplicationRecord
164
- has_many_attached :proc_files
323
+ has_one_attached :avatar
165
324
 
166
- validates :proc_files, limit: { max: -> (record) { record.admin? ? 100 : 10 } }
325
+ validates :avatar, size: { less_than: 2.megabytes } # restricts the file size to < 2MB
326
+ validates :avatar, size: { less_than_or_equal_to: 2.megabytes } # restricts the file size to <= 2MB
327
+ validates :avatar, size: { greater_than: 1.kilobyte } # restricts the file size to > 1KB
328
+ validates :avatar, size: { greater_than_or_equal_to: 1.kilobyte } # restricts the file size to >= 1KB
329
+ validates :avatar, size: { between: 1.kilobyte..2.megabytes } # restricts the file size to between 1KB and 2MB
167
330
  end
168
-
169
331
  ```
170
332
 
171
- ## Internationalization (I18n)
333
+ #### Best practices
172
334
 
173
- Active Storage Validations uses I18n for error messages. For this, add these keys in your translation file:
335
+ It is always a good practice to limit the maximum file size to a reasonable value (like 2MB for avatar images). This helps prevent server storage issues, reduces upload/download times, and ensures better performance. Large files can consume excessive bandwidth and storage space, potentially impacting both server resources and user experience.
336
+ Plus, not setting a size limit inside your Rails app might lead into your server throwing a `413 Content Too Large` error, which is not as nice as a Rails validation error.
337
+
338
+ #### Error messages (I18n)
174
339
 
175
340
  ```yml
176
341
  en:
177
342
  errors:
178
343
  messages:
179
- content_type_invalid: "has an invalid content type"
180
- file_size_not_less_than: "file size must be less than %{max_size} (current size is %{file_size})"
181
- file_size_not_less_than_or_equal_to: "file size must be less than or equal to %{max_size} (current size is %{file_size})"
182
- file_size_not_greater_than: "file size must be greater than %{min_size} (current size is %{file_size})"
183
- file_size_not_greater_than_or_equal_to: "file size must be greater than or equal to %{min_size} (current size is %{file_size})"
184
- file_size_not_between: "file size must be between %{min_size} and %{max_size} (current size is %{file_size})"
185
- total_file_size_not_less_than: "total file size must be less than %{max_size} (current size is %{total_file_size})"
186
- total_file_size_not_less_than_or_equal_to: "total file size must be less than or equal to %{max_size} (current size is %{total_file_size})"
187
- total_file_size_not_greater_than: "total file size must be greater than %{min_size} (current size is %{total_file_size})"
188
- total_file_size_not_greater_than_or_equal_to: "total file size must be greater than or equal to %{min_size} (current size is %{total_file_size})"
189
- total_file_size_not_between: "total file size must be between %{min_size} and %{max_size} (current size is %{total_file_size})"
190
- limit_out_of_range: "total number is out of range"
191
- image_metadata_missing: "is not a valid image"
192
- dimension_min_inclusion: "must be greater than or equal to %{width} x %{height} pixel"
193
- dimension_max_inclusion: "must be less than or equal to %{width} x %{height} pixel"
194
- dimension_width_inclusion: "width is not included between %{min} and %{max} pixel"
195
- dimension_height_inclusion: "height is not included between %{min} and %{max} pixel"
196
- dimension_width_greater_than_or_equal_to: "width must be greater than or equal to %{length} pixel"
197
- dimension_height_greater_than_or_equal_to: "height must be greater than or equal to %{length} pixel"
198
- dimension_width_less_than_or_equal_to: "width must be less than or equal to %{length} pixel"
199
- dimension_height_less_than_or_equal_to: "height must be less than or equal to %{length} pixel"
200
- dimension_width_equal_to: "width must be equal to %{length} pixel"
201
- dimension_height_equal_to: "height must be equal to %{length} pixel"
202
- aspect_ratio_not_square: "must be a square image"
203
- aspect_ratio_not_portrait: "must be a portrait image"
204
- aspect_ratio_not_landscape: "must be a landscape image"
205
- aspect_ratio_is_not: "must have an aspect ratio of %{aspect_ratio}"
206
- image_not_processable: "is not a valid image"
207
- ```
208
-
209
- In several cases, Active Storage Validations provides variables to help you customize messages:
344
+ file_size_not_less_than: "file size must be less than %{max} (current size is %{file_size})"
345
+ file_size_not_less_than_or_equal_to: "file size must be less than or equal to %{max} (current size is %{file_size})"
346
+ file_size_not_greater_than: "file size must be greater than %{min} (current size is %{file_size})"
347
+ file_size_not_greater_than_or_equal_to: "file size must be greater than or equal to %{min} (current size is %{file_size})"
348
+ file_size_not_between: "file size must be between %{min} and %{max} (current size is %{file_size})"
349
+ ```
210
350
 
211
- ### Aspect ratio
212
- The keys starting with `aspect_ratio_` support two variables that you can use:
213
- - `aspect_ratio` containing the expected aspect ratio, especially useful for custom aspect ratio
351
+ The `size` validator error messages expose 4 values that you can use:
352
+ - `file_size` containing the current file size (e.g. `1.5MB`)
353
+ - `min` containing the minimum allowed file size (e.g. `1KB`)
354
+ - `max` containing the maximum allowed file size (e.g. `2MB`)
214
355
  - `filename` containing the current file name
215
356
 
216
- For example :
357
+ ---
358
+
359
+ ### Total size
360
+
361
+ Validates the total file size for several files.
362
+
363
+ #### Options
364
+
365
+ The `total_size` validator has 5 possible options:
366
+ - `less_than`: defines the strict maximum allowed total file size
367
+ - `less_than_or_equal_to`: defines the maximum allowed total file size
368
+ - `greater_than`: defines the strict minimum allowed total file size
369
+ - `greater_than_or_equal_to`: defines the minimum allowed total file size
370
+ - `between`: defines the allowed total file size range
371
+
372
+ #### Examples
373
+
374
+ Use it like this:
375
+ ```ruby
376
+ class User < ApplicationRecord
377
+ has_many_attached :certificates
378
+
379
+ validates :certificates, total_size: { less_than: 10.megabytes } # restricts the total size to < 10MB
380
+ validates :certificates, total_size: { less_than_or_equal_to: 10.megabytes } # restricts the total size to <= 10MB
381
+ validates :certificates, total_size: { greater_than: 1.kilobyte } # restricts the total size to > 1KB
382
+ validates :certificates, total_size: { greater_than_or_equal_to: 1.kilobyte } # restricts the total size to >= 1KB
383
+ validates :certificates, total_size: { between: 1.kilobyte..10.megabytes } # restricts the total size to between 1KB and 10MB
384
+ end
385
+ ```
386
+
387
+ #### Error messages (I18n)
217
388
 
218
389
  ```yml
219
- aspect_ratio_is_not: "must be a %{aspect_ratio} image"
390
+ en:
391
+ errors:
392
+ messages:
393
+ total_file_size_not_less_than: "total file size must be less than %{max} (current size is %{total_file_size})"
394
+ total_file_size_not_less_than_or_equal_to: "total file size must be less than or equal to %{max} (current size is %{total_file_size})"
395
+ total_file_size_not_greater_than: "total file size must be greater than %{min} (current size is %{total_file_size})"
396
+ total_file_size_not_greater_than_or_equal_to: "total file size must be greater than or equal to %{min} (current size is %{total_file_size})"
397
+ total_file_size_not_between: "total file size must be between %{min} and %{max} (current size is %{total_file_size})"
220
398
  ```
221
399
 
222
- ### Content type
223
- The `content_type_invalid` key has three variables that you can use:
224
- - `content_type` containing the exact content type of the sent file
225
- - `human_content_type` containing a more user-friendly version of the sent file content type (e.g. 'TXT' for 'text/plain')
226
- - `authorized_types` containing the list of authorized content types
227
- - `filename` containing the current file name
400
+ The `total_size` validator error messages expose 4 values that you can use:
401
+ - `total_file_size` containing the current total file size (e.g. `1.5MB`)
402
+ - `min` containing the minimum allowed total file size (e.g. `1KB`)
403
+ - `max` containing the maximum allowed total file size (e.g. `2MB`)
404
+
405
+ ---
406
+
407
+ ### Dimension
408
+
409
+ Validates the dimension of the attached image / video files.
410
+
411
+ #### Options
412
+
413
+ The `dimension` validator has several possible options:
414
+ - `width`: defines the exact allowed width (integer)
415
+ - `min`: defines the minimum allowed width (integer)
416
+ - `max`: defines the maximum allowed width (integer)
417
+ - `in`: defines the allowed width range (range)
418
+ - `height`: defines the exact allowed height (integer)
419
+ - `min`: defines the minimum allowed height (integer)
420
+ - `max`: defines the maximum allowed height (integer)
421
+ - `in`: defines the allowed height range (range)
422
+ - `min`: defines the minimum allowed width and height (range)
423
+ - `max`: defines the maximum allowed width and height (range)
424
+
425
+ #### Examples
426
+
427
+ Use it like this:
428
+ ```ruby
429
+ class User < ApplicationRecord
430
+ has_one_attached :avatar
228
431
 
229
- For example :
432
+ validates :avatar, dimension: { width: 100 } # restricts the width to 100 pixels
433
+ validates :avatar, dimension: { width: { min: 80, max: 100 } } # restricts the width to between 80 and 100 pixels
434
+ validates :avatar, dimension: { width: { in: 80..100 } } # restricts the width to between 80 and 100 pixels
435
+ validates :avatar, dimension: { height: 100 } # restricts the height to 100 pixels
436
+ validates :avatar, dimension: { height: { min: 600, max: 1800 } } # restricts the height to between 600 and 1800 pixels
437
+ validates :avatar, dimension: { height: { in: 600..1800 } } # restricts the height to between 600 and 1800 pixels
438
+ validates :avatar, dimension: { min: 80..600, max: 100..1800 } # restricts the width to between 80 and 100 pixels, and the height to between 600 and 1800 pixels
439
+ end
440
+ ```
441
+
442
+ #### Error messages (I18n)
230
443
 
231
444
  ```yml
232
- content_type_invalid: "has an invalid content type : %{content_type}, authorized types are %{authorized_types}"
445
+ en:
446
+ errors:
447
+ messages:
448
+ dimension_min_not_included_in: "must be greater than or equal to %{width} x %{height} pixel"
449
+ dimension_max_not_included_in: "must be less than or equal to %{width} x %{height} pixel"
450
+ dimension_width_not_included_in: "width is not included between %{min} and %{max} pixel"
451
+ dimension_height_not_included_in: "height is not included between %{min} and %{max} pixel"
452
+ dimension_width_not_greater_than_or_equal_to: "width must be greater than or equal to %{length} pixel"
453
+ dimension_height_not_greater_than_or_equal_to: "height must be greater than or equal to %{length} pixel"
454
+ dimension_width_not_less_than_or_equal_to: "width must be less than or equal to %{length} pixel"
455
+ dimension_height_not_less_than_or_equal_to: "height must be less than or equal to %{length} pixel"
456
+ dimension_width_not_equal_to: "width must be equal to %{length} pixel"
457
+ dimension_height_not_equal_to: "height must be equal to %{length} pixel"
458
+ media_metadata_missing: "is not a valid media file"
233
459
  ```
234
460
 
235
- ### Dimension
236
- The keys starting with `dimension_` support six variables that you can use:
461
+ The `dimension` validator error messages expose 6 values that you can use:
237
462
  - `min` containing the minimum width or height allowed
238
463
  - `max` containing the maximum width or height allowed
239
464
  - `width` containing the minimum or maximum width allowed
240
465
  - `height` containing the minimum or maximum width allowed
241
466
  - `length` containing the exact width or height allowed
242
- - `filename` containing the current file name
467
+ - `filename` containing the current filename in error
243
468
 
244
- For example :
469
+ ---
245
470
 
246
- ```yml
247
- dimension_min_inclusion: "must be greater than or equal to %{width} x %{height} pixel."
248
- ```
471
+ ### Duration
249
472
 
250
- ### File size
251
- The keys starting with `file_size_not_` support four variables that you can use:
252
- - `file_size` containing the current file size
253
- - `min` containing the minimum file size
254
- - `max` containing the maximum file size
255
- - `filename` containing the current file name
473
+ Validates the duration of the attached audio / video files.
256
474
 
257
- For example :
475
+ #### Options
258
476
 
259
- ```yml
260
- file_size_not_between: "file size must be between %{min_size} and %{max_size} (current size is %{file_size})"
261
- ```
477
+ The `duration` validator has 5 possible options:
478
+ - `less_than`: defines the strict maximum allowed file duration
479
+ - `less_than_or_equal_to`: defines the maximum allowed file duration
480
+ - `greater_than`: defines the strict minimum allowed file duration
481
+ - `greater_than_or_equal_to`: defines the minimum allowed file duration
482
+ - `between`: defines the allowed file duration range
262
483
 
263
- ### Total file size
264
- The keys starting with `total_file_size_not_` support three variables that you can use:
265
- - `total_file_size` containing the current total file size
266
- - `min` containing the minimum file size
267
- - `max` containing the maximum file size
484
+ #### Examples
268
485
 
269
- For example :
486
+ Use it like this:
487
+ ```ruby
488
+ class User < ApplicationRecord
489
+ has_one_attached :avatar
270
490
 
271
- ```yml
272
- total_file_size_not_between: "total file size must be between %{min_size} and %{max_size} (current size is %{total_file_size})"
491
+ validates :avatar, duration: { less_than: 2.minutes } # restricts the file duration to < 2 minutes
492
+ validates :avatar, duration: { less_than_or_equal_to: 2.minutes } # restricts the file duration to <= 2 minutes
493
+ validates :avatar, duration: { greater_than: 1.second } # restricts the file duration to > 1 second
494
+ validates :avatar, duration: { greater_than_or_equal_to: 1.second } # restricts the file duration to >= 1 second
495
+ validates :avatar, duration: { between: 1.second..2.minutes } # restricts the file duration to between 1 second and 2 minutes
496
+ end
273
497
  ```
274
498
 
275
- ### Number of files
276
- The `limit_out_of_range` key supports two variables that you can use:
277
- - `min` containing the minimum number of files
278
- - `max` containing the maximum number of files
279
-
280
- For example :
499
+ #### Error messages (I18n)
281
500
 
282
501
  ```yml
283
- limit_out_of_range: "total number is out of range. range: [%{min}, %{max}]"
502
+ en:
503
+ errors:
504
+ messages:
505
+ duration_not_less_than: "duration must be less than %{max} (current duration is %{duration})"
506
+ duration_not_less_than_or_equal_to: "duration must be less than or equal to %{max} (current duration is %{duration})"
507
+ duration_not_greater_than: "duration must be greater than %{min} (current duration is %{duration})"
508
+ duration_not_greater_than_or_equal_to: "duration must be greater than or equal to %{min} (current duration is %{duration})"
509
+ duration_not_between: "duration must be between %{min} and %{max} (current duration is %{duration})"
284
510
  ```
285
511
 
286
- ### Processable image
287
- The `image_not_processable` key supports one variable that you can use:
512
+ The `duration` validator error messages expose 4 values that you can use:
513
+ - `duration` containing the current duration size (e.g. `2 minutes`)
514
+ - `min` containing the minimum allowed duration size (e.g. `1 second`)
515
+ - `max` containing the maximum allowed duration size (e.g. `2 minutes`)
288
516
  - `filename` containing the current file name
289
517
 
290
- For example :
518
+ ---
519
+
520
+ ### Aspect ratio
521
+
522
+ Validates the aspect ratio of the attached files.
523
+
524
+ #### Options
525
+
526
+ The `aspect_ratio` validator has several options:
527
+ - `with`: defines the exact allowed aspect ratio (e.g. `:is_16/9`)
528
+ - `in`: defines the allowed aspect ratios (e.g. `%i[square landscape]`)
529
+
530
+ This validator can define aspect ratios in several ways:
531
+ - Symbols:
532
+ - prebuilt aspect ratios: `:square`, `:portrait`, `:landscape`
533
+ - custom aspect ratios (it must be of type `is_xx_yy`): `:is_16_9`, `:is_4_3`, etc.
534
+
535
+ #### Examples
536
+
537
+ Use it like this:
538
+ ```ruby
539
+ class User < ApplicationRecord
540
+ has_one_attached :avatar
541
+
542
+ validates :avatar, aspect_ratio: :square # restricts the aspect ratio to 1:1
543
+ validates :avatar, aspect_ratio: :portrait # restricts the aspect ratio to x:y where y > x
544
+ validates :avatar, aspect_ratio: :landscape # restricts the aspect ratio to x:y where x > y
545
+ validates :avatar, aspect_ratio: :is_16_9 # restricts the aspect ratio to 16:9
546
+ validates :avatar, aspect_ratio: %i[square is_16_9] # restricts the aspect ratio to 1:1 and 16:9
547
+ end
548
+ ```
549
+
550
+ #### Error messages (I18n)
291
551
 
292
552
  ```yml
293
- image_not_processable: "is not a valid image (file: %{filename})"
553
+ en:
554
+ errors:
555
+ messages:
556
+ aspect_ratio_not_square: "must be square (current file is %{width}x%{height}px)"
557
+ aspect_ratio_not_portrait: "must be portrait (current file is %{width}x%{height}px)"
558
+ aspect_ratio_not_landscape: "must be landscape (current file is %{width}x%{height}px)"
559
+ aspect_ratio_not_x_y: "must be %{authorized_aspect_ratios} (current file is %{width}x%{height}px)"
560
+ aspect_ratio_invalid: "has an invalid aspect ratio (valid aspect ratios are %{authorized_aspect_ratios})"
561
+ media_metadata_missing: "is not a valid media file"
294
562
  ```
295
563
 
296
- ## Installation
564
+ The `aspect_ratio` validator error messages expose 4 values that you can use:
565
+ - `authorized_aspect_ratios` containing the authorized aspect ratios
566
+ - `width` containing the current width of the image/video
567
+ - `height` containing the current height of the image/video
568
+ - `filename` containing the current filename in error
569
+
570
+ ---
571
+
572
+ ### Processable file
573
+
574
+ Validates if the attached files can be processed by MiniMagick or Vips (image) or ffmpeg (video/audio).
297
575
 
298
- Add this line to your application's Gemfile:
576
+ #### Options
299
577
 
578
+ The `processable_file` validator has no options.
579
+
580
+ #### Examples
581
+
582
+ Use it like this:
300
583
  ```ruby
301
- gem 'active_storage_validations'
584
+ class User < ApplicationRecord
585
+ has_one_attached :avatar
302
586
 
303
- # Optional, to use :dimension validator or :aspect_ratio validator
304
- gem 'mini_magick', '>= 4.9.5'
305
- # Or
306
- gem 'ruby-vips', '>= 2.1.0'
587
+ validates :avatar, processable_file: true # ensures that the file is processable by MiniMagick or Vips (image) or ffmpeg (video/audio)
588
+ end
307
589
  ```
308
590
 
309
- And then execute:
591
+ #### Error messages (I18n)
310
592
 
311
- ```bash
312
- $ bundle
593
+ ```yml
594
+ en:
595
+ errors:
596
+ messages:
597
+ file_not_processable: "is not identified as a valid media file"
313
598
  ```
314
599
 
315
- ## Sample
600
+ The `processable_file` validator error messages expose 1 value that you can use:
601
+ - `filename` containing the current filename in error
602
+
603
+ ---
604
+
605
+ ## Upgrading from 1.x to 2.x
606
+
607
+ If you are upgrading from 1.x to 2.x, you will be pleased to note that a lot of things have been added and improved!
608
+
609
+ Added features:
610
+ - `duration` validator has been added for audio / video files
611
+ - `dimension` validator now supports videos
612
+ - `aspect_ratio` validator now supports videos
613
+ - `processable_image` validator is now `processable_file` validator and supports image/video/audio
614
+ - Major performance improvement have been added: we now only perform the expensive io analysis operation on the newly attached files. For previously attached files, we validate them using Rails `ActiveStorage::Blob#metadata` internal mecanism ([more here](https://github.com/rails/rails/blob/main/activestorage/app/models/active_storage/blob/analyzable.rb)).
615
+ - All error messages have been given an upgrade and new variables that you can use
616
+
617
+ But this major version bump also comes with some breaking changes. Below are the main breaking changes you need to be aware of:
618
+ - Error messages
619
+ - We advise you to replace all the v1 translations by the new v2 rather than changing them one by one. A majority of messages have been completely rewritten to be more consistent and easier to understand.
620
+ - If you wish to change them one by one, here is the list of changes to make:
621
+ - Some validator errors have been totally changed:
622
+ - `limit` validator keys have been totally reworked
623
+ - `dimension` validator keys have been totally reworked
624
+ - `content_type` validator keys have been totally reworked
625
+ - `processable_image` validator keys have been totally reworked
626
+ - Some keys have been changed:
627
+ - `image_metadata_missing` has been replaced by `media_metadata_missing`
628
+ - `aspect_ratio_is_not` has been replaced by `aspect_ratio_not_x_y`
629
+ - Some error messages variables names have been changed to improve readability:
630
+ - `aspect_ratio` validator:
631
+ - `aspect_ratio` has been replaced by `authorized_aspect_ratios`
632
+ - `content_type` validator:
633
+ - `authorized_types` has been replaced by `authorized_human_content_types`
634
+ - `size` validator:
635
+ - `min_size` has been replaced by `min`
636
+ - `max_size` has been replaced by `max`
637
+ - `total_size` validator:
638
+ - `min_size` has been replaced by `min`
639
+ - `max_size` has been replaced by `max`
640
+
641
+ - `content_type` validator
642
+ - The `:in` option now only accepts 'valid' content types (ie content types deemed by Marcel as valid).
643
+ - The check was mistakenly only performed on the `:with` option previously. Therefore, invalid content types were accepted in the `:in` option, which is not the expected behavior.
644
+ - This might break some cases when you had for example `content_type: ['image/png', 'image/jpg']`, because `image/jpg` is not a valid content type, it should be replaced by `image/jpeg`.
645
+ - An `ArgumentError` is now raised if `image/jpg` is used to make it easier to fix. You should now only use `image/jpeg`.
646
+
647
+ - `processable_image` validator
648
+ - The validator has been replaced by `processable_file` validator, be sure to replace `processable_image: true` to `processable_file: true`
649
+ - The associated matcher has also been updated accordingly, be sure to replace `validate_processable_image_of` to `validate_processable_file_of`
316
650
 
317
- Very simple example of validation with file attached, content type check and custom error message.
651
+ ## Internationalization (I18n)
652
+
653
+ Active Storage Validations uses I18n for error messages. Add these keys in your translation files to make them available:
654
+
655
+ ```yml
656
+ en:
657
+ errors:
658
+ messages:
659
+ content_type_invalid:
660
+ one: "has an invalid content type (authorized content type is %{authorized_human_content_types})"
661
+ other: "has an invalid content type (authorized content types are %{authorized_human_content_types})"
662
+ content_type_spoofed:
663
+ one: "has a content type that is not equivalent to the one that is detected through its content (authorized content type is %{authorized_human_content_types})"
664
+ other: "has a content type that is not equivalent to the one that is detected through its content (authorized content types are %{authorized_human_content_types})"
665
+ file_size_not_less_than: "file size must be less than %{max} (current size is %{file_size})"
666
+ file_size_not_less_than_or_equal_to: "file size must be less than or equal to %{max} (current size is %{file_size})"
667
+ file_size_not_greater_than: "file size must be greater than %{min} (current size is %{file_size})"
668
+ file_size_not_greater_than_or_equal_to: "file size must be greater than or equal to %{min} (current size is %{file_size})"
669
+ file_size_not_between: "file size must be between %{min} and %{max} (current size is %{file_size})"
670
+ total_file_size_not_less_than: "total file size must be less than %{max} (current size is %{total_file_size})"
671
+ total_file_size_not_less_than_or_equal_to: "total file size must be less than or equal to %{max} (current size is %{total_file_size})"
672
+ total_file_size_not_greater_than: "total file size must be greater than %{min} (current size is %{total_file_size})"
673
+ total_file_size_not_greater_than_or_equal_to: "total file size must be greater than or equal to %{min} (current size is %{total_file_size})"
674
+ total_file_size_not_between: "total file size must be between %{min} and %{max} (current size is %{total_file_size})"
675
+ duration_not_less_than: "duration must be less than %{max} (current duration is %{duration})"
676
+ duration_not_less_than_or_equal_to: "duration must be less than or equal to %{max} (current duration is %{duration})"
677
+ duration_not_greater_than: "duration must be greater than %{min} (current duration is %{duration})"
678
+ duration_not_greater_than_or_equal_to: "duration must be greater than or equal to %{min} (current duration is %{duration})"
679
+ duration_not_between: "duration must be between %{min} and %{max} (current duration is %{duration})"
680
+ limit_out_of_range:
681
+ zero: "no files attached (must have between %{min} and %{max} files)"
682
+ one: "only 1 file attached (must have between %{min} and %{max} files)"
683
+ other: "total number of files must be between %{min} and %{max} files (there are %{count} files attached)"
684
+ limit_min_not_reached:
685
+ zero: "no files attached (must have at least %{min} files)"
686
+ one: "only 1 file attached (must have at least %{min} files)"
687
+ other: "%{count} files attached (must have at least %{min} files)"
688
+ limit_max_exceeded:
689
+ zero: "no files attached (maximum is %{max} files)"
690
+ one: "too many files attached (maximum is %{max} files, got %{count})"
691
+ other: "too many files attached (maximum is %{max} files, got %{count})"
692
+ media_metadata_missing: "is not a valid media file"
693
+ dimension_min_not_included_in: "must be greater than or equal to %{width} x %{height} pixel"
694
+ dimension_max_not_included_in: "must be less than or equal to %{width} x %{height} pixel"
695
+ dimension_width_not_included_in: "width is not included between %{min} and %{max} pixel"
696
+ dimension_height_not_included_in: "height is not included between %{min} and %{max} pixel"
697
+ dimension_width_not_greater_than_or_equal_to: "width must be greater than or equal to %{length} pixel"
698
+ dimension_height_not_greater_than_or_equal_to: "height must be greater than or equal to %{length} pixel"
699
+ dimension_width_not_less_than_or_equal_to: "width must be less than or equal to %{length} pixel"
700
+ dimension_height_not_less_than_or_equal_to: "height must be less than or equal to %{length} pixel"
701
+ dimension_width_not_equal_to: "width must be equal to %{length} pixel"
702
+ dimension_height_not_equal_to: "height must be equal to %{length} pixel"
703
+ aspect_ratio_not_square: "must be square (current file is %{width}x%{height}px)"
704
+ aspect_ratio_not_portrait: "must be portrait (current file is %{width}x%{height}px)"
705
+ aspect_ratio_not_landscape: "must be landscape (current file is %{width}x%{height}px)"
706
+ aspect_ratio_not_x_y: "must be %{authorized_aspect_ratios} (current file is %{width}x%{height}px)"
707
+ aspect_ratio_invalid: "has an invalid aspect ratio (valid aspect ratios are %{authorized_aspect_ratios})"
708
+ file_not_processable: "is not identified as a valid media file"
709
+ ```
318
710
 
319
- [![Sample](https://raw.githubusercontent.com/igorkasyanchuk/active_storage_validations/master/docs/preview.png)](https://raw.githubusercontent.com/igorkasyanchuk/active_storage_validations/master/docs/preview.png)
711
+ Other translation files are available (here)[https://github.com/igorkasyanchuk/active_storage_validations/tree/master/config/locales].
320
712
 
321
713
  ## Test matchers
322
- Provides RSpec-compatible and Minitest-compatible matchers for testing the validators.
714
+
715
+ The gem also provides RSpec-compatible and Minitest-compatible matchers for testing the validators.
323
716
 
324
717
  ### RSpec
325
718
 
719
+ #### Setup
326
720
  In `spec_helper.rb`, you'll need to require the matchers:
327
721
 
328
722
  ```ruby
329
723
  require 'active_storage_validations/matchers'
330
724
  ```
331
725
 
332
- And _include_ the module:
726
+ And include the module:
333
727
 
334
728
  ```ruby
335
729
  RSpec.configure do |config|
@@ -337,6 +731,7 @@ RSpec.configure do |config|
337
731
  end
338
732
  ```
339
733
 
734
+ #### Matchers
340
735
  Matcher methods available:
341
736
 
342
737
  ```ruby
@@ -349,8 +744,8 @@ describe User do
349
744
  # attached
350
745
  it { is_expected.to validate_attached_of(:avatar) }
351
746
 
352
- # processable_image
353
- it { is_expected.to validate_processable_image_of(:avatar) }
747
+ # processable_file
748
+ it { is_expected.to validate_processable_file_of(:avatar) }
354
749
 
355
750
  # limit
356
751
  # #min, #max
@@ -388,6 +783,14 @@ describe User do
388
783
  it { is_expected.to validate_total_size_of(:avatar).greater_than(1.kilobyte) }
389
784
  it { is_expected.to validate_total_size_of(:avatar).greater_than_or_equal_to(1.kilobyte) }
390
785
  it { is_expected.to validate_total_size_of(:avatar).between(100..500.kilobytes) }
786
+
787
+ # duration:
788
+ # #less_than, #less_than_or_equal_to, #greater_than, #greater_than_or_equal_to, #between
789
+ it { is_expected.to validate_duration_of(:introduction).less_than(50.seconds) }
790
+ it { is_expected.to validate_duration_of(:introduction).less_than_or_equal_to(50.seconds) }
791
+ it { is_expected.to validate_duration_of(:introduction).greater_than(1.minute) }
792
+ it { is_expected.to validate_duration_of(:introduction).greater_than_or_equal_to(1.minute) }
793
+ it { is_expected.to validate_duration_of(:introduction).between(100..500.seconds) }
391
794
  end
392
795
  ```
393
796
  (Note that matcher methods are chainable)
@@ -409,6 +812,8 @@ end
409
812
  ```
410
813
 
411
814
  ### Minitest
815
+
816
+ #### Setup
412
817
  To use the matchers, make sure you have the [shoulda-context](https://github.com/thoughtbot/shoulda-context) gem up and running.
413
818
 
414
819
  You need to require the matchers:
@@ -417,7 +822,7 @@ You need to require the matchers:
417
822
  require 'active_storage_validations/matchers'
418
823
  ```
419
824
 
420
- And _extend_ the module:
825
+ And extend the module:
421
826
 
422
827
  ```ruby
423
828
  class ActiveSupport::TestCase
@@ -425,19 +830,19 @@ class ActiveSupport::TestCase
425
830
  end
426
831
  ```
427
832
 
833
+ #### Matchers
428
834
  Then you can use the matchers with the syntax specified in the RSpec section, just use `should validate_method` instead of `it { is_expected_to validate_method }` as specified in the [shoulda-context](https://github.com/thoughtbot/shoulda-context) gem.
429
835
 
430
- ## Todo
431
836
 
432
- * verify with remote storages (s3, etc)
433
- * verify how it works with direct upload
434
- * add more translations
837
+ ## Contributing
435
838
 
436
- ## Tests & Contributing
839
+ If you want to contribute to the project, you will have to fork the repository and create a new branch from the `master` branch. Then build your feature, or fix the issue, and create a pull request. Be sure to add tests for your changes.
437
840
 
438
- To run tests in root folder of gem:
841
+ Before submitting your pull request, run the tests to make sure everything works as expected.
439
842
 
440
- * `BUNDLE_GEMFILE=gemfiles/rails_6_1_4.gemfile bundle exec rake test` to run for Rails 7.0
843
+ To run the gem tests, launch the following commands in the root folder of gem repository:
844
+
845
+ * `BUNDLE_GEMFILE=gemfiles/rails_6_1_4.gemfile bundle exec rake test` to run for Rails 6.1.4
441
846
  * `BUNDLE_GEMFILE=gemfiles/rails_7_0.gemfile bundle exec rake test` to run for Rails 7.0
442
847
  * `BUNDLE_GEMFILE=gemfiles/rails_7_1.gemfile bundle exec rake test` to run for Rails 7.1
443
848
  * `BUNDLE_GEMFILE=gemfiles/rails_7_2.gemfile bundle exec rake test` to run for Rails 7.2
@@ -460,86 +865,22 @@ BUNDLE_GEMFILE=gemfiles/rails_8_0.gemfile bundle exec rake test
460
865
 
461
866
  Tips:
462
867
  - To focus a specific test, use the `focus` class method provided by [minitest-focus](https://github.com/minitest/minitest-focus)
463
- - To focus a specific file, use the TEST option provided by minitest, e.g. to only run size_validator_test.rb file you will execute the following command: `bundle exec rake test TEST=test/validators/size_validator_test.rb`
868
+ - To focus a specific file, use the TEST option provided by minitest, e.g. to only run `size_validator_test.rb` file you will launch the following command: `bundle exec rake test TEST=test/validators/size_validator_test.rb`
464
869
 
465
870
 
466
- ## Contributing
871
+ ## Additional information
872
+
873
+ ### Contributors (BIG THANK YOU!)
467
874
 
468
- You are welcome to contribute.
469
-
470
- [<img src="https://opensource-heroes.com/svg/embed/igorkasyanchuk/active_storage_validations"
471
- />](https://opensource-heroes.com/r/igorkasyanchuk/active_storage_validations)
472
-
473
- ## Contributors (BIG THANK YOU)
474
- - https://github.com/schweigert
475
- - https://github.com/tleneveu
476
- - https://github.com/reckerswartz
477
- - https://github.com/Uysim
478
- - https://github.com/D-system
479
- - https://github.com/ivanelrey
480
- - https://github.com/phlegx
481
- - https://github.com/rr-dev
482
- - https://github.com/dsmalko
483
- - https://github.com/danderozier
484
- - https://github.com/cseelus
485
- - https://github.com/vkinelev
486
- - https://github.com/reed
487
- - https://github.com/connorshea
488
- - https://github.com/Atul9
489
- - https://github.com/victorbueno
490
- - https://github.com/UICJohn
491
- - https://github.com/giovannibonetti
492
- - https://github.com/dlepage
493
- - https://github.com/StefSchenkelaars
494
- - https://github.com/willnet
495
- - https://github.com/mohanklein
496
- - https://github.com/High5Apps
497
- - https://github.com/mschnitzer
498
- - https://github.com/sinankeskin
499
- - https://github.com/alejandrodevs
500
- - https://github.com/molfar
501
- - https://github.com/connorshea
502
- - https://github.com/yshmarov
503
- - https://github.com/fongfan999
504
- - https://github.com/cooperka
505
- - https://github.com/dolarsrg
506
- - https://github.com/jayshepherd
507
- - https://github.com/ohbarye
508
- - https://github.com/randsina
509
- - https://github.com/vietqhoang
510
- - https://github.com/kemenaran
511
- - https://github.com/jrmhaig
512
- - https://github.com/evedovelli
513
- - https://github.com/JuanVqz
514
- - https://github.com/luiseugenio
515
- - https://github.com/equivalent
516
- - https://github.com/NARKOZ
517
- - https://github.com/stephensolis
518
- - https://github.com/kwent
519
- - https://github.com/Animesh-Ghosh
520
- - https://github.com/gr8bit
521
- - https://github.com/codegeek319
522
- - https://github.com/clwy-cn
523
- - https://github.com/kukicola
524
- - https://github.com/sobrinho
525
- - https://github.com/iainbeeston
526
- - https://github.com/marckohlbrugge
527
- - https://github.com/Mth0158
528
- - https://github.com/technicalpickles
529
- - https://github.com/ricsdeol
530
- - https://github.com/Fonsan
531
- - https://github.com/tagliala
532
- - https://github.com/ocarreterom
533
- - https://github.com/aditya-cherukuri
534
- - https://github.com/searls
535
- - https://github.com/yenshirak
536
- - https://github.com/wataori
537
- - https://github.com/Scorpahr
538
-
539
-
540
- ## License
875
+ We have a long list of valued contributors. Check them all at:
876
+
877
+ https://github.com/igorkasyanchuk/active_storage_validations/graphs/contributors
878
+
879
+ ### License
541
880
 
542
881
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
543
882
 
883
+ <br>
884
+
544
885
  [<img src="https://github.com/igorkasyanchuk/rails_time_travel/blob/main/docs/more_gems.png?raw=true"
545
886
  />](https://www.railsjazz.com/?utm_source=github&utm_medium=bottom&utm_campaign=active_storage_validations)