Resumen de Active Storage - LPC-Ltda/Ruby-on-Rails GitHub Wiki
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 adjunta esos archivos a los objetos de Active Record. Viene con un servicio local basado en disco para desarrollo y pruebas y soporta archivos espejo para servicios subordinados para copias de seguridad y migraciones.
Al usar Active Storage, una aplicación puede transformar las cargas de imágenes con ImageMagick, generar representaciones de imágenes de cargas que no son imágenes, como archivos PDF y videos, y extraer metadatos de archivos arbitrarios.
Active Storage utiliza dos tablas en la base de datos de su aplicación llamadas active_storage_blobs
y active_storage_attachments
. Después de crear una nueva aplicación (o actualizar su aplicación a Rails 5.2), ejecute rails active_storage:install
para generar una migración que cree estas tablas. Use rails db:migrate
para ejecutar la migración.
Declare servicios de almacenamiento activo en config/storage.yml
. Para cada servicio que use su aplicación, proporcione un nombre y la configuración requerida. El siguiente ejemplo declara tres servicios denominados 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: ""
Indique a Active Storage qué servicio debe usar configurando Rails.application.config.active_storage.service
. Debido a que es probable que cada entorno utilice un servicio diferente, se recomienda hacerlo en función del entorno. Para usar el servicio de disco del ejemplo anterior en el entorno de desarrollo, debe agregar lo siguiente a config/environment/development.rb
:
# Store files locally.
config.active_storage.service = :local
To use the Amazon S3 service in production, you add the following to config/environments/production.rb
:
# Store files on Amazon S3.
config.active_storage.service = :amazon
Continúe leyendo para obtener más información sobre los adaptadores de servicio integrados (por ejemplo, Disco y S3) y la configuración que requieren.
2.1 Disk Service
Declare a Disk service in config/storage.yml
:
local:
service: Disk
root: <%= Rails.root.join("storage") %>
2.2 Amazon S3 Service
Declare un servicio S3 en config/storage.yml
:
amazon:
service: S3
access_key_id: ""
secret_access_key: ""
region: ""
bucket: ""
Agregue la gema aws-sdk-s3
a su Gemfile
:
Agregue la gema aws-sdk-s3 a su Gemfile:
Las características principales de Active Storage requieren los siguientes permisos:
s3:ListBucket
,s3:PutObject
,s3:GetObject
ys3:DeleteObject
. Si tiene opciones de carga adicionales configuradas, como la configuración de las ACL, es posible que se requieran permisos adicionales.
Si desea usar variables de entorno, archivos de configuración SDK estándar, perfiles, perfiles de instancia IAM o roles de tareas, puede omitir las
access_key_id
,secret_access_key
yregion
del ejemplo anterior. El servicio Amazon S3 admite todas las opciones de autenticación descritas en AWS SDK documentation.
2.3 Microsoft Azure Storage Service
Declare un servicio Azure Storage en config/storage.yml
:
azure:
service: AzureStorage
storage_account_name: ""
storage_access_key: ""
container: ""
Agregue la gema azure-storage
al Gemfile
:
gem "azure-storage", require: false
2.4 Google Cloud Storage Service
Declare un servicio Google Cloud Storage en config/storage.yml
:
google:
service: GCS
credentials: <%= Rails.root.join("path/to/keyfile.json") %>
project: ""
bucket: ""
Opcionalmente, proporcione un Hash de credenciales en lugar de una ruta de archivo de claves (keyfiles):
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: ""
Agregue la gema google-cloud-storage
a su Gemfile
:
gem "google-cloud-storage", "~> 1.8", require: false
2.5 Mirror Service
Puede mantener múltiples servicios sincronizados definiendo un servicio espejo. Cuando un archivo se carga o elimina, se realiza en todos los servicios duplicados. Los servicios duplicados se pueden utilizar para facilitar una migración entre servicios en producción. Puede iniciar la creación de reflejo en el nuevo servicio, copiar los archivos existentes del servicio antiguo al nuevo, y luego ir todo incluido en el nuevo servicio. Defina cada uno de los servicios que le gustaría usar como se describe anteriormente y haga referencia a ellos desde un servicio duplicado.
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
Los archivos se sirven desde el servicio principal.
Esto no es compatible con la característica de carga directa (direct uploads).
3.1 has_one_attached
La macro has_one_attached
configura una asignación uno a uno entre registros y archivos. Cada registro puede tener un archivo adjunto.
Por ejemplo, supongamos que su aplicación tiene un modelo de usuario. Si desea que cada usuario tenga un avatar, defina el modelo de User así:
class User < ApplicationRecord
has_one_attached :avatar
end
Puedes crear un usuario con un 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
Llame a avatar.attach
para adjuntar un avatar a un usuario existente:
Current.user.avatar.attach(params[:avatar])
Llamar avatar.attached?
para determinar si un usuario en particular tiene un avatar:
Current.user.avatar.attached?
3.2 has_many_attached
La macro has_many_attached
establece una relación de uno a varios entre registros y archivos. Cada registro puede tener muchos archivos adjuntos.
Por ejemplo, supongamos que su aplicación tiene un modelo de mensaje. Si desea que cada mensaje tenga muchas imágenes, defina el modelo de mensaje así:
class Message < ApplicationRecord
has_many_attached :images
end
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
Llame images.attach
para agregar imágenes a un mensaje existente:
@message.images.attach(params[:images])
Llame images.attached?
para determinar si un mensaje particular tiene alguna imagen:
@message.images.attached?
3.3 Attaching File/IO Objects
A veces es necesario adjuntar un archivo que no llega a través de una solicitud HTTP. Por ejemplo, es posible que desee adjuntar un archivo que generó en el disco o que descargó de una URL enviada por el usuario. También es posible que desee adjuntar un archivo de dispositivo en una prueba modelo. Para hacerlo, proporcione un Hash que contenga al menos un objeto IO abierto y un nombre de archivo:
@message.image.attach(io: File.open('/path/to/file'), filename: 'file.pdf')
Cuando sea posible, proporcione un tipo de contenido también. Active Storage intenta determinar el tipo de contenido de un archivo a partir de sus datos. Si no puede hacerlo, vuelve al tipo de contenido que proporciona.
@message.image.attach(io: File.open('/path/to/file'), filename: 'file.pdf', content_type: 'application/pdf')
Si no proporciona un tipo de contenido y Active Storage no puede determinar el tipo de contenido del archivo de forma automática, el valor predeterminado es application/octet-stream
.
Para eliminar un archivo adjunto de un modelo, llame a purge
en el archivo adjunto. La eliminación se puede hacer en segundo plano si su aplicación está configurada para usar el trabajo activo. La purga elimina el blob y el archivo del servicio de almacenamiento.
# Synchronously destroy the avatar and actual resource files.
user.avatar.purge
# Destroy the associated models and actual resource files async, via Active Job.
user.avatar.purge_later
Genere una URL permanente para el blob que apunta a la aplicación. Al acceder, se devuelve una redirección al punto final del servicio real. Esta indirección desacopla la URL pública de la real y permite, por ejemplo, duplicar adjuntos en diferentes servicios para alta disponibilidad. La redirección tiene un vencimiento de HTTP de 5 min.
url_for(user.avatar)
Para crear un enlace de descarga, use el helper rails_blob_{path|url}
. El uso de este ayudante le permite establecer la disposición.
rails_blob_path(user.avatar, disposition: "attachment")
Si necesita crear un enlace desde fuera del contexto del controlador/vista (trabajos en segundo plano, Cronjobs, etc.), puede acceder a la rails_blob_path
de esta manera:
Rails.application.routes.url_helpers.rails_blob_path(user.avatar, only_path: true)
Si necesita procesar los blobs en el servidor, por ejemplo, al realizar análisis o conversiones adicionales, puede descargar el blob y obtener un objeto binario:
binary = user.avatar.download
En algunos casos, es posible que desee convertirlo en un archivo real en el disco para pasar la ruta del archivo a programas externos (como analizadores de virus, convertidores, optimizadores, minificadores, etc.). En este caso, puede incluir el módulo ActiveStorage::Downloading
en su clase, que proporciona helpers para descargar directamente en los archivos y evitar almacenar el archivo en la memoria. ActiveStorage::Downloading
espera que se defina un método de blob.
class VirusScanner
include ActiveStorage::Downloading
attr_reader :blob
def initialize(blob)
@blob = blob
end
def scan
download_blob_to_tempfile do |file|
system 'scan_virus', file.path
end
end
end
De forma predeterminada, download_blob_to_tempfile
crea archivos en Dir.tmpdir
. Si necesita usar otro directorio, anule ActiveStorage::Downloading#tempdir
en su clase:
class VirusScanner
include ActiveStorage::Downloading
# ...
private
def tempdir
'/path/to/tmp'
end
end
Si el programa externo se ejecuta como un programa separado, es posible que también desee modificar el archivo y su directorio, ya que otros usuarios no pueden acceder a él porque Tempfile establecerá los permisos en 0600.
Para crear una variación de la imagen, llame a la variante en el Blob. Puede pasar cualquier transformación compatible con MiniMagick
al método.
Para habilitar variantes, agregue mini_magick
a su Gemfile
:
gem 'mini_magick'
Cuando el navegador llegue a la URL variante, Active Storage transformará perezosamente el blob original en el formato especificado y lo redirigirá a su nueva ubicación de servicio.
<%= image_tag user.avatar.variant(resize: "100x100") %>
Algunos archivos que no son de imagen pueden previsualizarse: es decir, pueden presentarse como imágenes. Por ejemplo, un archivo de video se puede previsualizar extrayendo su primer fotograma. Fuera de la caja, Active Storage admite la vista previa de videos y documentos PDF.
<ul>
<% @message.files.each do |file| %>
<li>
<%= image_tag file.preview(resize: "100x100>") %>
</li>
<% end %>
</ul>
La extracción de vistas previas requiere aplicaciones de terceros,
ffmpeg
para video y herramientas múltiples para archivos PDF. Estas bibliotecas no son proporcionadas por Rails. Debe instalarlos usted mismo para usar los previsualizadores incorporados. Antes de instalar y utilizar software de terceros, asegúrese de comprender las implicaciones de la licencia de hacerlo.
Active Storage, con su biblioteca de JavaScript incluida, permite cargar directamente desde el cliente a la nube.
9.1 instalación de Direct upload
1.- Incluya activestorage.js
en el paquete de JavaScript de su aplicación.
Utilizando el assets pipeline:
//= require activestorage
Usando el paquete npm:
import * as ActiveStorage from "activestorage"
ActiveStorage.start()
2.- Anote las entradas del archivo con la URL de carga directa.
<%= form.file_field :attachments, multiple: true, direct_upload: true %>
3.- ¡Eso es! Las cargas comienzan al enviar el formulario.
9.2 Direct upload JavaScript events
Event name | Event target | Event data (event.detail) | Descripcion |
---|---|---|---|
direct-uploads:start | <form> | None | Se envió un formulario que contiene archivos para campos de carga directa. |
direct-upload:initialize | <input> | {id, file} | Enviado para cada archivo después del envío del formulario. |
direct-upload:start | <input> | {id, file} | Se está iniciando una carga directa. |
direct-upload:before-blob-request | <input> | {id, file, xhr} | Antes de realizar una solicitud a su aplicación para 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 las solicitudes para almacenar archivos progresan. |
direct-upload:error | <input> | {id, file, error} | Ocurrió un error. Se mostrará una alerta a menos que este evento se cancele. |
direct-upload:end | <input> | {id, file} | Una carga directa ha finalizado. |
direct-uploads:end | <form> | None | Todas las subidas directas han finalizado. |
9.3 Ejemplo
Puede utilizar estos eventos para mostrar el progreso de una carga.
(Video ilustrativo)
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">${file.name}</span>
</div>
`)
})
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")
})
Añadir 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.4 Integracion con Librerías y Frameworks
Si desea usar la función Carga directa desde un marco de JavaScript, o si desea integrar soluciones personalizadas de arrastrar y soltar, puede usar la clase DirectUpload para este propósito. Al recibir un archivo de la biblioteca de su elección, cree una instancia de DirectUpload y llame a su método de creación. Crear toma una devolución de llamada para invocar cuando se completa la carga.
import { DirectUpload } from "activestorage"
const input = document.querySelector('input[type=file]')
// Bind to file drop - use the ondrop on a parent element or use a
// library like Dropzone
const onDrop = (event) => {
event.preventDefault()
const files = event.dataTransfer.files;
Array.from(files).forEach(file => uploadFile(file))
}
// Bind to normal file selection
input.addEventListener('change', (event) => {
Array.from(input.files).forEach(file => uploadFile(file))
// you might clear the selected files from the input
input.value = null
})
const uploadFile = (file) {
// your form needs the file_field direct_upload: true, which
// provides data-direct-upload-url
const url = input.dataset.directUploadUrl
const upload = new DirectUpload(file, url)
upload.create((error, blob) => {
if (error) {
// Handle the error
} else {
// Add an appropriately-named hidden input to the form with a
// value of blob.signed_id so that the blob ids will be
// transmitted in the normal upload flow
const hiddenField = document.createElement('input')
hiddenField.setAttribute("type", "hidden");
hiddenField.setAttribute("value", blob.signed_id);
hiddenField.name = input.name
document.querySelector('form').appendChild(hiddenField)
}
})
}
Si necesita realizar un seguimiento del progreso de la carga del archivo, puede pasar un tercer parámetro al constructor de DirectUpload. Durante la carga, DirectUpload llamará al método directUploadWillStoreFileWithXHR del objeto. A continuación, puede enlazar su propio controlador de progreso en el XHR.
import { DirectUpload } from "activestorage"
class Uploader {
constructor(file, url) {
this.upload = new DirectUpload(this.file, this.url, this)
}
upload(file) {
this.upload.create((error, blob) => {
if (error) {
// Handle the error
} else {
// Add an appropriately-named hidden input to the form
// with a value of blob.signed_id
}
})
}
directUploadWillStoreFileWithXHR(request) {
request.upload.addEventListener("progress",
event => this.directUploadDidProgress(event))
}
directUploadDidProgress(event) {
// Use event.loaded and event.total to update the progress bar
}
}
Las pruebas del sistema limpian los datos de prueba al deshacer una transacción. Debido a que destroy nunca se llama a un objeto, los archivos adjuntos nunca se limpian. Si desea borrar los archivos, puede hacerlo en una devolución de llamada after_teardown. Si lo hace aquí, se asegurará de que todas las conexiones creadas durante la prueba estén completas y no recibirá un error de Active Storage que indique que no puede encontrar un archivo.
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
driven_by :selenium, using: :chrome, screen_size: [1400, 1400]
def remove_uploaded_files
FileUtils.rm_rf("#{Rails.root}/storage_test")
end
def after_teardown
super
remove_uploaded_files
end
end
Si las pruebas de su sistema verifican la eliminación de un modelo con archivos adjuntos y está utilizando Trabajo activo, configure su entorno de prueba para que use 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.
También es posible que desee utilizar una definición de servicio independiente para el entorno de prueba para que sus pruebas no eliminen los archivos que cree durante el desarrollo.
# Use inline job processing to make things happen immediately
config.active_job.queue_adapter = :inline
# Separate file storage in the test environment
config.active_storage.service = :local_test
Si necesita admitir un servicio en la nube distinto de estos, deberá implementar el Servicio. Cada servicio extiende ActiveStorage::Service
al implementar los métodos necesarios para cargar y descargar archivos a la nube.