active_storage_validations 1.0.4 → 3.0.2

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.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +785 -245
  3. data/config/locales/da.yml +63 -0
  4. data/config/locales/de.yml +60 -19
  5. data/config/locales/en-GB.yml +63 -0
  6. data/config/locales/en.yml +60 -20
  7. data/config/locales/es.yml +60 -19
  8. data/config/locales/fr.yml +60 -19
  9. data/config/locales/it.yml +60 -19
  10. data/config/locales/ja.yml +60 -19
  11. data/config/locales/nl.yml +60 -19
  12. data/config/locales/pl.yml +60 -19
  13. data/config/locales/pt-BR.yml +60 -19
  14. data/config/locales/ru.yml +60 -19
  15. data/config/locales/sv.yml +63 -0
  16. data/config/locales/tr.yml +60 -19
  17. data/config/locales/uk.yml +60 -19
  18. data/config/locales/vi.yml +60 -19
  19. data/config/locales/zh-CN.yml +60 -19
  20. data/lib/active_storage_validations/analyzer/audio_analyzer.rb +58 -0
  21. data/lib/active_storage_validations/analyzer/content_type_analyzer.rb +60 -0
  22. data/lib/active_storage_validations/analyzer/image_analyzer/image_magick.rb +46 -0
  23. data/lib/active_storage_validations/analyzer/image_analyzer/vips.rb +56 -0
  24. data/lib/active_storage_validations/analyzer/image_analyzer.rb +49 -0
  25. data/lib/active_storage_validations/analyzer/null_analyzer.rb +18 -0
  26. data/lib/active_storage_validations/analyzer/pdf_analyzer.rb +89 -0
  27. data/lib/active_storage_validations/analyzer/shared/asv_ff_probable.rb +61 -0
  28. data/lib/active_storage_validations/analyzer/video_analyzer.rb +130 -0
  29. data/lib/active_storage_validations/analyzer.rb +88 -0
  30. data/lib/active_storage_validations/aspect_ratio_validator.rb +157 -97
  31. data/lib/active_storage_validations/attached_validator.rb +22 -5
  32. data/lib/active_storage_validations/base_comparison_validator.rb +83 -0
  33. data/lib/active_storage_validations/content_type_validator.rb +219 -31
  34. data/lib/active_storage_validations/dimension_validator.rb +187 -97
  35. data/lib/active_storage_validations/duration_validator.rb +70 -0
  36. data/lib/active_storage_validations/extensors/asv_blob_metadatable.rb +56 -0
  37. data/lib/active_storage_validations/extensors/asv_marcelable.rb +12 -0
  38. data/lib/active_storage_validations/limit_validator.rb +76 -9
  39. data/lib/active_storage_validations/matchers/aspect_ratio_validator_matcher.rb +119 -0
  40. data/lib/active_storage_validations/matchers/attached_validator_matcher.rb +48 -25
  41. data/lib/active_storage_validations/matchers/base_comparison_validator_matcher.rb +150 -0
  42. data/lib/active_storage_validations/matchers/content_type_validator_matcher.rb +98 -39
  43. data/lib/active_storage_validations/matchers/dimension_validator_matcher.rb +93 -55
  44. data/lib/active_storage_validations/matchers/duration_validator_matcher.rb +39 -0
  45. data/lib/active_storage_validations/matchers/limit_validator_matcher.rb +127 -0
  46. data/lib/active_storage_validations/matchers/pages_validator_matcher.rb +39 -0
  47. data/lib/active_storage_validations/matchers/processable_file_validator_matcher.rb +78 -0
  48. data/lib/active_storage_validations/matchers/shared/asv_active_storageable.rb +19 -0
  49. data/lib/active_storage_validations/matchers/shared/asv_allow_blankable.rb +28 -0
  50. data/lib/active_storage_validations/matchers/shared/asv_attachable.rb +72 -0
  51. data/lib/active_storage_validations/matchers/shared/asv_contextable.rb +57 -0
  52. data/lib/active_storage_validations/matchers/shared/asv_messageable.rb +28 -0
  53. data/lib/active_storage_validations/matchers/shared/asv_rspecable.rb +27 -0
  54. data/lib/active_storage_validations/matchers/shared/asv_validatable.rb +56 -0
  55. data/lib/active_storage_validations/matchers/size_validator_matcher.rb +17 -71
  56. data/lib/active_storage_validations/matchers/total_size_validator_matcher.rb +47 -0
  57. data/lib/active_storage_validations/matchers.rb +17 -21
  58. data/lib/active_storage_validations/pages_validator.rb +61 -0
  59. data/lib/active_storage_validations/processable_file_validator.rb +37 -0
  60. data/lib/active_storage_validations/railtie.rb +14 -0
  61. data/lib/active_storage_validations/shared/asv_active_storageable.rb +30 -0
  62. data/lib/active_storage_validations/shared/asv_analyzable.rb +89 -0
  63. data/lib/active_storage_validations/shared/asv_attachable.rb +236 -0
  64. data/lib/active_storage_validations/shared/asv_errorable.rb +64 -0
  65. data/lib/active_storage_validations/shared/asv_loggable.rb +11 -0
  66. data/lib/active_storage_validations/shared/asv_optionable.rb +29 -0
  67. data/lib/active_storage_validations/shared/asv_symbolizable.rb +14 -0
  68. data/lib/active_storage_validations/size_validator.rb +24 -41
  69. data/lib/active_storage_validations/total_size_validator.rb +52 -0
  70. data/lib/active_storage_validations/version.rb +1 -1
  71. data/lib/active_storage_validations.rb +27 -13
  72. metadata +113 -31
  73. data/lib/active_storage_validations/metadata.rb +0 -151
  74. data/lib/active_storage_validations/option_proc_unfolding.rb +0 -16
  75. data/lib/active_storage_validations/processable_image_validator.rb +0 -43
data/README.md CHANGED
@@ -6,228 +6,773 @@
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.
10
+ [!["Buy Me A Coffee"](https://github.com/igorkasyanchuk/get-smart/blob/main/docs/snapshot-bmc-button-small.png?raw=true)](https://buymeacoffee.com/igorkasyanchuk)
11
+
12
+ Active Storage Validations is a gem that allows you to add validations for Active Storage attributes.
13
+
14
+ This gems is doing it right for you! Just use `validates :avatar, attached: true, content_type: 'image/png'` and that's it!
15
+
16
+ ## Table of Contents
17
+
18
+ - [Getting started](#getting-started)
19
+ - [Installation](#installation)
20
+ - [Error messages (I18n)](#error-messages-i18n)
21
+ - [Using image metadata validators](#using-image-metadata-validators)
22
+ - [Using video and audio metadata validators](#using-video-and-audio-metadata-validators)
23
+ - [Using pdf metadata validators](#using-pdf-metadata-validators)
24
+ - [Using content type spoofing protection validator option](#using-content-type-spoofing-protection-validator-option)
25
+ - [Validators](#validators)
26
+ - [Attached](#attached)
27
+ - [Limit](#limit)
28
+ - [Content type](#content-type)
29
+ - [Size](#size)
30
+ - [Total size](#total-size)
31
+ - [Dimension](#dimension)
32
+ - [Duration](#duration)
33
+ - [Aspect ratio](#aspect-ratio)
34
+ - [Processable file](#processable-file)
35
+ - [Pages](#pages)
36
+ - [Upgrading from 1.x to 2.x](#upgrading-from-1x-to-2x)
37
+ - [Upgrading from 2.x to 3.x](#upgrading-from-2x-to-3x)
38
+ - [Internationalization (I18n)](#internationalization-i18n)
39
+ - [Test matchers](#test-matchers)
40
+ - [Contributing](#contributing)
41
+ - [Additional information](#additional-information)
42
+
43
+ ## Getting started
44
+
45
+ ### Installation
46
+
47
+ Active Storage Validations work with Rails 6.1.4 onwards. Add this line to your application's Gemfile:
12
48
 
13
- This gems doing it for you. Just use `attached: true` or `content_type: 'image/png'` validation.
49
+ ```ruby
50
+ gem 'active_storage_validations'
51
+ ```
52
+
53
+ And then execute:
54
+
55
+ ```sh
56
+ $ bundle
57
+ ```
58
+
59
+ ### Error messages (I18n)
60
+
61
+ Once you have installed the gem, I18n error messages will be added automatically to your app. See [Internationalization (I18n)](#internationalization-i18n) section for more details.
62
+
63
+ ### Using image metadata validators
64
+
65
+ Optionally, to use the image metadata validators (`dimension`, `aspect_ratio` and `processable_file`), you will have to add one of the corresponding gems:
66
+
67
+ ```ruby
68
+ gem 'mini_magick', '>= 4.9.5'
69
+ # Or
70
+ gem 'ruby-vips', '>= 2.1.0'
71
+ ```
72
+
73
+ 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).
74
+
75
+ ### Using video and audio metadata validators
76
+
77
+ 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).
78
+
79
+ ### Using pdf metadata validators
80
+
81
+ To use the pdf metadata validators (`dimension`, `aspect_ratio`, `processable_file` and `pages`), you will not need to add any gems. However you will need to have the `poppler` tool installed on your system (once again, be sure to have it installed both on your local and in your CI / production environments).
14
82
 
15
- ## What it can do
83
+ ### Using content type spoofing protection validator option
16
84
 
17
- * validates if file(s) attached
18
- * validates content type
19
- * validates size of files
20
- * validates dimension of images/videos
21
- * validates number of uploaded files (min/max required)
22
- * validates aspect ratio (if square, portrait, landscape, is_16_9, ...)
23
- * validates if file can be processed by MiniMagick or Vips
24
- * custom error messages
25
- * allow procs for dynamic determination of values
85
+ To use the `spoofing_protection` option with the `content_type` validator, you only need to have the UNIX `file` command on your system.
26
86
 
27
- ## Usage
87
+ If you want some inspiration about how to add `imagemagick`, `libvips`, `ffmpeg` or `poppler` to your docker image, you can check how we do it for the gem CI (https://github.com/igorkasyanchuk/active_storage_validations/blob/master/.github/workflows/main.yml)
28
88
 
29
- For example you have a model like this and you want to add validation.
89
+ ## Validators
30
90
 
91
+ **List of validators:**
92
+ - [Attached](#attached): validates if file(s) attached
93
+ - [Limit](#limit): validates number of uploaded files
94
+ - [Content type](#content-type): validates file content type
95
+ - [Size](#size): validates file size
96
+ - [Total size](#total-size): validates total file size for several files
97
+ - [Dimension](#dimension): validates image / video dimensions
98
+ - [Duration](#duration): validates video / audio duration
99
+ - [Aspect ratio](#aspect-ratio): validates image / video aspect ratio
100
+ - [Processable file](#processable-file): validates if a file can be processed
101
+ - [Pages](#pages): validates pdf number of pages
102
+ <br>
103
+ <br>
104
+
105
+ **Proc usage**<br>
106
+ Every validator can use procs instead of values in all the validator examples:
31
107
  ```ruby
32
108
  class User < ApplicationRecord
33
- has_one_attached :avatar
34
- has_many_attached :photos
35
- has_one_attached :image
36
-
37
- validates :name, presence: true
38
-
39
- validates :avatar, attached: true, content_type: 'image/png',
40
- dimension: { width: 200, height: 200 }
41
- validates :photos, attached: true, content_type: ['image/png', 'image/jpeg'],
42
- dimension: { width: { min: 800, max: 2400 },
43
- height: { min: 600, max: 1800 }, message: 'is not given between dimension' }
44
- validates :image, attached: true,
45
- processable_image: true,
46
- content_type: ['image/png', 'image/jpeg'],
47
- aspect_ratio: :landscape
109
+ has_many_attached :files
110
+
111
+ validates :files, limit: { max: -> (record) { record.admin? ? 100 : 10 } }
48
112
  end
49
113
  ```
50
114
 
51
- or
115
+ **Performance optimization**<br>
116
+ 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.
117
+
118
+ 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.
119
+
120
+ ---
121
+
122
+ ### Attached
123
+
124
+ Validates if the attachment is present.
52
125
 
126
+ #### Options
127
+
128
+ The `attached` validator has no options.
129
+
130
+ #### Examples
131
+
132
+ Use it like this:
53
133
  ```ruby
54
- class Project < ApplicationRecord
55
- has_one_attached :logo
56
- has_one_attached :preview
57
- has_one_attached :attachment
58
- has_many_attached :documents
59
-
60
- validates :title, presence: true
61
-
62
- validates :logo, attached: true, size: { less_than: 100.megabytes , message: 'is too large' }
63
- validates :preview, attached: true, size: { between: 1.kilobyte..100.megabytes , message: 'is not given between size' }
64
- validates :attachment, attached: true, content_type: { in: 'application/pdf', message: 'is not a PDF' }
65
- validates :documents, limit: { min: 1, max: 3 }
134
+ class User < ApplicationRecord
135
+ has_one_attached :avatar
136
+
137
+ validates :avatar, attached: true # ensures that avatar has an attached file
66
138
  end
67
139
  ```
68
140
 
69
- ### More examples
141
+ #### Error messages (I18n)
142
+
143
+ ```yml
144
+ en:
145
+ errors:
146
+ messages:
147
+ blank: "can't be blank"
148
+ ```
149
+
150
+ The error message for this validator relies on Rails own `blank` error message.
151
+
152
+ ---
153
+
154
+ ### Limit
70
155
 
71
- - Content type validation using symbols. In order to infer the correct mime type from the symbol, the types must be registered with `Marcel::EXTENSIONS` (`MimeMagic::EXTENSIONS` for Rails <= 6.1.3).
156
+ Validates the number of uploaded files.
72
157
 
158
+ #### Options
159
+
160
+ The `limit` validator has 2 possible options:
161
+ - `min`: defines the minimum allowed number of files
162
+ - `max`: defines the maximum allowed number of files
163
+
164
+ #### Examples
165
+
166
+ Use it like this:
73
167
  ```ruby
74
168
  class User < ApplicationRecord
75
- has_one_attached :avatar
76
- has_many_attached :photos
77
-
78
- validates :avatar, attached: true, content_type: :png # Marcel::Magic.by_extension(:png).to_s => 'image/png'
79
- # Rails <= 6.1.3; MimeMagic.by_extension(:png).to_s => 'image/png'
80
- # or
81
- validates :photos, attached: true, content_type: [:png, :jpg, :jpeg]
82
- # or
83
- validates :avatar, content_type: /\Aimage\/.*\z/
169
+ has_many_attached :certificates
170
+
171
+ validates :certificates, limit: { min: 1, max: 10 } # restricts the number of files to between 1 and 10
84
172
  end
85
173
  ```
86
174
 
87
- - Dimension validation with `width`, `height` and `in`.
175
+ #### Error messages (I18n)
176
+
177
+ ```yml
178
+ en:
179
+ errors:
180
+ messages:
181
+ limit_out_of_range:
182
+ zero: "no files attached (must have between %{min} and %{max} files)"
183
+ one: "only 1 file attached (must have between %{min} and %{max} files)"
184
+ other: "total number of files must be between %{min} and %{max} files (there are %{count} files attached)"
185
+ limit_min_not_reached:
186
+ zero: "no files attached (must have at least %{min} files)"
187
+ one: "only 1 file attached (must have at least %{min} files)"
188
+ other: "%{count} files attached (must have at least %{min} files)"
189
+ limit_max_exceeded:
190
+ zero: "no files attached (maximum is %{max} files)"
191
+ one: "too many files attached (maximum is %{max} files, got %{count})"
192
+ other: "too many files attached (maximum is %{max} files, got %{count})"
193
+ ```
194
+
195
+ The `limit` validator error messages expose 3 values that you can use:
196
+ - `min` containing the minimum allowed number of files (e.g. `1`)
197
+ - `max` containing the maximum allowed number of files (e.g. `10`)
198
+ - `count` containing the current number of files (e.g. `5`)
199
+
200
+ ---
201
+
202
+ ### Content type
203
+
204
+ Validates if the attachment has an allowed content type.
88
205
 
206
+ #### Options
207
+
208
+ The `content_type` validator has 3 possible options:
209
+ - `with`: defines the allowed content type (string, symbol or regex)
210
+ - `in`: defines the allowed content types (array of strings or symbols)
211
+ - `spoofing_protection`: enables content type spoofing protection (boolean, defaults to `false`)
212
+
213
+ As mentioned above, this validator can define content types in several ways:
214
+ - String: `image/png` or `png`
215
+ - Symbol: `:png`
216
+ - Regex: `/\Avideo\/.*\z/`
217
+
218
+ #### Examples
219
+
220
+ Use it like this:
89
221
  ```ruby
90
222
  class User < ApplicationRecord
91
223
  has_one_attached :avatar
92
- has_many_attached :photos
93
224
 
94
- validates :avatar, dimension: { width: { in: 80..100 }, message: 'is not given between dimension' }
95
- validates :photos, dimension: { height: { in: 600..1800 } }
225
+ validates :avatar, content_type: 'image/png' # only allows PNG images
226
+ validates :avatar, content_type: :png # only allows PNG images, same as { with: :png }
227
+ validates :avatar, content_type: /\Avideo\/.*\z/ # only allows video files
228
+ validates :avatar, content_type: ['image/png', 'image/jpeg'] # only allows PNG and JPEG images
229
+ validates :avatar, content_type: { in: [:png, :jpeg], spoofing_protection: true } # only allows PNG, JPEG and their variants, with spoofing protection enabled
96
230
  end
97
231
  ```
98
232
 
99
- - Dimension validation with `min` and `max` range for width and height:
233
+ #### Best practices
100
234
 
235
+ When using the `content_type` validator, it is recommended to reflect the allowed content types in the html [`accept`](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept) attribute 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).
236
+
237
+ For example, if you want to allow PNG and JPEG images only, you can do this:
101
238
  ```ruby
102
239
  class User < ApplicationRecord
240
+ ACCEPTED_CONTENT_TYPES = ['image/png', 'image/jpeg'].freeze
241
+
103
242
  has_one_attached :avatar
104
- has_many_attached :photos
105
-
106
- validates :avatar, dimension: { min: 200..100 }
107
- # Equivalent to:
108
- # validates :avatar, dimension: { width: { min: 200 }, height: { min: 100 } }
109
- validates :photos, dimension: { min: 200..100, max: 400..200 }
110
- # Equivalent to:
111
- # validates :avatar, dimension: { width: { min: 200, max: 400 }, height: { min: 100, max: 200 } }
243
+
244
+ validates :avatar, content_type: ACCEPTED_CONTENT_TYPES
112
245
  end
113
246
  ```
114
247
 
115
- - Aspect ratio validation:
248
+ ```erb
249
+ <%= form_with model: @user do |f| %>
250
+ <%= f.file_field :avatar,
251
+ accept: ACCEPTED_CONTENT_TYPES.join(',') %>
252
+ <% end %>
253
+ ```
254
+
255
+ #### Content type shorthands
256
+
257
+ 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:
116
258
 
259
+ ```ruby
260
+ Marcel::MimeType.extend "application/ino", extensions: %w(ino), parents: "text/plain" # Registering arduino INO files
261
+ ```
262
+
263
+ Be sure to at least include one the `extensions`, `parents` or `magic` option, otherwise the content type will not be registered.
264
+
265
+ #### Content type spoofing protection
266
+
267
+ 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.
268
+
269
+ <details>
270
+ <summary>
271
+ What is content type spoofing?
272
+ </summary>
273
+
274
+ 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).
275
+ </details>
276
+
277
+ <details>
278
+ <summary>
279
+ How do we prevent it?
280
+ </summary>
281
+
282
+ 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.
283
+
284
+ 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.
285
+ </details>
286
+
287
+ <details>
288
+ <summary>
289
+ Edge cases
290
+ </summary>
291
+
292
+ The difficulty to accurately predict a mime type may generate false positives, if so there are two solutions available:
293
+ - 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!)
294
+ - 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!
295
+ </details>
296
+
297
+
298
+ #### Error messages (I18n)
299
+
300
+ ```yml
301
+ en:
302
+ errors:
303
+ messages:
304
+ content_type_invalid:
305
+ one: "has an invalid content type (authorized content type is %{authorized_human_content_types})"
306
+ other: "has an invalid content type (authorized content types are %{authorized_human_content_types})"
307
+ content_type_spoofed:
308
+ 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})"
309
+ 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})"
310
+ ```
311
+
312
+ The `content_type` validator error messages expose 7 values that you can use:
313
+ - `content_type` containing the content type of the sent file (e.g. `image/png`)
314
+ - `human_content_type` containing a more user-friendly version of the sent file content type (e.g. 'TXT' for 'text/plain')
315
+ - `detected_content_type` containing the detected content type of the sent file using `spoofing_protection` option (e.g. `image/png`)
316
+ - `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')
317
+ - `authorized_human_content_types` containing the list of authorized content types (e.g. 'PNG, JPEG' for `['image/png', 'image/jpeg']`)
318
+ - `count` containing the number of authorized content types (e.g. `2`)
319
+ - `filename` containing the filename
320
+
321
+ ---
322
+
323
+ ### Size
324
+
325
+ Validates each attached file size.
326
+
327
+ #### Options
328
+
329
+ The `size` validator has 5 possible options:
330
+ - `less_than`: defines the strict maximum allowed file size
331
+ - `less_than_or_equal_to`: defines the maximum allowed file size
332
+ - `greater_than`: defines the strict minimum allowed file size
333
+ - `greater_than_or_equal_to`: defines the minimum allowed file size
334
+ - `between`: defines the allowed file size range
335
+ - `equal_to`: defines the allowed file size
336
+
337
+ #### Examples
338
+
339
+ Use it like this:
117
340
  ```ruby
118
341
  class User < ApplicationRecord
119
342
  has_one_attached :avatar
120
- has_one_attached :photo
121
- has_many_attached :photos
122
343
 
123
- validates :avatar, aspect_ratio: :square
124
- validates :photo, aspect_ratio: :landscape
344
+ validates :avatar, size: { less_than: 2.megabytes } # restricts the file size to < 2MB
345
+ validates :avatar, size: { less_than_or_equal_to: 2.megabytes } # restricts the file size to <= 2MB
346
+ validates :avatar, size: { greater_than: 1.kilobyte } # restricts the file size to > 1KB
347
+ validates :avatar, size: { greater_than_or_equal_to: 1.kilobyte } # restricts the file size to >= 1KB
348
+ validates :avatar, size: { between: 1.kilobyte..2.megabytes } # restricts the file size to between 1KB and 2MB
349
+ validates :avatar, size: { equal_to: 1.megabyte } # restricts the file size to exactly 1MB
350
+ end
351
+ ```
352
+
353
+ #### Best practices
125
354
 
126
- # you can also pass dynamic aspect ratio, like :is_4_3, :is_16_9, etc
127
- validates :photos, aspect_ratio: :is_4_3
355
+ 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.
356
+ 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.
357
+
358
+ #### Error messages (I18n)
359
+
360
+ ```yml
361
+ en:
362
+ errors:
363
+ messages:
364
+ file_size_not_less_than: "file size must be less than %{max} (current size is %{file_size})"
365
+ file_size_not_less_than_or_equal_to: "file size must be less than or equal to %{max} (current size is %{file_size})"
366
+ file_size_not_greater_than: "file size must be greater than %{min} (current size is %{file_size})"
367
+ file_size_not_greater_than_or_equal_to: "file size must be greater than or equal to %{min} (current size is %{file_size})"
368
+ file_size_not_between: "file size must be between %{min} and %{max} (current size is %{file_size})"
369
+ file_size_not_equal_to: "file size must be equal to %{exact} (current size is %{file_size})"
370
+ ```
371
+
372
+ The `size` validator error messages expose 4 values that you can use:
373
+ - `file_size` containing the current file size (e.g. `1.5MB`)
374
+ - `min` containing the minimum allowed file size (e.g. `1KB`)
375
+ - `exact` containing the allowed file size (e.g. `1MB`)
376
+ - `max` containing the maximum allowed file size (e.g. `2MB`)
377
+ - `filename` containing the current file name
378
+
379
+ ---
380
+
381
+ ### Total size
382
+
383
+ Validates the total file size for several files.
384
+
385
+ #### Options
386
+
387
+ The `total_size` validator has 5 possible options:
388
+ - `less_than`: defines the strict maximum allowed total file size
389
+ - `less_than_or_equal_to`: defines the maximum allowed total file size
390
+ - `greater_than`: defines the strict minimum allowed total file size
391
+ - `greater_than_or_equal_to`: defines the minimum allowed total file size
392
+ - `between`: defines the allowed total file size range
393
+ - `equal_to`: defines the allowed total file size
394
+
395
+ #### Examples
396
+
397
+ Use it like this:
398
+ ```ruby
399
+ class User < ApplicationRecord
400
+ has_many_attached :certificates
401
+
402
+ validates :certificates, total_size: { less_than: 10.megabytes } # restricts the total size to < 10MB
403
+ validates :certificates, total_size: { less_than_or_equal_to: 10.megabytes } # restricts the total size to <= 10MB
404
+ validates :certificates, total_size: { greater_than: 1.kilobyte } # restricts the total size to > 1KB
405
+ validates :certificates, total_size: { greater_than_or_equal_to: 1.kilobyte } # restricts the total size to >= 1KB
406
+ validates :certificates, total_size: { between: 1.kilobyte..10.megabytes } # restricts the total size to between 1KB and 10MB
407
+ validates :certificates, total_size: { equal_to: 1.megabyte } # restricts the total file size to exactly 1MB
128
408
  end
129
409
  ```
130
410
 
131
- - Proc Usage:
411
+ #### Error messages (I18n)
132
412
 
133
- Procs can be used instead of values in all the above examples. They will be called on every validation.
413
+ ```yml
414
+ en:
415
+ errors:
416
+ messages:
417
+ total_file_size_not_less_than: "total file size must be less than %{max} (current size is %{total_file_size})"
418
+ 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})"
419
+ total_file_size_not_greater_than: "total file size must be greater than %{min} (current size is %{total_file_size})"
420
+ 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})"
421
+ total_file_size_not_between: "total file size must be between %{min} and %{max} (current size is %{total_file_size})"
422
+ total_file_size_not_equal_to: "total file size must be equal to %{exact} (current size is %{total_file_size})"
423
+ ```
424
+
425
+ The `total_size` validator error messages expose 4 values that you can use:
426
+ - `total_file_size` containing the current total file size (e.g. `1.5MB`)
427
+ - `min` containing the minimum allowed total file size (e.g. `1KB`)
428
+ - `exact` containing the allowed total file size (e.g. `1MB`)
429
+ - `max` containing the maximum allowed total file size (e.g. `2MB`)
430
+
431
+ ---
432
+
433
+ ### Dimension
434
+
435
+ Validates the dimension of the attached image / video files.
436
+ It can also be used for pdf files, but it will only analyze the pdf first page, and will assume a DPI of 72.
437
+ (be sure to have the right dependencies installed as mentioned in [installation](#installation))
438
+
439
+ #### Options
440
+
441
+ The `dimension` validator has several possible options:
442
+ - `width`: defines the allowed width (integer)
443
+ - `min`: defines the minimum allowed width (integer)
444
+ - `max`: defines the maximum allowed width (integer)
445
+ - `in`: defines the allowed width range (range)
446
+ - `height`: defines the allowed height (integer)
447
+ - `min`: defines the minimum allowed height (integer)
448
+ - `max`: defines the maximum allowed height (integer)
449
+ - `in`: defines the allowed height range (range)
450
+ - `min`: defines the minimum allowed width and height (range)
451
+ - `max`: defines the maximum allowed width and height (range)
452
+
453
+ #### Examples
454
+
455
+ Use it like this:
134
456
  ```ruby
135
457
  class User < ApplicationRecord
136
- has_many_attached :proc_files
458
+ has_one_attached :avatar
137
459
 
138
- validates :proc_files, limit: { max: -> (record) { record.admin? ? 100 : 10 } }
460
+ validates :avatar, dimension: { width: 100 } # restricts the width to 100 pixels
461
+ validates :avatar, dimension: { width: { min: 80, max: 100 } } # restricts the width to between 80 and 100 pixels
462
+ validates :avatar, dimension: { width: { in: 80..100 } } # restricts the width to between 80 and 100 pixels
463
+ validates :avatar, dimension: { height: 100 } # restricts the height to 100 pixels
464
+ validates :avatar, dimension: { height: { min: 600, max: 1800 } } # restricts the height to between 600 and 1800 pixels
465
+ validates :avatar, dimension: { height: { in: 600..1800 } } # restricts the height to between 600 and 1800 pixels
466
+ 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
139
467
  end
468
+ ```
140
469
 
470
+ #### Error messages (I18n)
471
+
472
+ ```yml
473
+ en:
474
+ errors:
475
+ messages:
476
+ dimension_min_not_included_in: "must be greater than or equal to %{width} x %{height} pixels"
477
+ dimension_max_not_included_in: "must be less than or equal to %{width} x %{height} pixels"
478
+ dimension_width_not_included_in: "width is not included between %{min} and %{max} pixels"
479
+ dimension_height_not_included_in: "height is not included between %{min} and %{max} pixels"
480
+ dimension_width_not_greater_than_or_equal_to: "width must be greater than or equal to %{length} pixels"
481
+ dimension_height_not_greater_than_or_equal_to: "height must be greater than or equal to %{length} pixels"
482
+ dimension_width_not_less_than_or_equal_to: "width must be less than or equal to %{length} pixels"
483
+ dimension_height_not_less_than_or_equal_to: "height must be less than or equal to %{length} pixels"
484
+ dimension_width_not_equal_to: "width must be equal to %{length} pixels"
485
+ dimension_height_not_equal_to: "height must be equal to %{length} pixels"
486
+ media_metadata_missing: "is not a valid media file"
141
487
  ```
142
488
 
143
- ## Internationalization (I18n)
489
+ The `dimension` validator error messages expose 6 values that you can use:
490
+ - `min` containing the minimum width or height allowed
491
+ - `max` containing the maximum width or height allowed
492
+ - `width` containing the minimum or maximum width allowed
493
+ - `height` containing the minimum or maximum width allowed
494
+ - `length` containing the exact width or height allowed
495
+ - `filename` containing the current filename in error
496
+
497
+ ---
498
+
499
+ ### Duration
500
+
501
+ Validates the duration of the attached audio / video files.
502
+ (be sure to have the right dependencies installed as mentioned in [installation](#installation))
503
+
504
+ #### Options
505
+
506
+ The `duration` validator has 5 possible options:
507
+ - `less_than`: defines the strict maximum allowed file duration
508
+ - `less_than_or_equal_to`: defines the maximum allowed file duration
509
+ - `greater_than`: defines the strict minimum allowed file duration
510
+ - `greater_than_or_equal_to`: defines the minimum allowed file duration
511
+ - `between`: defines the allowed file duration range
512
+ - `equal_to`: defines the allowed duration
513
+
514
+ #### Examples
515
+
516
+ Use it like this:
517
+ ```ruby
518
+ class User < ApplicationRecord
519
+ has_one_attached :intro_song
520
+
521
+ validates :intro_song, duration: { less_than: 2.minutes } # restricts the file duration to < 2 minutes
522
+ validates :intro_song, duration: { less_than_or_equal_to: 2.minutes } # restricts the file duration to <= 2 minutes
523
+ validates :intro_song, duration: { greater_than: 1.second } # restricts the file duration to > 1 second
524
+ validates :intro_song, duration: { greater_than_or_equal_to: 1.second } # restricts the file duration to >= 1 second
525
+ validates :intro_song, duration: { between: 1.second..2.minutes } # restricts the file duration to between 1 second and 2 minutes
526
+ validates :intro_song, duration: { equal_to: 1.minute } # restricts the duration to exactly 1 minute
527
+ end
528
+ ```
144
529
 
145
- Active Storage Validations uses I18n for error messages. For this, add these keys in your translation file:
530
+ #### Error messages (I18n)
146
531
 
147
532
  ```yml
148
533
  en:
149
534
  errors:
150
535
  messages:
151
- content_type_invalid: "has an invalid content type"
152
- file_size_out_of_range: "size %{file_size} is not between required range"
153
- limit_out_of_range: "total number is out of range"
154
- image_metadata_missing: "is not a valid image"
155
- dimension_min_inclusion: "must be greater than or equal to %{width} x %{height} pixel."
156
- dimension_max_inclusion: "must be less than or equal to %{width} x %{height} pixel."
157
- dimension_width_inclusion: "width is not included between %{min} and %{max} pixel."
158
- dimension_height_inclusion: "height is not included between %{min} and %{max} pixel."
159
- dimension_width_greater_than_or_equal_to: "width must be greater than or equal to %{length} pixel."
160
- dimension_height_greater_than_or_equal_to: "height must be greater than or equal to %{length} pixel."
161
- dimension_width_less_than_or_equal_to: "width must be less than or equal to %{length} pixel."
162
- dimension_height_less_than_or_equal_to: "height must be less than or equal to %{length} pixel."
163
- dimension_width_equal_to: "width must be equal to %{length} pixel."
164
- dimension_height_equal_to: "height must be equal to %{length} pixel."
165
- aspect_ratio_not_square: "must be a square image"
166
- aspect_ratio_not_portrait: "must be a portrait image"
167
- aspect_ratio_not_landscape: "must be a landscape image"
168
- aspect_ratio_is_not: "must have an aspect ratio of %{aspect_ratio}"
169
- aspect_ratio_unknown: "has an unknown aspect ratio"
170
- image_not_processable: "is not a valid image"
171
- ```
172
-
173
- In some cases, Active Storage Validations provides variables to help you customize messages:
174
-
175
- The "content_type_invalid" key has two variables that you can use, a variable named "content_type" containing the content type of the send file and a variable named "authorized_types" containing the list of authorized content types.
176
-
177
- The variables are not used by default to leave the choice to the user.
178
-
179
- For example :
536
+ duration_not_less_than: "duration must be less than %{max} (current duration is %{duration})"
537
+ duration_not_less_than_or_equal_to: "duration must be less than or equal to %{max} (current duration is %{duration})"
538
+ duration_not_greater_than: "duration must be greater than %{min} (current duration is %{duration})"
539
+ duration_not_greater_than_or_equal_to: "duration must be greater than or equal to %{min} (current duration is %{duration})"
540
+ duration_not_between: "duration must be between %{min} and %{max} (current duration is %{duration})"
541
+ duration_not_equal_to: "duration must be equal to %{exact} (current duration is %{duration})"
542
+ ```
543
+
544
+ The `duration` validator error messages expose 4 values that you can use:
545
+ - `duration` containing the current duration size (e.g. `2 minutes`)
546
+ - `min` containing the minimum allowed duration size (e.g. `1 second`)
547
+ - `exact` containing the allowed duration (e.g. `3 seconds`)
548
+ - `max` containing the maximum allowed duration size (e.g. `2 minutes`)
549
+ - `filename` containing the current file name
550
+
551
+ ---
552
+
553
+ ### Aspect ratio
554
+
555
+ Validates the aspect ratio of the attached image / video files.
556
+ It can also be used for pdf files, but it will only analyze the pdf first page.
557
+ (be sure to have the right dependencies installed as mentioned in [installation](#installation))
558
+
559
+ #### Options
560
+
561
+ The `aspect_ratio` validator has several options:
562
+ - `with`: defines the allowed aspect ratio (e.g. `:is_16/9`)
563
+ - `in`: defines the allowed aspect ratios (e.g. `%i[square landscape]`)
564
+
565
+ This validator can define aspect ratios in several ways:
566
+ - Symbols:
567
+ - prebuilt aspect ratios: `:square`, `:portrait`, `:landscape`
568
+ - custom aspect ratios (it must be of type `is_xx_yy`): `:is_16_9`, `:is_4_3`, etc.
569
+
570
+ #### Examples
571
+
572
+ Use it like this:
573
+ ```ruby
574
+ class User < ApplicationRecord
575
+ has_one_attached :avatar
576
+
577
+ validates :avatar, aspect_ratio: :square # restricts the aspect ratio to 1:1
578
+ validates :avatar, aspect_ratio: :portrait # restricts the aspect ratio to x:y where y > x
579
+ validates :avatar, aspect_ratio: :landscape # restricts the aspect ratio to x:y where x > y
580
+ validates :avatar, aspect_ratio: :is_16_9 # restricts the aspect ratio to 16:9
581
+ validates :avatar, aspect_ratio: %i[square is_16_9] # restricts the aspect ratio to 1:1 and 16:9
582
+ end
583
+ ```
584
+
585
+ #### Error messages (I18n)
180
586
 
181
587
  ```yml
182
- content_type_invalid: "has an invalid content type : %{content_type}"
588
+ en:
589
+ errors:
590
+ messages:
591
+ aspect_ratio_not_square: "must be square (current file is %{width}x%{height}px)"
592
+ aspect_ratio_not_portrait: "must be portrait (current file is %{width}x%{height}px)"
593
+ aspect_ratio_not_landscape: "must be landscape (current file is %{width}x%{height}px)"
594
+ aspect_ratio_not_x_y: "must be %{authorized_aspect_ratios} (current file is %{width}x%{height}px)"
595
+ aspect_ratio_invalid: "has an invalid aspect ratio (valid aspect ratios are %{authorized_aspect_ratios})"
596
+ media_metadata_missing: "is not a valid media file"
183
597
  ```
184
598
 
185
- Also the "limit_out_of_range" key supports two variables the "min" and "max".
599
+ The `aspect_ratio` validator error messages expose 4 values that you can use:
600
+ - `authorized_aspect_ratios` containing the authorized aspect ratios
601
+ - `width` containing the current width of the image/video
602
+ - `height` containing the current height of the image/video
603
+ - `filename` containing the current filename in error
604
+
605
+ ---
186
606
 
187
- For example :
607
+ ### Processable file
608
+
609
+ Validates if the attached files can be processed by MiniMagick or Vips (image), ffmpeg (video/audio) or poppler (pdf).
610
+ (be sure to have the right dependencies installed as mentioned in [installation](#installation))
611
+
612
+ #### Options
613
+
614
+ The `processable_file` validator has no options.
615
+
616
+ #### Examples
617
+
618
+ Use it like this:
619
+ ```ruby
620
+ class User < ApplicationRecord
621
+ has_one_attached :avatar
622
+
623
+ validates :avatar, processable_file: true # ensures that the file is processable by MiniMagick or Vips (image) or ffmpeg (video/audio)
624
+ end
625
+ ```
626
+
627
+ #### Error messages (I18n)
188
628
 
189
629
  ```yml
190
- limit_out_of_range: "total number is out of range. range: [%{min}, %{max}]"
630
+ en:
631
+ errors:
632
+ messages:
633
+ file_not_processable: "is not identified as a valid media file"
191
634
  ```
192
635
 
193
- ## Installation
636
+ The `processable_file` validator error messages expose 1 value that you can use:
637
+ - `filename` containing the current filename in error
194
638
 
195
- Add this line to your application's Gemfile:
639
+ ---
196
640
 
197
- ```ruby
198
- # Rails 5.2 and Rails 6
199
- gem 'active_storage_validations'
641
+ ### Pages
200
642
 
201
- # Optional, to use :dimension validator or :aspect_ratio validator
202
- gem 'mini_magick', '>= 4.9.5'
203
- # Or
204
- gem 'ruby-vips', '>= 2.1.0'
643
+ Validates each attached pdf file number of pages.
644
+ (be sure to have the right dependencies installed as mentioned in [installation](#installation))
645
+
646
+ #### Options
647
+
648
+ The `pages` validator has 6 possible options:
649
+ - `less_than`: defines the strict maximum allowed number of pages
650
+ - `less_than_or_equal_to`: defines the maximum allowed number of pages
651
+ - `greater_than`: defines the strict minimum allowed number of pages
652
+ - `greater_than_or_equal_to`: defines the minimum allowed number of pages
653
+ - `between`: defines the allowed number of pages range
654
+ - `equal_to`: defines the allowed number of pages
655
+
656
+ #### Examples
657
+
658
+ Use it like this:
659
+ ```ruby
660
+ class User < ApplicationRecord
661
+ has_one_attached :contract
662
+
663
+ validates :contract, pages: { less_than: 2 } # restricts the number of pages to < 2
664
+ validates :contract, pages: { less_than_or_equal_to: 2 } # restricts the number of pages to <= 2
665
+ validates :contract, pages: { greater_than: 1 } # restricts the number of pages to > 1
666
+ validates :contract, pages: { greater_than_or_equal_to: 1 } # restricts the number of pages to >= 1
667
+ validates :contract, pages: { between: 1..2 } # restricts the number of pages to between 1 and 2
668
+ validates :contract, pages: { equal_to: 1 } # restricts the number of pages to exactly 1
669
+ end
205
670
  ```
206
671
 
207
- And then execute:
672
+ #### Error messages (I18n)
208
673
 
209
- ```bash
210
- $ bundle
674
+ ```yml
675
+ en:
676
+ errors:
677
+ messages:
678
+ pages_not_less_than: "page count must be less than %{max} (current page count is %{pages})"
679
+ pages_not_less_than_or_equal_to: "page count must be less than or equal to %{max} (current page count is %{pages})"
680
+ pages_not_greater_than: "page count must be greater than %{min} (current page count is %{pages})"
681
+ pages_not_greater_than_or_equal_to: "page count must be greater than or equal to %{min} (current page count is %{pages})"
682
+ pages_not_between: "page count must be between %{min} and %{max} (current page count is %{pages})"
683
+ pages_not_equal_to: "page count must be equal to %{exact} (current page count is %{pages})"
211
684
  ```
212
685
 
213
- ## Sample
686
+ The `pages` validator error messages expose 5 values that you can use:
687
+ - `pages` containing the current file number of pages (e.g. `7`)
688
+ - `min` containing the minimum allowed number of pages (e.g. `1`)
689
+ - `exact` containing the allowed number of pages (e.g. `3`)
690
+ - `max` containing the maximum allowed number of pages (e.g. `5`)
691
+ - `filename` containing the current file name
692
+
693
+ ---
694
+
695
+ ## Upgrading from 1.x to 2.x
696
+
697
+ 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!
698
+
699
+ Added features:
700
+ - `duration` validator has been added for audio / video files
701
+ - `dimension` validator now supports videos
702
+ - `aspect_ratio` validator now supports videos
703
+ - `processable_image` validator is now `processable_file` validator and supports image/video/audio
704
+ - 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)).
705
+ - All error messages have been given an upgrade and new variables that you can use
706
+
707
+ But this major version bump also comes with some breaking changes. Below are the main breaking changes you need to be aware of:
708
+ - Error messages
709
+ - 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.
710
+ - If you wish to change them one by one, here is the list of changes to make:
711
+ - Some validator errors have been totally changed:
712
+ - `limit` validator keys have been totally reworked
713
+ - `dimension` validator keys have been totally reworked
714
+ - `content_type` validator keys have been totally reworked
715
+ - `processable_image` validator keys have been totally reworked
716
+ - Some keys have been changed:
717
+ - `image_metadata_missing` has been replaced by `media_metadata_missing`
718
+ - `aspect_ratio_is_not` has been replaced by `aspect_ratio_not_x_y`
719
+ - Some error messages variables names have been changed to improve readability:
720
+ - `aspect_ratio` validator:
721
+ - `aspect_ratio` has been replaced by `authorized_aspect_ratios`
722
+ - `content_type` validator:
723
+ - `authorized_types` has been replaced by `authorized_human_content_types`
724
+ - `size` validator:
725
+ - `min_size` has been replaced by `min`
726
+ - `max_size` has been replaced by `max`
727
+ - `total_size` validator:
728
+ - `min_size` has been replaced by `min`
729
+ - `max_size` has been replaced by `max`
730
+
731
+ - `content_type` validator
732
+ - The `:in` option now only accepts 'valid' content types (ie content types deemed by Marcel as valid).
733
+ - 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.
734
+ - 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`.
735
+ - An `ArgumentError` is now raised if `image/jpg` is used to make it easier to fix. You should now only use `image/jpeg`.
736
+
737
+ - `processable_image` validator
738
+ - The validator has been replaced by `processable_file` validator, be sure to replace `processable_image: true` to `processable_file: true`
739
+ - The associated matcher has also been updated accordingly, be sure to replace `validate_processable_image_of` to `validate_processable_file_of`
740
+
741
+ ## Upgrading from 2.x to 3.x
742
+
743
+ Version 3 comes with the ability to support single page pdf `dimension` / `aspect_ratio` analysis, we had to make a breaking change:
744
+ - To analyze PDFs, you must install the `poppler` PDF processing dependency
745
+ - It's a Rails-supported PDF processing dependency (https://guides.rubyonrails.org/active_storage_overview.html#requirements)
746
+ - To install it, check their documentation at this [link](https://pdf2image.readthedocs.io/en/latest/installation.html).
747
+ - To check if it's installed, execute `pdftoppm -h`.
748
+ - To install this tool in your CI / production environments, you can check how we do it in our own CI (https://github.com/igorkasyanchuk/active_storage_validations/blob/master/.github/workflows/main.yml)
749
+
750
+ We also added the `pages` validator to validate pdf number of pages, and the `equal_to` option to `duration`, `size` and `total_size` validators.
751
+
752
+ Note that, if you do not perform these metadata validations on pdfs, the gem will work the same as in version 2.
753
+
754
+ ## Internationalization (I18n)
755
+
756
+ Active Storage Validations uses I18n for error messages. The error messages are automatically loaded in your Rails app if your language translations are present in the gem.
214
757
 
215
- Very simple example of validation with file attached, content type check and custom error message.
758
+ Translation files are available [here](https://github.com/igorkasyanchuk/active_storage_validations/tree/master/config/locales). We currently have translations for `da`, `de`, `en`, `en-GB`, `es`, `fr`, `it`, `ja`, `nl`, `pl`, `pt-BR`, `ru`, `sv`, `tr`, `uk`, `vi` and `zh-CN`. Feel free to drop a PR to add your language ✌️.
216
759
 
217
- [![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)
760
+ If you wish to customize the error messages, just copy, paste and update the translation files into your application locales.
218
761
 
219
762
  ## Test matchers
220
- Provides RSpec-compatible and Minitest-compatible matchers for testing the validators.
763
+
764
+ The gem also provides RSpec-compatible and Minitest-compatible matchers for testing the validators.
221
765
 
222
766
  ### RSpec
223
767
 
768
+ #### Setup
224
769
  In `spec_helper.rb`, you'll need to require the matchers:
225
770
 
226
771
  ```ruby
227
772
  require 'active_storage_validations/matchers'
228
773
  ```
229
774
 
230
- And _include_ the module:
775
+ And include the module:
231
776
 
232
777
  ```ruby
233
778
  RSpec.configure do |config|
@@ -235,41 +780,110 @@ RSpec.configure do |config|
235
780
  end
236
781
  ```
237
782
 
238
- Example (Note that the options are chainable):
783
+ #### Matchers
784
+ Matcher methods available:
239
785
 
240
786
  ```ruby
241
787
  describe User do
788
+ # aspect_ratio:
789
+ # #allowing, #rejecting
790
+ it { is_expected.to validate_aspect_ratio_of(:avatar).allowing(:square, :portrait) } # possible to use an Array or *splatted array
791
+ it { is_expected.to validate_aspect_ratio_of(:avatar).rejecting(:square, :landscape) } # possible to use an Array or *splatted array
792
+
793
+ # attached
242
794
  it { is_expected.to validate_attached_of(:avatar) }
243
795
 
244
- it { is_expected.to validate_content_type_of(:avatar).allowing('image/png', 'image/gif') }
245
- it { is_expected.to validate_content_type_of(:avatar).rejecting('text/plain', 'text/xml') }
796
+ # processable_file
797
+ it { is_expected.to validate_processable_file_of(:avatar) }
798
+
799
+ # limit
800
+ # #min, #max
801
+ it { is_expected.to validate_limits_of(:avatar).min(1) }
802
+ it { is_expected.to validate_limits_of(:avatar).max(5) }
803
+
804
+ # content_type:
805
+ # #allowing, #rejecting
806
+ it { is_expected.to validate_content_type_of(:avatar).allowing('image/png', 'image/gif') } # possible to use an Array or *splatted array
807
+ it { is_expected.to validate_content_type_of(:avatar).rejecting('text/plain', 'text/xml') } # possible to use an Array or *splatted array
246
808
 
809
+ # dimension:
810
+ # #width, #height, #width_min, #height_min, #width_max, #height_max, #width_between, #height_between
247
811
  it { is_expected.to validate_dimensions_of(:avatar).width(250) }
248
812
  it { is_expected.to validate_dimensions_of(:avatar).height(200) }
249
- it { is_expected.to validate_dimensions_of(:avatar).width(250).height(200).with_message('Invalid dimensions.') }
250
813
  it { is_expected.to validate_dimensions_of(:avatar).width_min(200) }
251
- it { is_expected.to validate_dimensions_of(:avatar).width_max(500) }
252
814
  it { is_expected.to validate_dimensions_of(:avatar).height_min(100) }
815
+ it { is_expected.to validate_dimensions_of(:avatar).width_max(500) }
253
816
  it { is_expected.to validate_dimensions_of(:avatar).height_max(300) }
254
817
  it { is_expected.to validate_dimensions_of(:avatar).width_between(200..500) }
255
818
  it { is_expected.to validate_dimensions_of(:avatar).height_between(100..300) }
256
819
 
820
+ # size:
821
+ # #less_than, #less_than_or_equal_to, #greater_than, #greater_than_or_equal_to, #between, #equal_to
257
822
  it { is_expected.to validate_size_of(:avatar).less_than(50.kilobytes) }
258
823
  it { is_expected.to validate_size_of(:avatar).less_than_or_equal_to(50.kilobytes) }
259
824
  it { is_expected.to validate_size_of(:avatar).greater_than(1.kilobyte) }
260
825
  it { is_expected.to validate_size_of(:avatar).greater_than_or_equal_to(1.kilobyte) }
261
826
  it { is_expected.to validate_size_of(:avatar).between(100..500.kilobytes) }
827
+ it { is_expected.to validate_size_of(:avatar).equal_to(5.megabytes) }
828
+
829
+ # total_size:
830
+ # #less_than, #less_than_or_equal_to, #greater_than, #greater_than_or_equal_to, #between, #equal_to
831
+ it { is_expected.to validate_total_size_of(:avatar).less_than(50.kilobytes) }
832
+ it { is_expected.to validate_total_size_of(:avatar).less_than_or_equal_to(50.kilobytes) }
833
+ it { is_expected.to validate_total_size_of(:avatar).greater_than(1.kilobyte) }
834
+ it { is_expected.to validate_total_size_of(:avatar).greater_than_or_equal_to(1.kilobyte) }
835
+ it { is_expected.to validate_total_size_of(:avatar).between(100..500.kilobytes) }
836
+ it { is_expected.to validate_total_size_of(:avatar).equal_to(5.megabytes) }
837
+
838
+ # duration:
839
+ # #less_than, #less_than_or_equal_to, #greater_than, #greater_than_or_equal_to, #between, #equal_to
840
+ it { is_expected.to validate_duration_of(:introduction).less_than(50.seconds) }
841
+ it { is_expected.to validate_duration_of(:introduction).less_than_or_equal_to(50.seconds) }
842
+ it { is_expected.to validate_duration_of(:introduction).greater_than(1.minute) }
843
+ it { is_expected.to validate_duration_of(:introduction).greater_than_or_equal_to(1.minute) }
844
+ it { is_expected.to validate_duration_of(:introduction).between(100..500.seconds) }
845
+ it { is_expected.to validate_duration_of(:avatar).equal_to(5.minutes) }
846
+
847
+ # pages:
848
+ # #less_than, #less_than_or_equal_to, #greater_than, #greater_than_or_equal_to, #between, #equal_to
849
+ it { is_expected.to validate_pages_of(:contract).less_than(50) }
850
+ it { is_expected.to validate_pages_of(:contract).less_than_or_equal_to(50) }
851
+ it { is_expected.to validate_pages_of(:contract).greater_than(5) }
852
+ it { is_expected.to validate_pages_of(:contract).greater_than_or_equal_to(5) }
853
+ it { is_expected.to validate_pages_of(:contract).between(100..500) }
854
+ it { is_expected.to validate_pages_of(:contract).equal_to(5) }
855
+ end
856
+ ```
857
+ (Note that matcher methods are chainable)
858
+
859
+ All matchers can currently be customized with Rails validation options:
860
+
861
+ ```ruby
862
+ describe User do
863
+ # :allow_blank
864
+ it { is_expected.to validate_attached_of(:avatar).allow_blank }
865
+
866
+ # :on
867
+ it { is_expected.to validate_attached_of(:avatar).on(:update) }
868
+ it { is_expected.to validate_attached_of(:avatar).on(%i[update custom]) }
869
+
870
+ # :message
871
+ it { is_expected.to validate_dimensions_of(:avatar).width(250).with_message('Invalid dimensions.') }
262
872
  end
263
873
  ```
264
874
 
265
875
  ### Minitest
266
- To use the following syntax, make sure you have the [shoulda-context](https://github.com/thoughtbot/shoulda-context) gem up and running. To make use of the matchers you need to require the matchers:
876
+
877
+ #### Setup
878
+ To use the matchers, make sure you have the [shoulda-context](https://github.com/thoughtbot/shoulda-context) gem up and running.
879
+
880
+ You need to require the matchers:
267
881
 
268
882
  ```ruby
269
883
  require 'active_storage_validations/matchers'
270
884
  ```
271
885
 
272
- And _extend_ the module:
886
+ And extend the module:
273
887
 
274
888
  ```ruby
275
889
  class ActiveSupport::TestCase
@@ -277,136 +891,62 @@ class ActiveSupport::TestCase
277
891
  end
278
892
  ```
279
893
 
280
- Example (Note that the options are chainable):
894
+ #### Matchers
895
+ 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.
281
896
 
282
- ```ruby
283
- class UserTest < ActiveSupport::TestCase
284
- should validate_attached_of(:avatar)
285
-
286
- should validate_content_type_of(:avatar).allowing('image/png', 'image/gif')
287
- should validate_content_type_of(:avatar).rejecting('text/plain', 'text/xml')
288
-
289
- should validate_dimensions_of(:avatar).width(250)
290
- should validate_dimensions_of(:avatar).height(200)
291
- should validate_dimensions_of(:avatar).width(250).height(200).with_message('Invalid dimensions.')
292
- should validate_dimensions_of(:avatar).width_min(200)
293
- should validate_dimensions_of(:avatar).width_max(500)
294
- should validate_dimensions_of(:avatar).height_min(100)
295
- should validate_dimensions_of(:avatar).height_max(300)
296
- should validate_dimensions_of(:avatar).width_between(200..500)
297
- should validate_dimensions_of(:avatar).height_between(100..300)
298
-
299
- should validate_size_of(:avatar).less_than(50.kilobytes)
300
- should validate_size_of(:avatar).less_than_or_equal_to(50.kilobytes)
301
- should validate_size_of(:avatar).greater_than(1.kilobyte)
302
- should validate_size_of(:avatar).greater_than_or_equal_to(1.kilobyte)
303
- should validate_size_of(:avatar).between(100..500.kilobytes)
304
- end
305
- ```
306
897
 
307
- ## Todo
898
+ ## Contributing
308
899
 
309
- * verify with remote storages (s3, etc)
310
- * verify how it works with direct upload
311
- * better error message when content_size is invalid
312
- * add more translations
900
+ 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.
313
901
 
314
- ## Tests & Contributing
902
+ Before submitting your pull request, run the tests to make sure everything works as expected.
315
903
 
316
- To run tests in root folder of gem:
904
+ To run the gem tests, launch the following commands in the root folder of gem repository:
317
905
 
318
- * `BUNDLE_GEMFILE=gemfiles/rails_6_0.gemfile bundle exec rake test` to run for Rails 6.0
319
- * `BUNDLE_GEMFILE=gemfiles/rails_6_1.gemfile bundle exec rake test` to run for Rails 6.1
906
+ * `BUNDLE_GEMFILE=gemfiles/rails_6_1_4.gemfile bundle exec rake test` to run for Rails 6.1.4
320
907
  * `BUNDLE_GEMFILE=gemfiles/rails_7_0.gemfile bundle exec rake test` to run for Rails 7.0
321
- * `BUNDLE_GEMFILE=gemfiles/rails_next.gemfile bundle exec rake test` to run for Rails main branch
908
+ * `BUNDLE_GEMFILE=gemfiles/rails_7_1.gemfile bundle exec rake test` to run for Rails 7.1
909
+ * `BUNDLE_GEMFILE=gemfiles/rails_7_2.gemfile bundle exec rake test` to run for Rails 7.2
910
+ * `BUNDLE_GEMFILE=gemfiles/rails_8_0.gemfile bundle exec rake test` to run for Rails 8.0
911
+ * `BUNDLE_GEMFILE=gemfiles/rails_next.gemfile bundle exec rake test` to run for Rails main
322
912
 
323
913
  Snippet to run in console:
324
914
 
325
- ```
326
- BUNDLE_GEMFILE=gemfiles/rails_6_0.gemfile bundle
327
- BUNDLE_GEMFILE=gemfiles/rails_6_1.gemfile bundle
915
+ ```bash
916
+ BUNDLE_GEMFILE=gemfiles/rails_6_1_4.gemfile bundle
328
917
  BUNDLE_GEMFILE=gemfiles/rails_7_0.gemfile bundle
918
+ BUNDLE_GEMFILE=gemfiles/rails_7_1.gemfile bundle
919
+ BUNDLE_GEMFILE=gemfiles/rails_7_2.gemfile bundle
920
+ BUNDLE_GEMFILE=gemfiles/rails_8_0.gemfile bundle
329
921
  BUNDLE_GEMFILE=gemfiles/rails_next.gemfile bundle
330
- BUNDLE_GEMFILE=gemfiles/rails_6_0.gemfile bundle exec rake test
331
- BUNDLE_GEMFILE=gemfiles/rails_6_1.gemfile bundle exec rake test
922
+ BUNDLE_GEMFILE=gemfiles/rails_6_1_4.gemfile bundle exec rake test
332
923
  BUNDLE_GEMFILE=gemfiles/rails_7_0.gemfile bundle exec rake test
924
+ BUNDLE_GEMFILE=gemfiles/rails_7_1.gemfile bundle exec rake test
925
+ BUNDLE_GEMFILE=gemfiles/rails_7_2.gemfile bundle exec rake test
926
+ BUNDLE_GEMFILE=gemfiles/rails_8_0.gemfile bundle exec rake test
333
927
  BUNDLE_GEMFILE=gemfiles/rails_next.gemfile bundle exec rake test
334
928
  ```
335
929
 
336
- ## Known issues
930
+ Tips:
931
+ - To focus a specific test, use the `focus` class method provided by [minitest-focus](https://github.com/minitest/minitest-focus)
932
+ - 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`
337
933
 
338
- - There is an issue in Rails which it possible to get if you have added a validation and generating for example an image preview of attachments. It can be fixed with this:
339
934
 
340
- ```erb
341
- <% if @user.avatar.attached? && @user.avatar.attachment.blob.present? && @user.avatar.attachment.blob.persisted? %>
342
- <%= image_tag @user.avatar %>
343
- <% end %>
344
- ```
935
+ ## Additional information
345
936
 
346
- This is a Rails issue, and is fixed in Rails 6.
937
+ ### Contributors (BIG THANK YOU!)
347
938
 
348
- ## Contributing
349
- You are welcome to contribute.
350
-
351
- ## Contributors (BIG THANK YOU)
352
- - https://github.com/schweigert
353
- - https://github.com/tleneveu
354
- - https://github.com/reckerswartz
355
- - https://github.com/Uysim
356
- - https://github.com/D-system
357
- - https://github.com/ivanelrey
358
- - https://github.com/phlegx
359
- - https://github.com/rr-dev
360
- - https://github.com/dsmalko
361
- - https://github.com/danderozier
362
- - https://github.com/cseelus
363
- - https://github.com/vkinelev
364
- - https://github.com/reed
365
- - https://github.com/connorshea
366
- - https://github.com/Atul9
367
- - https://github.com/victorbueno
368
- - https://github.com/UICJohn
369
- - https://github.com/giovannibonetti
370
- - https://github.com/dlepage
371
- - https://github.com/StefSchenkelaars
372
- - https://github.com/willnet
373
- - https://github.com/mohanklein
374
- - https://github.com/High5Apps
375
- - https://github.com/mschnitzer
376
- - https://github.com/sinankeskin
377
- - https://github.com/alejandrodevs
378
- - https://github.com/molfar
379
- - https://github.com/connorshea
380
- - https://github.com/yshmarov
381
- - https://github.com/fongfan999
382
- - https://github.com/cooperka
383
- - https://github.com/dolarsrg
384
- - https://github.com/jayshepherd
385
- - https://github.com/ohbarye
386
- - https://github.com/randsina
387
- - https://github.com/vietqhoang
388
- - https://github.com/kemenaran
389
- - https://github.com/jrmhaig
390
- - https://github.com/tagliala
391
- - https://github.com/evedovelli
392
- - https://github.com/JuanVqz
393
- - https://github.com/luiseugenio
394
- - https://github.com/equivalent
395
- - https://github.com/NARKOZ
396
- - https://github.com/stephensolis
397
- - https://github.com/kwent
398
- - https://github.com/Animesh-Ghosh
399
- - https://github.com/gr8bit
400
- - https://github.com/codegeek319
401
- - https://github.com/clwy-cn
402
- - https://github.com/kukicola
403
- - https://github.com/sobrinho
404
- - https://github.com/iainbeeston
405
- - https://github.com/marckohlbrugge
406
-
407
- ## License
939
+ We have a long list of valued contributors. Check them all at:
940
+
941
+ https://github.com/igorkasyanchuk/active_storage_validations/graphs/contributors
942
+
943
+ ### License
408
944
 
409
945
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
410
946
 
947
+ <br>
948
+
411
949
  [<img src="https://github.com/igorkasyanchuk/rails_time_travel/blob/main/docs/more_gems.png?raw=true"
412
950
  />](https://www.railsjazz.com/?utm_source=github&utm_medium=bottom&utm_campaign=active_storage_validations)
951
+
952
+ [!["Buy Me A Coffee"](https://github.com/igorkasyanchuk/get-smart/blob/main/docs/snapshot-bmc-button.png?raw=true)](https://buymeacoffee.com/igorkasyanchuk)