1 ¿Qué es Active Storage?
Active Storage facilita la carga de archivos a un servicio de almacenamiento en la nube como Amazon S3, Google Cloud Storage o Microsoft Azure Storage, y la adjunta a objetos de Active Record. Viene con un servicio basado en disco local para desarrollo y pruebas, y admite la duplicación de archivos en servicios subordinados para copias de seguridad y migraciones.
Usando Active Storage, una aplicación puede transformar cargas de imágenes o generar representaciones de imágenes de cargas que no son imágenes, como PDF y videos, y extraer metadatos de archivos arbitrarios.
1.1 Requisitos
Varias características de Active Storage dependen de software de terceros que Rails no instalará y que deben instalarse por separado:
- libvips v8.6+ o ImageMagick para análisis y transformaciones de imágenes.
- ffmpeg v3.4+ para vistas previas de videos y ffprobe para análisis de video/audio.
- poppler o muPDF para vistas previas de PDF.
El análisis y las transformaciones de imágenes también requieren la gema image_processing
. Descoméntala en tu Gemfile
, o agrégala si es necesario:
gem "image_processing", ">= 1.2"
CONSEJO: En comparación con libvips, ImageMagick es más conocido y más ampliamente disponible. Sin embargo, libvips puede ser hasta 10 veces más rápido y consumir 1/10 de la memoria. Para archivos JPEG, esto se puede mejorar aún más reemplazando libjpeg-dev
con libjpeg-turbo-dev
, que es 2-7 veces más rápido.
ADVERTENCIA: Antes de instalar y usar software de terceros, asegúrate de entender las implicaciones de licencia al hacerlo. MuPDF, en particular, está licenciado bajo AGPL y requiere una licencia comercial para algunos usos.
2 Configuración
$ bin/rails active_storage:install
$ bin/rails db:migrate
Esto configura la configuración y crea las tres tablas que utiliza Active Storage: active_storage_blobs
, active_storage_attachments
y active_storage_variant_records
.
Tabla | Propósito |
---|---|
active_storage_blobs |
Almacena datos sobre los archivos cargados, como el nombre de archivo y el tipo de contenido. |
active_storage_attachments |
Una tabla de unión polimórfica que conecta tus modelos con los blobs. Si el nombre de la clase de tu modelo cambia, deberás ejecutar una migración en esta tabla para actualizar el record_type subyacente al nuevo nombre de clase de tu modelo. |
active_storage_variant_records |
Si se habilita el seguimiento de variantes, almacena registros para cada variante que se ha generado. |
ADVERTENCIA: Si estás utilizando UUID en lugar de enteros como clave primaria en tus modelos, debes configurar Rails.application.config.generators { |g| g.orm :active_record, primary_key_type: :uuid }
en un archivo de configuración.
Declara los servicios de Active Storage en config/storage.yml
. Para cada servicio que tu aplicación utiliza, proporciona un nombre y la configuración necesaria. El ejemplo a continuación declara tres servicios llamados local
, test
y amazon
:
local:
service: Disk
root: <%= Rails.root.join("storage") %>
test:
service: Disk
root: <%= Rails.root.join("tmp/storage") %>
amazon:
service: S3
access_key_id: ""
secret_access_key: ""
bucket: ""
region: "" # por ejemplo, 'us-east-1'
Indica a Active Storage qué servicio utilizar configurando Rails.application.config.active_storage.service
. Debido a que cada entorno probablemente utilizará un servicio diferente, se recomienda hacer esto de manera específica para cada entorno. Para utilizar el servicio de disco del ejemplo anterior en el entorno de desarrollo, agregarías lo siguiente a config/environments/development.rb
:
# Almacena archivos localmente.
config.active_storage.service = :local
Para utilizar el servicio de S3 en producción, agregarías lo siguiente a config/environments/production.rb
:
# Almacena archivos en Amazon S3.
config.active_storage.service = :amazon
Para utilizar el servicio de prueba durante las pruebas, agregarías lo siguiente a config/environments/test.rb
:
# Almacena archivos cargados en el sistema de archivos local en un directorio temporal.
config.active_storage.service = :test
NOTA: Los archivos de configuración específicos del entorno tendrán prioridad: en producción, por ejemplo, el archivo config/storage/production.yml
(si existe) tendrá prioridad sobre el archivo config/storage.yml
.
Se recomienda utilizar Rails.env
en los nombres de los buckets para reducir aún más el riesgo de destruir accidentalmente datos de producción.
amazon:
service: S3
# ...
bucket: tu_propio_bucket-<%= Rails.env %>
google:
service: GCS
# ...
bucket: tu_propio_bucket-<%= Rails.env %>
azure:
service: AzureStorage
# ...
container: tu_nombre_de_contenedor-<%= Rails.env %>
Sigue leyendo para obtener más información sobre los adaptadores de servicio integrados (por ejemplo, Disk
y S3
) y la configuración que requieren.
2.1 Servicio de Disco
Declara un servicio de Disco en config/storage.yml
:
local:
service: Disk
root: <%= Rails.root.join("storage") %>
2.2 Servicio S3 (Amazon S3 y APIs compatibles con S3)
Para conectarse a Amazon S3, declara un servicio S3 en config/storage.yml
:
amazon:
service: S3
access_key_id: ""
secret_access_key: ""
region: ""
bucket: ""
Opcionalmente, proporciona opciones de cliente y carga:
amazon:
service: S3
access_key_id: ""
secret_access_key: ""
region: ""
bucket: ""
http_open_timeout: 0
http_read_timeout: 0
retry_limit: 0
upload:
server_side_encryption: "" # 'aws:kms' o 'AES256'
cache_control: "private, max-age=<%= 1.day.to_i %>"
CONSEJO: Establece tiempos de espera y límites de reintento HTTP sensatos para tu aplicación. En ciertos escenarios de fallos, la configuración predeterminada del cliente de AWS puede hacer que las conexiones se mantengan durante varios minutos y provoquen la acumulación de solicitudes.
Agrega la gema aws-sdk-s3
a tu Gemfile
:
gem "aws-sdk-s3", require: false
NOTA: Las características principales de Active Storage requieren los siguientes permisos: s3:ListBucket
, s3:PutObject
, s3:GetObject
y s3:DeleteObject
. El acceso público adicionalmente requiere s3:PutObjectAcl
. Si tienes opciones de carga adicionales configuradas, como la configuración de ACL, es posible que se requieran permisos adicionales.
NOTA: Si deseas utilizar variables de entorno, archivos de configuración estándar del SDK, perfiles, perfiles de instancia IAM o roles de tareas, puedes omitir las claves access_key_id
, secret_access_key
y region
en el ejemplo anterior. El servicio S3 admite todas las opciones de autenticación descritas en la documentación del SDK de AWS.
Para conectarse a una API de almacenamiento de objetos compatible con S3, como DigitalOcean Spaces, proporciona el endpoint
:
digitalocean:
service: S3
endpoint: https://nyc3.digitaloceanspaces.com
access_key_id: ...
secret_access_key: ...
# ...y otras opciones
Hay muchas otras opciones disponibles. Puedes consultarlas en la documentación de AWS S3 Client.
2.3 Servicio de Almacenamiento de Microsoft Azure
Declara un servicio de almacenamiento de Azure en config/storage.yml
:
azure:
service: AzureStorage
storage_account_name: ""
storage_access_key: ""
container: ""
Agrega la gema azure-storage-blob
a tu Gemfile
:
gem "azure-storage-blob", "~> 2.0", require: false
2.4 Servicio de Almacenamiento de Google Cloud
Declara un servicio de almacenamiento de Google Cloud en config/storage.yml
:
google:
service: GCS
credentials: <%= Rails.root.join("path/to/keyfile.json") %>
project: ""
bucket: ""
Opcionalmente, proporciona un Hash de credenciales en lugar de una ruta de archivo de clave:
google:
service: GCS
credentials:
type: "service_account"
project_id: ""
private_key_id: <%= Rails.application.credentials.dig(:gcs, :private_key_id) %>
private_key: <%= Rails.application.credentials.dig(:gcs, :private_key).dump %>
client_email: ""
client_id: ""
auth_uri: "https://accounts.google.com/o/oauth2/auth"
token_uri: "https://accounts.google.com/o/oauth2/token"
auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs"
client_x509_cert_url: ""
project: ""
bucket: ""
Opcionalmente, proporciona una metadatos Cache-Control para establecer en los activos cargados:
google:
service: GCS
...
cache_control: "public, max-age=3600"
Opcionalmente, utiliza IAM en lugar de credentials
al firmar URLs. Esto es útil si estás autenticando tus aplicaciones de GKE con Workload Identity, consulta esta publicación de blog de Google Cloud para obtener más información.
google:
service: GCS
...
iam: true
Opcionalmente, utiliza un GSA específico al firmar URLs. Cuando se utiliza IAM, se contactará al servidor de metadatos para obtener el correo electrónico del GSA, pero este servidor de metadatos no siempre está presente (por ejemplo, en pruebas locales) y es posible que desees utilizar un GSA no predeterminado.
google:
service: GCS
...
iam: true
gsa_email: "[email protected]"
Agrega la gema google-cloud-storage
a tu Gemfile
:
gem "google-cloud-storage", "~> 1.11", require: false
2.5 Servicio de Espejo
Puedes mantener varios servicios sincronizados definiendo un servicio de espejo. Un servicio de espejo replica las cargas y eliminaciones en dos o más servicios subordinados.
Un servicio de espejo está destinado a ser utilizado temporalmente durante una migración entre servicios en producción. Puedes comenzar a reflejar en un nuevo servicio, copiar archivos preexistentes del antiguo al nuevo, y luego utilizar completamente el nuevo servicio.
NOTA: La sincronización no es atómica. Es posible que una carga se realice correctamente en el servicio principal y falle en cualquiera de los servicios subordinados. Antes de utilizar completamente un nuevo servicio, verifica que se hayan copiado todos los archivos.
Define cada uno de los servicios que deseas reflejar como se describe anteriormente. Haz referencia a ellos por su nombre al definir un servicio de espejo:
s3_west_coast:
service: S3
access_key_id: ""
secret_access_key: ""
region: ""
bucket: ""
s3_east_coast:
service: S3
access_key_id: ""
secret_access_key: ""
region: ""
bucket: ""
production:
service: Mirror
primary: s3_east_coast
mirrors:
- s3_west_coast
Aunque todos los servicios secundarios reciben cargas, las descargas siempre son manejadas por el servicio principal.
Los servicios de espejo son compatibles con las cargas directas. Los archivos nuevos se cargan directamente en el servicio principal. Cuando un archivo cargado directamente se adjunta a un registro, se encola un trabajo en segundo plano para copiarlo en los servicios secundarios.
2.6 Acceso público
Por defecto, Active Storage asume acceso privado a los servicios. Esto significa generar URLs firmadas y de un solo uso para los blobs. Si prefieres que los blobs sean accesibles públicamente, especifica public: true
en el archivo config/storage.yml
de tu aplicación:
gcs: &gcs
service: GCS
project: ""
private_gcs:
<<: *gcs
credentials: <%= Rails.root.join("path/to/private_key.json") %>
bucket: ""
public_gcs:
<<: *gcs
credentials: <%= Rails.root.join("path/to/public_key.json") %>
bucket: ""
public: true
Asegúrate de que tus buckets estén correctamente configurados para el acceso público. Consulta la documentación sobre cómo habilitar los permisos de lectura pública para los servicios de almacenamiento de Amazon S3, Google Cloud Storage y Microsoft Azure. Amazon S3 también requiere que tengas el permiso s3:PutObjectAcl
.
Cuando conviertas una aplicación existente para usar public: true
, asegúrate de actualizar cada archivo individual en el bucket para que sea legible públicamente antes de hacer el cambio.
3 Adjuntar archivos a registros
3.1 has_one_attached
La macro has_one_attached
establece una relación uno a uno entre registros y archivos. Cada registro puede tener un archivo adjunto.
Por ejemplo, supongamos que tu aplicación tiene un modelo User
. Si quieres que cada usuario tenga un avatar, define el modelo User
de la siguiente manera:
class User < ApplicationRecord
has_one_attached :avatar
end
o si estás usando Rails 6.0+, puedes ejecutar un comando generador de modelo como este:
bin/rails generate model User avatar:attachment
Puedes crear un usuario con un avatar:
<%= form.file_field :avatar %>
class SignupController < ApplicationController
def create
user = User.create!(user_params)
session[:user_id] = user.id
redirect_to root_path
end
private
def user_params
params.require(:user).permit(:email_address, :password, :avatar)
end
end
Llama a avatar.attach
para adjuntar un avatar a un usuario existente:
user.avatar.attach(params[:avatar])
Llama a avatar.attached?
para determinar si un usuario en particular tiene un avatar:
user.avatar.attached?
En algunos casos, es posible que desees anular un servicio predeterminado para un adjunto específico. Puedes configurar servicios específicos por adjunto utilizando la opción service
:
class User < ApplicationRecord
has_one_attached :avatar, service: :s3
end
Puedes configurar variantes específicas por adjunto llamando al método variant
en el objeto adjunto proporcionado:
class User < ApplicationRecord
has_one_attached :avatar do |attachable|
attachable.variant :thumb, resize_to_limit: [100, 100]
end
end
Llama a avatar.variant(:thumb)
para obtener una variante de pulgar de un avatar:
<%= image_tag user.avatar.variant(:thumb) %>
También puedes usar variantes específicas para las vistas previas:
class User < ApplicationRecord
has_one_attached :video do |attachable|
attachable.variant :thumb, resize_to_limit: [100, 100]
end
end
<%= image_tag user.video.preview(:thumb) %>
3.2 has_many_attached
La macro has_many_attached
establece una relación uno a muchos entre registros y archivos. Cada registro puede tener muchos archivos adjuntos.
Por ejemplo, supongamos que tu aplicación tiene un modelo Message
. Si quieres que cada mensaje tenga muchas imágenes, define el modelo Message
de la siguiente manera:
class Message < ApplicationRecord
has_many_attached :images
end
o si estás usando Rails 6.0+, puedes ejecutar un comando generador de modelo como este:
bin/rails generate model Message images:attachments
Puedes crear un mensaje con imágenes:
class MessagesController < ApplicationController
def create
message = Message.create!(message_params)
redirect_to message
end
private
def message_params
params.require(:message).permit(:title, :content, images: [])
end
end
Llama a images.attach
para agregar nuevas imágenes a un mensaje existente:
@message.images.attach(params[:images])
Llama a images.attached?
para determinar si un mensaje en particular tiene imágenes:
@message.images.attached?
La anulación del servicio predeterminado se realiza de la misma manera que has_one_attached
, utilizando la opción service
:
class Message < ApplicationRecord
has_many_attached :images, service: :s3
end
La configuración de variantes específicas se realiza de la misma manera que has_one_attached
, llamando al método variant
en el objeto adjunto proporcionado:
class Message < ApplicationRecord
has_many_attached :images do |attachable|
attachable.variant :thumb, resize_to_limit: [100, 100]
end
end
3.3 Adjuntar objetos de archivo/IO
A veces necesitas adjuntar un archivo que no llega a través de una solicitud HTTP. Por ejemplo, es posible que desees adjuntar un archivo que generaste en el disco o descargaste desde una URL enviada por el usuario. También es posible que desees adjuntar un archivo de prueba en un modelo. Para hacer eso, proporciona un Hash que contenga al menos un objeto IO abierto y un nombre de archivo:
@message.images.attach(io: File.open('/path/to/file'), filename: 'file.pdf')
Cuando sea posible, proporciona también un tipo de contenido. Active Storage intenta determinar el tipo de contenido de un archivo a partir de sus datos. Si no puede hacerlo, utilizará el tipo de contenido que proporciones.
ruby
@message.images.attach(io: File.open('/path/to/file'), filename: 'file.pdf', content_type: 'application/pdf')
Puede evitar la inferencia del tipo de contenido de los datos pasando
identify: false
junto con content_type
.
@message.images.attach(
io: File.open('/path/to/file'),
filename: 'file.pdf',
content_type: 'application/pdf',
identify: false
)
Si no proporciona un tipo de contenido y Active Storage no puede determinar automáticamente el tipo de contenido del archivo, se establece por defecto en application/octet-stream.
4 Eliminación de archivos
Para eliminar un archivo adjunto de un modelo, llame a purge
en el
archivo adjunto. Si su aplicación está configurada para usar Active Job, la eliminación se puede hacer
en segundo plano llamando a purge_later
.
La eliminación borra el blob y el archivo del servicio de almacenamiento.
# Destruye sincrónicamente el avatar y los archivos de recursos reales.
user.avatar.purge
# Destruye los modelos asociados y los archivos de recursos reales de forma asincrónica, a través de Active Job.
user.avatar.purge_later
5 Servicio de archivos
Active Storage admite dos formas de servir archivos: redireccionamiento y proxy.
ADVERTENCIA: Todos los controladores de Active Storage son accesibles públicamente de forma predeterminada. Las URL generadas son difíciles de adivinar, pero permanentes por diseño. Si sus archivos requieren un nivel más alto de protección, considere implementar Controladores Autenticados.
5.1 Modo de redireccionamiento
Para generar una URL permanente para un blob, puede pasar el blob al
ayudante de vista url_for
. Esto genera una
URL con el signed_id
del blob
que se enruta al RedirectController
del blob.
url_for(user.avatar)
# => /rails/active_storage/blobs/:signed_id/my-avatar.png
El RedirectController
redirige al punto final de servicio real. Esto
desacopla la URL del servicio de la URL real y permite, por ejemplo, reflejar los archivos adjuntos en diferentes servicios para una alta disponibilidad. La
redirección tiene una expiración HTTP de 5 minutos.
Para crear un enlace de descarga, use el ayudante rails_blob_{path|url}
. Usando este
ayudante le permite establecer la disposición.
rails_blob_path(user.avatar, disposition: "attachment")
ADVERTENCIA: Para evitar ataques XSS, Active Storage fuerza la cabecera Content-Disposition a "attachment" para algunos tipos de archivos. Para cambiar este comportamiento, consulte las opciones de configuración disponibles en Configuración de aplicaciones Rails.
Si necesita crear un enlace desde fuera del contexto del controlador/vista (trabajos en segundo plano, Cronjobs, etc.), puede acceder a rails_blob_path
de esta manera:
Rails.application.routes.url_helpers.rails_blob_path(user.avatar, only_path: true)
5.2 Modo de proxy
Opcionalmente, los archivos también se pueden servir mediante proxy. Esto significa que los servidores de su aplicación descargarán los datos del archivo desde el servicio de almacenamiento en respuesta a las solicitudes. Esto puede ser útil para servir archivos desde una CDN.
Puede configurar Active Storage para que use el proxy de forma predeterminada:
# config/initializers/active_storage.rb
Rails.application.config.active_storage.resolve_model_to_route = :rails_storage_proxy
O si desea proxy explícitamente adjuntos específicos, hay ayudantes de URL que puede usar en forma de rails_storage_proxy_path
y rails_storage_proxy_url
.
<%= image_tag rails_storage_proxy_path(@user.avatar) %>
5.2.1 Poner una CDN delante de Active Storage
Además, para usar una CDN para los archivos adjuntos de Active Storage, deberá generar URL con el modo de proxy para que sean servidos por su aplicación y la CDN almacenará en caché el archivo adjunto sin ninguna configuración adicional. Esto funciona de forma predeterminada porque el controlador de proxy de Active Storage establece una cabecera HTTP que indica a la CDN que almacene en caché la respuesta.
También debe asegurarse de que las URL generadas utilicen el host de la CDN en lugar del host de su aplicación. Hay varias formas de lograr esto, pero en general implica ajustar su archivo config/routes.rb
para que pueda generar las URL adecuadas para los archivos adjuntos y sus variaciones. Como ejemplo, podría agregar esto:
# config/routes.rb
direct :cdn_image do |model, options|
expires_in = options.delete(:expires_in) { ActiveStorage.urls_expire_in }
if model.respond_to?(:signed_id)
route_for(
:rails_service_blob_proxy,
model.signed_id(expires_in: expires_in),
model.filename,
options.merge(host: ENV['CDN_HOST'])
)
else
signed_blob_id = model.blob.signed_id(expires_in: expires_in)
variation_key = model.variation.key
filename = model.blob.filename
route_for(
:rails_blob_representation_proxy,
signed_blob_id,
variation_key,
filename,
options.merge(host: ENV['CDN_HOST'])
)
end
end
y luego generar rutas de esta manera:
<%= cdn_image_url(user.avatar.variant(resize_to_limit: [128, 128])) %>
5.3 Controladores autenticados
Todos los controladores de Active Storage son accesibles públicamente de forma predeterminada. Las
URL generadas utilizan un signed_id
simple, lo que las hace difíciles de
adivinar pero permanentes. Cualquier persona que conozca la URL del blob podrá acceder a ella,
incluso si un before_action
en su ApplicationController
requeriría iniciar sesión. Si sus archivos requieren un nivel más alto de protección, puede
implementar sus propios controladores autenticados, basados en
ActiveStorage::Blobs::RedirectController
,
ActiveStorage::Blobs::ProxyController
,
ActiveStorage::Representations::RedirectController
y
ActiveStorage::Representations::ProxyController
Para permitir que solo una cuenta acceda a su propio logotipo, puede hacer lo siguiente: ```ruby
config/routes.rb
resource :account do resource :logo end ```
# app/controllers/logos_controller.rb
class LogosController < ApplicationController
# A través de ApplicationController:
# incluir Autenticar, EstablecerCuentaActual
def show
redirect_to Current.account.logo.url
end
end
<%= image_tag account_logo_path %>
Y luego debes desactivar las rutas predeterminadas de Active Storage con:
config.active_storage.draw_routes = false
para evitar que los archivos se accedan con las URL públicamente accesibles.
6 Descargar archivos
A veces necesitas procesar un blob después de que se haya cargado, por ejemplo, para convertirlo a un formato diferente. Usa el método download
del adjunto para leer los datos binarios de un blob en memoria:
binary = user.avatar.download
Es posible que desees descargar un blob a un archivo en disco para que un programa externo (por ejemplo, un escáner de virus o un transcodificador de medios) pueda operar en él. Usa el método open
del adjunto para descargar un blob a un archivo temporal en disco:
message.video.open do |file|
system '/ruta/al/escáner/de/virus', file.path
# ...
end
Es importante saber que el archivo aún no está disponible en el callback after_create
, sino solo en after_create_commit
.
7 Análisis de archivos
Active Storage analiza los archivos una vez que se han cargado encolando un trabajo en Active Job. Los archivos analizados almacenarán información adicional en el hash de metadatos, incluyendo analyzed: true
. Puedes verificar si un blob ha sido analizado llamando a analyzed?
en él.
El análisis de imágenes proporciona los atributos width
y height
. El análisis de video proporciona estos, así como duration
, angle
, display_aspect_ratio
y booleanos video
y audio
para indicar la presencia de esos canales. El análisis de audio proporciona los atributos duration
y bit_rate
.
8 Mostrar imágenes, videos y PDFs
Active Storage admite la representación de una variedad de archivos. Puedes llamar a representation
en un adjunto para mostrar una variante de imagen, o una vista previa de un video o PDF. Antes de llamar a representation
, verifica si el adjunto se puede representar llamando a representable?
. Algunos formatos de archivo no se pueden previsualizar con Active Storage de forma predeterminada (por ejemplo, documentos de Word); si representable?
devuelve false, es posible que desees enlazar al archivo en su lugar.
<ul>
<% @message.files.each do |file| %>
<li>
<% if file.representable? %>
<%= image_tag file.representation(resize_to_limit: [100, 100]) %>
<% else %>
<%= link_to rails_blob_path(file, disposition: "attachment") do %>
<%= image_tag "placeholder.png", alt: "Descargar archivo" %>
<% end %>
<% end %>
</li>
<% end %>
</ul>
Internamente, representation
llama a variant
para imágenes, y a preview
para archivos que se pueden previsualizar. También puedes llamar a estos métodos directamente.
8.1 Carga diferida vs inmediata
De forma predeterminada, Active Storage procesará las representaciones de forma diferida. Este código:
image_tag file.representation(resize_to_limit: [100, 100])
Generará una etiqueta <img>
con el src
apuntando al
ActiveStorage::Representations::RedirectController
. El navegador realizará
una solicitud a ese controlador, que realizará lo siguiente:
- Procesar el archivo y cargar el archivo procesado si es necesario.
- Devolver una redirección
302
al archivo ya sea a- el servicio remoto (por ejemplo, S3).
- o a
ActiveStorage::Blobs::ProxyController
que devolverá el contenido del archivo si el modo de proxy está habilitado.
La carga diferida del archivo permite que funciones como URL de un solo uso funcionen sin ralentizar la carga inicial de la página.
Esto funciona bien para la mayoría de los casos.
Si deseas generar URL para imágenes de inmediato, puedes llamar a .processed.url
:
image_tag file.representation(resize_to_limit: [100, 100]).processed.url
El rastreador de variantes de Active Storage mejora el rendimiento de esto, almacenando un
registro en la base de datos si la representación solicitada se ha procesado antes.
Por lo tanto, el código anterior solo realizará una llamada a la API del servicio remoto (por ejemplo, S3)
una vez, y una vez que se almacene una variante, la utilizará. El rastreador de variantes se ejecuta
automáticamente, pero se puede desactivar a través de config.active_storage.track_variants
.
Si estás renderizando muchas imágenes en una página, el ejemplo anterior podría resultar
en consultas N+1 cargando todos los registros de variantes. Para evitar estas consultas N+1,
utiliza los ámbitos nombrados en ActiveStorage::Attachment
.
message.images.with_all_variant_records.each do |file|
image_tag file.representation(resize_to_limit: [100, 100]).processed.url
end
8.2 Transformar imágenes
La transformación de imágenes te permite mostrar la imagen en las dimensiones que elijas.
Para crear una variación de una imagen, llama a variant
en el adjunto. Puedes
pasar cualquier transformación admitida por el procesador de variantes al método.
Cuando el navegador accede a la URL de la variante, Active Storage transformará
perezosamente el blob original en el formato especificado y redireccionará a su nueva
ubicación de servicio.
<%= image_tag user.avatar.variant(resize_to_limit: [100, 100]) %>
Si se solicita una variante, Active Storage aplicará automáticamente transformaciones dependiendo del formato de la imagen:
Los tipos de contenido que son variables (según lo dictado por
config.active_storage.variable_content_types
) y no se consideran imágenes web (según lo dictado porconfig.active_storage.web_image_content_types
), se convertirán a PNG.Si no se especifica
quality
, se utilizará la calidad predeterminada del procesador de variantes para el formato.
Active Storage puede utilizar tanto Vips como MiniMagick como procesador de variantes. El predeterminado depende de la versión objetivo de config.load_defaults
, y el procesador se puede cambiar configurando config.active_storage.variant_processor
.
Los dos procesadores no son completamente compatibles, por lo que al migrar una aplicación existente entre MiniMagick y Vips, se deben realizar algunos cambios si se utilizan opciones específicas del formato:
<!-- MiniMagick -->
<%= image_tag user.avatar.variant(resize_to_limit: [100, 100], format: :jpeg, sampling_factor: "4:2:0", strip: true, interlace: "JPEG", colorspace: "sRGB", quality: 80) %>
<!-- Vips -->
<%= image_tag user.avatar.variant(resize_to_limit: [100, 100], format: :jpeg, saver: { subsample_mode: "on", strip: true, interlace: true, quality: 80 }) %>
Los parámetros disponibles están definidos por la gema image_processing
y dependen del procesador de variantes que estés utilizando, pero ambos admiten los siguientes parámetros:
Parámetro | Ejemplo | Descripción |
---|---|---|
resize_to_limit |
resize_to_limit: [100, 100] |
Reduce el tamaño de la imagen para que se ajuste a las dimensiones especificadas mientras se mantiene la relación de aspecto original. Solo redimensionará la imagen si es más grande que las dimensiones especificadas. |
resize_to_fit |
resize_to_fit: [100, 100] |
Redimensiona la imagen para que se ajuste a las dimensiones especificadas mientras se mantiene la relación de aspecto original. Reducirá el tamaño de la imagen si es más grande que las dimensiones especificadas o aumentará el tamaño si es más pequeña. |
resize_to_fill |
resize_to_fill: [100, 100] |
Redimensiona la imagen para que llene las dimensiones especificadas mientras se mantiene la relación de aspecto original. Si es necesario, recortará la imagen en la dimensión más grande. |
resize_and_pad |
resize_and_pad: [100, 100] |
Redimensiona la imagen para que se ajuste a las dimensiones especificadas mientras se mantiene la relación de aspecto original. Si es necesario, rellenará el área restante con un color transparente si la imagen de origen tiene un canal alfa, de lo contrario, será negro. |
crop |
crop: [20, 50, 300, 300] |
Extrae un área de una imagen. Los dos primeros argumentos son los bordes izquierdo y superior del área a extraer, mientras que los dos últimos argumentos son el ancho y la altura del área a extraer. |
rotate |
rotate: 90 |
Rota la imagen el ángulo especificado. |
image_processing
tiene más opciones disponibles (como saver
, que permite configurar la compresión de la imagen) en su propia documentación para los procesadores Vips y MiniMagick.
8.3 Vista previa de archivos
Algunos archivos que no son imágenes se pueden previsualizar, es decir, se pueden presentar como imágenes. Por ejemplo, se puede previsualizar un archivo de video extrayendo su primer fotograma. De forma predeterminada, Active Storage admite la previsualización de videos y documentos PDF. Para crear un enlace a una vista previa generada de forma diferida, utiliza el método preview
del adjunto:
<%= image_tag message.video.preview(resize_to_limit: [100, 100]) %>
Para agregar soporte para otro formato, agrega tu propio generador de vistas previas. Consulta la documentación de ActiveStorage::Preview
para obtener más información.
9 Cargas directas
Active Storage, con su biblioteca JavaScript incluida, admite la carga directa desde el cliente a la nube.
9.1 Uso
Incluye
activestorage.js
en el paquete de JavaScript de tu aplicación.Usando el pipeline de activos:
//= require activestorage
Usando el paquete npm:
import * as ActiveStorage from "@rails/activestorage" ActiveStorage.start()
Agrega
direct_upload: true
a tu campo de archivo:<%= form.file_field :attachments, multiple: true, direct_upload: true %>
O, si no estás utilizando un
FormBuilder
, agrega el atributo de datos directamente:<input type="file" data-direct-upload-url="<%= rails_direct_uploads_url %>" />
Configura CORS en los servicios de almacenamiento de terceros para permitir solicitudes de carga directa.
¡Eso es todo! Las cargas comienzan al enviar el formulario.
9.2 Configuración de intercambio de recursos de origen cruzado (CORS)
Para que las cargas directas a un servicio de terceros funcionen, deberás configurar el servicio para permitir solicitudes de origen cruzado desde tu aplicación. Consulta la documentación de CORS de tu servicio:
Asegúrate de permitir:
- Todos los orígenes desde los cuales se accede a tu aplicación
- El método de solicitud
PUT
- Los siguientes encabezados:
Origin
Content-Type
Content-MD5
Content-Disposition
(excepto para Azure Storage)x-ms-blob-content-disposition
(solo para Azure Storage)x-ms-blob-type
(solo para Azure Storage)Cache-Control
(para GCS, solo si se establececache_control
) No se requiere configuración CORS para el servicio de Disco ya que comparte el origen de tu aplicación.
9.2.1 Ejemplo: Configuración CORS de S3
[
{
"AllowedHeaders": [
"*"
],
"AllowedMethods": [
"PUT"
],
"AllowedOrigins": [
"https://www.example.com"
],
"ExposeHeaders": [
"Origin",
"Content-Type",
"Content-MD5",
"Content-Disposition"
],
"MaxAgeSeconds": 3600
}
]
9.2.2 Ejemplo: Configuración CORS de Google Cloud Storage
[
{
"origin": ["https://www.example.com"],
"method": ["PUT"],
"responseHeader": ["Origin", "Content-Type", "Content-MD5", "Content-Disposition"],
"maxAgeSeconds": 3600
}
]
9.2.3 Ejemplo: Configuración CORS de Azure Storage
<Cors>
<CorsRule>
<AllowedOrigins>https://www.example.com</AllowedOrigins>
<AllowedMethods>PUT</AllowedMethods>
<AllowedHeaders>Origin, Content-Type, Content-MD5, x-ms-blob-content-disposition, x-ms-blob-type</AllowedHeaders>
<MaxAgeInSeconds>3600</MaxAgeInSeconds>
</CorsRule>
</Cors>
9.3 Eventos de JavaScript de carga directa
Nombre del evento | Objetivo del evento | Datos del evento (event.detail ) |
Descripción |
---|---|---|---|
direct-uploads:start |
<form> |
Ninguno | Se envió un formulario que contiene archivos para campos de carga directa. |
direct-upload:initialize |
<input> |
{id, file} |
Se envía para cada archivo después del envío del formulario. |
direct-upload:start |
<input> |
{id, file} |
Comienza una carga directa. |
direct-upload:before-blob-request |
<input> |
{id, file, xhr} |
Antes de hacer una solicitud a tu aplicación para obtener metadatos de carga directa. |
direct-upload:before-storage-request |
<input> |
{id, file, xhr} |
Antes de hacer una solicitud para almacenar un archivo. |
direct-upload:progress |
<input> |
{id, file, progress} |
A medida que avanzan las solicitudes para almacenar archivos. |
direct-upload:error |
<input> |
{id, file, error} |
Ocurrió un error. Se mostrará una alerta a menos que se cancele este evento. |
direct-upload:end |
<input> |
{id, file} |
Finaliza una carga directa. |
direct-uploads:end |
<form> |
Ninguno | Todas las cargas directas han finalizado. |
9.4 Ejemplo
Puedes usar estos eventos para mostrar el progreso de una carga.
Para mostrar los archivos cargados en un formulario:
// direct_uploads.js
addEventListener("direct-upload:initialize", event => {
const { target, detail } = event
const { id, file } = detail
target.insertAdjacentHTML("beforebegin", `
<div id="direct-upload-${id}" class="direct-upload direct-upload--pending">
<div id="direct-upload-progress-${id}" class="direct-upload__progress" style="width: 0%"></div>
<span class="direct-upload__filename"></span>
</div>
`)
target.previousElementSibling.querySelector(`.direct-upload__filename`).textContent = file.name
})
addEventListener("direct-upload:start", event => {
const { id } = event.detail
const element = document.getElementById(`direct-upload-${id}`)
element.classList.remove("direct-upload--pending")
})
addEventListener("direct-upload:progress", event => {
const { id, progress } = event.detail
const progressElement = document.getElementById(`direct-upload-progress-${id}`)
progressElement.style.width = `${progress}%`
})
addEventListener("direct-upload:error", event => {
event.preventDefault()
const { id, error } = event.detail
const element = document.getElementById(`direct-upload-${id}`)
element.classList.add("direct-upload--error")
element.setAttribute("title", error)
})
addEventListener("direct-upload:end", event => {
const { id } = event.detail
const element = document.getElementById(`direct-upload-${id}`)
element.classList.add("direct-upload--complete")
})
Agrega estilos:
/* direct_uploads.css */
.direct-upload {
display: inline-block;
position: relative;
padding: 2px 4px;
margin: 0 3px 3px 0;
border: 1px solid rgba(0, 0, 0, 0.3);
border-radius: 3px;
font-size: 11px;
line-height: 13px;
}
.direct-upload--pending {
opacity: 0.6;
}
.direct-upload__progress {
position: absolute;
top: 0;
left: 0;
bottom: 0;
opacity: 0.2;
background: #0076ff;
transition: width 120ms ease-out, opacity 60ms 60ms ease-in;
transform: translate3d(0, 0, 0);
}
.direct-upload--complete .direct-upload__progress {
opacity: 0.4;
}
.direct-upload--error {
border-color: red;
}
input[type=file][data-direct-upload-url][disabled] {
display: none;
}
9.5 Soluciones personalizadas de arrastrar y soltar
Puedes usar la clase DirectUpload
para este propósito. Al recibir un archivo de tu biblioteca
de elección, instancia un objeto DirectUpload y llama a su método create. Create toma
un callback para invocar cuando se complete la carga.
import { DirectUpload } from "@rails/activestorage"
const input = document.querySelector('input[type=file]')
// Vincular a la caída de archivos - usar el ondrop en un elemento padre o usar una
// biblioteca como Dropzone
const onDrop = (event) => {
event.preventDefault()
const files = event.dataTransfer.files;
Array.from(files).forEach(file => uploadFile(file))
}
// Vincular a la selección normal de archivos
input.addEventListener('change', (event) => {
Array.from(input.files).forEach(file => uploadFile(file))
// puedes borrar los archivos seleccionados del input
input.value = null
})
const uploadFile = (file) => {
// tu formulario necesita el campo de archivo con direct_upload: true, que
// proporciona data-direct-upload-url
const url = input.dataset.directUploadUrl
const upload = new DirectUpload(file, url)
upload.create((error, blob) => {
if (error) {
// Maneja el error
} else {
// Agrega un campo oculto con el nombre adecuado al formulario con un
// valor de blob.signed_id para que los IDs de los blobs se transmitan
// en el flujo de carga normal
const hiddenField = document.createElement('input')
hiddenField.setAttribute("type", "hidden");
hiddenField.setAttribute("value", blob.signed_id);
hiddenField.name = input.name
document.querySelector('form').appendChild(hiddenField)
}
})
}
9.6 Seguimiento del progreso de la carga del archivo
Cuando se utiliza el constructor DirectUpload
, es posible incluir un tercer parámetro.
Esto permitirá que el objeto DirectUpload
invoque el método directUploadWillStoreFileWithXHR
durante el proceso de carga.
Luego puedes adjuntar tu propio controlador de progreso al XHR según tus necesidades.
```js
import { DirectUpload } from "@rails/activestorage"
class Uploader { constructor(file, url) { this.upload = new DirectUpload(this.file, this.url, this) }
upload(file) { this.upload.create((error, blob) => { if (error) { // Manejar el error } else { // Agregar un input oculto con el nombre apropiado al formulario // con un valor de blob.signed_id } }) }
directUploadWillStoreFileWithXHR(request) { request.upload.addEventListener("progress", event => this.directUploadDidProgress(event)) }
directUploadDidProgress(event) { // Usar event.loaded y event.total para actualizar la barra de progreso } } ```
9.7 Integración con bibliotecas o frameworks
Una vez que recibas un archivo de la biblioteca que hayas seleccionado, debes crear
una instancia de DirectUpload
y utilizar su método "create" para iniciar el proceso de carga,
agregando cualquier encabezado adicional requerido según sea necesario. El método "create" también requiere
que se proporcione una función de devolución de llamada que se activará una vez que la carga haya finalizado.
import { DirectUpload } from "@rails/activestorage"
class Uploader {
constructor(file, url, token) {
const headers = { 'Authentication': `Bearer ${token}` }
// INFO: El envío de encabezados es un parámetro opcional. Si decides no enviar encabezados,
// la autenticación se realizará utilizando cookies o datos de sesión.
this.upload = new DirectUpload(this.file, this.url, this, headers)
}
upload(file) {
this.upload.create((error, blob) => {
if (error) {
// Manejar el error
} else {
// Usar blob.signed_id como referencia de archivo en la siguiente solicitud
}
})
}
directUploadWillStoreFileWithXHR(request) {
request.upload.addEventListener("progress",
event => this.directUploadDidProgress(event))
}
directUploadDidProgress(event) {
// Usar event.loaded y event.total para actualizar la barra de progreso
}
}
Para implementar la autenticación personalizada, se debe crear un nuevo controlador en la aplicación de Rails, similar al siguiente:
class DirectUploadsController < ActiveStorage::DirectUploadsController
skip_forgery_protection
before_action :authenticate!
def authenticate!
@token = request.headers['Authorization']&.split&.last
return head :unauthorized unless valid_token?(@token)
end
end
NOTA: El uso de Direct Uploads a veces puede resultar en un archivo que se carga, pero que nunca se adjunta a un registro. Considera eliminar las cargas no adjuntas.
10 Pruebas
Utiliza fixture_file_upload
para probar la carga de un archivo en una prueba de integración o controlador.
Rails maneja los archivos como cualquier otro parámetro.
class SignupController < ActionDispatch::IntegrationTest
test "puede registrarse" do
post signup_path, params: {
name: "David",
avatar: fixture_file_upload("david.png", "image/png")
}
user = User.order(:created_at).last
assert user.avatar.attached?
end
end
10.1 Descartar archivos creados durante las pruebas
10.1.1 Pruebas de sistema
Las pruebas de sistema limpian los datos de prueba deshaciendo una transacción. Como nunca se llama a destroy
en un objeto, los archivos adjuntos nunca se limpian. Si deseas borrar los archivos, puedes hacerlo en un
callback after_teardown
. Hacerlo aquí asegura que todas las conexiones creadas durante la prueba estén completas y
no recibirás un error de Active Storage diciendo que no puede encontrar un archivo.
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
# ...
def after_teardown
super
FileUtils.rm_rf(ActiveStorage::Blob.service.root)
end
# ...
end
Si estás utilizando pruebas paralelas y el servicio DiskService
, debes configurar cada proceso para que utilice su propia
carpeta para Active Storage. De esta manera, el callback teardown
solo eliminará los archivos de las pruebas del proceso relevante.
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
# ...
parallelize_setup do |i|
ActiveStorage::Blob.service.root = "#{ActiveStorage::Blob.service.root}-#{i}"
end
# ...
end
Si tus pruebas de sistema verifican la eliminación de un modelo con archivos adjuntos y estás utilizando Active Job, configura tu entorno de prueba para usar el adaptador de cola en línea para que el trabajo de purga se ejecute inmediatamente en lugar de en un momento desconocido en el futuro.
# Utiliza el procesamiento de trabajos en línea para que las cosas sucedan de inmediato
config.active_job.queue_adapter = :inline
10.1.2 Pruebas de integración
De manera similar a las pruebas de sistema, los archivos cargados durante las pruebas de integración no se
eliminan automáticamente. Si deseas borrar los archivos, puedes hacerlo en un callback teardown
.
class ActionDispatch::IntegrationTest
def after_teardown
super
FileUtils.rm_rf(ActiveStorage::Blob.service.root)
end
end
Si estás utilizando pruebas paralelas y el servicio Disk, debes configurar cada proceso para que utilice su propia
carpeta para Active Storage. De esta manera, el callback teardown
solo eliminará los archivos de las pruebas del proceso relevante.
class ActionDispatch::IntegrationTest
parallelize_setup do |i|
ActiveStorage::Blob.service.root = "#{ActiveStorage::Blob.service.root}-#{i}"
end
end
10.2 Agregar archivos adjuntos a fixtures
Puedes agregar archivos adjuntos a tus fixtures existentes. Primero, debes crear un servicio de almacenamiento separado:
# config/storage.yml
test_fixtures:
service: Disk
root: <%= Rails.root.join("tmp/storage_fixtures") %>
Esto le indica a Active Storage dónde "cargar" los archivos de las fixtures, por lo que debe ser un directorio temporal. Al hacerlo
un directorio diferente al servicio regular test
, puedes separar los archivos de las fixtures de los archivos cargados durante una
prueba.
A continuación, crea archivos de fixture para las clases de Active Storage:
# active_storage/attachments.yml
david_avatar:
name: avatar
record: david (User)
blob: david_avatar_blob
# active_storage/blobs.yml
david_avatar_blob: <%= ActiveStorage::FixtureSet.blob filename: "david.png", service_name: "test_fixtures" %>
Luego, coloca un archivo en tu directorio de fixtures (la ruta predeterminada es test/fixtures/files
) con el nombre de archivo correspondiente.
Consulta la documentación de ActiveStorage::FixtureSet
para obtener más información.
Una vez que todo esté configurado, podrás acceder a los adjuntos en tus pruebas:
class UserTest < ActiveSupport::TestCase
def test_avatar
avatar = users(:david).avatar
assert avatar.attached?
assert_not_nil avatar.download
assert_equal 1000, avatar.byte_size
end
end
10.2.1 Limpiando los fixtures
Si bien los archivos cargados en las pruebas se eliminan al final de cada prueba, solo necesitas limpiar los archivos de fixture una vez: cuando todas tus pruebas se completen.
Si estás utilizando pruebas paralelas, llama a parallelize_teardown
:
class ActiveSupport::TestCase
# ...
parallelize_teardown do |i|
FileUtils.rm_rf(ActiveStorage::Blob.services.fetch(:test_fixtures).root)
end
# ...
end
Si no estás ejecutando pruebas paralelas, utiliza Minitest.after_run
o el equivalente para tu framework de pruebas
(por ejemplo, after(:suite)
para RSpec):
# test_helper.rb
Minitest.after_run do
FileUtils.rm_rf(ActiveStorage::Blob.services.fetch(:test_fixtures).root)
end
10.3 Configurando servicios
Puedes agregar config/storage/test.yml
para configurar los servicios que se utilizarán en el entorno de pruebas.
Esto es útil cuando se utiliza la opción service
.
class User < ApplicationRecord
has_one_attached :avatar, service: :s3
end
Sin config/storage/test.yml
, se utilizará el servicio s3
configurado en config/storage.yml
, incluso al ejecutar pruebas.
Se utilizará la configuración predeterminada y los archivos se cargarán en el proveedor de servicios configurado en config/storage.yml
.
En este caso, puedes agregar config/storage/test.yml
y utilizar el servicio Disk para el servicio s3
para evitar enviar solicitudes.
test:
service: Disk
root: <%= Rails.root.join("tmp/storage") %>
s3:
service: Disk
root: <%= Rails.root.join("tmp/storage") %>
11 Implementando soporte para otros servicios en la nube
Si necesitas admitir un servicio en la nube que no sea estos, deberás
implementar el servicio. Cada servicio extiende
ActiveStorage::Service
implementando los métodos necesarios para cargar y descargar archivos en la nube.
12 Eliminación de cargas no adjuntas
Hay casos en los que se carga un archivo pero nunca se adjunta a un registro. Esto puede ocurrir al utilizar Cargas directas. Puedes consultar los registros no adjuntos utilizando el ámbito unattached. A continuación se muestra un ejemplo utilizando una tarea personalizada de rake.
namespace :active_storage do
desc "Elimina los blobs de Active Storage no adjuntos. Ejecutar regularmente."
task purge_unattached: :environment do
ActiveStorage::Blob.unattached.where(created_at: ..2.days.ago).find_each(&:purge_later)
end
end
ADVERTENCIA: La consulta generada por ActiveStorage::Blob.unattached
puede ser lenta y potencialmente disruptiva en aplicaciones con bases de datos más grandes.
Comentarios
Se te anima a ayudar a mejorar la calidad de esta guía.
Por favor, contribuye si encuentras algún error tipográfico o factual. Para empezar, puedes leer nuestra contribución a la documentación sección.
También puedes encontrar contenido incompleto o desactualizado. Por favor, añade cualquier documentación faltante para main. Asegúrate de revisar Edge Guides primero para verificar si los problemas ya están resueltos o no en la rama principal. Consulta las Directrices de las Guías de Ruby on Rails para el estilo y las convenciones.
Si por alguna razón encuentras algo que corregir pero no puedes solucionarlo tú mismo, por favor abre un problema.
Y por último, cualquier tipo de discusión sobre la documentación de Ruby on Rails es muy bienvenida en el Foro oficial de Ruby on Rails.