Robban
Robban

Reputation: 1221

How do I duplicate a file stored in ActiveStorage in Rails 5.2

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

Answers (7)

Yogesh Khater
Yogesh Khater

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

Suhas Sharma
Suhas Sharma

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

Evan
Evan

Reputation: 571

It worked for me:

copy.poster_image.attach(original.poster_image.blob)

Upvotes: 13

jethro
jethro

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

Neil Cameron
Neil Cameron

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

George Claghorn
George Claghorn

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

Benjamin Curtis
Benjamin Curtis

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

Related Questions