ActiveStorage File Attachment Validation - ruby-on-rails

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

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

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.

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

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...

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.

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
# ...

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

Related

Rails 5 + Shrine + Polymorphic model + Pretty Location

I'm using Shrine for direct uploads to S3 and I'm trying to user the pretty_location plugin to set the location in my S3 bucket.
I have a document model has the file_datatext attribute and is connected to a FileUploader:
class Document < ApplicationRecord
belongs_to :documentable, polymorphic: true
include FileUploader[:file]
validates :file, presence: true
end
Other models are associated with the document model through the following concern:
module Documentable
extend ActiveSupport::Concern
included do
has_one :document, as: :documentable, dependent: :destroy
accepts_nested_attributes_for :document, allow_destroy: true
end
end
This is my FileUploader:
class FileUploader < Shrine
Attacher.promote { |data| PromoteJob.perform_later(data) }
Attacher.delete { |data| DeleteJob.perform_later(data) }
plugin :upload_options, cache: {acl: "public-read"}
plugin :upload_options, store: {acl: "public-read"}
plugin :logging, logger: Rails.logger
plugin :pretty_location
plugin :processing
plugin :delete_promoted
plugin :recache
plugin :restore_cached_data
plugin :delete_raw
plugin :validation_helpers
def generate_location(io, context = {})
# do something depending on context[:record].documentable
end
end
When uploading files from the user's filesystem via the client browser through nested attributes all works as expected and I'm able to generate a pretty location in my S3 bucket.
However, I have another model where I am trying to upload to S3 a PDF file which is generated in the backend with the following setup.
class Invoice < ApplicationRecord
has_one :documents, as: :documentable, dependent: :destroy
end
The Invoice model doesn't use the concern as I want it to connect to the polymorphic document with a has_many association.
class Api::V1::InvoicesController < Api::V1::BaseController
def upload_pdf
pdf = InvoicePdf.new(#invoice)
attachment = pdf.render
file = StringIO.new(attachment)
file.class.class_eval { attr_accessor :original_filename, :content_type }
file.original_filename = "invoice_#{#invoice.reference}.pdf"
file.content_type = "application/pdf"
#document = #invoice.documents.new(file: file)
if #document.save
render "documents/show.json", status: :created
else
render json: { errors: #document.errors }, status: :unprocessable_entity
end
end
end
The upload works fine and I am able to upload the PDF to my S3 bucket, but I am not able to generate a pretty location because when I'm inside the generate_location method the context[:record] both the documentable_type and the documentable_id are nil.
This is a strange behaviour as in the rails console I am able to see that the association is correctly set after the upload has been done (without pretty_location) by running Invoice.last.documents.file.url.
I have tried creating the document record in different ways, have tried using the same documentable concern that works for other models but the result is alway the same and I have run out of ideas.
Does anyone have a clue why the documentable_type and documentable_id are not being passed into the context object inside the FileUploader?
The above setup actually works. I was using a breakpoint inside the generate_location FileUploader method and the api was breaking because that method was returning nil.
After fixing that, the first time it ran documentable was still nil but the method would run a second time with the documentable attributes present.
def generate_location(io, context = {})
return "" unless context[:record].documentable
path = if context[:record].documentable_type == "SomeClass"
# do something
elsif context[:record].documentable_type == "OtherClass"
# do something else
else
# do something else
end
return path
end

Show Active storage validation errors with form errors

I am using rails 5.2, bootstrap-4, bootstrap_form with active-storage file is uploading successfully. What I want is when I enter company name in form then it should check for company_logo.
I tried with this it is working good when I include error loop in form
Here in view
- if #company.errors.any?
#error_explanation
%ul
- #company.errors.full_messages.each do |message|
%li= message
Model Code
has_one_attached :company_logo
validates :name, :company_logo,presence: true
after_validation :is_logo?, if: Proc.new { |a| a.name? }
def is_logo?
errors.add(:base, 'Please upload your company logo.') if !self.company_logo.attached?
end
I want this kind of validation with file field
Actually active_storage doesn't support validation.
What i did for presence :
class CompanyModel < ApplicationRecord
has_one_attached :company_logo
validate :company_logo?
private
def company_logo?
errors.add(:base, 'Please upload your company logo.') unless company_logo.attached?
end
end
But this will upload the file to your storage and create an active_storage blob field in database...
The only workarround i found to delete file on storage and database field (so ugly):
def company_logo?
# Clean exit if there is a logo
return if company_logo.attached?
# Unless add error
errors.add(:base, 'Please upload your company logo.')
# Purge the blob
company_logo.record.company_logo_attachment.blob.purge
# Purge attachment
company_logo.purge
end
A nice solution is the active_storage_validations gem. Add to your Gemfile, then bundle install:
# Gemfile
gem 'active_storage_validations'
Inside your model, try something like this for a video attachment:
has_one_attached :video
validates :video,
size: { less_than: 50.megabytes,
message: 'Video size cannot be larger than 50 megabytes.' },
content_type:
{ in: %w(video/mov video/quicktime video/mp4 video/avi video/mpeg),
message: 'Upload must be a valid video type' }
References
The readme has many very useful examples.
This question and answers has some relevant information too.

Rails: how to validate an object field's value before save?

I'm writing a Redmine plugin that should check if some fields of an Issue are filled depending on values in other fields.
I've written a plugin that implements validate callback, but I don't know how to check field values which are going to be saved.
This is what I have so far:
module IssuePatch
def self.included(receiver)
receiver.class_eval do
unloadable
validate :require_comment_when_risk
protected
def require_comment_when_risk
risk_reduction = self.custom_value_for(3)
if risk_reduction.nil? || risk_reduction.value == 0
return true
end
comment2 = self.custom_value_for(4)
if comment2.nil? || comment2.value.empty?
errors.add(:comment2, "Comment2 is empty")
end
end
end
end
end
The problem here is that self.custom_value_for() returns the value already written to the DB, but not the one that is going to be written, so validation doesn't work. How do I check for the value that was passed from the web-form?
Any help will be greatly appreciated.
The nice thing about rails is that in your controller you don't have to validate anything. You are suppose to do all of this in your model. so in your model you should be doing something like
validates :value_that_you_care_about, :numericality => { :greater_than_or_equal_to => 0 }
or
validates :buyer_name, presence: true, :length => {:minimum => 4}
or
validates :delivery_location, presence: true
If any of these fail this will stop the object from being saved and if you are using rails scaffolding will actually highlight the field that is incorrect and give them and error message explaining what is wrong. You can also write your own validations such as
def enough_red_flowers inventory
if inventory.total_red_flowers-self.red_flower_quantity < 0
self.errors.add(:base, 'There are not enough Red Flowers Currently')
return false
end
inventory.total_red_flowers = inventory.total_red_flowers-self.red_flower_quantity
inventory.save
true
end
To write your own custom message just follow the example of self.errors.add(:base, 'your message')
You can find more validations here
Better way it's create custom validator
class FileValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
# some logic for validation
end
end
then in model:
validates :file, file: true

Auto orienting image when using delayed job

I have a photo sharing application that allows the user to drag and drop images which are then processed in delayed job and displayed in a gallery. I'm having some problems solving the problem with orientation on iPhone pictures. I have the following code:
initializers/auto_orient.rb
module Paperclip
class AutoOrient < Paperclip::Processor
def initialize(file, options = {}, *args)
#file = file
end
def make( *args )
dst = Tempfile.new([#basename, #format].compact.join("."))
dst.binmode
Paperclip.run('convert',"#{File.expand_path(#file.path)} -auto-orient #{File.expand_path(dst.path)}")
return dst
end
end
end
models/picture.rb
class Picture < ActiveRecord::Base
belongs_to :gallery
before_create :generate_slug
after_create :send_to_delayed_job
validates :slug, :uniqueness => true
scope :processing, where(:processing => true)
attr_accessible :image
has_attached_file :image,
:styles => {
:huge => "2048x1536>",
:small => "800x600>",
:thumb => "320x240>"
},
:processors => [:auto_orient, :thumbnail]
before_post_process :continue_processing
...
def process
self.image.reprocess!
self.processing = false
self.save(:validations => false)
end
private
def continue_processing
if self.new_record?
!self.processing
end
end
def send_to_delayed_job
Delayed::Job.enqueue ImageProcess.new(self.id), :queue => 'paperclip'
end
end
models/image_process.rb
class ImageProcess < Struct.new(:picture_id)
def perform
picture = Picture.find(self.picture_id)
picture.process
end
end
If I comment out the lines after_create :send_to_delayed_job and before_post_process i.e. the processing is done on the spot, the auto-orientation process works. But, when I put it through delayed job, no auto-orientation happens, just the resizing.
Does anyone have any ideas?
EDIT
It get's stranger. I moved to Carrierwave and the carrierwave_backgrounder gem. Ignoring background tasks for now, I have the following in my image_uploader.rb:
def auto_orient
manipulate! do |img|
img.auto_orient!
img
end
end
version :huge do
process :auto_orient
process resize_to_fit: [2048,1536]
end
This works. The images are in the correct orientation.
Now, if I add process_in_background :image to my picture.rb file in accordance with the instructions for carrier wave_backgrounder, auto_orient doesn't work.
I'm now going to try the store_in_background method to see if that makes a difference.
I noticed you have attr_accessible in your Picture model. I have been working with Delayed_Job recently and after many hours of struggling with it I figured out that it has serious issues with attr_accessible. There are workarounds, though.
More info is here (although you can Google for more on the topic)

How can I make Rails ActiveRecord automatically truncate values set to attributes with maximum length?

Assuming that I have a class such as the following:
class Book < ActiveRecord::Base
validates :title, :length => {:maximum => 10}
end
Is there a way (gem to install?) that I can have ActiveRecord automatically truncate values according to maximum length?
For instance, when I write:
b = Book.new
b.title = "123456789012345" # this is longer than maximum length of title 10
b.save
should save and return true?
If there is not such a way, how would you suggest that I proceed facing such a problem more generally?
Well, if you want the value truncated if its too long, you don't really need a validation, because it will always pass. I'd handle that like this:
class Book < ActiveRecord::Base
before_save :truncate_values
def truncate_values
self.title = self.title[0..9] if self.title.length > 10
end
end
I have come up with a new validator that does truncation. Here is how I did that:
I created the "validators" folder inside "app" folder and then created the file "length_truncate_validator.rb" with the following content:
class LengthTruncateValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
ml = options[:maximum]
record.send("#{attribute}=", value.mb_chars.slice(0,ml)) if value.mb_chars.length > ml unless value.nil? or ml.nil?
end
class << self
def maximum(record_class, attribute)
ltv = record_class.validators_on(attribute).detect { |v| v.is_a?(LengthTruncateValidator) }
ltv.options[:maximum] unless ltv.nil?
end
end
end
And inside my model class I have something like:
class Book < ActiveRecord::Base
validates :title, :length_truncate => {:maximum => 10}
end
which is quite handy and works the way I require.
But still, if you think that this one can be improved or done in another way, you are welcome.
This may not have been an option in 2011, but now there's a before_validation callback that will work.
class Book < ApplicationRecord
before_validation do
if self.params && self.params.length > 1000
self.params = self.title[0...10]
end
end
validate :title, length: { maximum: 10 }, allow_nil: true
end
I like the idea of using the before_validation callback. Here's my stab that automatically truncates all strings to within the database's limit
before_validation :truncate_strings
def truncate_strings
self.class.columns.each do |column|
next if column.type != :string
if self[column.name].length > column.limit
self[column.name] = self[column.name][0...column.limit]
end
end
end

Resources