Skip to content

Instantly share code, notes, and snippets.

@carlosramireziii
Last active September 22, 2023 21:39
Show Gist options
  • Star 15 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save carlosramireziii/6d0ca6b414d8a6af08371c30ba4dedcd to your computer and use it in GitHub Desktop.
Save carlosramireziii/6d0ca6b414d8a6af08371c30ba4dedcd to your computer and use it in GitHub Desktop.
A validator and RSpec matcher for restricting an attachment’s content type using Active Storage
require "rspec/expectations"
RSpec::Matchers.define :allow_content_type do |*content_types|
match do |record|
matcher.matches?(record, content_types)
end
chain :for do |attr_name|
matcher.for(attr_name)
end
chain :with_message do |message|
matcher.with_message(message)
end
private
def matcher
@matcher ||= AllowContentTypeMatcher.new
end
class AllowContentTypeMatcher
def for(attr_name)
@attr_name = attr_name
end
def with_message(message)
@message = message
end
def matches?(record, content_types)
Array.wrap(content_types).all? do |content_type|
record.send(attr_name).attach attachment_for(content_type)
record.valid?
!record.errors[attr_name].include? message
end
end
private
attr_reader :attr_name
def attachment_for(content_type)
suffix = content_type.to_s.split("/").last
{ io: StringIO.new("Hello world!"), filename: "test.#{suffix}", content_type: content_type }
end
def message
@message || I18n.translate("activerecord.errors.messages.content_type")
end
end
end
RSpec::Matchers.alias_matcher :allow_content_types, :allow_content_type
class ContentTypeValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless value.attached? && value.content_type.in?(content_types)
value.purge if record.new_record? # Only purge the offending blob if the record is new
record.errors.add(attribute, :content_type, options)
end
end
private
def content_types
options.fetch(:in)
end
end
en:
activerecord:
errors:
messages:
content_type: is not a valid file format
@murdoch
Copy link

murdoch commented Oct 6, 2018

Nice! How come you only purge the photo if it's a new record?

@efojs
Copy link

efojs commented Dec 28, 2018

Hi! Thank you!
I don't understand much in Rails, so please excuse me if the question is lame.
I watch table — active_storage_blobs — and records are not deleted (active_storage_attachments is OK).
Am I doing something wrong?

@meara
Copy link

meara commented Feb 22, 2019

This is great, thank you!

I modified this slightly for my use case such that it will allow the attachment to be nil.

class ContentTypeValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    return unless value.attached?
    return if value.content_type.in?(content_types)

    value.purge
    record.errors.add(attribute, :content_type, options)
  end

  private

  def content_types
    options.fetch(:in)
  end
end

@fydelio
Copy link

fydelio commented Mar 26, 2019

Where (directory structure) in rails would I store this validator?

Copy link

ghost commented May 7, 2019

I get FrozenError: can't modify frozen Hash. Not sure where it's coming from.

@frankolson
Copy link

@fydelio I store my validators in app/validators (i.e. app/validators/content_type_validator.rb)

@carlosramireziii
Copy link
Author

Nice! How come you only purge the photo if it's a new record?

Great question! If a persisted record has an existing attachment then we don't want to purge it on a validation error because then we'll lose the existing value. I believe this behavior will be different in Rails 6+ because attachments are no longer persisted immediately upon attachment.

I watch table — active_storage_blobs — and records are not deleted (active_storage_attachments is OK).

If you call destroy on an attachment then it won't automatically delete the associated blob. You'll want to make sure you use the purge method for deleting the attachment and blob.

Where (directory structure) in rails would I store this validator?

@fydelio I store my validators in app/validators (i.e. app/validators/content_type_validator.rb)

This is exactly the directory structure I use as well!

I modified this slightly for my use case such that it will allow the attachment to be nil.

Awesome!

@douglaspetrin
Copy link

douglaspetrin commented Sep 22, 2023

I get FrozenError: can't modify frozen Hash. Not sure where it's coming from.

@thelucid Yes, I got a similar error with the @meara approach. can't modify frozen attributes - Were you able to fix it? I added the check back and it worked as well:

def validate_each(record, attribute, value)

    return unless value.attached?
    return if value.content_type.in?(content_types)

    value.purge if record.new_record?
    record.errors.add(attribute, :content_type, **options)
  end

@carlosramireziii Thank you, it works nicely.

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