使用不同的文件名附加 ActiveStorage blob

Posted

技术标签:

【中文标题】使用不同的文件名附加 ActiveStorage blob【英文标题】:Attach ActiveStorage blob with a different filename 【发布时间】:2021-05-12 03:54:47 【问题描述】:

我正在寻找最优雅的方式来附加/复制具有不同名称的ActiveStorage blob

我目前最好的解决方案是使用 IO 接口并下载并重新上传文件,这似乎效率很低。

url = Rails.application.routes.url_helpers.rails_blob_url( contract.document )
attachements.push([
  io: open( path_url ),
  filename: "New contract filename"
])

我知道你可以做到这一点,这只是触及数据库:

contract.attach( contract.document.blob )

但我需要更改文件名。

ActiveStorage 文档在这个主题上似乎参差不齐,我真的找不到我要找的东西。

【问题讨论】:

可能符号链接可以帮助更改没有内容的文件名 ln -s old.png new.png ,然后只是将新记录以新名称放入 sql 数据库中 @itsnikolay Blob 存储在远程服务器上。在这种情况下,我使用的是类似 S3 的服务。 【参考方案1】:

Active Storage 的限制意味着您肯定需要复制 blob。在 Active Storage 中,blob 键是唯一索引的,因此不同的文件名不能引用它两次。

如果您的存储服务允许您在原地有效地复制 blob,那么 Rails 端的启用方法是create_before_direct_upload!。尽管名称有些误导,但此方法被专门记录为提供一个新的(但未附加的)blob 记录,期望内容本身将上传到应用程序之外。然后在后续操作中附加 Blob 记录。

这就是直接上传的工作原理,我们可以挂钩到相同的两阶段机制;在这种特殊情况下,“上传”将改为特定于服务的复制。

原位复制的方式因存储服务而异,但例如:

S3 的CopyObject 调用, AWS S3 Ruby SDK 的 copy_to 方法, Unix 文件系统的hard link。

因此,一个说明性的演示代码块可能是:

def blob_clone(current_blob, **options)
  blob_parameters =  
    filename: current_blob.filename,
    byte_size: current_blob.byte_size,
    checksum: current_blob.checksum,
    content_type: current_blob.content_type,
    metadata: current_blob.metadata,
    **options
  

  service = current_blob.service
  new_blob = ActiveStorage::Blob.create_before_direct_upload!(**blob_parameters)

  case service.class.name
  when 'ActiveStorage::Service::S3Service'
    bucket = service.bucket
    current_object = bucket.object(current_blob.key)
    current_object.copy_to(bucket.object(new_blob.key))
  when 'ActiveStorage::Service::DiskService'
    current_path = service.path_for(current_blob.key)
    new_path = service.path_for(new_blob.key)
    FileUtils.mkdir_p(File.dirname(new_path))
    FileUtils.ln(current_path, new_path)
  else
    raise ArgumentError, "unknown blob storage service #service.class.name"
  end

  new_blob
end

这样使用:

current_blob = mymodel.contract.blob
new_blob = blob_clone(current_blob, filename: 'newfilename.pdf')
new_model.contract.attach(new_blob)

虽然我建议根据您的应用程序结构和偏好重构此示例。

代码注释

在上面的插图中,case service.class.name 相当不可爱,只是为了保持演示代码自包含而编写。问题在于 Rails 甚至没有为未使用的后端存储服务加载类常量。如果您没有跨环境使用不同的存储服务,那么 case 语句可能是不必要的,如果这被完全实现为一个 gem,或者重构为一个初始化程序,我可能会巧妙地将它作为猴子修补/改进服务类本身。

【讨论】:

【参考方案2】:

如果您想指向同一个 blob 但名称不同,也许您可​​以尝试在数据库中添加所需的记录。

这是活动存储附件表的架构:

  create_table "active_storage_attachments", force: :cascade do |t|
    t.string "name", null: false
    t.string "record_type", null: false
    t.bigint "record_id", null: false
    t.bigint "blob_id", null: false
    t.datetime "created_at", null: false
    t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id"
    t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true
  end

您应该能够添加具有几乎相同值和不同名称的新记录。

可能是这样的:

my_object.my_attachments.create(blob_id: my_object.document_blob.id, name: 'new name')

【讨论】:

active_storage_attachments 中的name 不是指文件名,而是指您在其中使用的附件的名称:has_one_attached :avatar -> avatar。不过建议很好。当我看到那张桌子时,这也是我的第一个想法。【参考方案3】:

ActiveStorage 创建两个表active_storage_blobs,其中包括上传的文件信息,例如(文件名)和active_storage_attachments,它是模型记录和blob 之间的连接表。

# rails console
=> ActiveStorage::Attachment
(id: integer, name: string, record_type: string, record_id: integer, blob_id: integer, created_at: datetime)
=> ActiveRecord::Blob
(id: integer, key: string, filename: string, content_type: string, metadata: text, byte_size: integer, checksum: string, created_at: datetime)

假设改变 blob 的文件名

contract型号has_many_attachments :documents

contract_object.documents.first.blob.update(filename: 'new file name')

要复制 blob,您可以将 blob 对象传递给 attach,然后重命名文件名

【讨论】:

以上是关于使用不同的文件名附加 ActiveStorage blob的主要内容,如果未能解决你的问题,请参考以下文章

在 factorybot 中附加 ActiveStorage 文件

Rails ActiveStorage 附加到现有 S3 文件

使用 Rails 5.2 ActiveStorage 创建和保存 pdf 并稍后附加到电子邮件

使用 ActiveStorage 将 Cloudinary 图像附加到 Rails 模型

如何将文档附加到电子邮件 ActiveStorage 和 Cloudinary?

ActiveStorage::IntegrityError 何时引发?