class Para::Cloneable::AttachmentsCloner

Constants

ATTACHMENTS_RELATION_REGEX

Handle both one and many attachment relations

Attributes

clone[R]
dictionary[R]
original[R]

Public Class Methods

new(original, clone, dictionary) click to toggle source
# File lib/para/cloneable/attachments_cloner.rb, line 9
def initialize(original, clone, dictionary)
  @original = original
  @clone = clone
  @dictionary = dictionary
end

Public Instance Methods

clone!() click to toggle source
# File lib/para/cloneable/attachments_cloner.rb, line 15
def clone!
  return unless defined?(ActiveStorage)

  attachment_reflections = original.class.reflections.select do |name, reflection|
    name.to_s.match(ATTACHMENTS_RELATION_REGEX) &&
      reflection.options[:class_name] == 'ActiveStorage::Attachment'
  end

  attachment_reflections.each do |name, reflection|
    association_target = original.send(name)
    next unless association_target

    if reflection.collection?
      association_target.each do |attachment|
        clone_attachment_if_needed(name, attachment)
      end
    else
      clone_attachment_if_needed(name, association_target)
    end
  end
end
clone_attachment(name, original_attachment) click to toggle source
# File lib/para/cloneable/attachments_cloner.rb, line 48
def clone_attachment(name, original_attachment)
  association_name = name.gsub(ATTACHMENTS_RELATION_REGEX, '')
  original_blob = original_attachment.blob

  # Handle missing file in storage service by bypassing the attachment cloning
  return unless ActiveStorage::Blob.service.exist?(original_blob&.key)

  Para::ActiveStorageDownloader.new(original_attachment).download_blob_to_tempfile do |tempfile|
    attachment_target = clone.send(association_name)

    cloned_blob = ActiveStorage::Blob.create_and_upload!(
      io: tempfile,
      filename: original_blob.filename,
      content_type: original_blob.content_type
    )

    attachment_target.attach(cloned_blob)
    cloned_attachment = find_cloned_attachment(attachment_target, original_blob)

    # Store the cloned attachment and blob into the deep_cloneable dictionary used
    # by the `deep_clone` method to ensure that, if needed during the cloning
    # operation, they won't be cloned once more and are accessible for processing
    store_cloned(original_attachment, cloned_attachment)
    store_cloned(original_blob, cloned_blob)
  end
end
clone_attachment_if_needed(name, attachment) click to toggle source
# File lib/para/cloneable/attachments_cloner.rb, line 37
def clone_attachment_if_needed(name, attachment)
  return if attachment.nil?

  store_key = store_key_for(attachment)

  # If the attachment has already been cloned, we don't need to clone it again
  return if dictionary[store_key]&.key?(attachment)

  clone_attachment(name, attachment)
end
find_cloned_attachment(attachment_target, original_blob) click to toggle source

Seemlessly handle one and many attachment relations return values and fetch the attachment that we just cloned by comparing blobs checksum, as depending which ActiveStorage version we’re on (Rails 5.2 or 6), the ‘#attach` method doesn’t always return the same, so for now we still handle the Rails 5.2 case.

# File lib/para/cloneable/attachments_cloner.rb, line 79
def find_cloned_attachment(attachment_target, original_blob)
  attachments = if attachment_target.attachments.any?
                  attachment_target.attachments
                else
                  [attachment_target.attachment]
                end

  attachment = attachments.find do |att|
    att.blob.checksum == original_blob.checksum
  end
end
store_cloned(source, clone) click to toggle source

This stores the source and clone resources into the deep_clone dictionary, which simulates what the deep_cloneable gem does when it clones a resource

# File lib/para/cloneable/attachments_cloner.rb, line 94
def store_cloned(source, clone)
  store_key = store_key_for(source)

  dictionary[store_key] ||= {}
  dictionary[store_key][source] = clone
end
store_key_for(attachment) click to toggle source
# File lib/para/cloneable/attachments_cloner.rb, line 101
def store_key_for(attachment)
  attachment.class.name.tableize.to_sym
end