Reputation: 1221
I have a model that is using ActiveStorage:
class Package < ApplicationRecord
has_one_attached :poster_image
end
How do I create a copy of a Package object that contains a duplicate of the initial poster_image file. Something along the lines of:
original = Package.first
copy = original.dup
copy.poster_image.attach = original.poster_image.copy_of_file
Upvotes: 40
Views: 13880
Reputation: 1958
One more approach where we don't have to download and re-upload the file and if your service
supports copy API (for example: AWS has copy-object),
original_blob = original.poster_image.blob
cloned_blob = ActiveStorage::Blob.create_before_direct_upload!(
filename: original_blob.filename.to_s,
byte_size: original_blob.byte_size,
checksum: original_blob.checksum,
content_type: original_blob.content_type
)
YourS3CopyService
.new(bucket: cloned_blob.service.bucket.name)
.copy(original_blob.key, cloned_blob.key)
class YourS3CopyService < ActiveStorage::Service::S3Service
def copy(key, new_key)
client.client.copy_object upload_options.merge(
copy_source: "#{bucket.name}/#{key}",
bucket: bucket.name,
key: new_key
)
end
end
It follow same approach as activestorage does for direct-upload without having to download files on the server.
Upvotes: 0
Reputation: 81
A slight variation on Benjamin's answer did work for me.
copy.poster_image.attach({
io: StringIO.new(original.poster_image.blob.download),
filename: original.poster_image.blob.filename,
content_type: original.poster_image.blob.content_type
})
Upvotes: 7
Reputation: 571
It worked for me:
copy.poster_image.attach(original.poster_image.blob)
Upvotes: 13
Reputation: 172
Found the answer by looking through Rails's tests, specifically in the blob model test
So for this case
class Package < ApplicationRecord
has_one_attached :poster_image
end
You can duplicate the attachment as such
original = Package.first
copy = original.dup
copy.poster_image.attach \
:io => StringIO.new(original.poster_image.download),
:filename => original.poster_image.filename,
:content_type => original.poster_image.content_type
The same approach works with has_many_attachments
class Post < ApplicationRecord
has_many_attached :images
end
original = Post.first
copy = original.dup
original.images.each do |image|
copy.images.attach \
:io => StringIO.new(image.download),
:filename => image.filename,
:content_type => image.content_type
end
Upvotes: 11
Reputation: 71
In rails 5 Jethro's answer worked well. For Rails 6 I had to modify to this:
image_io = source_record.image.download
ct = source_record.image.content_type
fn = source_record.image.filename.to_s
ts = Time.now.to_i.to_s
new_blob = ActiveStorage::Blob.create_and_upload!(
io: StringIO.new(image_io),
filename: ts + '_' + fn,
content_type: ct,
)
new_record.image.attach(new_blob)
Source:
Upvotes: 7
Reputation: 26535
Update your model:
class Package < ApplicationRecord
has_one_attached :poster_image
end
Attach the source package’s poster image blob to the destination package:
source_package.dup.tap do |destination_package|
destination_package.poster_image.attach(source_package.poster_image.blob)
end
Upvotes: 44
Reputation: 1660
If you want a full copy of the file so that both the original record and the cloned record have their own copy of the attached file, do this:
In Rails 5.2, grab this code and put it in config/initializers/active_storage.rb
, then use this code to do a copy:
ActiveStorage::Downloader.new(original.poster_image).download_blob_to_tempfile do |tempfile|
copy.poster_image.attach({
io: tempfile,
filename: original.poster_image.blob.filename,
content_type: original.poster_image.blob.content_type
})
end
After Rails 5.2 (whenever a release includes this commit), then you can just do this:
original.poster_image.blob.open do |tempfile|
copy.poster_image.attach({
io: tempfile,
filename: original.poster_image.blob.filename,
content_type: original.poster_image.blob.content_type
})
end
Thanks, George, for your original answer and for your Rails contributions. :)
Upvotes: 23