I have a function to clone records in a rails application. In addition to the form data I would like to copy/attach any active storage file uploads that are attached to the source object to the new object. Any ideas on how to do this? Here is my action:
def copy
#source = Compitem.find(params[:id])
#compitem = #source.dup
render 'new'
end
class Compitem < ApplicationRecord
belongs_to :user
has_many_attached :uploads, dependent: :destroy
end
I ended up getting this working by using the https://github.com/moiristo/deep_cloneable gem. Final action:
def copy
#source = Compitem.find(params[:id])
#compitem = #source.deep_clone(include: :uploads_blobs)
#compitem.save
render 'new'
end
Just did this in one of my applications - it was a has_one rather than has_many but I think something like this should work for you, without adding any additional dependencies, in Rails 6+:
#compitem = #source.dup
#source.uploads.each do |original_file|
#compitem.uploads.attach(io: StringIO.new(original_file.download),
filename: original_file.filename,
content_type: original_file.content_type)
end
#compitem.save
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
I am trying to test for uploading documents in rails. I am using carrierwave to upload documents. The relevant code is given below.
Document Model Test Code
class DocumentTest < ActiveSupport::TestCase
def setup
#category = categories(:category_a)
#document = #category.documents.build(file_name: "abc", file: "abc.doc")
end
test "document valid" do
assert #document.valid?
end
Document Model Code
class Document < ActiveRecord::Base
belongs_to :category
mount_uploader :file, FileUploader
validates :file_name, presence: true
validates :file, presence: true
validate :file_size
end
file_uploader code
The following are the files I have whitelisted.
def extension_white_list
%w(pdf doc htm html docx xlsx xml)
end
I get a Failed assertion when I run the tests. I commented out the validates :file, presence: true code and the tests pass. I have file as type string in the database. However, passing in a string value is the same as passing a blank value. Therefore, the presence: true validation fails. I am not sure why that is failing even though I am passing in a string value. I think I need to pass in additional params or information for the tests to know what kind of file it is. Thanks!
This is not how you pass a file to the carrierwave model. Have a look at these examples:
https://github.com/carrierwaveuploader/carrierwave/wiki/How-to:-Use-test-factories
Also, carrierwave has its own validators you might want to use:
https://github.com/carrierwaveuploader/carrierwave/wiki/How-to:-Validate-uploads-with-Active-Record
I've this models:
Prodcuts -> Products_Images.
I can add multiple images from a form uploading the image that it's my computer. I want to add images from URLs instead of locally images.
Paperclip has added this feature:
https://github.com/thoughtbot/paperclip/wiki/Attachment-downloaded-from-a-URL
but I don't know how to apply it in a has_many association.
I've tried adding a method in ProductImages model and call it for each URL after product is created. I don't know if I must use this method directly in Product model.
Where should I try to put the method of the wiki of Paperclip?
Here is a great gist (that I did not write). It should get you there: https://gist.github.com/jgv/1502777
require 'open-uri'
class Photo < ActiveRecord::Base
has_attached_file :image # etc...
before_validation :download_remote_image, :if => :image_url_provided?
validates_presence_of :image_remote_url, :if => :image_url_provided?, :message => 'is invalid or inaccessible'
private
def image_url_provided?
!self.image_url.blank?
end
def download_remote_image
io = open(URI.parse(image_url))
self.original_filename = io.base_uri.path.split('/').last
self.image = io
self.image_remote_url = image_url
rescue # catch url errors with validations instead of exceptions (Errno::ENOENT, OpenURI::HTTPError, etc...)
end
end
All credit to the author.
In the future, it's usually best to post the code with your attempt to solve the problem.
first of all, i am using rails 3.1.3 and carrierwave from the master
branch of the github repo.
i use a after_init hook to determine fields based on an attribute of
the page model instance and define attribute accessors for these field
which store the values in a serialized hash (hope it's clear what i am
talking about). here is a stripped down version of what i am doing:
class Page < ActiveRecord::Base
serialize :fields, Hash
after_initialize :set_accessors
def set_accessors
case self.template
when 'standard'
class << self
define_method 'image' do
self.fields['image']
end
define_method 'image=' do |value|
self.fields['image'] = value
end
end
mount_uploader :image, PageImageUploader
end
end
end
end
leaving out the mount_uploader command gives me access to the
attribute as i want. but when i mount the uploader a get an error
message saying 'undefined method new for nil class'
i read in the source that there are the methods read_uploader and
write_uploader in the extensions module.
how do i have to override these to make the mount_uploader command
work with my 'virtual' attribute.
i hope somebody has an idea how i can solve this problem. thanks a lot
for your help.
best regard. dominik.
Same problem but solved in your model you should override read_uploader(column) and write_uploader(column, identifier) instance methods. I also have a problem with #{column}_will_change! and #{column}_changed? for a virtual column so I had to define them too:
class A < ActiveRecord::Base
serialize :meta, Hash
mount_uploader :image, ImageUploader
def image_will_change!
meta_will_change!
#image_changed = true
end
def image_changed?
#image_changed
end
def write_uploader(column, identifier)
self.meta[column.to_s] = identifier
end
def read_uploader(column)
self.meta[column.to_s]
end
end
Now there's also an add-on to carrierwave which provides the exact functionality as described by Antiarchitect:
https://github.com/timsly/carrierwave-serializable