Carrierwave: "recreate_versions!" strange behaviour - ruby-on-rails

I have an avatar uploader which has multiple versions. Some of them are generated synchronously on upload, others are generated conditionally on request (following this approach):
user.avatar.is_processing_delayed = true
user.avatar.recreate_versions!(:png_x60y80)
I put this code in a controller and serve the generated version from there once, after that it's served by nginx (since it's already generated). This does actually create the file and if I call user.avatar.png_x60x80.file on the same uploader instance right after recreate_versions! I get a file object. However, if I call it on another instance of the same avatar (e.g. User.find(user.id).avatar.png_x60x80.file) I get nil. This won't be a issue, but I believe that it causes the following problem: when I remove the user's avatar only versions that were created synchronously are deleted. Somehow recreare_versions! does not persist data about recreated versions. Is there something that I'm missing? I would also like to remove all versions that were created on request when avatar is updated, so that nginx wouldn't serve previously generated versions of an old avatar, but it's also problematic due to this problem.
Carrierwave version: 1.0.

Eventually I settled for the following workaround. Not perfect, but works as expected.
mount_uploader :avatar, AvatarUploader
def remove_avatar!(*args)
remove_all_avatar_versions
super(*args)
# Somehow wrapping the "remove_avatar!" method changes its behavior:
# model attribute is not updated and we have to update it manually.
write_attribute(:avatar, nil)
end
def avatar=(*args)
remove_all_avatar_versions
super(*args)
end
private
def remove_all_avatar_versions
return unless avatar?
avatar.versions.each_key do |v|
# You have to implement the avatar_path method.
path = avatar_path(v)
File.delete(path) if File.exist?(path)
end
end

Related

How to upload while resizing the original image itself in Shrine

I use Shrine in a Ruby on Rails application to create the process of resizing and uploading images to storage.
My current code is:
image_uploader.rb
require "image_processing/mini_magick"
class ImageUploader < Shrine
plugin :derivatives
Attacher.derivatives_processor do |original|
magick = ImageProcessing::MiniMagick.source(original)
{
resized: magick.resize_to_limit!(120, 120)
}
end
end
user.rb
class User < ApplicationRecord
include ImageUploader::Attachment(:image)
before_save :image_resize
def image_resize
self.image_derivatives!
end
end
I implemented it while reading the official documentation, but this is not desirable in two ways.
Requires trigger in model code. Can it be completed with only image_uploader.rb?
Access to images generated with this code requires a "resized" prefix(e.g. #user.image(:resized).url), and the original image will also remain in storage. I want to process the original image itself.
Is there a way to upload while solving these two issues?
You can add the following patch, which will trigger derivatives creation as part of promoting cached file to permanent storage:
# put this in your initializer
class Shrine::Attacher
def promote(*)
create_derivatives
super
end
end
You can just override the model method that retrieves the attached file to return the resized version. You can use the included plugin to do this for all models using this uploader:
class ImageUploader < Shrine
# ...
plugin :included do |name|
define_method(name) { super(:resized) }
end
end
As for the second question: It will still keep the original file in the storage, but just return the resized version instead by default. It's generally better to do this in a view decorator instead.
You always want to keep the original file in the storage, because you never know when you'll need to reprocess it. It can happen that you find your current resizing logic not to be ideal for certain filetypes and sizes, in which case you'll need to regenerate the resized version for previous attachments. And you wouldn't be able to do that if you didn't have the original file anymore.

Rails Active Storage set folder to store files

I'm using Active Storage to store files in a Rails 5.2 project. I've got files saving to S3, but they save with random string filenames and directly to the root of the bucket. I don't mind the random filenames (I actually prefer it for my use case) but would like to keep different attachments organized into folders in the bucket.
My model uses has_one_attached :file. I would like to specify to store all these files within a /downloads folder within S3 for example. I can't find any documentation regarding how to set these paths.
Something like has_one_attached :file, folder: '/downloads' would be great if that's possible...
The ultimate solution is to add an initializer. You can add a prefix based on an environment variable or your Rails.env :
# config/initializer/active_storage.rb
Rails.configuration.to_prepare do
ActiveStorage::Blob.class_eval do
before_create :generate_key_with_prefix
def generate_key_with_prefix
self.key = if prefix
File.join prefix, self.class.generate_unique_secure_token
else
self.class.generate_unique_secure_token
end
end
def prefix
ENV["SPACES_ROOT_FOLDER"]
end
end
end
It works perfectly with this. Other people suggest using Shrine.
Credit to for this great workaround : https://dev.to/drnic/how-to-isolate-your-rails-blobs-in-subfolders-1n0c
As of now ActiveStorage doesn't support that kind of functionality. Refer to this link. has_one_attached just accepts name and dependent.
Also in one of the GitHub issues, the maintainer clearly mentioned that they have clearly no idea of implementing something like this.
The workaround that I can imagine is, uploading the file from the front-end and then write a service that updates key field in active_storage_blob_statement
There is no official way to change the path which is determined by ActiveStorage::Blob#key and the source code is:
def key
self[:key] ||= self.class.generate_unique_secure_token
end
And ActieStorage::Blog.generate_unique_secure_token is
def generate_unique_secure_token
SecureRandom.base36(28)
end
So a workaround is to override the key method like the following:
# config/initializers/active_storage.rb
ActiveSupport.on_load(:active_storage_blob) do
def key
self[:key] ||= "my_folder/#{self.class.generate_unique_secure_token}"
end
end
Don't worry, this will not affect existing files. But you must be careful ActiveStorage is very new stuff, its source code is variant. When upgrading Rails version, remind yourself to take look whether this patch causes something wrong.
You can read ActiveStorage source code from here: https://github.com/rails/rails/tree/master/activestorage
Solution using Cloudinary service
If you're using Cloudinary you can set the folder on storage.yml:
cloudinary:
service: Cloudinary
folder: <%= Rails.env %>
With that, Cloudinary will automatically create folders based on your Rails env:
This is a long due issue with Active Storage that seems to have been worked around by the Cloudinary team. Thanks for the amazing work ❤️
# config/initializers/active_storage.rb
ActiveSupport.on_load(:active_storage_blob) do
def key
sql_find_order_id = "select * from active_storage_attachments where blob_id = #{self.id}"
active_storage_attachment = ActiveRecord::Base.connection.select_one(sql_find_order_id)
# this variable record_id contains the id of object association in has_one_attached
record_id = active_storage_attachment['record_id']
self[:key] = "my_folder/#{self.class.generate_unique_secure_token}"
self.save
self[:key]
end
end
Active Storage by default doesn't contain a path/folder feature but you can override the function by
model.file.attach(key: "downloads/filename", io: File.open(file), content_type: file.content_type, filename: "#{file.original_filename}")
Doing this will store the key with the path where you want to store the file in the s3 subdirectory and upload it at the exact place where you want.

Carrierwave: Saving Original Filename not working

I am using the latest Carrierwave (master branch) in Rails 4.2.1. I am needing to save the original filename (before sanitization) of the uploaded file. I found a section in Carrierwave Wiki about how to do it (https://github.com/carrierwaveuploader/carrierwave/wiki/How-to:-Create-random-and-unique-filenames-for-all-versioned-files#saving-the-original-filename). The relevant piece of code that goes in the uploader is this (according to the wiki entry):
# in `class PhotoUploader`
before :cache, :save_original_filename
def save_original_filename(file)
model.original_filename ||= file.original_filename if file.respond_to?(:original_filename)
end
But it's not working for me. I have a column called 'original_filename' in my database table. And the filename is being saved in that column, but its not original filename, it's actually sanitized filename.
Any idea where to hook this method in order to save original filename?
Thanks.
Apparently, there are a lot of people, including myself, who have come across this issue. For instance, this issue (https://github.com/carrierwaveuploader/carrierwave/issues/1835) has an elaboration to why this doesn't work as expected.
A workaround I have come across is explicitly setting the original_filename using the file instance in incoming parameter.
Something like the following.
<Model>.create({file: params[:file], original_filename: params[:file]&.original_filename]})

What's the proper way to copy a carrierwave file from one record to another?

I need to copy a file from one carrier wave object to another. They are different tables and different types of uploaders.
I started with:
user.avatar = image.content
(where user and image are model instances, avatar and content are the carrierwave mounted uploaders) which worked sometimes. It seems to work all the time locally, with a file storage, but intermittent when using fog and s3.
In a mailing list post I found this code:
user.avatar = image.content.file
that again worked sometimes.
My working solution so far is:
require "open-uri"
begin
user.avatar = open(image.url)
rescue Errno::ENOENT => e
begin
user.avatar = open(image.path)
rescue Errno::ENOENT => e
# Ok, whatever.
end
end
which is not only ugly, but fails to pass the extension validation because the opening of a remote file doesn't maintain the extension (jpg, png, etc.).
Perhaps one way you can do it is to set a remote image URL as per the Carrierwave gem documentation?
user.remote_avatar_url = image.url
From solutions discussed here I created simple CopyCarrierwaveFile gem to do this
usage is something like this:
original_resource = User.last
new_resource = User.new
CopyCarrierwaveFile::CopyFileService.new(original_resource, new_resource, :avatar).set_file
new_resource.save
nev_resource.avatar.url # https://...image.jpg
Here's a (albeit hacky) solution to that doesn't require an HTTP request to fetch the image:
module UploadCopier
def self.copy(old, new)
new.instance_variable_set('#_mounters', nil)
old.class.uploaders.each do |column, uploader|
new.send("#{column}=", old.send(column))
end
end
end
old_user = User.last
new_user = User.new
UploadCopier.copy(old_user, new_user)
new_user.save
I needed to copy a reference from one model to another model and I was successfully able to do so by doing the following:
my_new_model.update_column('attachment', my_other_model.attributes["attachment"]);
In this scenario, I did not care to actually make a copy of the file, nor did I care that 2 records were now linked to the same file (my system never deletes or modifies files after uploaded).
This may be useful to anyone who wants to just copy the reference to a file from one model to another model using the same uploader.
You can do this by copying files.
store_path is a carrierwave method from Uploader class. It returns uploaded file's folder relative path.
this clone file method should be called after model record is saved.
If record not saved, store_path may return wrong path if you specify store_dir with model id in uploader.
def clone_carrierwave_file(column_name)
origin_files = Dir[File.join(Rails.root, 'public', original_record.send(column_name).store_path, '*')]
return if origin_files.blank?
new_file_folder = File.join(Rails.root, 'public', send(column_name).store_path)
FileUtils.mkdir new_file_folder if !Dir.exist? new_file_folder
FileUtils.cp(origin_files, new_file_folder)
end
Hope it works.
I just wanted to copy an avatar reference from one object to another, and what worked for me was:
objectB.avatar.retrieve_from_store!(objectA.avatar.identifier)
objectB.save

CarrierWave: Create the same, unique filename for all versioned files

Before I go into detail I'll get right to the point: has anyone figured out a way to get Carrierwave to save files with their names as a timestamp or any arbitrary string that is unique to each file?
By default Carrierwave saves each file and its alternate versions in its own directory (named after the model ID number). I'm not a fan of this because instead of one directory with 1,000, for the sake of using a large round number, files (in my case pictures) in it we get one directory with 1,000 subdirectories each with one or two files. Yuck.
Now, when you override your Uploader's store_dir method to be something like the following:
def store_dir
"uploads/#{model.class.to_s.underscore}/#{mounted_as}"
end
you end up with the exact behavior that I want. All the files (pictures) go into one big happy folder. No more subfolders that stick around when the object gets deleted.
There's only one problem. File collisions. If you upload delicious_cake.jpg twice the second one will overwrite the first even if they are two different pictures of delicious cake! That's clearly why the store_dir method has the extra /#{model.id} tacked on the end of the value it returns.
So, what to do? After reading around a bit I discovered that in the generated uploader file there is an apparent solution commented out.
# Override the filename of the uploaded files:
# Avoid using model.id or version_name here, see uploader/store.rb for details.
# def filename
# "something.jpg" if original_filename
# end
After a little bit of searching I found someone who had done the following
def filename
#name ||= "#{secure_token}.#{file.extension}" if original_filename
end
This got me thinking, why not just do this
def filename
#name ||= "#{(Time.now.to_i.to_s + Time.now.usec.to_s).ljust(16, '0')}#{File.extname(original_filename)}"
end
That's when things got horribly broken. The problem with this is that filename apparently gets called for each version of the file so we end up with file names like 1312335603175322.jpg and thumb_1312335603195323.jpg. Notice the slight difference? Each file name is based on the time when filename was called for that particular version. That won't do at all.
I next tired using model.created_at for the basis of the timestamp. Only one problem, that returns nil for the first version since it hasn't been put in the database yet.
After some further thinking I decided to try the following in my pictures controller.
def create
if params[:picture] and params[:picture][:image]
params[:picture][:image].original_filename = "#{(Time.now.to_i.to_s + Time.now.usec.to_s).ljust(16, '0')}#{File.extname(params[:picture][:image].original_filename)}"
end
...
This overrides the original_filename property before Carrierwave even gets to it making it be a timestamp. It does exactly what I want. The original version of the file ends up with a name like 1312332906940106.jpg and the thumbnail version (or any other version) ends up with a name like thumb_1312332906940106.jpg.
But, this seems like an awful hack. This should be part of the model, or better yet part of the uploader mounted onto the model.
So, my question is, is there a better way to achieve this? Did I miss something crucial with Carrierwave that makes this easy? Is there a not so obvious but cleaner way of going about this? Working code is good, but working code that doesn't smell bad is better.
You can do something like this in your uploader file, and it will also work for versioned files (i.e. if you have one image and then create 3 other thumbnail versions of the same file, they will all have the same name, just with size info appended onto the name):
# Set the filename for versioned files
def filename
random_token = Digest::SHA2.hexdigest("#{Time.now.utc}--#{model.id.to_s}").first(20)
ivar = "##{mounted_as}_secure_token"
token = model.instance_variable_get(ivar)
token ||= model.instance_variable_set(ivar, random_token)
"#{model.id}_#{token}.jpg" if original_filename
end
This will create a filename like this for example: 76_a9snx8b81js8kx81kx92.jpg where 76 is the model's id and the other bit is a random SHA hex.
Check also the solution from carrierwave wiki available now https://github.com/carrierwaveuploader/carrierwave/wiki/How-to:-Use-a-timestamp-in-file-names
You can include a timestamp in filenames overriding the filename as you can read in Carrierwave docs:
class PhotoUploader < CarrierWave::Uploader::Base
def filename
#name ||= "#{timestamp}-#{super}" if original_filename.present? and
super.present?
end
def timestamp
var = :"##{mounted_as}_timestamp"
model.instance_variable_get(var) or model.instance_variable_set(var, Time.now.to_i)
end
end
Don't forget to memorize the result in an instance variable or you might get different timestamps written to the database and the file store.
The solution is the same as described in the official documentation
But it always returns original_filename as nil. So just change it to instance variable as #original_filename.present?

Resources