【问题标题】:Carrierwave: file hash and model id in filename/store_dirCarrierwave:文件名/store_dir 中的文件哈希和模型 ID
【发布时间】:2025-12-12 19:35:02
【问题描述】:

我在 Rails 4 项目中使用carrierwave,文件存储用于开发和测试,雾存储(用于存储在Amazon S3上)用于生产。

我想用这样的路径保存我的文件:

/model_class_name/part_of_hash/another_part_of_hash/hash-model_id.file_extension

(例如:/images/12/34/1234567-89.png 其中 1234567 是文件内容的 SHA1 哈希值,89 是数据库中相关图像模型的 id)。

到目前为止我尝试的是这样的:

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

这不起作用,因为:

  • 调用 filename 时,model.id 不可用
  • 调用 store_dir 时,file 并不总是可用

那么,我的问题

  • 是否可以在filename 中使用模型ID/属性? This link 表示不应该这样做;有没有办法解决它?
  • 是否可以在store_dir 中使用文件内容/属性?我没有找到这方面的文档,但到目前为止我的经验说“不”(见上文)。
  • 您将如何实现文件/目录命名以尽可能接近我在开头概述的内容?

【问题讨论】:

    标签: carrierwave


    【解决方案1】:
    • 在创建时可能无法在文件名中包含 id,因为文件名存储在数据库中,但 id 尚不可用。一个(诚然相当极端的)解决方法是在创建时使用临时值,然后after_commit on: :create,移动文件并更改数据库中的名称。可以使用after_create 对此进行优化,但我将由您决定。 (This 是carrierwave 实际上传文件的位置。)

    • store_dir直接包含文件属性是不可能的,因为store_dir 用于计算urlurl 需要知道 sha1,这需要访问文件,这需要知道 url 等。解决方法非常明显:在模型的数据库记录中缓存您感兴趣的属性(在本例中为 sha1),并在 @987654329 中使用它@。

    • id-in-filename 方法的更简单的变体是使用一些其他值,例如 uuid,并将该值存储在数据库中。有一些关于here的注释。

    【讨论】:

    • 我接受了你的回答,因为它回答了我问题中的每一点,谢谢。如果您对我的最终解决方案感兴趣,请查看我自己的详细答案...
    【解决方案2】:

    Taavo 的回答严格回答了我的问题。但我想快速详细说明我实施的最终解决方案,因为它也可能对其他人有所帮助......

    我放弃了在文件名中使用模型 id 并用随机字符串替换它的想法(文件名中模型 id 的整个想法是确保与不同模型关联的 2 个相同文件最终以不同的文件名;一些随机字符也确保了这一点)。

    所以我最终得到了像 filehash-randomstring.extension 这样的文件名。

    由于carrierwave将文件名保存在模型中,我意识到我已经在模型中拥有了可用的文件哈希(以文件名的第一部分的形式)。所以我只是在store_dir 中使用它来生成model_class_name/file_hash_part/another_file_hash_part 形式的路径。

    我的最终实现如下所示:

    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
    

    【讨论】:

      【解决方案3】:

      我最近遇到了同样的问题,在创建uploader 记录时,将文件名存储在数据库中时,model.id 尚不可用。我找到了这个解决方法。我不确定它是否尊重 RESTful 原则,我愿意接受建议。

      我修改了控制器,以便在创建图像后立即执行update_attributes,以便将包含现在存在的model.id 值的文件名保存在数据库中。

        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
      

      【讨论】: