'Attach ActiveStorage blob with a different filename
I am looking for the most elegant way of attaching/duplicating an ActiveStorage blob with a different name.
My current best solution is using the IO interface and downloading and re-uploading the file which seems rather inefficient.
url = Rails.application.routes.url_helpers.rails_blob_url( contract.document )
attachements.push([
io: open( path_url ),
filename: "New contract filename"
])
I know you can do this, which is only touching the database:
contract.attach( contract.document.blob )
But I need to change the filename.
The ActiveStorage documentation seems to be spotty on this subject and I couldn't really find what I was looking for.
Solution 1:[1]
The constraints of Active Storage mean that you definitely will need to duplicate the blob. Within Active Storage, the blob key is indexed uniquely, so it cannot be referenced twice by different filenames.
If your storage service allows you to duplicate the blob efficiently in situ, then the enabling method on the Rails side is create_before_direct_upload!. Despite the somewhat misleading name, this method is specifically documented to provide a new (but unattached) blob record, in expectation that the content itself will be uploaded outside of the application. The blob record is then attached in a subsequent operation.
That is how Direct Upload works, and we can hook into the same two-phase mechanism; in this special case, the "upload" will instead be a service-specific duplication.
The means to duplicate in-situ vary according to the storage service, but for example:
- S3's
CopyObjectcall, - The AWS S3 Ruby SDK's
copy_tomethod, - A Unix file system's hard link.
An illustrative chunk of demonstration code therefore might be:
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
to be used thus:
current_blob = mymodel.contract.blob
new_blob = blob_clone(current_blob, filename: 'newfilename.pdf')
new_model.contract.attach(new_blob)
although I recommend refactoring this example according to your application structure and preferences.
Note on the code
In the illustration above, the case service.class.name is rather unlovely, and is only written so to keep the demo code self-contained. The problem being that Rails doesn't even load the class constant for an unused backend storage service. If you are not using diverse storage services across environments, then the case-statement may be unnecessary, and if this were fully realised as a gem, or refactored into an initializer, I'd probably finesse it to be monkey-patching/refining the service classes themselves.
Solution 2:[2]
If you want to point to the same blob but with a different name, maybe you can try to add the needed record in the database.
This is the schema for the active storage attachments table:
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
You should be able to add a new record with almost the same values and a different name.
Maybe something like:
my_object.my_attachments.create(blob_id: my_object.document_blob.id, name: 'new name')
Solution 3:[3]
ActiveStorage creates two tables active_storage_blobs which includes the uploaded files information like (filename) and active_storage_attachments which is a join table between a model record and a 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)
To change the filename of a blob supposing that
contract model has_many_attachments :documents
contract_object.documents.first.blob.update(filename: 'new file name')
To duplicate a blob you can pass a blob object to attach, then rename the filename
Solution 4:[4]
The StringIO can convert downloaded blob to IO then you can use any name you want.
attachements.push([
io: StringIO.new( contract.document.download ),
filename: "New contract filename"
])
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | inopinatus |
| Solution 2 | arieljuod |
| Solution 3 | |
| Solution 4 | Manoj Nayak |
