Do it like => validates :photos, attached: true, content_type: ['image/png', 'image/jpg', 'image/jpeg'], size: { less_than: 500.kilobytes }, limit: { min: 1, max: 3 }, aspect_ratio: :landscape, dimension: { width: { in: 800..1600 }
See the codeActive Storage Validations is a gem that allows you to add validations for Active Storage attributes.
This gem is doing it right for you! Just use validates :avatar, attached: true, content_type: 'image/png' and that's it!
Active Storage Validations work with Rails 7.0.1 onwards and Ruby 3.3 onwards. Add this line to your application's Gemfile:
gem 'active_storage_validations'
And then execute:
$ bundle
Once you have installed the gem, I18n error messages will be added automatically to your app. See Internationalization (I18n) section for more details.
Optionally, to use the image metadata validators (dimension, aspect_ratio and processable_file), you will have to add one of the corresponding gems:
gem 'ruby-vips', '>= 2.1.0'
# Or
gem 'mini_magick', '>= 4.9.5'
Plus, you have to be sure to have the corresponding command-line tool installed on your system (libvips for ruby-vips, or ImageMagick for mini_magick — both locally and in CI / production).
We recommend libvips (ruby-vips + config.active_storage.variant_processor = :vips) for these validators. Rails already defaults Active Storage variants to libvips (ActiveStorage::Variant), and our image processor benchmarks show cold metadata analysis is about 8× faster than MiniMagick/ImageMagick on the same machine and fixtures. Warm validations (cached asv_* metadata) are similar for both.
To use the video and audio metadata validators (dimension, aspect_ratio, processable_file, duration and with_audio), 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).
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).
To use the spoofing_protection option with the content_type validator:
true / :file): the UNIX file command (usually preinstalled on UNIX systems):magika): the Google Magika CLI — install via brew install magika, their install script, cargo install --locked magika-cli, or pipx install magikaBoth backends are optional system tools (not Ruby gems). Prefer :magika when you can install the CLI — it is generally more accurate than file, especially on textual / ambiguous formats. Be sure to install Magika in CI / production if you enable :magika.
If you want some inspiration about how to add imagemagick, libvips, ffmpeg, poppler or magika 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)
Optional global settings can go in an initializer. Example template:
# config/initializers/active_storage_validations.rb
ActiveStorageValidations.configure do |config|
# Infer HTML accept= on file_field from content_type validators (default: true)
# config.infer_file_field_accept = false
# Max time for external analyzer commands: ffprobe, pdfinfo, file, magika, ImageMagick identify, libvips
# (default: 10.seconds; set to nil to disable)
# config.command_timeout = 10.seconds
end
# Optional: monitor analyzer timeouts
# ActiveSupport::Notifications.subscribe("timeout.active_storage_validations") do |*args|
# event = ActiveSupport::Notifications::Event.new(*args)
# Rails.logger.warn("[ASV] command timeout: #{event.payload}")
# end
# Optional: monitor how long metadata / content type analysis takes
# ActiveSupport::Notifications.subscribe("analyze.active_storage_validations") do |*args|
# event = ActiveSupport::Notifications::Event.new(*args)
# Rails.logger.info("[ASV] analyzed: #{event.payload}")
# end
The gem emits two ActiveSupport::Notifications events:
| Event | Payload | Emitted |
|---|---|---|
analyze.active_storage_validations | analyzer, command, timeout, timed_out, duration | Every time an analyzer command runs (i.e. on a cache miss) |
timeout.active_storage_validations | analyzer, command, timeout | Only when a command exceeds its deadline |
command_timeout bounds metadata analysis used by dimension, aspect_ratio, duration, with_audio, pages, processable_file, and content_type (with spoofing_protection). When a command times out, analysis fails closed and the validator adds its usual error (file_not_processable / media_metadata_missing / audio_missing / content-type errors) — there is no separate timeout error message.
The 10s default is enough for typical uploads. Raise it (or set nil) if you analyze very large videos/PDFs, especially on slow or network storage — otherwise those files can start failing validation after upgrade. See upgrade to 4.x.
Per-validator override (applies to the analysis triggered by that validator; the first analysis for a blob is cached):
validates :video, duration: { less_than: 5.minutes, timeout: 30.seconds }
Notes:
command_timeout (or per-validator timeout:) to nil disables the deadlineidentify through the same killable command runner (MiniMagick is only used to build the argv)timeout.active_storage_validationsList of validators:
Proc usage
Every validator can use procs instead of values in all the validator examples:
class User < ApplicationRecord
has_many_attached :files
validates :files, limit: { max: -> (record) { record.admin? ? 100 : 10 } }
end
Performance optimization
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.
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.
This also covers metadata the analyzer cannot extract from a given file (e.g. a duration validation on an image): the gem remembers that the value is unavailable, so the validation keeps failing with media_metadata_missing but the file is only analyzed once. Files that yield no metadata at all — missing command-line tool, timed out command, unreadable file — are not remembered and get analyzed again on the next validation.
Validates if the attachment is present.
The attached validator has no options.
Use it like this:
class User < ApplicationRecord
has_one_attached :avatar
validates :avatar, attached: true # ensures that avatar has an attached file
end
en:
errors:
messages:
blank: "can't be blank"
The error message for this validator relies on Rails own blank error message.
Validates the number of uploaded files.
The limit validator has 2 possible options:
min: defines the minimum allowed number of filesmax: defines the maximum allowed number of filesUse it like this:
class User < ApplicationRecord
has_many_attached :certificates
validates :certificates, limit: { min: 1, max: 10 } # restricts the number of files to between 1 and 10
end
en:
errors:
messages:
limit_out_of_range:
zero: "no files attached (must have between %{min} and %{max} files)"
one: "only 1 file attached (must have between %{min} and %{max} files)"
other: "total number of files must be between %{min} and %{max} files (there are %{count} files attached)"
limit_min_not_reached:
zero: "no files attached (must have at least %{min} files)"
one: "only 1 file attached (must have at least %{min} files)"
other: "%{count} files attached (must have at least %{min} files)"
limit_max_exceeded:
zero: "no files attached (maximum is %{max} files)"
one: "too many files attached (maximum is %{max} files, got %{count})"
other: "too many files attached (maximum is %{max} files, got %{count})"
The limit validator error messages expose 3 values that you can use:
min containing the minimum allowed number of files (e.g. 1)max containing the maximum allowed number of files (e.g. 10)count containing the current number of files (e.g. 5)Validates if the attachment has an allowed content type.
The content_type validator has several possible options:
with: defines the allowed content type (string, symbol or regex)in: defines the allowed content types (array of strings or symbols)spoofing_protection: enables content type spoofing protection (false by default). Allowed values: true / :file (UNIX file CLI), :magika (Google Magika CLI)timeout: overrides the global analyzer command timeout when spoofing protection runs file or magikaAs mentioned above, this validator can define content types in several ways:
image/png or png:png/\Avideo\/.*\z/Use it like this:
class User < ApplicationRecord
has_one_attached :avatar
validates :avatar, content_type: 'image/png' # only allows PNG images
validates :avatar, content_type: :png # only allows PNG images, same as { with: :png }
validates :avatar, content_type: /\Avideo\/.*\z/ # only allows video files
validates :avatar, content_type: ['image/png', 'image/jpeg'] # only allows PNG and JPEG images
validates :avatar, content_type: { in: [:png, :jpeg], spoofing_protection: true } # UNIX `file` backend (same as :file)
validates :avatar, content_type: { in: [:png, :jpeg], spoofing_protection: :magika } # Google Magika CLI backend
# Stronger protection for media/PDF: sniff + parse
validates :avatar, content_type: { in: [:png, :jpeg], spoofing_protection: true }, processable_file: true
end
accept attribute (FormBuilder)When using Rails' FormBuilder#file_field, the gem automatically sets the HTML accept attribute from your content_type validators. This improves UX by filtering selectable files in the browser dialog. It is only a frontend hint: a malicious user can still submit disallowed types, so keep the backend validation.
class User < ApplicationRecord
has_one_attached :avatar
validates :avatar, content_type: ['image/png', 'image/jpeg']
end
<%= form_with model: @user do |f| %>
<%= f.file_field :avatar %>
<%# => <input type="file" accept="image/png,image/jpeg" ...> %>
<% end %>
Explicit accept values are never overridden. You can also disable inference:
<%# Per field %>
<%= f.file_field :avatar, infer_accept: false %>
<%# Or set a custom accept value %>
<%= f.file_field :avatar, accept: "image/*" %>
# Globally — see [Configuration](#configuration) for a full initializer template
ActiveStorageValidations.infer_file_field_accept = false
# Equivalent:
ActiveStorageValidations.configure { |config| config.infer_file_field_accept = false }
Notes:
/\Aimage\/.*\z/ (or video / audio / etc.) are inferred, as image/*/\Aimage\/(png|gif)\z/) are skipped, since they cannot be reliably represented in acceptcontent_type options are skipped: they are not called while the form renders, because their result can depend on record state that is only set on submit, and a raising proc would break the whole view for what is merely a frontend hint. Backend validation is unchanged; pass a custom accept: on those fields if you want the picker filteredif: / unless:) are not evaluated: their content types are always included in accept, even when the condition would skip the validator for that record. Backend validation is unchanged; use infer_accept: false (or a custom accept:) if the picker must match the active conditionsIf 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. On Marcel 2 (Rails 8.2+), historical aliases such as audio/x-aac are still valid options and resolve to their canonical type (audio/aac). If the type is not registered, you can register it by adding the following code to your config/initializers/mime_types.rb file:
Marcel::MimeType.extend "application/ino", extensions: %w(ino), parents: "text/plain" # Registering arduino INO files
Be sure to at least include one the extensions, parents or magic option, otherwise the content type will not be registered.
By default, the gem does not prevent content type spoofing. Enable it with spoofing_protection:
validates :avatar, content_type: { in: [:png, :jpeg], spoofing_protection: true } # => :file (UNIX file CLI)
validates :avatar, content_type: { in: [:png, :jpeg], spoofing_protection: :file } # explicit
validates :avatar, content_type: { in: [:png, :jpeg], spoofing_protection: :magika } # Google Magika CLI
Override binary paths with ActiveStorage.paths[:file] / ActiveStorage.paths[:magika] if needed.
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).
Spoofing protection compares the declared Active Storage content type to a type detected by a sniffer CLI, then uses Marcel parent types so near-matches still pass:
:file (default) — UNIX file / libmagic, mostly magic bytes / headers. Zero extra install on most UNIX systems.:magika — Google Magika CLI (ML sniffer; samples begin/middle/end of the file). Generally more accurate than file, especially on textual / ambiguous formats (Google reports ~99% F1 vs ~88% for file --mime on overlapping types). Prefer :magika when you can install the CLI.Neither backend fully parses the file. They do not load the whole file into RAM. For already-persisted blobs (e.g. remote storage), the analyzer still downloads the blob to a local tempfile before sniffing. That download is streamed in chunks to disk, but it can still be costly for very large files. Local path uploads are analyzed in place.
Detected types are cached on the blob as asv_content_type + asv_content_type_backend. Switching backend re-analyzes. Legacy blobs that only have asv_content_type (no backend key) are treated as :file and keep using the cache — they are not re-analyzed. New (unsaved) blobs keep asv_* in memory until the record is saved; already-persisted blobs write the cache immediately so it survives reload.
Sniffers will not always return the exact same MIME as Active Storage (AS uses first ~4kb + filename + extension). Close parent types are accepted via Marcel::TYPE_PARENTS (e.g. video/x-ms-wmv vs video/x-ms-asf). On Marcel 2 (Rails 8.2+), aliased types such as text/xml / application/xml and audio/x-m4a / audio/mp4 are also treated as equivalent.
For stronger protection on images / video / audio / PDF, combine sniffing with parse validation:
validates :avatar, content_type: { in: [:png, :jpeg], spoofing_protection: true }, processable_file: true
The difficulty to accurately predict a mime type may generate false positives, if so there are several solutions available:
:file vs :magika)processable_file: trueMarcel::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!)en:
errors:
messages:
content_type_invalid:
one: "has an invalid content type (authorized content type is %{authorized_human_content_types})"
other: "has an invalid content type (authorized content types are %{authorized_human_content_types})"
content_type_spoofed:
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})"
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})"
The content_type validator error messages expose 7 values that you can use:
content_type containing the content type of the sent file (e.g. image/png)human_content_type containing a more user-friendly version of the sent file content type (e.g. 'TXT' for 'text/plain')detected_content_type containing the detected content type of the sent file using spoofing_protection option (e.g. image/png)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')authorized_human_content_types containing the list of authorized content types (e.g. 'PNG, JPEG' for ['image/png', 'image/jpeg'])count containing the number of authorized content types (e.g. 2)filename containing the filenameValidates each attached file size.
The size validator has 6 possible options:
less_than: defines the strict maximum allowed file sizeless_than_or_equal_to: defines the maximum allowed file sizegreater_than: defines the strict minimum allowed file sizegreater_than_or_equal_to: defines the minimum allowed file sizebetween: defines the allowed file size rangeequal_to: defines the allowed file sizeUse it like this:
class User < ApplicationRecord
has_one_attached :avatar
validates :avatar, size: { less_than: 2.megabytes } # restricts the file size to < 2MB
validates :avatar, size: { less_than_or_equal_to: 2.megabytes } # restricts the file size to <= 2MB
validates :avatar, size: { greater_than: 1.kilobyte } # restricts the file size to > 1KB
validates :avatar, size: { greater_than_or_equal_to: 1.kilobyte } # restricts the file size to >= 1KB
validates :avatar, size: { between: 1.kilobyte..2.megabytes } # restricts the file size to between 1KB and 2MB
validates :avatar, size: { equal_to: 1.megabyte } # restricts the file size to exactly 1MB
end
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.
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.
en:
errors:
messages:
file_size_not_less_than: "file size must be less than %{max} (current size is %{file_size})"
file_size_not_less_than_or_equal_to: "file size must be less than or equal to %{max} (current size is %{file_size})"
file_size_not_greater_than: "file size must be greater than %{min} (current size is %{file_size})"
file_size_not_greater_than_or_equal_to: "file size must be greater than or equal to %{min} (current size is %{file_size})"
file_size_not_between: "file size must be between %{min} and %{max} (current size is %{file_size})"
file_size_not_equal_to: "file size must be equal to %{exact} (current size is %{file_size})"
The size validator error messages expose 4 values that you can use:
file_size containing the current file size (e.g. 1.5MB)min containing the minimum allowed file size (e.g. 1KB)exact containing the allowed file size (e.g. 1MB)max containing the maximum allowed file size (e.g. 2MB)filename containing the current file nameValidates the total file size for several files.
The total_size validator has 6 possible options:
less_than: defines the strict maximum allowed total file sizeless_than_or_equal_to: defines the maximum allowed total file sizegreater_than: defines the strict minimum allowed total file sizegreater_than_or_equal_to: defines the minimum allowed total file sizebetween: defines the allowed total file size rangeequal_to: defines the allowed total file sizeUse it like this:
class User < ApplicationRecord
has_many_attached :certificates
validates :certificates, total_size: { less_than: 10.megabytes } # restricts the total size to < 10MB
validates :certificates, total_size: { less_than_or_equal_to: 10.megabytes } # restricts the total size to <= 10MB
validates :certificates, total_size: { greater_than: 1.kilobyte } # restricts the total size to > 1KB
validates :certificates, total_size: { greater_than_or_equal_to: 1.kilobyte } # restricts the total size to >= 1KB
validates :certificates, total_size: { between: 1.kilobyte..10.megabytes } # restricts the total size to between 1KB and 10MB
validates :certificates, total_size: { equal_to: 1.megabyte } # restricts the total file size to exactly 1MB
end
en:
errors:
messages:
total_file_size_not_less_than: "total file size must be less than %{max} (current size is %{total_file_size})"
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})"
total_file_size_not_greater_than: "total file size must be greater than %{min} (current size is %{total_file_size})"
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})"
total_file_size_not_between: "total file size must be between %{min} and %{max} (current size is %{total_file_size})"
total_file_size_not_equal_to: "total file size must be equal to %{exact} (current size is %{total_file_size})"
The total_size validator error messages expose 4 values that you can use:
total_file_size containing the current total file size (e.g. 1.5MB)min containing the minimum allowed total file size (e.g. 1KB)exact containing the allowed total file size (e.g. 1MB)max containing the maximum allowed total file size (e.g. 2MB)Validates the dimension of the attached image / video files. It can also be used for pdf files, but it will only analyze the pdf first page, and will assume a DPI of 72. (be sure to have the right dependencies installed as mentioned in Getting started)
The dimension validator has several possible options:
width: defines the allowed width (integer)
min: defines the minimum allowed width (integer)max: defines the maximum allowed width (integer)in: defines the allowed width range (range)height: defines the allowed height (integer)
min: defines the minimum allowed height (integer)max: defines the maximum allowed height (integer)in: defines the allowed height range (range)min: defines the minimum allowed width and height (range)max: defines the maximum allowed width and height (range)timeout: overrides the global analyzer command timeout for this validationUse it like this:
class User < ApplicationRecord
has_one_attached :avatar
validates :avatar, dimension: { width: 100 } # restricts the width to 100 pixels
validates :avatar, dimension: { width: { min: 80, max: 100 } } # restricts the width to between 80 and 100 pixels
validates :avatar, dimension: { width: { in: 80..100 } } # restricts the width to between 80 and 100 pixels
validates :avatar, dimension: { height: 100 } # restricts the height to 100 pixels
validates :avatar, dimension: { height: { min: 600, max: 1800 } } # restricts the height to between 600 and 1800 pixels
validates :avatar, dimension: { height: { in: 600..1800 } } # restricts the height to between 600 and 1800 pixels
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
end
en:
errors:
messages:
dimension_min_not_included_in: "must be greater than or equal to %{width} x %{height} pixels"
dimension_max_not_included_in: "must be less than or equal to %{width} x %{height} pixels"
dimension_width_not_included_in: "width is not included between %{min} and %{max} pixels"
dimension_height_not_included_in: "height is not included between %{min} and %{max} pixels"
dimension_width_not_greater_than_or_equal_to: "width must be greater than or equal to %{length} pixels"
dimension_height_not_greater_than_or_equal_to: "height must be greater than or equal to %{length} pixels"
dimension_width_not_less_than_or_equal_to: "width must be less than or equal to %{length} pixels"
dimension_height_not_less_than_or_equal_to: "height must be less than or equal to %{length} pixels"
dimension_width_not_equal_to: "width must be equal to %{length} pixels"
dimension_height_not_equal_to: "height must be equal to %{length} pixels"
media_metadata_missing: "is not a valid media file"
The dimension validator error messages expose 6 values that you can use:
min containing the minimum width or height allowedmax containing the maximum width or height allowedwidth containing the minimum or maximum width allowedheight containing the minimum or maximum width allowedlength containing the exact width or height allowedfilename containing the current filename in errorValidates the duration of the attached audio / video files. (be sure to have the right dependencies installed as mentioned in Using video and audio metadata validators)
The duration validator has several possible options:
less_than: defines the strict maximum allowed file durationless_than_or_equal_to: defines the maximum allowed file durationgreater_than: defines the strict minimum allowed file durationgreater_than_or_equal_to: defines the minimum allowed file durationbetween: defines the allowed file duration rangeequal_to: defines the allowed durationtimeout: overrides the global analyzer command timeout for this validationUse it like this:
class User < ApplicationRecord
has_one_attached :intro_song
validates :intro_song, duration: { less_than: 2.minutes } # restricts the file duration to < 2 minutes
validates :intro_song, duration: { less_than_or_equal_to: 2.minutes } # restricts the file duration to <= 2 minutes
validates :intro_song, duration: { greater_than: 1.second } # restricts the file duration to > 1 second
validates :intro_song, duration: { greater_than_or_equal_to: 1.second } # restricts the file duration to >= 1 second
validates :intro_song, duration: { between: 1.second..2.minutes } # restricts the file duration to between 1 second and 2 minutes
validates :intro_song, duration: { equal_to: 1.minute } # restricts the duration to exactly 1 minute
validates :intro_song, duration: { less_than: 5.minutes, timeout: 5.seconds } # custom analyzer timeout
end
en:
errors:
messages:
duration_not_less_than: "duration must be less than %{max} (current duration is %{duration})"
duration_not_less_than_or_equal_to: "duration must be less than or equal to %{max} (current duration is %{duration})"
duration_not_greater_than: "duration must be greater than %{min} (current duration is %{duration})"
duration_not_greater_than_or_equal_to: "duration must be greater than or equal to %{min} (current duration is %{duration})"
duration_not_between: "duration must be between %{min} and %{max} (current duration is %{duration})"
duration_not_equal_to: "duration must be equal to %{exact} (current duration is %{duration})"
The duration validator error messages expose 4 values that you can use:
duration containing the current duration size (e.g. 2 minutes)min containing the minimum allowed duration size (e.g. 1 second)exact containing the allowed duration (e.g. 3 seconds)max containing the maximum allowed duration size (e.g. 2 minutes)filename containing the current file nameValidates whether attached video / audio files contain an audio track. (be sure to have the right dependencies installed as mentioned in Using video and audio metadata validators)
The with_audio validator supports:
with: true requires an audio track and false rejects onetimeout: overrides the global analyzer command timeout for this validationUse it like this:
class User < ApplicationRecord
has_one_attached :video
has_one_attached :silent_video
validates :video, with_audio: true
validates :silent_video, with_audio: { with: false }
validates :video, with_audio: { timeout: 5.seconds }
end
Rails treats a bare with_audio: false as a disabled validator. Use the hash form with_audio: { with: false } to reject files that contain audio.
Files that cannot contain an audio stream at all (images, pdfs) are treated as having no audio track: they fail with_audio: true and pass with_audio: { with: false }.
en:
errors:
messages:
audio_missing: "must have an audio track"
audio_present: "must not have an audio track"
The with_audio validator error message exposes the filename value containing the current file name.
Validates the aspect ratio of the attached image / video files. It can also be used for pdf files, but it will only analyze the pdf first page. (be sure to have the right dependencies installed as mentioned in Getting started)
The aspect_ratio validator has several options:
with: defines the allowed aspect ratio (e.g. :is_16/9)in: defines the allowed aspect ratios (e.g. %i[square landscape])timeout: overrides the global analyzer command timeout for this validationThis validator can define aspect ratios in several ways:
:square, :portrait, :landscapeis_xx_yy): :is_16_9, :is_4_3, etc.Use it like this:
class User < ApplicationRecord
has_one_attached :avatar
validates :avatar, aspect_ratio: :square # restricts the aspect ratio to 1:1
validates :avatar, aspect_ratio: :portrait # restricts the aspect ratio to x:y where y > x
validates :avatar, aspect_ratio: :landscape # restricts the aspect ratio to x:y where x > y
validates :avatar, aspect_ratio: :is_16_9 # restricts the aspect ratio to 16:9
validates :avatar, aspect_ratio: %i[square is_16_9] # restricts the aspect ratio to 1:1 and 16:9
end
en:
errors:
messages:
aspect_ratio_not_square: "must be square (current file is %{width}x%{height}px)"
aspect_ratio_not_portrait: "must be portrait (current file is %{width}x%{height}px)"
aspect_ratio_not_landscape: "must be landscape (current file is %{width}x%{height}px)"
aspect_ratio_not_x_y: "must be %{authorized_aspect_ratios} (current file is %{width}x%{height}px)"
aspect_ratio_invalid: "has an invalid aspect ratio (valid aspect ratios are %{authorized_aspect_ratios})"
media_metadata_missing: "is not a valid media file"
The aspect_ratio validator error messages expose 4 values that you can use:
authorized_aspect_ratios containing the authorized aspect ratioswidth containing the current width of the image/videoheight containing the current height of the image/videofilename containing the current filename in errorValidates if the attached files can be processed by MiniMagick or Vips (image), ffmpeg (video/audio) or poppler (pdf). (be sure to have the right dependencies installed as mentioned in Getting started)
The processable_file validator supports:
timeout: overrides the global analyzer command timeout for this validationUse it like this:
class User < ApplicationRecord
has_one_attached :avatar
validates :avatar, processable_file: true # ensures that the file is processable by MiniMagick or Vips (image) or ffmpeg (video/audio)
validates :avatar, processable_file: { timeout: 5.seconds }
end
Rails 7.2.3.2+ / 8.0.5.1+ / 8.1.3.1+ call Vips.block_untrusted(true), so libvips refuses to load formats marked untrusted (e.g. SVG, BMP). Analysis then returns empty metadata by design. processable_file does not treat that as failure for those content types. dimension / aspect_ratio still need width/height — use MiniMagick for those formats, or skip those validators for them.
en:
errors:
messages:
file_not_processable: "is not identified as a valid media file"
The processable_file validator error messages expose 1 value that you can use:
filename containing the current filename in errorValidates each attached pdf file number of pages. (be sure to have the right dependencies installed as mentioned in Using pdf metadata validators)
The pages validator has several possible options:
less_than: defines the strict maximum allowed number of pagesless_than_or_equal_to: defines the maximum allowed number of pagesgreater_than: defines the strict minimum allowed number of pagesgreater_than_or_equal_to: defines the minimum allowed number of pagesbetween: defines the allowed number of pages rangeequal_to: defines the allowed number of pagestimeout: overrides the global analyzer command timeout for this validationUse it like this:
class User < ApplicationRecord
has_one_attached :contract
validates :contract, pages: { less_than: 2 } # restricts the number of pages to < 2
validates :contract, pages: { less_than_or_equal_to: 2 } # restricts the number of pages to <= 2
validates :contract, pages: { greater_than: 1 } # restricts the number of pages to > 1
validates :contract, pages: { greater_than_or_equal_to: 1 } # restricts the number of pages to >= 1
validates :contract, pages: { between: 1..2 } # restricts the number of pages to between 1 and 2
validates :contract, pages: { equal_to: 1 } # restricts the number of pages to exactly 1
end
en:
errors:
messages:
pages_not_less_than: "page count must be less than %{max} (current page count is %{pages})"
pages_not_less_than_or_equal_to: "page count must be less than or equal to %{max} (current page count is %{pages})"
pages_not_greater_than: "page count must be greater than %{min} (current page count is %{pages})"
pages_not_greater_than_or_equal_to: "page count must be greater than or equal to %{min} (current page count is %{pages})"
pages_not_between: "page count must be between %{min} and %{max} (current page count is %{pages})"
pages_not_equal_to: "page count must be equal to %{exact} (current page count is %{pages})"
The pages validator error messages expose 5 values that you can use:
pages containing the current file number of pages (e.g. 7)min containing the minimum allowed number of pages (e.g. 1)exact containing the allowed number of pages (e.g. 3)max containing the maximum allowed number of pages (e.g. 5)filename containing the current file nameActive 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.
Translation files are available here. 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 ✌️.
If you wish to customize the error messages, just copy, paste and update the translation files into your application locales.
The gem also provides RSpec-compatible and Minitest-compatible matchers for testing the validators.
In spec_helper.rb, you'll need to require the matchers:
require 'active_storage_validations/matchers'
And include the module:
RSpec.configure do |config|
config.include ActiveStorageValidations::Matchers
end
Matcher methods available:
describe User do
# aspect_ratio:
# #allowing, #rejecting
it { is_expected.to validate_aspect_ratio_of(:avatar).allowing(:square, :portrait) } # possible to use an Array or *splatted array
it { is_expected.to validate_aspect_ratio_of(:avatar).rejecting(:square, :landscape) } # possible to use an Array or *splatted array
# attached
it { is_expected.to validate_attached_of(:avatar) }
# processable_file
it { is_expected.to validate_processable_file_of(:avatar) }
# limit
# #min, #max
it { is_expected.to validate_limits_of(:avatar).min(1) }
it { is_expected.to validate_limits_of(:avatar).max(5) }
# content_type:
# #allowing, #rejecting, #spoofing_protection
it { is_expected.to validate_content_type_of(:avatar).allowing('image/png', 'image/gif') } # possible to use an Array or *splatted array
it { is_expected.to validate_content_type_of(:avatar).rejecting('text/plain', 'text/xml') } # possible to use an Array or *splatted array
it { is_expected.to validate_content_type_of(:avatar).allowing('image/png').spoofing_protection } # true / :file
it { is_expected.to validate_content_type_of(:avatar).allowing('image/png').spoofing_protection(:magika) }
# dimension:
# #width, #height, #width_min, #height_min, #width_max, #height_max, #width_between, #height_between
it { is_expected.to validate_dimensions_of(:avatar).width(250) }
it { is_expected.to validate_dimensions_of(:avatar).height(200) }
it { is_expected.to validate_dimensions_of(:avatar).width_min(200) }
it { is_expected.to validate_dimensions_of(:avatar).height_min(100) }
it { is_expected.to validate_dimensions_of(:avatar).width_max(500) }
it { is_expected.to validate_dimensions_of(:avatar).height_max(300) }
it { is_expected.to validate_dimensions_of(:avatar).width_between(200..500) }
it { is_expected.to validate_dimensions_of(:avatar).height_between(100..300) }
# size:
# #less_than, #less_than_or_equal_to, #greater_than, #greater_than_or_equal_to, #between, #equal_to
it { is_expected.to validate_size_of(:avatar).less_than(50.kilobytes) }
it { is_expected.to validate_size_of(:avatar).less_than_or_equal_to(50.kilobytes) }
it { is_expected.to validate_size_of(:avatar).greater_than(1.kilobyte) }
it { is_expected.to validate_size_of(:avatar).greater_than_or_equal_to(1.kilobyte) }
it { is_expected.to validate_size_of(:avatar).between(100..500.kilobytes) }
it { is_expected.to validate_size_of(:avatar).equal_to(5.megabytes) }
# total_size:
# #less_than, #less_than_or_equal_to, #greater_than, #greater_than_or_equal_to, #between, #equal_to
it { is_expected.to validate_total_size_of(:avatar).less_than(50.kilobytes) }
it { is_expected.to validate_total_size_of(:avatar).less_than_or_equal_to(50.kilobytes) }
it { is_expected.to validate_total_size_of(:avatar).greater_than(1.kilobyte) }
it { is_expected.to validate_total_size_of(:avatar).greater_than_or_equal_to(1.kilobyte) }
it { is_expected.to validate_total_size_of(:avatar).between(100..500.kilobytes) }
it { is_expected.to validate_total_size_of(:avatar).equal_to(5.megabytes) }
# duration:
# #less_than, #less_than_or_equal_to, #greater_than, #greater_than_or_equal_to, #between, #equal_to
it { is_expected.to validate_duration_of(:introduction).less_than(50.seconds) }
it { is_expected.to validate_duration_of(:introduction).less_than_or_equal_to(50.seconds) }
it { is_expected.to validate_duration_of(:introduction).greater_than(1.minute) }
it { is_expected.to validate_duration_of(:introduction).greater_than_or_equal_to(1.minute) }
it { is_expected.to validate_duration_of(:introduction).between(100..500.seconds) }
it { is_expected.to validate_duration_of(:avatar).equal_to(5.minutes) }
# with_audio
it { is_expected.to validate_with_audio_of(:video) }
it { is_expected.to validate_with_audio_of(:silent_video).without_audio }
# pages:
# #less_than, #less_than_or_equal_to, #greater_than, #greater_than_or_equal_to, #between, #equal_to
it { is_expected.to validate_pages_of(:contract).less_than(50) }
it { is_expected.to validate_pages_of(:contract).less_than_or_equal_to(50) }
it { is_expected.to validate_pages_of(:contract).greater_than(5) }
it { is_expected.to validate_pages_of(:contract).greater_than_or_equal_to(5) }
it { is_expected.to validate_pages_of(:contract).between(100..500) }
it { is_expected.to validate_pages_of(:contract).equal_to(5) }
end
(Note that matcher methods are chainable)
All matchers can currently be customized with these options:
describe User do
# :on
it { is_expected.to validate_attached_of(:avatar).on(:update) }
it { is_expected.to validate_attached_of(:avatar).on(%i[update custom]) }
# :except_on (Rails >= 8.0)
it { is_expected.to validate_attached_of(:avatar).except_on(:update) }
it { is_expected.to validate_attached_of(:avatar).except_on(%i[update custom]) }
# :message
it { is_expected.to validate_dimensions_of(:avatar).width(250).with_message('Invalid dimensions.') }
# :timeout (analyzer command timeout — metadata validators + content_type with spoofing)
it { is_expected.to validate_duration_of(:video).less_than(5.minutes).timeout(30.seconds) }
it { is_expected.to validate_with_audio_of(:video).timeout(5.seconds) }
it { is_expected.to validate_processable_file_of(:avatar).timeout(5.seconds) }
end
To use the matchers, make sure you have the shoulda-context gem up and running. No extra mock library is required (minitest-mock is not needed on Minitest 6).
You need to require the matchers:
require 'active_storage_validations/matchers'
And extend the module:
class ActiveSupport::TestCase
extend ActiveStorageValidations::Matchers
end
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 gem.
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.
AI coding agents: see AGENTS.md for architecture, test commands, and contribution patterns. Commit and PR titles follow Conventional Commits.
Before submitting your pull request, run the tests to make sure everything works as expected.
To run the gem tests, launch the following commands in the root folder of gem repository:
BUNDLE_GEMFILE=gemfiles/rails_7_0_1.gemfile bundle exec rake spec to run for Rails 7.0.1BUNDLE_GEMFILE=gemfiles/rails_7_1.gemfile bundle exec rake spec to run for Rails 7.1BUNDLE_GEMFILE=gemfiles/rails_7_2.gemfile bundle exec rake spec to run for Rails 7.2BUNDLE_GEMFILE=gemfiles/rails_8_0.gemfile bundle exec rake spec to run for Rails 8.0BUNDLE_GEMFILE=gemfiles/rails_8_1.gemfile bundle exec rake spec to run for Rails 8.1BUNDLE_GEMFILE=gemfiles/rails_next.gemfile bundle exec rake spec to run for Rails mainSnippet to run in console:
BUNDLE_GEMFILE=gemfiles/rails_7_0_1.gemfile bundle
BUNDLE_GEMFILE=gemfiles/rails_7_1.gemfile bundle
BUNDLE_GEMFILE=gemfiles/rails_7_2.gemfile bundle
BUNDLE_GEMFILE=gemfiles/rails_8_0.gemfile bundle
BUNDLE_GEMFILE=gemfiles/rails_8_1.gemfile bundle
BUNDLE_GEMFILE=gemfiles/rails_next.gemfile bundle
BUNDLE_GEMFILE=gemfiles/rails_7_0_1.gemfile bundle exec rake spec
BUNDLE_GEMFILE=gemfiles/rails_7_1.gemfile bundle exec rake spec
BUNDLE_GEMFILE=gemfiles/rails_7_2.gemfile bundle exec rake spec
BUNDLE_GEMFILE=gemfiles/rails_8_0.gemfile bundle exec rake spec
BUNDLE_GEMFILE=gemfiles/rails_8_1.gemfile bundle exec rake spec
BUNDLE_GEMFILE=gemfiles/rails_next.gemfile bundle exec rake spec
Tips:
:focus (or use fit / fdescribe); RSpec is configured with filter_run_when_matching :focusbundle exec rspec spec/validators/size_validator_spec.rbIMAGE_PROCESSOR=vips / IMAGE_PROCESSOR=mini_magick. Locally, unset means validators use MiniMagick (ASV default) and both analyzer unit specs run; setting the env selects that processor for validators and excludes the other processor’s tagged examples (so they do not show as pending)Optional wall-clock / ips suite for metadata validators (cold analysis vs cached asv_* hits) lives under benchmark/. See benchmark/README.md for setup, how to run, and how to refresh benchmark/BASELINE.md. CI runs the suite informationally (no fail-on-regression).
We have a long list of valued contributors. Check them all at:
https://github.com/igorkasyanchuk/active_storage_validations/graphs/contributors
The gem is available as open source under the terms of the MIT License.
Overall
Maintained
Code review
RubyGems
Latest release
Releases this year
543 followers · starred Jan 2019
708 followers · starred Aug 2018
255 followers · starred Nov 2019
301 followers · starred Mar 2022
Do it like => validates :photos, attached: true, content_type: ['image/png', 'image/jpg', 'image/jpeg'], size: { less_than: 500.kilobytes }, limit: { min: 1, max: 3 }, aspect_ratio: :landscape, dimension: { width: { in: 800..1600 }
See the codeActive Storage Validations is a gem that allows you to add validations for Active Storage attributes.
This gem is doing it right for you! Just use validates :avatar, attached: true, content_type: 'image/png' and that's it!
Active Storage Validations work with Rails 7.0.1 onwards and Ruby 3.3 onwards. Add this line to your application's Gemfile:
gem 'active_storage_validations'
And then execute:
$ bundle
Once you have installed the gem, I18n error messages will be added automatically to your app. See Internationalization (I18n) section for more details.
Optionally, to use the image metadata validators (dimension, aspect_ratio and processable_file), you will have to add one of the corresponding gems:
gem 'ruby-vips', '>= 2.1.0'
# Or
gem 'mini_magick', '>= 4.9.5'
Plus, you have to be sure to have the corresponding command-line tool installed on your system (libvips for ruby-vips, or ImageMagick for mini_magick — both locally and in CI / production).
We recommend libvips (ruby-vips + config.active_storage.variant_processor = :vips) for these validators. Rails already defaults Active Storage variants to libvips (ActiveStorage::Variant), and our image processor benchmarks show cold metadata analysis is about 8× faster than MiniMagick/ImageMagick on the same machine and fixtures. Warm validations (cached asv_* metadata) are similar for both.
To use the video and audio metadata validators (dimension, aspect_ratio, processable_file, duration and with_audio), 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).
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).
To use the spoofing_protection option with the content_type validator:
true / :file): the UNIX file command (usually preinstalled on UNIX systems):magika): the Google Magika CLI — install via brew install magika, their install script, cargo install --locked magika-cli, or pipx install magikaBoth backends are optional system tools (not Ruby gems). Prefer :magika when you can install the CLI — it is generally more accurate than file, especially on textual / ambiguous formats. Be sure to install Magika in CI / production if you enable :magika.
If you want some inspiration about how to add imagemagick, libvips, ffmpeg, poppler or magika 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)
Optional global settings can go in an initializer. Example template:
# config/initializers/active_storage_validations.rb
ActiveStorageValidations.configure do |config|
# Infer HTML accept= on file_field from content_type validators (default: true)
# config.infer_file_field_accept = false
# Max time for external analyzer commands: ffprobe, pdfinfo, file, magika, ImageMagick identify, libvips
# (default: 10.seconds; set to nil to disable)
# config.command_timeout = 10.seconds
end
# Optional: monitor analyzer timeouts
# ActiveSupport::Notifications.subscribe("timeout.active_storage_validations") do |*args|
# event = ActiveSupport::Notifications::Event.new(*args)
# Rails.logger.warn("[ASV] command timeout: #{event.payload}")
# end
# Optional: monitor how long metadata / content type analysis takes
# ActiveSupport::Notifications.subscribe("analyze.active_storage_validations") do |*args|
# event = ActiveSupport::Notifications::Event.new(*args)
# Rails.logger.info("[ASV] analyzed: #{event.payload}")
# end
The gem emits two ActiveSupport::Notifications events:
| Event | Payload | Emitted |
|---|---|---|
analyze.active_storage_validations | analyzer, command, timeout, timed_out, duration | Every time an analyzer command runs (i.e. on a cache miss) |
timeout.active_storage_validations | analyzer, command, timeout | Only when a command exceeds its deadline |
command_timeout bounds metadata analysis used by dimension, aspect_ratio, duration, with_audio, pages, processable_file, and content_type (with spoofing_protection). When a command times out, analysis fails closed and the validator adds its usual error (file_not_processable / media_metadata_missing / audio_missing / content-type errors) — there is no separate timeout error message.
The 10s default is enough for typical uploads. Raise it (or set nil) if you analyze very large videos/PDFs, especially on slow or network storage — otherwise those files can start failing validation after upgrade. See upgrade to 4.x.
Per-validator override (applies to the analysis triggered by that validator; the first analysis for a blob is cached):
validates :video, duration: { less_than: 5.minutes, timeout: 30.seconds }
Notes:
command_timeout (or per-validator timeout:) to nil disables the deadlineidentify through the same killable command runner (MiniMagick is only used to build the argv)timeout.active_storage_validationsList of validators:
Proc usage
Every validator can use procs instead of values in all the validator examples:
class User < ApplicationRecord
has_many_attached :files
validates :files, limit: { max: -> (record) { record.admin? ? 100 : 10 } }
end
Performance optimization
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.
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.
This also covers metadata the analyzer cannot extract from a given file (e.g. a duration validation on an image): the gem remembers that the value is unavailable, so the validation keeps failing with media_metadata_missing but the file is only analyzed once. Files that yield no metadata at all — missing command-line tool, timed out command, unreadable file — are not remembered and get analyzed again on the next validation.
Validates if the attachment is present.
The attached validator has no options.
Use it like this:
class User < ApplicationRecord
has_one_attached :avatar
validates :avatar, attached: true # ensures that avatar has an attached file
end
en:
errors:
messages:
blank: "can't be blank"
The error message for this validator relies on Rails own blank error message.
Validates the number of uploaded files.
The limit validator has 2 possible options:
min: defines the minimum allowed number of filesmax: defines the maximum allowed number of filesUse it like this:
class User < ApplicationRecord
has_many_attached :certificates
validates :certificates, limit: { min: 1, max: 10 } # restricts the number of files to between 1 and 10
end
en:
errors:
messages:
limit_out_of_range:
zero: "no files attached (must have between %{min} and %{max} files)"
one: "only 1 file attached (must have between %{min} and %{max} files)"
other: "total number of files must be between %{min} and %{max} files (there are %{count} files attached)"
limit_min_not_reached:
zero: "no files attached (must have at least %{min} files)"
one: "only 1 file attached (must have at least %{min} files)"
other: "%{count} files attached (must have at least %{min} files)"
limit_max_exceeded:
zero: "no files attached (maximum is %{max} files)"
one: "too many files attached (maximum is %{max} files, got %{count})"
other: "too many files attached (maximum is %{max} files, got %{count})"
The limit validator error messages expose 3 values that you can use:
min containing the minimum allowed number of files (e.g. 1)max containing the maximum allowed number of files (e.g. 10)count containing the current number of files (e.g. 5)Validates if the attachment has an allowed content type.
The content_type validator has several possible options:
with: defines the allowed content type (string, symbol or regex)in: defines the allowed content types (array of strings or symbols)spoofing_protection: enables content type spoofing protection (false by default). Allowed values: true / :file (UNIX file CLI), :magika (Google Magika CLI)timeout: overrides the global analyzer command timeout when spoofing protection runs file or magikaAs mentioned above, this validator can define content types in several ways:
image/png or png:png/\Avideo\/.*\z/Use it like this:
class User < ApplicationRecord
has_one_attached :avatar
validates :avatar, content_type: 'image/png' # only allows PNG images
validates :avatar, content_type: :png # only allows PNG images, same as { with: :png }
validates :avatar, content_type: /\Avideo\/.*\z/ # only allows video files
validates :avatar, content_type: ['image/png', 'image/jpeg'] # only allows PNG and JPEG images
validates :avatar, content_type: { in: [:png, :jpeg], spoofing_protection: true } # UNIX `file` backend (same as :file)
validates :avatar, content_type: { in: [:png, :jpeg], spoofing_protection: :magika } # Google Magika CLI backend
# Stronger protection for media/PDF: sniff + parse
validates :avatar, content_type: { in: [:png, :jpeg], spoofing_protection: true }, processable_file: true
end
accept attribute (FormBuilder)When using Rails' FormBuilder#file_field, the gem automatically sets the HTML accept attribute from your content_type validators. This improves UX by filtering selectable files in the browser dialog. It is only a frontend hint: a malicious user can still submit disallowed types, so keep the backend validation.
class User < ApplicationRecord
has_one_attached :avatar
validates :avatar, content_type: ['image/png', 'image/jpeg']
end
<%= form_with model: @user do |f| %>
<%= f.file_field :avatar %>
<%# => <input type="file" accept="image/png,image/jpeg" ...> %>
<% end %>
Explicit accept values are never overridden. You can also disable inference:
<%# Per field %>
<%= f.file_field :avatar, infer_accept: false %>
<%# Or set a custom accept value %>
<%= f.file_field :avatar, accept: "image/*" %>
# Globally — see [Configuration](#configuration) for a full initializer template
ActiveStorageValidations.infer_file_field_accept = false
# Equivalent:
ActiveStorageValidations.configure { |config| config.infer_file_field_accept = false }
Notes:
/\Aimage\/.*\z/ (or video / audio / etc.) are inferred, as image/*/\Aimage\/(png|gif)\z/) are skipped, since they cannot be reliably represented in acceptcontent_type options are skipped: they are not called while the form renders, because their result can depend on record state that is only set on submit, and a raising proc would break the whole view for what is merely a frontend hint. Backend validation is unchanged; pass a custom accept: on those fields if you want the picker filteredif: / unless:) are not evaluated: their content types are always included in accept, even when the condition would skip the validator for that record. Backend validation is unchanged; use infer_accept: false (or a custom accept:) if the picker must match the active conditionsIf 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. On Marcel 2 (Rails 8.2+), historical aliases such as audio/x-aac are still valid options and resolve to their canonical type (audio/aac). If the type is not registered, you can register it by adding the following code to your config/initializers/mime_types.rb file:
Marcel::MimeType.extend "application/ino", extensions: %w(ino), parents: "text/plain" # Registering arduino INO files
Be sure to at least include one the extensions, parents or magic option, otherwise the content type will not be registered.
By default, the gem does not prevent content type spoofing. Enable it with spoofing_protection:
validates :avatar, content_type: { in: [:png, :jpeg], spoofing_protection: true } # => :file (UNIX file CLI)
validates :avatar, content_type: { in: [:png, :jpeg], spoofing_protection: :file } # explicit
validates :avatar, content_type: { in: [:png, :jpeg], spoofing_protection: :magika } # Google Magika CLI
Override binary paths with ActiveStorage.paths[:file] / ActiveStorage.paths[:magika] if needed.
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).
Spoofing protection compares the declared Active Storage content type to a type detected by a sniffer CLI, then uses Marcel parent types so near-matches still pass:
:file (default) — UNIX file / libmagic, mostly magic bytes / headers. Zero extra install on most UNIX systems.:magika — Google Magika CLI (ML sniffer; samples begin/middle/end of the file). Generally more accurate than file, especially on textual / ambiguous formats (Google reports ~99% F1 vs ~88% for file --mime on overlapping types). Prefer :magika when you can install the CLI.Neither backend fully parses the file. They do not load the whole file into RAM. For already-persisted blobs (e.g. remote storage), the analyzer still downloads the blob to a local tempfile before sniffing. That download is streamed in chunks to disk, but it can still be costly for very large files. Local path uploads are analyzed in place.
Detected types are cached on the blob as asv_content_type + asv_content_type_backend. Switching backend re-analyzes. Legacy blobs that only have asv_content_type (no backend key) are treated as :file and keep using the cache — they are not re-analyzed. New (unsaved) blobs keep asv_* in memory until the record is saved; already-persisted blobs write the cache immediately so it survives reload.
Sniffers will not always return the exact same MIME as Active Storage (AS uses first ~4kb + filename + extension). Close parent types are accepted via Marcel::TYPE_PARENTS (e.g. video/x-ms-wmv vs video/x-ms-asf). On Marcel 2 (Rails 8.2+), aliased types such as text/xml / application/xml and audio/x-m4a / audio/mp4 are also treated as equivalent.
For stronger protection on images / video / audio / PDF, combine sniffing with parse validation:
validates :avatar, content_type: { in: [:png, :jpeg], spoofing_protection: true }, processable_file: true
The difficulty to accurately predict a mime type may generate false positives, if so there are several solutions available:
:file vs :magika)processable_file: trueMarcel::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!)en:
errors:
messages:
content_type_invalid:
one: "has an invalid content type (authorized content type is %{authorized_human_content_types})"
other: "has an invalid content type (authorized content types are %{authorized_human_content_types})"
content_type_spoofed:
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})"
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})"
The content_type validator error messages expose 7 values that you can use:
content_type containing the content type of the sent file (e.g. image/png)human_content_type containing a more user-friendly version of the sent file content type (e.g. 'TXT' for 'text/plain')detected_content_type containing the detected content type of the sent file using spoofing_protection option (e.g. image/png)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')authorized_human_content_types containing the list of authorized content types (e.g. 'PNG, JPEG' for ['image/png', 'image/jpeg'])count containing the number of authorized content types (e.g. 2)filename containing the filenameValidates each attached file size.
The size validator has 6 possible options:
less_than: defines the strict maximum allowed file sizeless_than_or_equal_to: defines the maximum allowed file sizegreater_than: defines the strict minimum allowed file sizegreater_than_or_equal_to: defines the minimum allowed file sizebetween: defines the allowed file size rangeequal_to: defines the allowed file sizeUse it like this:
class User < ApplicationRecord
has_one_attached :avatar
validates :avatar, size: { less_than: 2.megabytes } # restricts the file size to < 2MB
validates :avatar, size: { less_than_or_equal_to: 2.megabytes } # restricts the file size to <= 2MB
validates :avatar, size: { greater_than: 1.kilobyte } # restricts the file size to > 1KB
validates :avatar, size: { greater_than_or_equal_to: 1.kilobyte } # restricts the file size to >= 1KB
validates :avatar, size: { between: 1.kilobyte..2.megabytes } # restricts the file size to between 1KB and 2MB
validates :avatar, size: { equal_to: 1.megabyte } # restricts the file size to exactly 1MB
end
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.
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.
en:
errors:
messages:
file_size_not_less_than: "file size must be less than %{max} (current size is %{file_size})"
file_size_not_less_than_or_equal_to: "file size must be less than or equal to %{max} (current size is %{file_size})"
file_size_not_greater_than: "file size must be greater than %{min} (current size is %{file_size})"
file_size_not_greater_than_or_equal_to: "file size must be greater than or equal to %{min} (current size is %{file_size})"
file_size_not_between: "file size must be between %{min} and %{max} (current size is %{file_size})"
file_size_not_equal_to: "file size must be equal to %{exact} (current size is %{file_size})"
The size validator error messages expose 4 values that you can use:
file_size containing the current file size (e.g. 1.5MB)min containing the minimum allowed file size (e.g. 1KB)exact containing the allowed file size (e.g. 1MB)max containing the maximum allowed file size (e.g. 2MB)filename containing the current file nameValidates the total file size for several files.
The total_size validator has 6 possible options:
less_than: defines the strict maximum allowed total file sizeless_than_or_equal_to: defines the maximum allowed total file sizegreater_than: defines the strict minimum allowed total file sizegreater_than_or_equal_to: defines the minimum allowed total file sizebetween: defines the allowed total file size rangeequal_to: defines the allowed total file sizeUse it like this:
class User < ApplicationRecord
has_many_attached :certificates
validates :certificates, total_size: { less_than: 10.megabytes } # restricts the total size to < 10MB
validates :certificates, total_size: { less_than_or_equal_to: 10.megabytes } # restricts the total size to <= 10MB
validates :certificates, total_size: { greater_than: 1.kilobyte } # restricts the total size to > 1KB
validates :certificates, total_size: { greater_than_or_equal_to: 1.kilobyte } # restricts the total size to >= 1KB
validates :certificates, total_size: { between: 1.kilobyte..10.megabytes } # restricts the total size to between 1KB and 10MB
validates :certificates, total_size: { equal_to: 1.megabyte } # restricts the total file size to exactly 1MB
end
en:
errors:
messages:
total_file_size_not_less_than: "total file size must be less than %{max} (current size is %{total_file_size})"
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})"
total_file_size_not_greater_than: "total file size must be greater than %{min} (current size is %{total_file_size})"
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})"
total_file_size_not_between: "total file size must be between %{min} and %{max} (current size is %{total_file_size})"
total_file_size_not_equal_to: "total file size must be equal to %{exact} (current size is %{total_file_size})"
The total_size validator error messages expose 4 values that you can use:
total_file_size containing the current total file size (e.g. 1.5MB)min containing the minimum allowed total file size (e.g. 1KB)exact containing the allowed total file size (e.g. 1MB)max containing the maximum allowed total file size (e.g. 2MB)Validates the dimension of the attached image / video files. It can also be used for pdf files, but it will only analyze the pdf first page, and will assume a DPI of 72. (be sure to have the right dependencies installed as mentioned in Getting started)
The dimension validator has several possible options:
width: defines the allowed width (integer)
min: defines the minimum allowed width (integer)max: defines the maximum allowed width (integer)in: defines the allowed width range (range)height: defines the allowed height (integer)
min: defines the minimum allowed height (integer)max: defines the maximum allowed height (integer)in: defines the allowed height range (range)min: defines the minimum allowed width and height (range)max: defines the maximum allowed width and height (range)timeout: overrides the global analyzer command timeout for this validationUse it like this:
class User < ApplicationRecord
has_one_attached :avatar
validates :avatar, dimension: { width: 100 } # restricts the width to 100 pixels
validates :avatar, dimension: { width: { min: 80, max: 100 } } # restricts the width to between 80 and 100 pixels
validates :avatar, dimension: { width: { in: 80..100 } } # restricts the width to between 80 and 100 pixels
validates :avatar, dimension: { height: 100 } # restricts the height to 100 pixels
validates :avatar, dimension: { height: { min: 600, max: 1800 } } # restricts the height to between 600 and 1800 pixels
validates :avatar, dimension: { height: { in: 600..1800 } } # restricts the height to between 600 and 1800 pixels
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
end
en:
errors:
messages:
dimension_min_not_included_in: "must be greater than or equal to %{width} x %{height} pixels"
dimension_max_not_included_in: "must be less than or equal to %{width} x %{height} pixels"
dimension_width_not_included_in: "width is not included between %{min} and %{max} pixels"
dimension_height_not_included_in: "height is not included between %{min} and %{max} pixels"
dimension_width_not_greater_than_or_equal_to: "width must be greater than or equal to %{length} pixels"
dimension_height_not_greater_than_or_equal_to: "height must be greater than or equal to %{length} pixels"
dimension_width_not_less_than_or_equal_to: "width must be less than or equal to %{length} pixels"
dimension_height_not_less_than_or_equal_to: "height must be less than or equal to %{length} pixels"
dimension_width_not_equal_to: "width must be equal to %{length} pixels"
dimension_height_not_equal_to: "height must be equal to %{length} pixels"
media_metadata_missing: "is not a valid media file"
The dimension validator error messages expose 6 values that you can use:
min containing the minimum width or height allowedmax containing the maximum width or height allowedwidth containing the minimum or maximum width allowedheight containing the minimum or maximum width allowedlength containing the exact width or height allowedfilename containing the current filename in errorValidates the duration of the attached audio / video files. (be sure to have the right dependencies installed as mentioned in Using video and audio metadata validators)
The duration validator has several possible options:
less_than: defines the strict maximum allowed file durationless_than_or_equal_to: defines the maximum allowed file durationgreater_than: defines the strict minimum allowed file durationgreater_than_or_equal_to: defines the minimum allowed file durationbetween: defines the allowed file duration rangeequal_to: defines the allowed durationtimeout: overrides the global analyzer command timeout for this validationUse it like this:
class User < ApplicationRecord
has_one_attached :intro_song
validates :intro_song, duration: { less_than: 2.minutes } # restricts the file duration to < 2 minutes
validates :intro_song, duration: { less_than_or_equal_to: 2.minutes } # restricts the file duration to <= 2 minutes
validates :intro_song, duration: { greater_than: 1.second } # restricts the file duration to > 1 second
validates :intro_song, duration: { greater_than_or_equal_to: 1.second } # restricts the file duration to >= 1 second
validates :intro_song, duration: { between: 1.second..2.minutes } # restricts the file duration to between 1 second and 2 minutes
validates :intro_song, duration: { equal_to: 1.minute } # restricts the duration to exactly 1 minute
validates :intro_song, duration: { less_than: 5.minutes, timeout: 5.seconds } # custom analyzer timeout
end
en:
errors:
messages:
duration_not_less_than: "duration must be less than %{max} (current duration is %{duration})"
duration_not_less_than_or_equal_to: "duration must be less than or equal to %{max} (current duration is %{duration})"
duration_not_greater_than: "duration must be greater than %{min} (current duration is %{duration})"
duration_not_greater_than_or_equal_to: "duration must be greater than or equal to %{min} (current duration is %{duration})"
duration_not_between: "duration must be between %{min} and %{max} (current duration is %{duration})"
duration_not_equal_to: "duration must be equal to %{exact} (current duration is %{duration})"
The duration validator error messages expose 4 values that you can use:
duration containing the current duration size (e.g. 2 minutes)min containing the minimum allowed duration size (e.g. 1 second)exact containing the allowed duration (e.g. 3 seconds)max containing the maximum allowed duration size (e.g. 2 minutes)filename containing the current file nameValidates whether attached video / audio files contain an audio track. (be sure to have the right dependencies installed as mentioned in Using video and audio metadata validators)
The with_audio validator supports:
with: true requires an audio track and false rejects onetimeout: overrides the global analyzer command timeout for this validationUse it like this:
class User < ApplicationRecord
has_one_attached :video
has_one_attached :silent_video
validates :video, with_audio: true
validates :silent_video, with_audio: { with: false }
validates :video, with_audio: { timeout: 5.seconds }
end
Rails treats a bare with_audio: false as a disabled validator. Use the hash form with_audio: { with: false } to reject files that contain audio.
Files that cannot contain an audio stream at all (images, pdfs) are treated as having no audio track: they fail with_audio: true and pass with_audio: { with: false }.
en:
errors:
messages:
audio_missing: "must have an audio track"
audio_present: "must not have an audio track"
The with_audio validator error message exposes the filename value containing the current file name.
Validates the aspect ratio of the attached image / video files. It can also be used for pdf files, but it will only analyze the pdf first page. (be sure to have the right dependencies installed as mentioned in Getting started)
The aspect_ratio validator has several options:
with: defines the allowed aspect ratio (e.g. :is_16/9)in: defines the allowed aspect ratios (e.g. %i[square landscape])timeout: overrides the global analyzer command timeout for this validationThis validator can define aspect ratios in several ways:
:square, :portrait, :landscapeis_xx_yy): :is_16_9, :is_4_3, etc.Use it like this:
class User < ApplicationRecord
has_one_attached :avatar
validates :avatar, aspect_ratio: :square # restricts the aspect ratio to 1:1
validates :avatar, aspect_ratio: :portrait # restricts the aspect ratio to x:y where y > x
validates :avatar, aspect_ratio: :landscape # restricts the aspect ratio to x:y where x > y
validates :avatar, aspect_ratio: :is_16_9 # restricts the aspect ratio to 16:9
validates :avatar, aspect_ratio: %i[square is_16_9] # restricts the aspect ratio to 1:1 and 16:9
end
en:
errors:
messages:
aspect_ratio_not_square: "must be square (current file is %{width}x%{height}px)"
aspect_ratio_not_portrait: "must be portrait (current file is %{width}x%{height}px)"
aspect_ratio_not_landscape: "must be landscape (current file is %{width}x%{height}px)"
aspect_ratio_not_x_y: "must be %{authorized_aspect_ratios} (current file is %{width}x%{height}px)"
aspect_ratio_invalid: "has an invalid aspect ratio (valid aspect ratios are %{authorized_aspect_ratios})"
media_metadata_missing: "is not a valid media file"
The aspect_ratio validator error messages expose 4 values that you can use:
authorized_aspect_ratios containing the authorized aspect ratioswidth containing the current width of the image/videoheight containing the current height of the image/videofilename containing the current filename in errorValidates if the attached files can be processed by MiniMagick or Vips (image), ffmpeg (video/audio) or poppler (pdf). (be sure to have the right dependencies installed as mentioned in Getting started)
The processable_file validator supports:
timeout: overrides the global analyzer command timeout for this validationUse it like this:
class User < ApplicationRecord
has_one_attached :avatar
validates :avatar, processable_file: true # ensures that the file is processable by MiniMagick or Vips (image) or ffmpeg (video/audio)
validates :avatar, processable_file: { timeout: 5.seconds }
end
Rails 7.2.3.2+ / 8.0.5.1+ / 8.1.3.1+ call Vips.block_untrusted(true), so libvips refuses to load formats marked untrusted (e.g. SVG, BMP). Analysis then returns empty metadata by design. processable_file does not treat that as failure for those content types. dimension / aspect_ratio still need width/height — use MiniMagick for those formats, or skip those validators for them.
en:
errors:
messages:
file_not_processable: "is not identified as a valid media file"
The processable_file validator error messages expose 1 value that you can use:
filename containing the current filename in errorValidates each attached pdf file number of pages. (be sure to have the right dependencies installed as mentioned in Using pdf metadata validators)
The pages validator has several possible options:
less_than: defines the strict maximum allowed number of pagesless_than_or_equal_to: defines the maximum allowed number of pagesgreater_than: defines the strict minimum allowed number of pagesgreater_than_or_equal_to: defines the minimum allowed number of pagesbetween: defines the allowed number of pages rangeequal_to: defines the allowed number of pagestimeout: overrides the global analyzer command timeout for this validationUse it like this:
class User < ApplicationRecord
has_one_attached :contract
validates :contract, pages: { less_than: 2 } # restricts the number of pages to < 2
validates :contract, pages: { less_than_or_equal_to: 2 } # restricts the number of pages to <= 2
validates :contract, pages: { greater_than: 1 } # restricts the number of pages to > 1
validates :contract, pages: { greater_than_or_equal_to: 1 } # restricts the number of pages to >= 1
validates :contract, pages: { between: 1..2 } # restricts the number of pages to between 1 and 2
validates :contract, pages: { equal_to: 1 } # restricts the number of pages to exactly 1
end
en:
errors:
messages:
pages_not_less_than: "page count must be less than %{max} (current page count is %{pages})"
pages_not_less_than_or_equal_to: "page count must be less than or equal to %{max} (current page count is %{pages})"
pages_not_greater_than: "page count must be greater than %{min} (current page count is %{pages})"
pages_not_greater_than_or_equal_to: "page count must be greater than or equal to %{min} (current page count is %{pages})"
pages_not_between: "page count must be between %{min} and %{max} (current page count is %{pages})"
pages_not_equal_to: "page count must be equal to %{exact} (current page count is %{pages})"
The pages validator error messages expose 5 values that you can use:
pages containing the current file number of pages (e.g. 7)min containing the minimum allowed number of pages (e.g. 1)exact containing the allowed number of pages (e.g. 3)max containing the maximum allowed number of pages (e.g. 5)filename containing the current file nameActive 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.
Translation files are available here. 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 ✌️.
If you wish to customize the error messages, just copy, paste and update the translation files into your application locales.
The gem also provides RSpec-compatible and Minitest-compatible matchers for testing the validators.
In spec_helper.rb, you'll need to require the matchers:
require 'active_storage_validations/matchers'
And include the module:
RSpec.configure do |config|
config.include ActiveStorageValidations::Matchers
end
Matcher methods available:
describe User do
# aspect_ratio:
# #allowing, #rejecting
it { is_expected.to validate_aspect_ratio_of(:avatar).allowing(:square, :portrait) } # possible to use an Array or *splatted array
it { is_expected.to validate_aspect_ratio_of(:avatar).rejecting(:square, :landscape) } # possible to use an Array or *splatted array
# attached
it { is_expected.to validate_attached_of(:avatar) }
# processable_file
it { is_expected.to validate_processable_file_of(:avatar) }
# limit
# #min, #max
it { is_expected.to validate_limits_of(:avatar).min(1) }
it { is_expected.to validate_limits_of(:avatar).max(5) }
# content_type:
# #allowing, #rejecting, #spoofing_protection
it { is_expected.to validate_content_type_of(:avatar).allowing('image/png', 'image/gif') } # possible to use an Array or *splatted array
it { is_expected.to validate_content_type_of(:avatar).rejecting('text/plain', 'text/xml') } # possible to use an Array or *splatted array
it { is_expected.to validate_content_type_of(:avatar).allowing('image/png').spoofing_protection } # true / :file
it { is_expected.to validate_content_type_of(:avatar).allowing('image/png').spoofing_protection(:magika) }
# dimension:
# #width, #height, #width_min, #height_min, #width_max, #height_max, #width_between, #height_between
it { is_expected.to validate_dimensions_of(:avatar).width(250) }
it { is_expected.to validate_dimensions_of(:avatar).height(200) }
it { is_expected.to validate_dimensions_of(:avatar).width_min(200) }
it { is_expected.to validate_dimensions_of(:avatar).height_min(100) }
it { is_expected.to validate_dimensions_of(:avatar).width_max(500) }
it { is_expected.to validate_dimensions_of(:avatar).height_max(300) }
it { is_expected.to validate_dimensions_of(:avatar).width_between(200..500) }
it { is_expected.to validate_dimensions_of(:avatar).height_between(100..300) }
# size:
# #less_than, #less_than_or_equal_to, #greater_than, #greater_than_or_equal_to, #between, #equal_to
it { is_expected.to validate_size_of(:avatar).less_than(50.kilobytes) }
it { is_expected.to validate_size_of(:avatar).less_than_or_equal_to(50.kilobytes) }
it { is_expected.to validate_size_of(:avatar).greater_than(1.kilobyte) }
it { is_expected.to validate_size_of(:avatar).greater_than_or_equal_to(1.kilobyte) }
it { is_expected.to validate_size_of(:avatar).between(100..500.kilobytes) }
it { is_expected.to validate_size_of(:avatar).equal_to(5.megabytes) }
# total_size:
# #less_than, #less_than_or_equal_to, #greater_than, #greater_than_or_equal_to, #between, #equal_to
it { is_expected.to validate_total_size_of(:avatar).less_than(50.kilobytes) }
it { is_expected.to validate_total_size_of(:avatar).less_than_or_equal_to(50.kilobytes) }
it { is_expected.to validate_total_size_of(:avatar).greater_than(1.kilobyte) }
it { is_expected.to validate_total_size_of(:avatar).greater_than_or_equal_to(1.kilobyte) }
it { is_expected.to validate_total_size_of(:avatar).between(100..500.kilobytes) }
it { is_expected.to validate_total_size_of(:avatar).equal_to(5.megabytes) }
# duration:
# #less_than, #less_than_or_equal_to, #greater_than, #greater_than_or_equal_to, #between, #equal_to
it { is_expected.to validate_duration_of(:introduction).less_than(50.seconds) }
it { is_expected.to validate_duration_of(:introduction).less_than_or_equal_to(50.seconds) }
it { is_expected.to validate_duration_of(:introduction).greater_than(1.minute) }
it { is_expected.to validate_duration_of(:introduction).greater_than_or_equal_to(1.minute) }
it { is_expected.to validate_duration_of(:introduction).between(100..500.seconds) }
it { is_expected.to validate_duration_of(:avatar).equal_to(5.minutes) }
# with_audio
it { is_expected.to validate_with_audio_of(:video) }
it { is_expected.to validate_with_audio_of(:silent_video).without_audio }
# pages:
# #less_than, #less_than_or_equal_to, #greater_than, #greater_than_or_equal_to, #between, #equal_to
it { is_expected.to validate_pages_of(:contract).less_than(50) }
it { is_expected.to validate_pages_of(:contract).less_than_or_equal_to(50) }
it { is_expected.to validate_pages_of(:contract).greater_than(5) }
it { is_expected.to validate_pages_of(:contract).greater_than_or_equal_to(5) }
it { is_expected.to validate_pages_of(:contract).between(100..500) }
it { is_expected.to validate_pages_of(:contract).equal_to(5) }
end
(Note that matcher methods are chainable)
All matchers can currently be customized with these options:
describe User do
# :on
it { is_expected.to validate_attached_of(:avatar).on(:update) }
it { is_expected.to validate_attached_of(:avatar).on(%i[update custom]) }
# :except_on (Rails >= 8.0)
it { is_expected.to validate_attached_of(:avatar).except_on(:update) }
it { is_expected.to validate_attached_of(:avatar).except_on(%i[update custom]) }
# :message
it { is_expected.to validate_dimensions_of(:avatar).width(250).with_message('Invalid dimensions.') }
# :timeout (analyzer command timeout — metadata validators + content_type with spoofing)
it { is_expected.to validate_duration_of(:video).less_than(5.minutes).timeout(30.seconds) }
it { is_expected.to validate_with_audio_of(:video).timeout(5.seconds) }
it { is_expected.to validate_processable_file_of(:avatar).timeout(5.seconds) }
end
To use the matchers, make sure you have the shoulda-context gem up and running. No extra mock library is required (minitest-mock is not needed on Minitest 6).
You need to require the matchers:
require 'active_storage_validations/matchers'
And extend the module:
class ActiveSupport::TestCase
extend ActiveStorageValidations::Matchers
end
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 gem.
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.
AI coding agents: see AGENTS.md for architecture, test commands, and contribution patterns. Commit and PR titles follow Conventional Commits.
Before submitting your pull request, run the tests to make sure everything works as expected.
To run the gem tests, launch the following commands in the root folder of gem repository:
BUNDLE_GEMFILE=gemfiles/rails_7_0_1.gemfile bundle exec rake spec to run for Rails 7.0.1BUNDLE_GEMFILE=gemfiles/rails_7_1.gemfile bundle exec rake spec to run for Rails 7.1BUNDLE_GEMFILE=gemfiles/rails_7_2.gemfile bundle exec rake spec to run for Rails 7.2BUNDLE_GEMFILE=gemfiles/rails_8_0.gemfile bundle exec rake spec to run for Rails 8.0BUNDLE_GEMFILE=gemfiles/rails_8_1.gemfile bundle exec rake spec to run for Rails 8.1BUNDLE_GEMFILE=gemfiles/rails_next.gemfile bundle exec rake spec to run for Rails mainSnippet to run in console:
BUNDLE_GEMFILE=gemfiles/rails_7_0_1.gemfile bundle
BUNDLE_GEMFILE=gemfiles/rails_7_1.gemfile bundle
BUNDLE_GEMFILE=gemfiles/rails_7_2.gemfile bundle
BUNDLE_GEMFILE=gemfiles/rails_8_0.gemfile bundle
BUNDLE_GEMFILE=gemfiles/rails_8_1.gemfile bundle
BUNDLE_GEMFILE=gemfiles/rails_next.gemfile bundle
BUNDLE_GEMFILE=gemfiles/rails_7_0_1.gemfile bundle exec rake spec
BUNDLE_GEMFILE=gemfiles/rails_7_1.gemfile bundle exec rake spec
BUNDLE_GEMFILE=gemfiles/rails_7_2.gemfile bundle exec rake spec
BUNDLE_GEMFILE=gemfiles/rails_8_0.gemfile bundle exec rake spec
BUNDLE_GEMFILE=gemfiles/rails_8_1.gemfile bundle exec rake spec
BUNDLE_GEMFILE=gemfiles/rails_next.gemfile bundle exec rake spec
Tips:
:focus (or use fit / fdescribe); RSpec is configured with filter_run_when_matching :focusbundle exec rspec spec/validators/size_validator_spec.rbIMAGE_PROCESSOR=vips / IMAGE_PROCESSOR=mini_magick. Locally, unset means validators use MiniMagick (ASV default) and both analyzer unit specs run; setting the env selects that processor for validators and excludes the other processor’s tagged examples (so they do not show as pending)Optional wall-clock / ips suite for metadata validators (cold analysis vs cached asv_* hits) lives under benchmark/. See benchmark/README.md for setup, how to run, and how to refresh benchmark/BASELINE.md. CI runs the suite informationally (no fail-on-regression).
We have a long list of valued contributors. Check them all at:
https://github.com/igorkasyanchuk/active_storage_validations/graphs/contributors
The gem is available as open source under the terms of the MIT License.
Overall
Maintained
Code review
RubyGems
Latest release
Releases this year
543 followers · starred Jan 2019
708 followers · starred Aug 2018
255 followers · starred Nov 2019
301 followers · starred Mar 2022