37

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
Robban
  • 1,191
  • 2
  • 13
  • 25

6 Answers6

40

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
George Claghorn
  • 26,261
  • 3
  • 48
  • 48
  • 2
    I found i also had to have the dependent option set to false to avoid having the blob deleted if the original package was removed: `has_one_attached :poster_image, dependent: false` – Robban Apr 05 '18 at 10:49
  • Yes! Sorry for forgetting that. – George Claghorn Apr 05 '18 at 20:16
  • 3
    Unless I'm missing something, this doesn't create a copy of the file. It just makes it so both records point to the same file. Also it will leave the file on the storage system even if all records associated with the file get destroyed. – ndnenkov Jan 16 '19 at 11:57
  • 3
    Actually, are you sure this is up to date? I just tried it. It seems like `ActiveStorage::PurgeJob` does `SELECT 1 AS one FROM "active_storage_attachments" WHERE "active_storage_attachments"."blob_id" = 55 LIMIT 1` and doesn't delete the blob if there are other attachments referring to it. So `dependent: false` shouldn't be necessary? – ndnenkov Jan 16 '19 at 12:55
  • I'm referring to [this before destroy](https://github.com/rails/rails/blob/bfd296dda797e597e8a54709d1cd331cdffaa9f7/activestorage/app/models/active_storage/blob.rb#L36-L38). – ndnenkov Jan 16 '19 at 13:10
  • There is a before_destroy hook on Blob: `raise ActiveRecord::InvalidForeignKey if attachments.exists?` – Mirko Jan 26 '22 at 13:35
20

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. :)

Benjamin Curtis
  • 1,570
  • 12
  • 13
  • 1
    Unfortunately, the second part is still not released in a Rails version besides master. – stwienert Oct 01 '18 at 13:52
  • Works and I prefer it over [the solution below](https://stackoverflow.com/a/66356098/5925094), because directly attaching the image without opening before ends up being 90% flaky when running all tests. – Rich Steinmetz Feb 11 '23 at 11:30
  • With the other solution I was getting a `ActiveStorage::FileNotFoundError: ActiveStorage::FileNotFoundError` on almost every run. – Rich Steinmetz Feb 11 '23 at 11:32
  • With this solution I'm getting a IOError when the whole thing is wrapped inside of a ActiveRecord transaction, so it also has its drawbacks. – Rich Steinmetz Feb 11 '23 at 11:33
10

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
Roko
  • 1,233
  • 1
  • 11
  • 22
jethro
  • 168
  • 3
  • 13
8

It worked for me:

copy.poster_image.attach(original.poster_image.blob)
Evan
  • 521
  • 4
  • 11
7

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:

Neil Cameron
  • 71
  • 1
  • 3
6

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 
  })

Benjamin Oakes
  • 12,262
  • 12
  • 65
  • 83
Suhas Sharma
  • 71
  • 1
  • 3
  • Thank you! This extended version with filename and content_type worked for me – pastullo Apr 10 '21 at 21:19
  • I prefer Benjamin's solution because directly attaching the image without opening before ends up being 90% flaky when running all tests, I'm getting a ActiveStorage::FileNotFoundError: ActiveStorage::FileNotFoundError on almost every run. – Rich Steinmetz Feb 11 '23 at 11:32