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).
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
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.
Related
In my app, I have a large complex model with many associations. I need to serve it as json. The model's size and complexity, however, mean that assembling the json version is costly both in terms of CPU and RAM. Fortunately, since the model is not frequently updated it's a good candidate for caching. I'd like to keep a cached version of the model's json string on a static file hosting service (e.g. S3), handled by activestorage.
The code below summarizes what's I've done so far. In the controller, incoming GET requests for the model are redirected to the cached version on activestorage. The model keeps the cached version up-to-date using an after_save callback.
class ComplicatedModelController < ApplicationController
def show
#complicated_model = ComplicatedModel.find(params[:id])
redirect_to #complicated_model.activestorage_cached_json_response.url
end
class ComplicatedModel < ActiveRecord::Base
has_one_attached :activestorage_cached_json_response
after_save do
# updated cached version - how can I do this WITHOUT retriggering the aftersave callback?
self.activestorage_cached_json_response.attach(
io: StringIO.new(self.as_json), # this step is costly
filename: "ComplicatedModel_#{self.id}.json",
content_type: 'application/json'
)
end
end
The problem is that updating the activestorage attachment within the aftersave callback creates an infinite loop: this step saves the model again, and so retriggers the callback. To make this strategy work, I'd need to update the activestorage attachment WITHOUT triggering the aftersave callback. Is this possible?
Ah, I figured it out. I just needed to use an :unless after the callback, as is explained here: https://stackoverflow.com/a/7386222/765287
Working code is below.
class ComplicatedModel < ActiveRecord::Base
attr_accessor :skip_aftersave_callback
has_one_attached :activestorage_cached_json_response
after_save :update_cache, unless: :skip_aftersave_callback # change here
def update_cache
# update_cache is always called in the after_save callback;
# this flag stops the callback after the .attach below
self.skip_aftersave_callback = true
self.activestorage_cached_json_response.attach(
io: StringIO.new(self.as_json), # this step is costly
filename: "ComplicatedModel_#{self.id}.json",
content_type: 'application/json'
)
end
end
I've introduced a new version on my Carrierwave Uploader. When I create a new Event it creates both versions correctly. But when I update it, only the file I attached gets uploaded, but versions do not get recreated.
I am using CarrierWave 1.2.2, and looking at the changelog, it doesn't seem to have been a bug that got fixed in the newer versions
class CoverUploader < CarrierWave::Uploader::Base
include CarrierWave::MiniMagick
if Rails.env.development? || Rails.env.test?
storage :file
elsif Rails.env.production?
storage :fog
end
# Override the directory where uploaded files will be stored.
# This is a sensible default for uploaders that are meant to be mounted:
def store_dir
if ENV['HEROKU_APP_NAME'].to_s.include?('-pr-')
"review_apps/#{model.class.to_s.underscore}/#{model.id}"
else
"#{Rails.env}/#{model.class.to_s.underscore}/#{model.id}"
end
end
# Provide a default URL as a default if there hasn't been a file uploaded:
def default_url(*args)
ActionController::Base.helpers.asset_path('test.jpg')
end
# Create different versions of your uploaded files:
version :optimised do
process convert: 'webp'
process :set_content_type_to_webp
def full_filename(_for_file = model.cover.file)
"cover_#{model.id}.webp"
end
def exists?
file&.exists?
end
end
def extension_blacklist
%w(webp)
end
private
# Required to actually force Amazon S3 to treat it like an image
def set_content_type_to_webp
file.instance_variable_set(:#content_type, 'image/webp')
end
end
#ogelacinyc was partly correct when he found the bug in full_filename. I went back to test normal functionality with creating another version, with a simple dimension change. I could then see that update would recreate the versions by itself, just like I expected.
That made me think that maybe there is something wrong with my version :optimised block. So after commenting one by one, I found that full_filename was the culprit. It could have been model.cover.file failing silently, but I think it was model.id, as can be seen in the description for filename method in Carrierwave
So instead, I grab the filename directly, extract extension and substitute it with webp:
def full_filename(for_file = model.file_name.file)
extension = File.extname(for_file)
"cover_#{for_file.sub(extension, '.webp')}"
end
Which works without problems!
You need to add an after_save callback to Event and then call recreate_versions! on your mounted uploader.
Assuming you have an Event model with the following, this would solve your problem.
class Event < ApplicationRecord
mount_uploader :cover_image, CoverUploader
after_save :recreate_versions!
delegate :recreate_versions!, to: :cover_image, allow_nil: true
end
See CarrierWave's README also.
In an application I wanted to send public file URL to a service in an after_create callback. So, the code (simplified) looked like this:
class UserProfile < ApplicationRecord
mount_uploader :video, VideoUploader
after_create :send_url_to_service
private
# Just logs the URL
def send_url_to_service
Rails.logger.info video.url
end
end
To my frustration, after the upload, the send_url_to_service callback always logged the cached file path - something like 'uploads/tmp/1473900000-123-0001-0123/file.mp4' instead of 'uploads/user_profiles/video/1/file.mp4'. I tried to write a method to form the URL from the actual file path, but it did not work because the file wasn't there yet.
So, the question is, how do you obtain a final file URL in a situation like this?
P. S. Please note, this is a self-answered question, I just wanted to share my experience.
The solution for me was to use the after_commit ..., on: :create callback instead of after_create:
class UserProfile < ApplicationRecord
mount_uploader :video, VideoUploader
after_commit :send_url_to_service, on: :create
private
# Just logs the URL
def send_url_to_service
Rails.logger.info video.url
end
end
The answer is pretty obvious, though I wasted a long time wandering around it. Explanation is simple: after_commit callback fires only after all information is successfully persisted. In my case the file was not yet persisted to storage directory (on after_create stage) - that is why I got the temporary file url instead of the actual one. Hope this helps somebody and saves their time.
Using Rails 7.0.4 and carrierwave 2.0, using after_commit :foo, on: :create callback still returned tmp paths for me.
What worked was using Carrierwave callbacks, specifically after :store, :foo in the uploader class itself. This ensures that the call to fetch the file's URL only occurs after your file is uploaded to a cloud storage.
# app/uploaders/video_uploader.rb
class VideoUploader < CarrierWave::Uploader::Base
after :store, :foo
def foo(_new_file)
Rails.logger.info model.video.url
end
end
Notes:
The foo method requires an argument to work. In this case, an unused _new_file argument.
The model variable points to the instance object the uploader is attached to.
I'm trying to do somethinkg I've done hundered of times, and today it will not work... And I have no idea why.
I try to create an object with an attached file, and when object is created, do some treatment on the attached file. Or, the file is not present at the specified place !
I have an other model in the same application, doing Imagemagick treatments, and files are in the right directories. Here is no image treatment.
Here is some code
class Test
has_attached_file :file
after_create :do_some_stuff
def do_some_stuff
raise "File not found" if !File.exists?(self.file.path)
end
end
I obtain my File not found exception.
I tried to see if PostProcessing were executed, like this :
class Test
has_attached_file :file
after_post_process :print_log
after_create :print_created
def print_log
$stderr.puts "Processed."
end
def print_created
$stderr.puts "Created."
end
end
The "Processed" is correctly printed before the after_create method...
Do you have any idea ?
My configuration :
Rails 2.3.18
Ruby 1.8.6
Rspec 1.3.2
Paperclip 2.3.0
OK, my fault.
I read the code, and Paperclip call the save method with an after_save. And after_save is called after after_create.
I am using carrierwave and mongoid on a rails 3 application and am having an issue with an after_save callback. Consider the following
class Video
include Mongoid::Document
field :name
mount_uploader :file, VideoUploader
after_create :enqueue_for_encoding
protected
def enqueue_for_encoding
// point your encoding service to where it expects the permanent file to reside
// in my case on s3
end
end
My issue is that in my enqueue_for_encoding method, file.url points to the local tmp directory not the s3 directory.
How do I get my enqueue_for_encoding method to be called when file.url points to s3?
Thanks!
Jonathan
Check out carrierwave's howto page on Callbacks
https://github.com/jnicklas/carrierwave/wiki/How-to%3A-use-callbacks
It worked for me
Okay, I figured it out. To took a bit of hacking. So currently carrierwave does not expose an after_create hook, all of it persisting and processing happens in the after_save callback. Here is the code I used to work around it:
# Video.rb
mount_uploader :file, VideoUploader
# overwrite the file setting to flag the model that we are creating rather than saving
def file=(obj)
#new_file = true
super(obj)
end
# chain the store_file! method to enqueue_for_encoding after storing the file AND
# if the file is new
alias_method :orig_store_file!, :store_file!
def store_file!
orig_store_file!
if #new_file #means dirty
#new_file = false
enqueue_for_encoding
end
true
end
UPDATE
Woops -- that didn't work. It almost did -- the url is correct, but it is being fired permanently. Meaning the file is still in process of being loaded, and is not fully stored when enqueue_for_encoding is called
It is possible to set your enqueue_for_encoding callback on the uploader itself. But I prefer to do it this way:
class Video
# mount the uploader first:
mount_uploader :file, VideoUploader
# then add the callback:
after_save :enqueue_for_encoding, on: :create
end
You could try removing your after_create callback in the model and add the following to your uploader:
# video_uploader.rb
process :encode
def encode
model.enqueue_for_encoding
end
The process callbacks are called after the file is saved (I think) which should allow you to hook in once your file is up on S3.