Active Storage 2 - LPC-Ltda/Ruby-on-Rails GitHub Wiki

De todas las cosas nuevas y brillantes que la última actualización importante antes de Rails 6 trae, Active Storage se destaca más. Por primera vez en la historia de Rails, obtenemos una solución integrada para manejar la carga de archivos en nuestros proyectos. Según DHH, Active Storage se extrajo de Basecamp 3, por lo que afirma ser un marco "nacido de la producción".

Hablaremos de Active Storage primero, y luego, si nos acompaña hasta el final de esta guía, veremos otras características de Rails 5.2.

Con cosas atachadas

Descargo de responsabilidad: no vamos a comparar Active Storage con las soluciones existentes, ya sea CarrierWave, Paperclip o Shrine, sino que intentamos hacer una introducción al marco para principiantes a medida que lo conozcamos nosotros mismos.

La habilitación del Active Storage en su aplicación comienza con una tarea de Rake: ejecutar rails active_storage: install en la línea de comandos agregará una nueva migración a su carpeta db/migrate. Una vez ejecutado, crea dos tablas que Active Storage necesita para cumplir con sus promesas: active_storage_attachments y active_storage_blobs. Esto es lo que hacen, según el README del marco:

“Active Storage usa asociaciones polimórficas a través del modelo de unión de Attachment, que luego se conecta al Blob real. Los modelos de blob almacenan metadatos adjuntos (nombre de archivo, tipo de contenido, etc.) y su clave de identificación en el servicio de almacenamiento ".

Este enfoque pone a Active Storage aparte de la competencia. Paperclip, Carrierwave, Shrine: todas estas soluciones populares requieren que agregue columnas a los modelos existentes. La única gema ampliamente utilizada que se basa en atributos virtuales para manejar los archivos adjuntos es Attachinary, una solución propietaria ridículamente fácil de usar que está bloqueada para el almacenamiento de Cloudinary.

Al parecer, Active Storage toma la misma dirección, pero le permite elegir dónde guardar sus archivos: ya sea su hardware o los proveedores de nube populares. Amazon S3, Google Cloud Storage y Microsoft Azure Storage son compatibles de forma inmediata.

A veces esto está OK para el scaffold

Es hora de ver Active Storage en acción. Suponiendo que ya ha creado una nueva aplicación, comente la gema jbuilder en Gemfile y ejecute bundle install, ya que nos dará un andamio más limpio. Después de todo, solo queremos echar un vistazo al almacenamiento activo y no dedicar tiempo a desarrollar CRUD completo solo para demostrar nuestra originalidad:

$ rails g scaffold post title:string content:text

Comencemos adjuntando una sola imagen a nuestra publicación. Agregue una sola línea de código a su definición de modelo:

# app/models/post.rb
class Post < ApplicationRecord
  has_one_attached :image
end

Ahora echemos un vistazo al posts_controller.rb que fue creado amablemente por un generador de scaffolds. Lo único que debemos hacer aquí es hacer una lista blanca de un parámetro :image dentro del método idiomático de post_params y hacer que se vea así:

# app/controllers/posts_controller.rb
def post_params
  params.require(:post).permit(:title, :content, :image)
end

Por cierto, si desea adjuntar un archivo al modelo existente en otro lugar en el código del controlador, aquí le indicamos cómo hacerlo:

@post.image.attach(params[:image])

Nota: si está utilizando create o update sobre un recurso en la acción de su controlador y pasar el adjunto como un parámetro permitido, la línea anterior no es necesaria y romperá las cosas. Algunos tutoriales anteriores requieren que haga el attach un archivo explícitamente, pero ese ya no es el caso.

Ahora para las vistas: en el _form.html.erb generado, agregue un file_field justo arriba del botón de envío:

<!-- app/views/posts/_form.html.erb -->
<div class="field">
  <%= form.label :image %>
  <%= form.file_field :image %>
</div>

Y para mostrar nuestra imagen:

<!-- app/views/posts/show.html.erb -->
<% if @post.image.attached? %>
<!-- @post.image.present? will always return true, use attached? to check presence -->
  <p>
    <strong>Image:</strong>
    <br>
    <%= image_tag @post.image %>
  </p>
<% end %>

¡Eso es! Rails maneja todos los detalles esenciales de la carga de formularios de varias partes para usted. Vamos a ver si funciona. Inicie el servidor con rails s y vaya a localhost:3000/posts/new en su navegador. Elige cualquier imagen que te guste y crea una publicación.

La edición de esta publicación y el cambio de una imagen también funcionarán de inmediato, solo inténtelo. ¡Voila, los archivos subidos están habilitados en tu aplicación!

En resumen, aquí está todo lo que hicimos:

  • Model: Llamamos al método has_one_attached en la definición del modelo con un símbolo que se convertirá en un atributo virtual en cada instancia de nuestro modelo. Elegimos nombrarla :image, pero podría haber sido :cualquier_cosa_que_quieras.
  • Controller: Ponemos image en la lista blanca como parámetro permitido.
  • Views: Agregamos un file_field a nuestro formulario y mostramos una imagen cargada en la image_tag.

Ahora, veamos lo que sucedió tras bambalinas cuando enviamos ese formulario. Lo primero que notamos cuando echamos un vistazo a los registros del servidor, cada declaración SQL ahora está acompañada por una referencia a una línea de código que la generó. Anteriormente, ese era un trabajo de la gema Query Trace; ahora esta característica está al horno en rails. Si no está satisfecho con el resultado (hay un error que afecta a ciertas configuraciones de rbenv), establezca la opción config.active_record.verbose_query_logs en false dentro de development.rb.

Podemos ver que Rails procesa el formulario, almacena el archivo recibido en el disco, codifica su ubicación como una clave, hace referencia a esa clave en la tabla active_storage_blobs, crea un nuevo registro en la tabla de publicaciones y luego asocia una publicación con un Blob a través de active_storage_attachments.

Y esto es lo que sucede cuando se invoca la acción show de nuestro PostsController a través de GET:

Una solicitud se convierte en tres: ActiveStorage::BlobsController y ActiveStorage::DiskController están involucrados en la entrega de un archivo. De esta manera, la URL pública de su imagen siempre se desacopla de su ubicación real. Si está utilizando un servicio en la nube, BlobsController redirigirá a una URL firmada correcta en su nube.

Veamos qué podemos hacer ahora con nuestra instancia de publicación dentro de la consola de Rails:

Tenga en cuenta que para generar una URL para un archivo adjunto debemos llamar a service_url y no a url. Ver ayudantes como url_for y image_tag saben cómo hacer esto por nosotros, por lo que rara vez tendrá que llamar explícitamente a ese método.

Resolviendo N+1

Dado que la representación de un archivo adjunto en una vista da como resultado al menos tres consultas de base de datos (una para el modelo principal, una para la tabla active_storage_attachments y otra para active_storage_blobs), deberíamos preocuparnos cuando recorremos una colección de objetos Active Record que todos tienen ¿archivos adjuntos? ¡Vamos a averiguar! Modifique su index.html.erb para mostrar una imagen para cada publicación o al menos imprima su nombre de archivo (post.image.filename también activará todas las consultas), actualice /posts en su navegador y eche un vistazo al registro:

¿Ves el problema? La hidra N+1 vuelve a levantar su fea cabeza. Afortunadamente, el almacenamiento activo proporciona una solución: genera un scope with_attached_image (o with_attached_your_attachment_name) que incluye los blobs asociados. Todo lo que necesitas hacer es cambiar @posts = Post.all en tu PostsController#index a @posts = Post.with_attached_image. Aquí está el resultado:

¡Bonito! Pero, ¿qué sucede si desea utilizar eager_load o precargar en lugar de incluir porque no confía en que Active Record siempre tome la decisión correcta para usted? Así es como lo haces:

class Post < ApplicationRecord
  scope :with_eager_loaded_image, -> { eager_load(image_attachment: :blob) }
  scope :with_preloaded_image, -> { preload(image_attachment: :blob) }
end

Tenga en cuenta que usamos la precarga anidada aquí: cargamos blobs a través de adjuntos (image_attachment es un nombre de la asociación agregada por Active Storage en este caso particular, si elige nombrar sus adjuntos de manera diferente, el nombre de la asociación también cambiará).

Multiples attachments

Configurar tu CRUD para múltiples archivos adjuntos también es pan comido.

  • Model:
# app/models/post.rb
class Post < ApplicationRecord
  has_many_attached :images
  # Note that implicit association has a plural form in this case
  scope :with_eager_loaded_images, -> { eager_load(images_attachments: :blob) }
end
  • Controller:
# app/controllers/posts_controller.rb
def post_params
  params.require(:post).permit(:title, :content, images: [])
end
  • Views:
<!-- app/views/posts/_form.html.erb -->
<div class="field">
  <%= form.label :images %>
  <%= form.file_field :images, multiple: true %>
</div>

<!-- app/views/posts/show.html.erb -->
<% if @post.images.attached? %>
<p>
  <strong>Images:</strong>
  <br>
  <% @post.images.each do |image| %>
    <%= image_tag(image) %>
  <% end %>
</p>
<% end %>

Si desea eliminar un archivo adjunto (o todos ellos), hay dos métodos para eso: purge y purge_later. El segundo manejará la eliminación de archivos en segundo plano a través del ActiveStorage::PurgeJob integrado. Esta depuración asíncrona también se llamará de forma predeterminada si elimina el modelo principal.

Variantes de imagen con ImageMagick

Actualmente, nuestras imágenes se muestran exactamente como un usuario las subió, y eso no es lo que normalmente queremos. El almacenamiento activo admite transformaciones de imagen con ImageMagick. Todo lo que necesita hacer es habilitar una gema mini_magick que ya se ha agregado a su Gemfile (y se ha comentado). Te permite realizar todas las transformaciones de ImageMagick. Por ejemplo:

<%= image_tag image.variant(resize: "500x500", monochrome: true) %>

Creará una URL para esa variante específica de ese blob específico, pero ActiveStorage::VariantsController no manejará la transformación en sí misma hasta que la imagen sea solicitada por primera vez por un navegador. El almacenamiento activo intenta demorar una operación potencialmente costosa: el blob original debe descargarse del servicio, transformarse en la memoria de su servidor y cargarse nuevamente al servicio.

Si desea procesar primero el archivo y luego obtener la URL, puede llamar a image.variant (resize: "100x100"). processed.service_url. Verificará si la transformación para esa variante en particular se había realizado antes, de ser así, no se repetiría.

También puede generar vistas previas para videos (con ffmpeg) y PDF (con mutool), pero es su trabajo asegurarse de que esas herramientas estén disponibles, ya que Rails no proporciona sus bibliotecas.

Una vez que haya habilitado ImageMagick, todos sus archivos entrantes serán analizados automáticamente (y de forma asíncrona) en busca de metadatos; puede llamar a post.image.metadata para obtener un hash que se verá así: {"width" => 1200, "height" => 700, "analyzed" => verdadero}.

No más secrets

Bueno, todo parecía funcionar perfectamente bien a nivel local, pero ¿no teníamos que configurar algo? No realmente, de una manera familiar "omakase" de Rails, ya se hizo una configuración básica para usted en config/storage.yml. Echemos un vistazo:

test:
  service: Disk
  root: <%= Rails.root.join("tmp/storage") %>

local:
  service: Disk
  root: <%= Rails.root.join("storage") %>

# Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
# amazon:
#   service: S3
#   access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
#   secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
#   region: us-east-1
#   bucket: your_own_bucket

En development.rb ya tiene config.active_storage.service =: local seteado, por lo que Rails sabe que debe usar su disco cuando trabaja con Active Storage en localhost. También hay configuraciones para los servicios en la nube de Amazon, Google y Microsoft, todos comentados.

Poner las claves directamente en storage.yml sería una mala idea, ya que probablemente querrá tener este archivo en su control de código fuente. Rails pensó en el futuro y asumió que sus claves están almacenadas dentro de Rails.application.credentials. Espera, ¿pero no fue llamado secrets? Sí, lo fue, pero, en palabras de DHH mismo:

“La combinación de config/secrets.yml, config/secrets.yml.enc y SECRET_BASE_KEY es confusa. No está claro qué debe incluir en estos secretos y si SECRET_BASE_KEY está relacionado con la configuración en general ".

Para poner fin a esta confusión, Rails 5.2 introduce un nuevo concepto llamado Credentials.

Se almacenan dentro de config/credentials.yml.enc en forma cifrada, por lo que es seguro que estén comprometidos con el control de código fuente. ¡No más perder el tiempo con variables de entorno o sincronizar claves modificadas en todo el equipo!

El archivo que Git no debe rastrear bajo ninguna circunstancia (y ya está listado en .gitignore para los nuevos proyectos de Rails 5.2) es config/master.key. Contiene la clave autogenerada que permite descifrar sus credenciales. Como la documentación nos avisa:

"No pierdas esta llave maestra! Póngalo en un administrador de contraseñas al que su equipo pueda acceder. Si lo pierde, nadie, incluido usted, podrá acceder a las credenciales cifradas ".

Entonces, ¿cómo edita sus credenciales, si el archivo que las contiene está siempre cifrado? Rails 5.2 tiene una nueva tarea para eso: rails credentials:edit. Este comando abrirá su editor predeterminado con un archivo de texto sin formato en el que puede poner sus claves en el formato key_name: key_value. También se permite la anidación de YAML. Luego puede acceder a sus credenciales con Rails.application.credentials.key_name o, si usa claves anidadas, con Rails.application.credentials.dig(:section_name,:nested_key_name). Una vez que guarde y cierre el archivo temporal, su contenido se codificará en config/credentials.yml.enc. También puede imprimir sus llaves a la Terminal con rails credentials:show.

Ahora, siempre que todos los miembros de su equipo tengan el mismo archivo master.key, puede colaborar de forma segura a través de Git y no tener miedo de volver a exponer información confidencial. Para la producción, deberá configurar una sola variable de entorno RAILS_MASTER_KEY.

Nota: para que las credenciales funcionen con un editor externo como Atom, debe llamar a la tarea EDITOR="atom --wait" credentials:edit. O use un editor de shell, que puede ser más adecuado para editar rápidamente algunas teclas: EDITOR=vi credentials:edit.

Active Storage en la nube

Para una demostración simple, usaremos Amazon S3. Obviamente, necesitará un bucket con acceso público de lectura. Una vez que lo tienes, el resto es tan fácil como 1-2-3:

  1. Inside storage.yml elimina comentarios de la sección amazon:.
  2. En development.rb, cambie el servicio predeterminado a S3 con config.active_storage.service =: amazon.
  3. Tipee rails credentials:edit en su consola y coloque sus claves en el siguiente formato:
aws:
  access_key_id: 123
  secret_access_key: 456

¡Eso es! Todos los archivos cargados y sus variantes ahora se manejan automáticamente a través de Amazon S3. post.image.service_url ahora generará una URL firmada para su instancia de bucket.

Upload Directo

El verano pasado, mientras Active Storage todavía estaba en desarrollo activo, el desarrollador Fabio Akita expresó su preocupación por el manejo de archivos grandes cuando su proyecto habilitado para Active Storage está alojado en una plataforma con un sistema de archivos efímeros como Heroku. En un hilo de Twitter, el creador de Rails DHH se convenció de implementar una Carga directa a la nube desde el navegador, omitiendo por completo el back-end de la aplicación, similar a lo que Attachinary ha implementado para enviar archivos al almacenamiento Cloudinary.

Rails Guides ya tiene una excelente cobertura de una función de carga directa junto con fragmentos de código JavaScript (finalmente, ¡ES6 en lugar de CoffeeScript!).

Esta es la rapidez con la que probarlo (los pasos se muestran con Webpacker en mente, así que asegúrese de haber ejecutado rails webpacker:install para su aplicación):

$ yarn add activestorage
// app/javascript/packs/application.js
import * as ActiveStorage from "activestorage";
import "../utils/direct_uploads.js"

ActiveStorage.start();
// app/javascript/utils/direct_uploads.js
// Create this folder and this file, then cut and paste code from
// http://edgeguides.rubyonrails.org/active_storage_overview.html#example
<!-- app/views/posts/_form.html.erb -->
<div class="field">
  <%= form.label :images %>
  <%= form.file_field :images, multiple: true, direct_upload: true %>
</div>

direct_upload:true genera el siguiente HTML para el campo de archivo:

<input multiple="multiple" data-direct-upload-url="http://localhost:3000/rails/active_storage/direct_uploads" name="post[images][]" id="post_images" type="file">

El código de ejemplo de JavaScript demuestra el uso de Direct Upload JS events que le permiten seguir el ciclo de carga y reaccionar en consecuencia en la interfaz de usuario, al actualizar una barra de progreso, por ejemplo.

Coloque código CSS de muestra de la misma Rails Guides section dentro del archivo direct_uploads.css que puede crear en la carpeta app/asset/stylesheets. Ahora, cuando reinicia su servidor y se dirige a una nueva página de publicación, podrá elegir un archivo grande y ser testigo de que su servidor no está bloqueado en una tarea larga: la carga se realiza completamente en XHR con una barra de progreso dinámicamente actualizada :

Nota: Al momento de escribir este artículo, esta función no funcionaba en Firefox y daba como resultado un XML Parsing Error cuando se usaba con S3.

Si está utilizando S3, debe abrir la configuración de CORS en la pestaña "Permissions" de su grupo. Tenga en cuenta que la siguiente configuración solo es válida para una prueba rápida en desarrollo, ¡no la deje abierta en producción!

<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
    <AllowedOrigin>*</AllowedOrigin>
    <AllowedMethod>GET</AllowedMethod>
    <AllowedMethod>POST</AllowedMethod>
    <AllowedMethod>PUT</AllowedMethod>
    <AllowedMethod>DELETE</AllowedMethod>
    <MaxAgeSeconds>3000</MaxAgeSeconds>
    <AllowedHeader>*</AllowedHeader>
</CORSRule>
</CORSConfiguration>

Mirror, Espejo en la pared

Otra gran característica de Active Storage es la mirroring, esto le permite mantener archivos sincronizados en varios proveedores de almacenamiento en la nube para redundancia o durante la migración entre nubes. Por ejemplo, en storage.yml:

production:
  service: Mirror
  primary: local
  mirrors:
    - amazon
    - google

Luego, en production.rb puedes configurar tu servicio para :producción. Esta configuración almacenará los archivos cargados localmente y los respaldará en Amazon S3 y Google Cloud Storage al mismo tiempo. Una vez que se elimine el archivo, también se eliminará de ambas nubes.

Eso fue todo lo que pudimos decir sobre el almacenamiento activo en este momento; Todavía tiene que ser probado en la batalla en la producción para que podamos proporcionar una visión más profunda.

Ahora, echemos un vistazo a algunas otras características notables de Rails 5.2.

DSL para encabezado Content-Security-Policy

La seguridad importa, ¿no es así? Ahora es el momento de hacer que nuestras aplicaciones sean aún más seguras. El CSP está diseñado para restringir las solicitudes de red "entrantes" (es decir, lo que se puede cargar en su página) pero, como efecto secundario, también restringe las solicitudes salientes desde el navegador, lo que evita que los piratas informáticos puedan piratear.

Ahora es fácil configurar CSP en su aplicación Rail, ya sea globalmente:

# config/initializers/content_security_policy.rb
Rails.application.config.content_security_policy do |p|
  # allow everything from the current hostname by default (only secured)
  p.default_src :self, :https
  # allow loading fonts and images from data-uri
  p.font_src    :self, :https, :data
  # don't forget to add your cloud hostname for ActiveStorage assets
  p.img_src     :self, :https, :data, "cloudfront.example.com"
  # disallow <object> tags (Good-bye Flash!)
  p.object_src  :none
  # allow inline <style> (remove :unsafe_inline if you don't want it)
  p.style_src   :self, :https, :unsafe_inline
end

O por controlador (incluso dinámicamente):

class PostsController < ApplicationController
  # Extends/overrides global policy
  content_security_policy do |p|
    # set user-specific domain as base for the policy
    p.base_uri :self, -> { "https://#{current_user.domain}.example.com" }
  end
end

Sin embargo, hay una advertencia importante sin documentar (en el momento de este artículo). Si está utilizando Webpacker y webpack-dev-server, deberá actualizar el CSP para el entorno de desarrollo y permitir las conexiones a http://localhost:3035 y ws://localhost:3035, de lo contrario Rails bloqueará las conexiones de socket web necesarias para la funcionalidad de recarga en caliente de Webpack. Lea más en este número. Gracias a Nick Savrov por descubrir esto:

# You need to configure your CSP like so if you use Webpacker
Rails.application.config.content_security_policy do |p|
  p.font_src    :self, :https, :data
  p.img_src     :self, :https, :data
  p.object_src  :none
  p.style_src   :self, :https, :unsafe_inline

  if Rails.env.development?
    p.script_src :self, :https, :unsafe_eval
    p.default_src :self, :https, :unsafe_eval
    p.connect_src :self, :https, 'http://localhost:3035', 'ws://localhost:3035'
  else
    p.script_src :self, :https
    p.default_src :self, :https
  end
end

Esta es también una razón por la que el equipo de Rails ha decidido deshabilitar la configuración de CSP de forma predeterminada.

Current Todo

¿Alguna vez te has preguntado cómo acceder a current_user en tu modelo? De acuerdo con StackOverflow, aproximadamente un tercio de todas las preguntas que contienen "current_user" tratan sobre el uso de este método en un modelo.

La situación cambiará en Rails 5.2: ahora podemos agregar un singleton Current mágico que actúa como una tienda global accesible desde cualquier lugar dentro de su aplicación:

# app/models/current.rb
class Current < ActiveSupport::CurrentAttributes
  attribute :user
end

Todo lo que tiene que hacer es configurar al usuario en algún lugar de su controlador para que sea accesible en models, jobs, mailers, desde cualquier lugar:

class ApplicationController < ActionController::Base
  before_action :set_current_user

  private

  def set_current_user
    Current.user = current_user
  end
end

# and now in your model
class Post < ApplicationRecord
  # You don't have to specify the user when creating a post,
  # the current one would be used by default
  belongs_to :user, default: -> { Current.user }
end

Esta idea no es nueva; ya tenemos una gema request_store de Steve Klabnik que hace lo mismo para cualquier aplicación de Rack.

Nota: Puede decir: “¡Esta función rompe el principio de Separación de Preocupaciones!” Sí, lo hace. No lo uses si se siente mal.

Early Hints de HTTP/2

Early Hints es una característica HTTP/2 que le permite hacer que los navegadores realicen una descarga previa de los recursos antes de encontrarlos dentro del HTML de la página. De esta manera, podemos reducir el tiempo de carga de la página aprovechando la capacidad de HTTP/2 para canalizar las solicitudes.

Todo lo que tiene que hacer es ejecutar su servidor Puma con el indicador --early_hints y usar un proxy compatible con HTTP/2 delante de él (como h2o).

Lea más sobre Early Hints en esta publicación de Eileen Uchitelle, autora de la característica.

Bootsnap

Trabajar con un marco de funciones pesadas como Rails conlleva algunas concesiones, uno de ellos es el tiempo de arranque. Una aplicación monolítica grande puede tardar uno o dos minutos en iniciarse (o ejecutar una tarea).

Ahora, en la parte superior de Spring, que permite que la aplicación Rails pre-arranque entre ejecuciones, obtenemos Bootsnap, una herramienta mágica de Shopify que acelera la carga de archivos Ruby y YAML, lo que da como resultado un arranque en frío de 2 a 4 veces más rápido. Está incluido en su Gemfile por defecto.

Bootsnap es especialmente útil cuando desarrolla con Docker (donde configurar Spring no es tan trivial) o trata con servidores CI.

⚠️ **GitHub.com Fallback** ⚠️