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:
- Implementa las migraciones a la base de datos de ActiveStorage.
- Configura el almacenamiento.
- Copia la base de datos.
- Copia los archivos.
- Actualiza tus pruebas.
- Actualiza tus vistas.
- Actualiza tus controladores.
- Actualiza tus modelos.
Sigue las instrucciones para instalar ActiveStorage. Muy probablemente vas a
querer agregar la gema mini_magick
a tu Gemfile.
rails active_storage:install
De nuevo, sigue las instrucciones para configurar ActiveStorage.
Las tablas active_storage_blobs
yactive_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:
class User < ApplicationRecord
has_attached_file :avatar
end
Tus migraciones de Paperclip producirán una tabla como la siguiente:
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:
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
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.
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
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:
#!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
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:
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
En Paperclip se ven así:
image_tag @user.avatar.url(:medium)
En ActiveStorage se ven así:
image_tag @user.avatar.variant(resize: "250x250")
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:
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
:
def index
@users = User.all.order(:name).includes(:avatar_attachment)
end
Sigue la guía sobre cómo adjuntar archivos a los registros. Por ejemplo, un
User
con un avatar
se representa como:
class User < ApplicationRecord
has_one_attached :avatar
end
Cualquier cambio de tamaño se hace en la vista como un variant
.
Quita la gema de tu Gemfile
y corre bundle
. Corre tus pruebas porque ya
terminaste!