Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Carrierwave: file hash and model id in filename/store_dir

Tags:

carrierwave

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?
like image 255
severin Avatar asked Jan 12 '23 13:01

severin


2 Answers

  • Including the id in the filename on create may not be possible, since the filename is stored in the database but the id isn't available yet. An (admittedly rather extreme) workaround would be to use a temporary value on create, and then after_commit on: :create, move the file and change the name in the database. It may be possible to optimize this with an after_create, but I'll leave that up to you. (This is where carrierwave actually uploads the file.)

  • Including file attributes directly within the store_dir isn't possible, since store_dir is used to calculate the urlurl would require knowing the sha1, which requires having access to the file, which requires knowing the url, etc. The workaround is pretty obvious: cache the attributes in which you're interested (in this case the sha1) in the model's database record, and use that in the store_dir.

  • The simpler variant on the id-in-filename approach is to use some other value, such as a uuid, and store that value in the database. There are some notes on that here.

like image 51
Taavo Avatar answered Jan 19 '23 11:01

Taavo


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
like image 32
severin Avatar answered Jan 19 '23 12:01

severin