I'm using carrierwave in a Rails 4 project with the file storage for development and testing and the fog storage (for storing on Amazon S3) for production.
I would like to save my files with paths like this:
/model_class_name/part_of_hash/another_part_of_hash/hash-model_id.file_extension
(example: /images/12/34/1234567-89.png
where 1234567 is the SHA1 hash of the file content and 89 is the id of the associated image model in the database).
What I tried so far is this:
class MyUploader < CarrierWave::Uploader::Base
def store_dir
"#{model.class.name.underscore}/#{sha1_for(file)[0..1]}/#{sha1_for(file)[2..3]}"
end
def filename
"#{sha1_for(file)}-#{model.id}.#{file.extension}" if original_file
end
private
def sha1_for file
Digest::SHA1.hexdigest file.read
end
end
This does not work because:
model.id
is not available when filename
is called
file
is not always available when store_dir
is called
So, coming to my questions:
- is it possible to use model ids/attributes within
filename
? This link says it should not be done; is there a way to work around it?
- is it possible to use file content/attributes within
store_dir
? I found no documentation on this but my experiences so far say "no" (see above).
- how would you implement file/directory naming to get something as close as possible to what I outlined in the beginning?
Taavo's answer strictly answers my questions. But I want to quickly detail the final solution I implemented since it may helps someone else, too...
I gave up the idea to use the model id in the filename and replaced it with a random string instead (the whole idea of the model id in the filename was to just ensure that 2 identical files associated with different models end up with different file names; and some random characters ensure that as well).
So I ended up with filenames like filehash-randomstring.extension
.
Since carrierwave saves the filename in the model, I realized that I already have the file hash available in the model (in the form of the first part of the filename). So I just used this within store_dir
to generate a path in the form model_class_name/file_hash_part/another_file_hash_part
.
My final implementation looks like this:
class MyUploader < Carrierwave::Uploader::Base
def store_dir
# file name saved on the model. It is in the form:
# filehash-randomstring.extension, see below...
filename = model.send(:"#{mounted_as}_identifier")
"#{model.class.name.underscore}/#{filename[0..1]}/#{filename[3..4]}"
end
def filename
if original_filename
existing = model.send(:"#{mounted_as}_identifier")
# reuse the existing file name from the model if present.
# otherwise, generate a new one (and cache it in an instance variable)
@generated_filename ||= if existing.present?
existing
else
"#{sha1_for file}-#{SecureRandom.hex(4)}.#{file.extension}"
end
end
end
private
def sha1_for file
Digest::SHA1.hexdigest file.read
end
end
I came across the same problem recently, where the model.id
was not available yet when storing the filename in the DB, upon creation of the uploader
record. I found this workaround. I am not sure if it is respecting RESTful principles, I am open to suggestions.
I modified the controller, so that right after the creation of the image, an update_attributes
is executed, so that the filename including the now existing model.id
value is saved in the DB.
def create
@uploader = Uploader.new(uploader_params)
if @uploader.save
if @uploader.update_attributes(uploader_params)
render json: @uploader, status: :created
end
else
render json: @uploader.errors, status: :unprocessable_entity
end
end