Filip Kis
Filip Kis

Reputation: 1660

File does not get stored (mounted) if processing of versions fails

I upgraded carrierwave from 0.11.0 to 1.2.3 and realised that a, for me crucial, behaviour has changed and broke my logic. Here is the example of my uploader.

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

  storage :fog

  version :thumb do
    process :convert => :jpg

    def default_url
      '/assets/document_thumb.png'
    end
  end

end

And the model that it's mounted too:

class Material < ActiveRecord::Base

  attr_accessible :name, :file

  mount_uploader :file, FileUploader, validate_processing: false

  before_create :create_file_hash

  def create_file_hash
    self.hash_digest = Digest::MD5.hexdigest(file.read)
  end

end

In the old carrierwave, even if the version processing (e.g. in this case the convert) failed the main version of the file would still be uploaded and stored. However, now in cases when processing fails (not always, but I can't do conditional processing as my case is more complex then here illustrated) nothing gets stored. The file attribute remains an empty (blank) uploader and nothing is uploaded to the fog storage.

Any idea on how to get back the old behaviour?

In other words, how to ignore any errors with processing of versions. Or not trigger processing of versions in the after_cache callback but rather some later time down the line?

I think I've tracked down this issue to the following change in Mounter#cache method:

def cache(new_files)
  return if not new_files or new_files == ""
  @uploaders = new_files.map do |new_file|
    uploader = blank_uploader
    uploader.cache!(new_file)
    uploader
  end

  @integrity_error = nil
  @processing_error = nil
rescue CarrierWave::IntegrityError => e
  @integrity_error = e
  raise e unless option(:ignore_integrity_errors)
rescue CarrierWave::ProcessingError => e
  @processing_error = e
  raise e unless option(:ignore_processing_errors)
end

Which used to just do the uploader.cache!(new_file) directly (not in map) and then uploader got updated along the way and returned to the model when needed. However, now the processing error causes the map block to exit and @uploaders array never gets updated with the uploader that worked (i.e. for the original file).

Upvotes: 0

Views: 749

Answers (2)

kgunbin
kgunbin

Reputation: 61

One possible solution would be overriding the cache! method in you uploader instead:

class FileUploader < CarrierWave::Uploader::Base
  def cache!(*)
    super
  rescue CarrierWave::ProcessingError => e
    Rails.logger.debug "FileUploader: Error creating thumbnail: #{e}"
    nil
  end
  ...
end

That way, it works for every model

Upvotes: 1

Filip Kis
Filip Kis

Reputation: 1660

Half a day of effort later here is the solution I've come up with that doesn't involve monkey patching carrierwave.

class Material < ActiveRecord::Base

  attr_accessible :name, :file

  mount_uploader :file, FileUploader, validate_processing: false      

  #----> This is to manually trigger thumbnail creation <----
  before_create :create_thumbnail

  def create_thumbnail
    file.thumb.cache!(file.file)
  rescue CarrierWave::ProcessingError => e
    Rails.logger.debug "FileUploader: Error creating thumbnail: #{e}"
  end

  # rest of the model code
end

So here we have create_thumbnail method triggered in before_create callback that manually calls the cache! method on the thumb uploader. The file.file is at this moment (i.e. before create, so before the file has been uploaded to the storage) pointing to the temporary cached file. Which is exactly what we want (we don't want to re-download the file from the storage just to create thumbnails.

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

  storage :fog

  #----> Add the if condition to versions <----
  version :thumb, if: :has_versions? do
    process :convert => :jpg

    #----> This is needed to trigger processing later <----
    after :cache, :process!

    def default_url
      '/assets/document_thumb.png'
    end
  end

  #---> This is to avoid versions before the main version is fully processed and cached <---
  def has_versions?
    !(model.new_record? && model[:file].nil?)
  end

end

Now this is the tricky party. We need to initially disable the version creations and for that reason we have the has_versions? method that checks if the file is a new record. Now that check is not enough, because in our before_create callback the model is still new record (i.e. it hasn't yet been persisted).

However, what's the difference between the first time the uploader tries to create the versions (and which, if it fails, prevents original file from caching as described in the question) and the moment we call it in our before_create callback is that in the second case the file attribute of the model will be set.

Be careful, however, because you cannot do model.file since that points to the uploader (and if called here where I'm calling it it would actually cause a stack overflow). You need to access it as model[:file].

The final trick is that for some reason just calling cache! in the model would not actually trigger the processing. The processing was supposed to be triggered during the initial run (which we prevented for other versions) and since the original file is cached, carrierwave expects the versions are as well, so they don't need processing. But adding the after :cache, :process! ensures that it's triggered.

Don't know if anybody will find this useful or if I've gone about the problem the wrong way.

Either way, I'd love to hear comments.

Happy I made it work for my case and that I can continue using latest gem version.

Upvotes: 0

Related Questions