Active Storage - Bug workaround in initializer not working - ruby-on-rails

I'm trying to work around a known issue in Active Storage where the MIME type of a stored file is incorrectly set, without the ability to override it.
https://github.com/rails/rails/issues/32632
This has been addressed in the master branch of Rails, however it doesn't appear to be released yet (project is currently using 5.2.0). Therefor I'm trying to work around the issue using one of the comments provided in the issue:
Within a new initializer (\config\initializers\active_record_fix.rb):
Rails.application.config.after_initialize do
# Defeat the ActiveStorage MIME type detection.
ActiveStorage::Blob.class_eval do
def extract_content_type(io)
return content_type if content_type
Marcel::MimeType.for io, name: filename.to_s, declared_type: content_type
end
end
end
I'm processing and storing a zip file within a background job using delayed_jobs. The initializer doesn't appear to be getting called. I have restarted the server. I'm running the project locally using heroku local to process background jobs.
Here is the code storing the file:
file.attach(io: File.open(temp_zip_path), filename: 'Download.zip', content_type: 'application/zip')
Any ideas why the code above is not working? Active Storage likes to somewhat randomly decide this ZIP file is a PDF and save the content type as application\pdf. Unrelated, attempting to manually override the content_type after attaching doesn't work:
file.content_type = 'application/zip'
file.save # No errors, but record doesn't update the content_type

Try with Rails.application.config.to_prepare in place of after_initialize initialization event.
more info :
https://guides.rubyonrails.org/configuring.html#initialization-events
https://guides.rubyonrails.org/v5.2.0/initialization.html

Related

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]})

Carrierware: store file in directories accordint to created_at date

I am using carrierwave to handle my uploads. I have specified the store_dir following way:
def store_dir
"uploads/#{Time.now.year}/#{Time.now.month}/#{Time.now.day}"
end
Uploading files work like a charm - each time I upload a file it ends up in directory where it should end; i.e. "today's directory".
When I try to download the file, carrierwave is constructing the download path dynamically based on store_dir options. So lets say a file which was uploaded on 1.12.2012 is available on the following path on fliesystem:
/uploads/2012/12/01/file.ext
will be retrieved by carrierwave as:
/uploads/2012/12/12/file.ext
Which obviously leads to "Cannot read file" error.
I came with 2 different possible solutions:
Create a separate filed where I will be storing the actual filepath to the file upon it's creation and then will use this value to retrieve file.
Overload retrieve_from_store! method (which is part of carrierwave gem) and make it construct path based on created_at field from the file record than rather from store_dir.
I am inclining to the second possibility since it feels not that dirty. Yet both feel "not-rails-way". Which one will be better to use and why? Or maybe carrierwave provides a way to solve this issue?
Totally guessing here but by looking at the docs I think something like this should work:
def store_dir
"uploads/#{model.created_at.year}/#{model.created_at.month}/#{model.created_at.day}"
end

When using paperclip on Rails3, the some characters( # and ~) get erased or altered from the file name when uploading

I'm not sure if this is a paperclip issue. Tried it on gitlab and the same thing happened.
I have a back end for an iOS app written in Rails, and when I upload an image file with the # character in the filename, it gets erased upon uploading, if I have a file named,
aaa#2x.jpg
it gets saved as
aaa2x.jpg
Also, ~ gets converted into a _.
This is a problem because iOS apps presume that retina supported images are named with the #2x prefix.
I can regex the file name post upload and change it in the database and rename the file, but that seems like an odd hack to do, anyone have any idea whats happening? How to have the file name saved properly to begin with?
According to this article: http://en.wikipedia.org/wiki/HFS_Plus, you should be able to use any character, including NUL in file names. But OS APIs may limit some characters for legacy reasons.
It can be server or client issue, try to debug your application and check file name provided in request.request_parameters it should contain valid file name.
If you going to use uploaded files in URLs you should transliterate them before upload, this also resolve your problem. To do this you can use this extension:
module TransliteratePaperclip
def transliterate_file_name(paperclip_file)
paperclip_file=[paperclip_file] unless paperclip_file.is_a?(Enumerable)
paperclip_file.each do |file|
filename=read_attribute("#{file}_file_name")
if filename.present?
extension = File.extname(filename).gsub(/^\.+/, '')
filename = filename.gsub(/\.#{extension}$/, '')
self.send(file).instance_write(:file_name, "#{filename.parameterize}.#{extension.parameterize}")
end
end
end
end
# include the extension
ActiveRecord::Base.send(:include, TransliteratePaperclip)
put this code in /config/initializers/paperclip_transliterate.rb and in your paperclip model:
before_post_process { |c| transliterate_file_name(:file) }
where :file is attribute defined by has_attached_file.

How do you access the raw content of a file uploaded with Paperclip / Ruby on Rails?

I'm using Paperclip / S3 for file uploading. I upload text-like files (not .txt, but they are essentially a .txt). In a show controller, I want to be able to get the contents of the uploaded file, but don't see contents as one of its attributes. What can I do here?
attachment_file_name: "test.md", attachment_content_type: "application/octet-stream", attachment_file_size: 58, attachment_updated_at: "2011-06-22 01:01:40"
PS - Seems like all the Paperclip tutorials are about images, not text files.
In Paperclip 3.0.1 you could just use the io_adapter which doesn't require writing an extra file to (and removing from) the local file system.
Paperclip.io_adapters.for(attachment.file).read
#jon-m answer needs to be updated to reflect the latest changes to paperclip, in order for this to work needs to change to something like:
class Document
has_attached_file :revision
def revision_contents(path = 'tmp/tmp.any')
revision.copy_to_local_file :original, path
File.open(path).read
end
end
A bit convoluted as #jwadsack mentioned using Paperclip.io_adapters.for method accomplishes the same and seems like a better, cleaner way to do this IMHO.
To access the file you can use the path method:
csv_file.path
http://rdoc.info/gems/paperclip/Paperclip/Attachment#path-instance_method
This can be used along with for example the CSV reader.
Here's how I access the raw contents of my attachment:
class Document
has_attached_file :revision
def revision_contents
revision.copy_to_local_file.read
end
end
Please note, I've omitted my paperclip configuration options and any sort of error handling.
You would need to load the contents of the file (using Rubys File.open) into a variable before you show it. This may be an expensive operation if your app gets lots of use, so it may be worthwhile reading the contents of the file and putting it into a text column in your database after uploading it.
Attachment already inherits from IOStream. http://rdoc.info/github/thoughtbot/paperclip/master/Paperclip/Attachment
So it should just be "#{attachment}" or <% RDiscount.new(attachment).to_html %> or send_data(attachment). However you wanted to display the data.
This is a method I used for upload from paperclip to active storage and should provide some guidance on temporarily working with a file in memory. Note: This should only be used for relatively small files.
Written for gem paperclip 6.1.0
Where I have a simple model
class Post
has_attached_file :image
end
Working with a temp file in ruby so we do not have to worry about closing the file
Tempfile.create do |tmp_file|
post.image.copy_to_local_file(nil, tmp_file.path)
post.image_temp.attach(
io: tmp_file,
filename: post.image_file_name,
content_type: post.image_content_type
)
end

Resources