Rails Active Storage set folder to store files - ruby-on-rails

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.

Related

Uploading a file in Rails

I'm new to rails, and I'm writing a RESTful website using the CRUD technique. So far I have created three pages, all of which allow the user to create, edit, and delete a row from the database. However, my fourth page will need to include an upload file form, but a) I don't know how the filesystem works with Rails thus I don't know where files should be stored. The file would be around 100kb and couldn't be stored in temporary storage because it will be constantly downloaded. And b) I don't know how to write to a file.
It would be great if you could tell me how to do what I mentioned above - create an upload input on an input form, and to then write the file to a filepath in a separate directory.
Update 2018
While everything written below still holds true, Rails 5.2 now includes active_storage, which allows stuff like uploading directly to S3 (or other cloud storage services), image transformations, etc. You should check out the rails guide and decide for yourself what fits your needs.
While there are plenty of gems that solve file uploading pretty nicely (see https://www.ruby-toolbox.com/categories/rails_file_uploads for a list), rails has built-in helpers which make it easy to roll your own solution.
Use the file_field-form helper in your form, and rails handles the uploading for you:
<%= form_for #person do |f| %>
<%= f.file_field :picture %>
<% end %>
You will have access in the controller to the uploaded file as follows:
uploaded_io = params[:person][:picture]
File.open(Rails.root.join('public', 'uploads', uploaded_io.original_filename), 'wb') do |file|
file.write(uploaded_io.read)
end
It depends on the complexity of what you want to achieve, but this is totally sufficient for easy file uploading/downloading tasks. This example is taken from the rails guides, you can go there for further information: http://guides.rubyonrails.org/form_helpers.html#uploading-files
Sept 2018
For anyone checking this question recently, Rails 5.2+ now has ActiveStorage by default & I highly recommend checking it out.
Since it is part of the core Rails 5.2+ now, it is very well integrated & has excellent capabilities out of the box (still all other well-known gems like Carrierwave, Shrine, paperclip,... are great but this one offers very good features that we can consider for any new Rails project)
Paperclip team deprecated the gem in favor of the Rails ActiveStorage.
Here is the github page for the ActiveStorage & plenty of resources are available everywhere
Also I found this video to be very helpful to understand the features of Activestorage
There is a nice gem especially for uploading files : carrierwave. If the wiki does not help , there is a nice RailsCast about the best way to use it . Summarizing , there is a field type file in Rails forms , which invokes the file upload dialog. You can use it , but the 'magic' is done by carrierwave gem .
I don't know what do you mean with "how to write to a file" , but I hope this is a nice start.
Okay. If you do not want to store the file in database and store in the application, like assets (custom folder), you can define non-db instance variable defined by attr_accessor: document and use form_for - f.file_field to get the file,
In controller,
#person = Person.new(person_params)
Here person_params return whitelisted params[:person] (define yourself)
Save file as,
dir = "#{Rails.root}/app/assets/custom_path"
FileUtils.mkdir(dir) unless File.directory? dir
document = #person.document.document_file_name # check document uploaded params
File.copy_stream(#font.document, "#{dir}/#{document}")
Note, Add this path in .gitignore & if you want to use this file again add this path asset_pathan of application by application.rb
Whenever form read file field, it get store in tmp folder, later you can store at your place, I gave example to store at assets
note: Storing files like this will increase the size of the application, better to store in the database using paperclip.
In your intiallizer/carrierwave.rb
if Rails.env.development? || Rails.env.test?
config.storage = :file
config.root = "#{Rails.root}/public"
if Rails.env.test?
CarrierWave.configure do |config|
config.storage = :file
config.enable_processing = false
end
end
end
use this to store in a file while running on local

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

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?

How to make carrierwave delete the file when destroying a record?

I'm using the carrierwave gem to upload files.
I have built a system for users to flag images as inappropriate and for admins to remove the images. From what I can tell, calling destroy on the image will only remove the path name from the table.
Is there a way to have carrierwave actually remove the file itself? Or should rails automatically remove the file when I destroy the image path?
Like #mu_is_too_short said, you can use File#delete.
Here's a code snippet you could use as a helper with a little tweaking in your rails app.
def remove_file(file)
File.delete(file)
end
or if you just have the filename stored in file
def remove_file(file)
File.delete("./path/to/#{file}")
end
Not sure what CarrierWave offers for this, but you could use FileUtils in the Ruby standard library with an ActiveRecord callback.
For instance,
require 'FileUtils'
before_destroy :remove_hard_image
def remove_hard_image
FileUtils.rm(path_to_image)
end
Sidenote: This code is from memory.
If one wants to delete a file but does not want to specify the full filename you can use the below.
Can also be used to delete many files or all files in a directory with a specific extension...
file = Rails.root.join("tmp", "foo*")
or
file = Rails.root.join("tmp", ".pdf")
files = Dir.glob(file) #will build an array of the full filepath & filename(s)
files.each do |f|
File.delete(f)
end

Resources