Skip to content

Instantly share code, notes, and snippets.

@zealot128
Last active April 6, 2023 12:14
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zealot128/53210ce85281f09a393ed8de9df8446e to your computer and use it in GitHub Desktop.
Save zealot128/53210ce85281f09a393ed8de9df8446e to your computer and use it in GitHub Desktop.
ActiveStorage migration guide and caveats from Carrierwave (similar starting point for Paperclip)

Migrate Carrierwave to ActiveStorage

Migrate, e.g. organisation.logo (Carrierwave "LogoUploader") to organisation.logo2 (ActiveStorage)

require 'migrate_attachment'
migrate_attachment!(
  klass: Organisation, 
  attachment_attribute: :logo, 
  carrierwave_uploader: LogoUploader, 
  active_storage_column: :logo2
)

Checklist:

  • Use with caution! only tested with file adapters, not Cloud storage yet.
  • In this example we migrate from logo to logo2 so we can replace all occurences in the codebase. If you are confident, you could also migrate to the same column name (logo -> logo). But make sure to replace all logo.url(...) stuff etc.
  • Only works if you not made any funky things with Carrierwave path (e.g. base path on parts of model methods). Add the missing methods to the meta programming part, as well as a vanilla table with "normal" id column.
  • Does not migrate any versions. Try to mimick your Carrierwave versions like:
  mount_uploader :logo, LogoUploader
  ...
  version :medium do
    process resize_to_fit: [200, 1500]
  end
  
# becomes:
  
  has_one_attached :logo2

  def logo_medium
    logo2.variant(
      combine_options: {
        gravity: "center",
        resize: "200x1500>",
        crop: "200x1500+0+0"
      })
  end
  • programatically setting blobs + nil does not work. Use:
    • organisation.logo2.attach(io: tempfile, filename: filename), instead of organisation.logo = tempfile
    • organisation.logo2.detach instead of organisation.logo2 = nil
  • referencing:
    • image_tag organisation.logo2
    • if need url e.g. for serializer or inside pdf scripts etc, one need to use the Rails url helper:
      • blob: Rails.application.routes.url_helpers.rails_blob_path(organisation.logo2, only_path: true)
      • variant: Rails.application.routes.url_helpers.rails_representation_path(organisation.logo_medium, only_path: true)
  • AS does not have a remote_ATTACHMENT that downloads automatically from remote server
  • grep the codebase for organisation.logo.url and organisation.logo.path, organisation.logo.versions
  • If you need to access the blob for local processing, use AS::Downloader config in accompanied file (organisation.logo2.open { |tempfile| .... }), if on Rails 5.2.0.
# lib/migrate_attachment.rb
def migrate_attachment!(klass:, attachment_attribute:, carrierwave_uploader:, active_storage_column: attachment_attribute)
old_class_name = "Old#{klass.name}"
new_class_name = "New#{klass.name}"
eval <<~DOC
class #{new_class_name} < ActiveRecord::Base
self.table_name = #{klass.name}.table_name
has_one_attached :#{active_storage_column}
end
class #{old_class_name} < ActiveRecord::Base
self.table_name = #{klass.name}.table_name
mount_uploader :#{attachment_attribute}, #{carrierwave_uploader.name}
def self.to_s
#{klass}.to_s
end
end
DOC
new_class = new_class_name.constantize
old_class_name.constantize.find_each do |item|
next unless item.send(attachment_attribute).present?
attachment = item.send(attachment_attribute)
attachment.cache_stored_file!
file = attachment.sanitized_file.file
content_type = item.send(attachment_attribute).content_type
copy = new_class.find(item.id)
copy.send(active_storage_column).attach(io: file, content_type: content_type, filename: item.attributes[attachment_attribute.to_s])
copy.save
end
ActiveStorage::Attachment.where(record_type: new_class_name).update_all record_type: klass.to_s
end
# config/initializers/activestorage.rb
# frozen_string_literal: true
if Rails.version > "5.2.0"
raise "Check if this commit is already included in your current rails version:
https://github.com/rails/rails/commit/ee21b7c2eb64def8f00887a9fafbd77b85f464f1#diff-3fd88ddd945ad24c4bd6f76c64c8790a
"
end
module ActiveStorage
class Downloader #:nodoc:
def initialize(blob, tempdir: nil)
@blob = blob
@tempdir = tempdir
end
def download_blob_to_tempfile
open_tempfile do |file|
download_blob_to file
verify_integrity_of file
yield file
end
end
private
attr_reader :blob, :tempdir
def open_tempfile
file = Tempfile.open([ "ActiveStorage-#{blob.id}-", blob.filename.extension_with_delimiter ], tempdir)
begin
yield file
ensure
file.close!
end
end
def download_blob_to(file)
file.binmode
blob.download { |chunk| file.write(chunk) }
file.flush
file.rewind
end
def verify_integrity_of(file)
unless Digest::MD5.file(file).base64digest == blob.checksum
raise ActiveStorage::IntegrityError
end
end
end
end
module AsDownloadPatch
def open(tempdir: nil, &block)
ActiveStorage::Downloader.new(self, tempdir: tempdir).download_blob_to_tempfile(&block)
end
end
Rails.application.config.to_prepare do
ActiveStorage::Blob.send(:include, AsDownloadPatch)
end
@scarroll32
Copy link

Use with caution! only tested with file adapters, not Cloud storage yet.

Has it been tested with AWS at this point?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment