29

Is there a way to validate attachments with ActiveStorage? For example, if I want to validate the content type or the file size?

Something like Paperclip's approach would be great!

  validates_attachment_content_type :logo, content_type: /\Aimage\/.*\Z/
  validates_attachment_size :logo, less_than: 1.megabytes
Tom Rossi
  • 11,604
  • 5
  • 65
  • 96

7 Answers7

41

Well, it ain't pretty, but this may be necessary until they bake in some validation:

  validate :logo_validation

  def logo_validation
    if logo.attached?
      if logo.blob.byte_size > 1000000
        logo.purge
        errors[:base] << 'Too big'
      elsif !logo.blob.content_type.starts_with?('image/')
        logo.purge
        errors[:base] << 'Wrong format'
      end
    end
  end
Tom Rossi
  • 11,604
  • 5
  • 65
  • 96
  • 4
    The old value will be overwritten even if invalid. – swordray Jan 09 '18 at 21:31
  • this works for me, but with this way the result attach a parameter called `size` and is delegated to the AR attachment, this crash because of AR doesn't have a `size`method and returns the following message: `undefined method 'size' for #`, have you had that error previously ? and have you solve it? Thanks – trejo08 Apr 18 '18 at 20:22
  • 3
    If you are saving the file locally, confirm that when the validation triggers, the `active_storage_blobs` record does not get created, and that the file does not get saved within the `storage` directory of the app. My experience is that custom ActiveStorage validations on the model only stops the `active_storage_attachments` record from getting created, but the file will still be saved to disk, and the `active_storage_blobs` record still gets saved in the database. You might want a job to clear out the orphaned attachments and blobs. – Neil May 15 '18 at 16:43
  • trejo08, assuming you're on Rails 5.2.0, use file.attachment instead of file.code, as the alias ties together with the test environment. – Dominic Jul 26 '18 at 20:29
  • 2
    this is the answer I was looking for. Not stupid random gems. I want to see the raw details of the validation and where to put the code. – Nick Res Nov 10 '18 at 21:15
  • 5
    purge not required with rails 6. Rails 6 doesn't persist file to storage if your logo_validation fails. It only upload/store file to storage if model is successfully saved. Yet to confirm this with direct upload. – Vijay Meena Sep 12 '19 at 03:44
19

ActiveStorage doesn't support validations right now. According to https://github.com/rails/rails/issues/31656.


Update:

Rails 6 will support ActiveStorage validations.

https://github.com/rails/rails/commit/e8682c5bf051517b0b265e446aa1a7eccfd47bf7

Uploaded files assigned to a record are persisted to storage when the record
is saved instead of immediately.
In Rails 5.2, the following causes an uploaded file in `params[:avatar]` to
be stored:
```ruby
@user.avatar = params[:avatar]
```
In Rails 6, the uploaded file is stored when `@user` is successfully saved.
swordray
  • 833
  • 9
  • 21
  • confirmed it. rails 6 doesn't persist file to storage if validation fails (model is not saved). I haven't checked the behavior on direct-upload to storage yet. – Vijay Meena Sep 12 '19 at 03:46
12

Came across this gem: https://github.com/igorkasyanchuk/active_storage_validations

    class User < ApplicationRecord
      has_one_attached :avatar
      has_many_attached :photos
    
      validates :name, presence: true
    
      validates :avatar, attached: true, content_type: 'image/png',
                                         dimension: { width: 200, height: 200 }
      validates :photos, attached: true, content_type: ['image/png', 'image/jpg', 'image/jpeg'],
                                         dimension: { width: { min: 800, max: 2400 },
                                                      height: { min: 600, max: 1800 }, message: 'is not given between dimension' }
    end
Benjamin Oakes
  • 12,262
  • 12
  • 65
  • 83
Dan Tappin
  • 2,692
  • 3
  • 37
  • 77
9

You can use awesome https://github.com/musaffa/file_validators gem

class Profile < ActiveRecord::Base
  has_one_attached :avatar
  validates :avatar, file_size: { less_than_or_equal_to: 100.kilobytes },
    file_content_type: { allow: ['image/jpeg', 'image/png'] }
end

I'm using it with form object so I'm not 100% sure it is working directly with AR but it should...

Alex Ganov
  • 147
  • 1
  • 3
1

Here is my solution to validate content types in Rails 5.2, that as you may know it has the pitfall that attachments are saved as soon as they are assigned to a model. It may also work for Rails 6. What I did is monkey-patch ActiveStorage::Attachment to include validations:

config/initializers/active_storage_attachment_validations.rb:

Rails.configuration.to_prepare do
  ActiveStorage::Attachment.class_eval do
    ALLOWED_CONTENT_TYPES = %w[image/png image/jpg image/jpeg].freeze

    validates :content_type, content_type: { in: ALLOWED_CONTENT_TYPES, message: 'of attached files is not valid' }
  end
end

app/validators/content_type_validator.rb:

class ContentTypeValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, _value)
    return true if types.empty?
    return true if content_type_valid?(record)

    errors_options = { authorized_types: types.join(', ') }
    errors_options[:message] = options[:message] if options[:message].present?
    errors_options[:content_type] = record.blob&.content_type
    record.errors.add(attribute, :content_type_invalid, errors_options)
  end

  private

  def content_type_valid?(record)
    record.blob&.content_type.in?(types)
  end

  def types
    Array.wrap(options[:with]) + Array.wrap(options[:in])
  end
end

Due to the implementation of the attach method in Rails 5:

    def attach(*attachables)
      attachables.flatten.collect do |attachable|
        if record.new_record?
          attachments.build(record: record, blob: create_blob_from(attachable))
        else
          attachments.create!(record: record, blob: create_blob_from(attachable))
        end
      end
    end

The create! method raises an ActiveRecord::RecordInvalid exception when validations fail, but it just needs to be rescued and that's all.

Pere Joan Martorell
  • 2,608
  • 30
  • 29
  • Thank you for your thoroughness! I had a related problem and was able to adapt this answer to my needs. – DawnPaladin Oct 18 '19 at 23:38
  • With this solution, attachments that fail validation are still persisted to the file system. They can be cleaned up with `ActiveStorage::Blob.unattached.each(&:purge)` – DawnPaladin Oct 18 '19 at 23:46
0

Copy contents of the ActiveStorage's DirectUploadsController in the app/controllers/active_storage/direct_uploads_controller.rb file and modify the create method. You can add authentication to this controller, add general validations on the file size or mime type, because create method of this controller creates the url for the file to be uploaded. So you can prevent any file upload by controlling size and mime type in this controller.

A simple validation could be:

# ...
def create
  raise SomeError if blob_args[:byte_size] > 10240 # 10 megabytes
  blob = ActiveStorage::Blob.create_before_direct_upload!(blob_args)
  render json: direct_upload_json(blob)
end
# ...
Aref Aslani
  • 1,560
  • 13
  • 27
  • 1
    I don't agree with the concept of moving validation to the controller. Controllers are hardly being tested and I don't believe that validations should be part of integration tests but rather unit tests. In my opinion, it is better to keep any validation efforts in the model whenever possible and perhaps ad some sort of client-side validation for usability purposes. But keep your controllers slim by all means - you will thank yourself in the end :) – Georg Keferböck Jul 23 '18 at 13:43
0

I found a way to validate and delete attachments with callback before_save. This is a useful approach because if you validate file during the transaction (and you want to purge it), after adding error and it will rollback deleting the attachment.

before_save :check_logo_file, on: %i[create update]

def check_favicon_content_type
    PartnerValidators::CustomPartnerFaviconValidator.new.validate(self)
end

module PartnerValidators
    class CustomPartnerFaviconValidator < ActiveModel::Validator
        ALLOWED_MIME_TYPES = %w(image/vnd.microsoft.icon image/x-icon image/png).freeze
        private_constant :ALLOWED_MIME_TYPES

        def validate(partner)
            if partner.favicon.attached? && invalid_content_type?(partner)
                partner.errors.add(:favicon, I18n.t("active_admin.errors.favicon"))
                partner.favicon.purge
            end
        end

        private

        def invalid_content_type?(partner)
            !partner.favicon.blob.content_type.in?(ALLOWED_MIME_TYPES)
        end
    end
end
Jean
  • 825
  • 8
  • 17