thoughtbot/paperclip

View on GitHub
MIGRATING-ES.md

Summary

Maintainability
Test Coverage
# Migrando de Paperclip a ActiveStorage

Paperclip y ActiveStorage resuelven problemas similares con soluciones
similares, por lo que pasar de uno a otro es simple.

El proceso de ir desde Paperclip hacia ActiveStorage es como sigue:

1. Implementa las migraciones a la base de datos de ActiveStorage.
2. Configura el almacenamiento.
3. Copia la base de datos.
4. Copia los archivos.
5. Actualiza tus pruebas.
6. Actualiza tus vistas.
7. Actualiza tus controladores.
8. Actualiza tus modelos.

## Implementa las migraciones a la base de datos de ActiveStorage

Sigue [las instrucciones para instalar ActiveStorage]. Muy probablemente vas a
querer agregar la gema `mini_magick` a tu Gemfile.


```sh
rails active_storage:install
```

[las instrucciones para instalar ActiveStorage]: https://github.com/rails/rails/blob/master/activestorage/README.md#installation

## Configura el almacenamiento

De nuevo, sigue [las instrucciones para configurar ActiveStorage].

[las instrucciones para configurar ActiveStorage]: http://edgeguides.rubyonrails.org/active_storage_overview.html#setup

## Copia la base de datos.

Las tablas `active_storage_blobs` y`active_storage_attachments` son en donde
ActiveStorage espera encontrar los metadatos del archivo. Paperclip almacena los
metadatos del archivo directamente en en la tabla del objeto asociado.

Vas a necesitar escribir una migración para esta conversión. Proveer un script
simple, es complicado porque están involucrados tus modelos. ¡Pero lo
intentaremos!

Así sería para un `User` con un `avatar` en Paperclip:

```ruby
class User < ApplicationRecord
  has_attached_file :avatar
end
```

Tus migraciones de Paperclip producirán una tabla como la siguiente:

```ruby
create_table "users", force: :cascade do |t|
  t.string "avatar_file_name"
  t.string "avatar_content_type"
  t.integer "avatar_file_size"
  t.datetime "avatar_updated_at"
end
```

Y tu la convertirás en estas tablas:

```ruby
create_table "active_storage_attachments", force: :cascade do |t|
  t.string "name", null: false
  t.string "record_type", null: false
  t.integer "record_id", null: false
  t.integer "blob_id", null: false
  t.datetime "created_at", null: false
  t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id"
  t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true
end
```

```ruby
create_table "active_storage_blobs", force: :cascade do |t|
  t.string "key", null: false
  t.string "filename", null: false
  t.string "content_type"
  t.text "metadata"
  t.bigint "byte_size", null: false
  t.string "checksum", null: false
  t.datetime "created_at", null: false
  t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true
end
```

Así que asumiendo que quieres dejar los archivos en el mismo lugar, _esta es tu
migración_. De otra forma, ve la siguiente sección primero y modifica la
migración como corresponda.

```ruby
Dir[Rails.root.join("app/models/**/*.rb")].sort.each { |file| require file }

class ConvertToActiveStorage < ActiveRecord::Migration[5.2]
  require 'open-uri'

  def up
    # postgres
    get_blob_id = 'LASTVAL()'
    # mariadb
    # get_blob_id = 'LAST_INSERT_ID()'
    # sqlite
    # get_blob_id = 'LAST_INSERT_ROWID()'

    active_storage_blob_statement = ActiveRecord::Base.connection.raw_connection.prepare(<<-SQL)
      INSERT INTO active_storage_blobs (
        key, filename, content_type, metadata, byte_size,
        checksum, created_at
      ) VALUES (?, ?, ?, '{}', ?, ?, ?)
    SQL

    active_storage_attachment_statement = ActiveRecord::Base.connection.raw_connection.prepare(<<-SQL)
      INSERT INTO active_storage_attachments (
        name, record_type, record_id, blob_id, created_at
      ) VALUES (?, ?, ?, #{get_blob_id}, ?)
    SQL

    models = ActiveRecord::Base.descendants.reject(&:abstract_class?)

    transaction do
      models.each do |model|
        attachments = model.column_names.map do |c|
          if c =~ /(.+)_file_name$/
            $1
          end
        end.compact

        model.find_each.each do |instance|
          attachments.each do |attachment|
            active_storage_blob_statement.execute(
              key(instance, attachment),
              instance.send("#{attachment}_file_name"),
              instance.send("#{attachment}_content_type"),
              instance.send("#{attachment}_file_size"),
              checksum(instance.send(attachment)),
              instance.updated_at.iso8601
            )

            active_storage_attachment_statement.
              execute(attachment, model.name, instance.id, instance.updated_at.iso8601)
          end
        end
      end
    end

    active_storage_attachment_statement.close
    active_storage_blob_statement.close
  end

  def down
    raise ActiveRecord::IrreversibleMigration
  end

  private

  def key(instance, attachment)
    SecureRandom.uuid
    # Alternativamente:
    # instance.send("#{attachment}_file_name")
  end

  def checksum(attachment)
    # archivos locales almacenados en disco:
    url = attachment.path
    Digest::MD5.base64digest(File.read(url))

    # archivos remotos almacenados en la computadora de alguién más:
    # url = attachment.url
    # Digest::MD5.base64digest(Net::HTTP.get(URI(url)))
  end
end
```

## Copia los archivos

La migración de arriba deja los archivos como estaban. Sin embargo,
los servicios de Paperclip y ActiveStorage utilizan diferentes ubicaciones.

Por defecto, Paperclip se ve así:

```
public/system/users/avatars/000/000/004/original/the-mystery-of-life.png
```

Y ActiveStorage se ve así:

```
storage/xM/RX/xMRXuT6nqpoiConJFQJFt6c9
```

Ese `xMRXuT6nqpoiConJFQJFt6c9` es el valor de `active_storage_blobs.key`. En la
migración de arriba usamos simplemente el nombre del archivo, pero tal vez
quieras usar un UUID.

Migrando los archivos en un hospedaje externo (S3, Azure Storage, GCS, etc.)
está fuera del alcance de este documento inicial. Así es como se vería para un
almacenamiento local:

```ruby
#!bin/rails runner

class ActiveStorageBlob < ActiveRecord::Base
end

class ActiveStorageAttachment < ActiveRecord::Base
  belongs_to :blob, class_name: 'ActiveStorageBlob'
  belongs_to :record, polymorphic: true
end

ActiveStorageAttachment.find_each do |attachment|
  name = attachment.name

  source = attachment.record.send(name).path
  dest_dir = File.join(
    "storage",
    attachment.blob.key.first(2),
    attachment.blob.key.first(4).last(2))
  dest = File.join(dest_dir, attachment.blob.key)

  FileUtils.mkdir_p(dest_dir)
  puts "Moving #{source} to #{dest}"
  FileUtils.cp(source, dest)
end
```

## Actualiza tus pruebas

En lugar de utilizar `have_attached_file`, será necesario que escribas tu propio
matcher. Aquí hay un matcher similar _en espíritu_ al que Paperclip provee:


```ruby
RSpec::Matchers.define :have_attached_file do |name|
  matches do |record|
    file = record.send(name)
    file.respond_to?(:variant) && file.respond_to?(:attach)
  end
end
```

## Actualiza tus vistas

En Paperclip se ven así:

```ruby
image_tag @user.avatar.url(:medium)
```

En ActiveStorage se ven así:

```ruby
image_tag @user.avatar.variant(resize: "250x250")
```

## Actualiza tus controladores

Esto no debería _requerir_ ningúna actualización. Sin embargo, si te fijas en
el schema de tu base de datos, notaras un join.

Por ejemplo si tu controlador tiene:

```ruby
def index
  @users = User.all.order(:name)
end
```

Y tu vista tiene:

```
<ul>
  <% @users.each do |user| %>
    <li><%= image_tag user.avatar.variant(resize: "10x10"), alt: user.name %></li>
  <% end %>
</ul>
```

Vas a terminar con un n+1, ya que descargas cada archivo adjunto dentro del
bucle.

Así que mientras que el controlador y el modelo funcionarán sin ningún cambio,
tal vez quieras revisar dos veces tus bucles y agregar `includes` en dónde haga
falta.

ActiveStorage agrega `avatar_attachment` y `avatar_blob` a las relaciones del
tipo `has-one`, así como `avatar_attachments` y `avatar_blobs` a las relaciones
de tipo `has-many`:

```ruby
def index
  @users = User.all.order(:name).includes(:avatar_attachment)
end
```

## Actualiza tus modelos

Sigue [la guía sobre cómo adjuntar archivos a los registros]. Por ejemplo, un
`User` con un `avatar` se representa como:

```ruby
class User < ApplicationRecord
  has_one_attached :avatar
end
```

Cualquier cambio de tamaño se hace en la vista como un `variant`.

[la guía sobre cómo adjuntar archivos a los registros]: http://edgeguides.rubyonrails.org/active_storage_overview.html#attaching-files-to-records

## Quita Paperclip

Quita la gema de tu `Gemfile` y corre `bundle`. Corre tus pruebas porque ya
terminaste!