Skip to content

Instantly share code, notes, and snippets.

@chrisbloom7
Created June 6, 2011 07:16
Show Gist options
  • Star 75 You must be signed in to star a gist
  • Fork 16 You must be signed in to fork a gist
  • Save chrisbloom7/1009861 to your computer and use it in GitHub Desktop.
Save chrisbloom7/1009861 to your computer and use it in GitHub Desktop.
A cheap knock off of the default validates_length_of validator, but checks the filesize of a Carrierwave attachment

Note that this validation runs both after the file is uploaded and after CarrierWave has processed the image. If your base uploader includes a filter to resize the image then the validation will be run against the resized image, not the original one that was uploaded. If this causes a problem for you, then you should avoid using a resizing filter on the base uploader and put any specific size requirements in a version instead.

So instead of this:

require 'carrierwave/processing/mini_magick'

class LogoUploader < CarrierWave::Uploader::Base
  include CarrierWave::MiniMagick

  process :quality => 80
  process :resize_to_limit => [800, 800]
  process :convert => 'png'
  
  # ...
end

Do this:

require 'carrierwave/processing/mini_magick'

class LogoUploader < CarrierWave::Uploader::Base
  include CarrierWave::MiniMagick

  process :convert => 'png'

  version :medium do
    process :quality => 80
    process :resize_to_limit => [800, 800]
  end

  # ...

end
# config/locales/en.yml
en:
errors:
messages:
wrong_size: "is the wrong size (should be %{file_size})"
size_too_small: "is too small (should be at least %{file_size})"
size_too_big: "is too big (should be at most %{file_size})"
# lib/file_size_validator.rb
# Based on: https://gist.github.com/795665
class FileSizeValidator < ActiveModel::EachValidator
MESSAGES = { :is => :wrong_size, :minimum => :size_too_small, :maximum => :size_too_big }.freeze
CHECKS = { :is => :==, :minimum => :>=, :maximum => :<= }.freeze
DEFAULT_TOKENIZER = lambda { |value| value.split(//) }
RESERVED_OPTIONS = [:minimum, :maximum, :within, :is, :tokenizer, :too_short, :too_long]
def initialize(options)
if range = (options.delete(:in) || options.delete(:within))
raise ArgumentError, ":in and :within must be a Range" unless range.is_a?(Range)
options[:minimum], options[:maximum] = range.begin, range.end
options[:maximum] -= 1 if range.exclude_end?
end
super
end
def check_validity!
keys = CHECKS.keys & options.keys
if keys.empty?
raise ArgumentError, 'Range unspecified. Specify the :within, :maximum, :minimum, or :is option.'
end
keys.each do |key|
value = options[key]
unless value.is_a?(Integer) && value >= 0
raise ArgumentError, ":#{key} must be a nonnegative Integer"
end
end
end
def validate_each(record, attribute, value)
raise(ArgumentError, "A CarrierWave::Uploader::Base object was expected") unless value.kind_of? CarrierWave::Uploader::Base
value = (options[:tokenizer] || DEFAULT_TOKENIZER).call(value) if value.kind_of?(String)
CHECKS.each do |key, validity_check|
next unless check_value = options[key]
value ||= [] if key == :maximum
value_size = value.size
next if value_size.send(validity_check, check_value)
errors_options = options.except(*RESERVED_OPTIONS)
errors_options[:file_size] = help.number_to_human_size check_value
default_message = options[MESSAGES[key]]
errors_options[:message] ||= default_message if default_message
record.errors.add(attribute, MESSAGES[key], errors_options)
end
end
def help
Helper.instance
end
class Helper
include Singleton
include ActionView::Helpers::NumberHelper
end
end
@chrisbloom7
Copy link
Author

I'm assuming you mean cached as in stored to the temp folder. No there is no way around that as CarrierWave has to store it somewhere first before the file can be available for validation. You could use an after_validation callback to cleanup the temp file if the validation fails, or have a worker process do that periodically on older temp files.

@bokor
Copy link

bokor commented Apr 16, 2012

Chris,

Thanks for getting back to me. Do you know what methods to call to make this happen? I can't seem to figure out how to get those to clean up. I have tried various things and they haven't worked. I would like to encapsulate everything on the Mounted Uploader, but not sure if that would work either.

@chrisbloom7
Copy link
Author

Before the image is saved the full path to the temp image is stored in the attribute you've mounted the uploader on. So for instance:

 > i = Contest.new
 => #<Contest > 
 > i.image = File.open(Rails.root.join('spec', 'support', 'images', '0.5_mb.jpg'))
 => #<File:/Users/chrisbloom7/Projects/foo/spec/support/images/0.5_mb.jpg> 
 > i.image
 => /Users/chrisbloom7/Projects/foo/tmp/uploads/development/20120416-1201-4288-3087/0.5_mb.jpg 

So in your after_validation callback method, check to see if the instance's errors hash contains a key for your mounted attribute, and if so delete the image in the specified path.

@bokor
Copy link

bokor commented Apr 16, 2012

Perfect...thank you!

@chrisbloom7
Copy link
Author

You might also try posting to the CW Google Group to see if the Uploaders have a built in mechanism for handling this.

@hyperrjas
Copy link

Hi guys, I get this error for spanish language:

I18n::MissingTranslationData in ItemsController#create
translation missing: es.number.human.storage_units.format

lib/file_size_validator.rb:51:in `block in validate_each'
lib/file_size_validator.rb:42:in `each'
lib/file_size_validator.rb:42:in `validate_each'
app/controllers/items_controller.rb:76:in `block in create'
app/controllers/items_controller.rb:75:in `create'

How can I fix this error for differents languages?
Thank you

@ruseel
Copy link

ruseel commented May 9, 2012

@hyperrjas Try 'rails-i18n' gem in your Gemfile.

@chrisbloom7
Copy link
Author

Edit: Try @ruseel's suggestion first

@hyperrjas Copy the entries from the config/locales/en.yml file above into config/locales/es.yml and provide the proper translations.

@hyperrjas
Copy link

Thank you the @ruseel's suggestion has solved the problem :D. Thank you very much!

@defp
Copy link

defp commented Oct 24, 2012

Thanks

@shlima
Copy link

shlima commented Mar 15, 2013

Thank you!

@marcusg
Copy link

marcusg commented Apr 23, 2013

thanks!

@danman01
Copy link

I'm getting an exception (unknown validator) when running rspec tests - is there a place I should include the file_size_validator somewhere in rspec? Thanks

@bosskovic
Copy link

I was getting "comparison of String with 0 failed" exception in the line 51 for the values larger then 1023, until I added precision:

errors_options[:file_size] = help.number_to_human_size check_value, :precision => 5

Now it works fine.

Ruby 2.0.0-p247
Rails 3.2.13

@szymon-przybyl
Copy link

why not merge it into CarrierWave? :)

@RKushnir
Copy link

I'm debugging the code because it's giving me undefined method '<=' for nil:NilClass), that's on the line next if value_size.send(validity_check, check_value).
The options I'm passing: validates :profile_img, file_size: {maximum: 2.megabytes.to_i}.

The following 2 lines don't make sense for me:

raise(ArgumentError, "A CarrierWave::Uploader::Base object was expected") unless value.kind_of? CarrierWave::Uploader::Base
value = (options[:tokenizer] || DEFAULT_TOKENIZER).call(value) if value.kind_of?(String)

Maybe I'm missing something, but value cannot be of type Uploader and String at the same time:

>   CarrierWave::Uploader::Base.new.kind_of? String
=> false 
> "abc".kind_of? CarrierWave::Uploader::Base
=> false 

@RKushnir
Copy link

I discovered this was caused by some coincidence: I was migrating from paperclip and some records already had uploads, so carrierwave was thinking they are valid. I'm hosting on Azure, so I used carrierwave-azure which makes an HTTP request to get the file size and returns nil, instead of 0, if the file is unavailable. The solution was to just clear the outdated values in the upload column. Maybe this helps someone in a similar situation.

Anyway, this validator is almost useless(besides making a human readable file size in the error message), as you can just use the standard Rails length validator.

@roberthimler
Copy link

Works for me the only problem is if the file is too big it ends up saving the file in my application's /uploads/tmp/ directory. Any ideas of how to avoid this from happening and delete the file?

@saeedSarpas
Copy link

What is the right way to set maximum file size amount for test env?
I did something like this:

file_size: { maximum: (Rails.env.test? ? 1.megabytes.to_i : 5.megabytes.to_i }

but I don't feel it's right.

@jmuheim
Copy link

jmuheim commented Jul 19, 2014

@saeedSarpas: I'm thinking about this, too. Is there a way to stub the validation within the test itself?

At least, I prettified your code a little:

validates :avatar, file_size: {maximum: (Rails.env.test? ? 5 : 100).kilobytes.to_i}

@heartfulbird
Copy link

Hi! I'm using carrierwave and file_size_validator
and I'm using sidekiq
and when I run "bundle exec sidekiq " in console
i see this errors
.../lib/file_size_validator.rb:5: warning: already initialized constant FileSizeValidator::MESSAGES
and the same about
FileSizeValidator::CHECKS
and ::DEFAULT_TOKENIZER
and ::RESERVED_OPTIONS

@musaffa
Copy link

musaffa commented Dec 11, 2014

file validators gem does file validations more elegantly. It also supports validations both before and after uploads.

@pioz
Copy link

pioz commented Apr 22, 2015

@vinayvinay
Copy link

Can anyone help me resolve this error that comes up while using this validator? For some reason the value.size calls .content_length on a nil.

lib/file_size_validator.rb:47:in `block in validate_each'
NoMethodError (undefined method `content_length' for nil:NilClass):
lib/file_size_validator.rb:42:in `each'
lib/file_size_validator.rb:42:in `validate_each'

@ch3m1c
Copy link

ch3m1c commented Nov 24, 2015

Anyone using this with multiple files upload from Carrierwave? :)

@osnysantos
Copy link

Im getting the same error as @vinayvinay

It seems that the asset was removed out of app and file_size_validator try to validate an nonexistent file.

@mariorcardoso
Copy link

@osnysantos I was experiencing the same error because the asset was not present (the url was broken). I added the the line next if value.present? && !(value.file.exists?) inside the validate_each loop so I don't get an exception when the file url is broken. It seems to work fine so far. If the url is broken I don't need to validate the file size.

checks.each do |key, validity_check|
      next unless check_value = options[key]

      value ||= [] if key == :maximum
      next if value.present? && !(value.file.exists?)

      value_size = value.size
      next if value_size.send(validity_check, check_value)

@pinty
Copy link

pinty commented Jun 27, 2017

Hello @chrisbloom7
As we would like to use your source code present on this page, is there any chance that we can get a license for the code written by you into the following file file_size_validator.rb ? We couldn't find any.
Thank you!

@alexanderadam
Copy link

alexanderadam commented Aug 5, 2017

Hey @pinty,

it's possible to replace it with file_validators gem mentioned above by it's author which has the MIT license.
Besides from that it's properly packaged, has specs and a dedicated issue tracker.

PS: I'm pretty sure I'm reading a document you (co-)authored 😉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment