1 O que é Active Storage?
O Active Storage facilita o upload de arquivos para um serviço de armazenamento como Amazon S3, Google Cloud Storage, ou Microsoft Azure Storage e anexa esses arquivos para objetos Active Record. Ele vem com um serviço local baseado em disco para desenvolvimento e teste e oferece suporte a espelhamento de arquivos em serviços destinados para backups e migrations.
Usando o Active Storage, uma aplicação pode transformar uploads de imagens ou gerar representações de uploads que não são imagens, como PDFs e vídeos, e extrair metadados de arquivos arbitrários.
1.1 Requirements
Vários recursos do Active Storage dependem de softwares de terceiros que o Rails não instala e devem ser instalados separadamente:
- libvips v8.6+ ou ImageMagick para análise de imagens e modificações
- ffmpeg v3.4+ para preview de vídeo e ffprobe para análise de vídeo/áudio
- poppler ou muPDF para pre-visualização de PDF
Análise e transformações de imagem também requerem a gem image_processing
. Descomente-a em seu Gemfile
ou adicione-a se necessário:
gem "image_processing", ">= 1.2"
Comparado a libvips, a ImageMagick é mais conhecida e mais amplamente disponível. No entanto, libvips pode ser até 10x mais rápido e consumir 1/10 da memória. Para arquivos JPEG, isso pode ser melhorado substituindo libjpeg-dev
por libjpeg-turbo-dev
, que é 2-7x mais rápido.
Antes de instalar e usar software de terceiros, certifique-se de compreender as implicações de licenciamento de fazê-lo. O MuPDF, em particular, é licenciado sob AGPL e requer uma licença comercial para alguns usos.
2 Configuração
O Active Storage usa três tabelas no banco de dados da sua aplicação chamadas
active_storage_blobs
, active_storage_variant_records
e active_storage_attachments
. Depois de criar uma nova aplicação (ou atualizar sua aplicação para Rails 5.2), execute
bin/rails active_storage:install
para gerar uma migration que cria essas
tabelas. Use bin/rails db:migrate
para executar a migration.
active_storage_attachments
é uma tabela de junção (join table) polimórfica que armazena o nome da classe do seu model. Se o nome da classe do seu model mudar, você precisará executar uma migration nesta tabela para atualizar o record_type
implícito para o novo nome da classe do seu model.
Se você estiver usando UUIDs em vez de inteiros como a chave primária em seus models, você precisará alterar o tipo de coluna de active_storage_attachments.record_id
e active_storage_variant_records.id
na migração.
Declare os serviços do Active Storage em config/storage.yml
. Para cada serviço que sua
aplicação usa, forneça um nome e a configuração necessária. O exemplo
abaixo declara três serviços chamados local
, test
e 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: "" # e.g. 'us-east-1'
Tell Active Storage which service to use by setting
Rails.application.config.active_storage.service
. Because each environment will
likely use a different service, it is recommended to do this on a
per-environment basis. To use the disk service from the previous example in the
development environment, you would add the following to
config/environments/development.rb
:
# Store files locally.
config.active_storage.service = :local
Para usar o serviço S3 em produção, você adiciona o seguinte ao
config/environments/production.rb
:
# Store files on Amazon S3.
config.active_storage.service = :amazon
Para usar o serviço test durante o teste, você adiciona o seguinte ao
config/environments/test.rb
:
# Store uploaded files on the local file system in a temporary directory.
config.active_storage.service = :test
Continue lendo para obter mais informações sobre os adaptadores de serviço integrados (por exemplo,
Disk
e S3
) e configurações que ele exigem.
Os arquivos de configuração específicos do ambiente terão precedência:
em produção, por exemplo, o arquivo config/storage/production.yml
(se existente)
terá precedência sobre o arquivo config/storage.yml
.
É recomendado usar Rails.env
nos nomes dos buckets para reduzir ainda mais o risco de destruição acidental de dados de produção.
amazon:
service: S3
# ...
bucket: your_own_bucket-<%= Rails.env %>
google:
service: GCS
# ...
bucket: your_own_bucket-<%= Rails.env %>
azure:
service: AzureStorage
# ...
container: your_container_name-<%= Rails.env %>
2.1 Serviço Disk
Declare um serviço Disk em config/storage.yml
:
local:
service: Disk
root: <%= Rails.root.join("storage") %>
2.2 Serviço S3 (Amazon S3 e APIs compatíveis com S3)
Para conectar ao Amazon S3, declare um serviço S3 em config/storage.yml
:
amazon:
service: S3
access_key_id: ""
secret_access_key: ""
region: ""
bucket: ""
Opcionalmente, você pode fornecer opções de cliente e upload:
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' ou 'AES256'
Defina tempos limites de HTTP timeout e limites de nova tentativa para sua aplicação. Em certos cenários de falha, a configuração do cliente AWS padrão pode fazer com que as conexões sejam retidas por vários minutos e levar à enfileiramento de requisições.
Adicione a gem aws-sdk-s3
no seu Gemfile
:
gem "aws-sdk-s3", require: false
Os principais recursos do Active Storage requerem as seguintes permissões: s3:ListBucket
, s3:PutObject
, s3:GetObject
, e s3:DeleteObject
. Acesso público requer também s3:PutObjectAcl
. Se você tiver opções de upload adicionais configuradas como configurações de ACLs, então permissões adicionais podem ser necessárias.
Se você quiser usar variáveis de ambiente, arquivos de configuração padrão do SDK, perfis,
perfis de instância do IAM ou funções de tarefa, você pode omitir as chaves access_key_id
, secret_access_key
,
e region
no exemplo acima. O serviço S3 suporta todas as opções de
autenticação descritas na documentação AWS SDK.
Para se conectar a uma API de armazenamento de objetos compatíveis com S3, como DigitalOcean Spaces, forneça o endpoint
:
digitalocean:
service: S3
endpoint: https://nyc3.digitaloceanspaces.com
access_key_id: ...
secret_access_key: ...
# ...e outras opções
Existem muitas outras opções disponíveis. Você pode verificá-los na documentação do AWS S3 Client.
2.3 Serviço Armazenamento da Microsoft Azure
Declare um serviço Azure Storage em config/storage.yml
:
azure:
service: AzureStorage
storage_account_name: ""
storage_access_key: ""
container: ""
Adicione a gem azure-storage-blob
no seu Gemfile
:
gem "azure-storage-blob", require: false
2.4 Serviço Google Cloud Storage
Declare um serviço Google Cloud Storage service in config/storage.yml
:
google:
service: GCS
credentials: <%= Rails.root.join("path/to/keyfile.json") %>
project: ""
bucket: ""
Opcionalmente forneça uma Hash de credenciais em vez de um caminho para o arquivo de chave:
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, forneça metadados Cache-Control para serem definidos nos recursos enviados:
google:
service: GCS
...
cache_control: "public, max-age=3600"
Opcionalmente, use IAM em vez das credentials
ao assinar URLs. Isso é útil se você estiver autenticando suas aplicações do GKE com o Workload Identity. Consulte esta postagem do blog do Google Cloud para obter mais informações.
google:
service: GCS
...
iam: true
Opcionalmente, use um GSA específico ao assinar URLs. Ao usar o IAM, o servidor de metadados será contatado para obter o e-mail do GSA, mas esse servidor de metadados nem sempre está presente (por exemplo, em ambientes locais ou testes) e você pode querer usar um GSA não padrão.
google:
service: GCS
...
iam: true
gsa_email: "[email protected]"
Adicione a gem google-cloud-storage
no seu Gemfile
:
gem "google-cloud-storage", "~> 1.11", require: false
2.5 Serviço Espelho
Você pode manter múltiplos serviços sincronizados definindo um serviço espelho. Quando um arquivo é carregado ou deletado, isso é feito em todos serviços espelhados.
Serviços espelhados podem ser usados para facilitar a migração entre serviços em produção. Você pode começar a espelhar para o novo serviço, copiar os arquivos existentes do antigo serviço para o novo, e então muda para o novo serviço.
O espelhamento não é atômico. É possível que um upload seja bem-sucedido no serviço principal e falha em qualquer um dos serviços subordinados. Antes de ir totalmente para um novo serviço, verifique se todos os arquivos foram copiados.
Defina cada um dos serviços que você gostaria de usar conforme descrito acima e faça referência para um serviço espelhado.
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
Embora todos os serviços secundários recebam uploads, os downloads são sempre tratados pelo serviço principal.
Os serviços de espelho são compatíveis com uploads diretos. Novos arquivos são diretamente carregados para o serviço principal. Quando um arquivo enviado é anexado a um registro, um job em segundo plano é enfileirado para copiá-lo para os serviços secundários.
2.6 Acesso público
Por padrão, o Active Storage assume acesso privado aos serviços. Isso significa gerar URLs assinadas e de uso único para os blobs. Se você preferir tornar os blobs acessíveis publicamente, especifique public: true
em config/storage.yml
na sua aplicação:
gcs: &gcs
service: GCS
project: ""
private_gcs:
<<: *gcs
credentials: <%= Rails.root.join("path/to/private_keyfile.json") %>
bucket: ""
public_gcs:
<<: *gcs
credentials: <%= Rails.root.join("path/to/public_keyfile.json") %>
bucket: ""
public: true
Tenha certeza que seus buckets estão configurados para acesso público. Veja a documentação sobre como ativar permissão de leitura pública para os serviços Amazon S3, Google Cloud Storage, e Microsoft Azure. A Amazon S3 requer também que você tenha permissão s3:PutObjectAcl
.
Ao converter uma aplicação existente para usar public: true
, certifique-se de atualizar cada arquivo individual no bucket para ser lido publicamente antes de alternar.
3 Anexar Arquivos a Registros
3.1 has_one_attached
O macro has_one_attached
configura um mapeamento um-para-um entre registros e
arquivos. Cada registro pode ter um arquivo anexado a ele.
Por exemplo, imagine que sua aplicação tenha um model User
. Se você quiser que cada usuário
tenha uma avatar, defina o model User
da seguinte forma:
class User < ApplicationRecord
has_one_attached :avatar
end
ou se você estiver usando Rails 6.0+, você pode executar um comando gerador de model como este:
bin/rails generate model User avatar:attachment
Você pode criar um usuário com um 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
Chamar avatar.attach
para anexar um avatar a um usuário existente:
user.avatar.attach(params[:avatar])
Chamar avatar.attached?
para determinar se um usuário em particular tem um avatar:
user.avatar.attached?
Em alguns casos, você pode querer substituir um serviço padrão para um anexo específico.
Você pode configurar serviços específicos por anexo usando a opção service
:
class User < ApplicationRecord
has_one_attached :avatar, service: :s3
end
Você pode configurar variantes específicas por objeto carregado chamando o método variant
no objeto gerado:
class User < ApplicationRecord
has_one_attached :avatar do |attachable|
attachable.variant :thumb, resize_to_limit: [100, 100]
end
end
Chame avatar.variant(:thumb)
para obter uma variante thumb de um avatar:
<%= image_tag user.avatar.variant(:thumb) %>
3.2 has_many_attached
O macro has_many_attached
configura um relacionamento um-para-muitos entre os registros
e arquivos. Cada registro pode ter muitos arquivos anexados a ele.
Por exemplo, imagine que sua aplicação tem um model Message
. Se você quiser que cada
mensagem tenha muitas imagens, defina o model Message
da seguinte forma:
class Message < ApplicationRecord
has_many_attached :images
end
ou se você estiver usando Rails 6.0+, você pode executar um comando gerador de model como este:
bin/rails generate model Message images:attachments
Você pode criar uma mensagem com images:
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
Chamar images.attach
para adicionar novas imagens para uma mensagem existente:
@message.images.attach(params[:images])
Chamar images.attached?
para determinar se uma mensagem em particular alguma imagem:
@message.images.attached?
Substituir o serviço padrão é feito da mesma maneira que has_one_attached
, usando a opção service
:
class Message < ApplicationRecord
has_many_attached :images, service: :s3
end
A configuração de variantes específicas é feita da mesma forma que has_one_attached
, chamando o método variant
no objeto gerado:
class Message < ApplicationRecord
has_many_attached :images do |attachable|
attachable.variant :thumb, resize_to_limit: [100, 100]
end
end
3.3 Anexando Objetos File/IO
Às vezes você precisa anexar um arquivo que não chega por meio de uma requisição HTTP. Por exemplo, você pode querer anexar um arquivo que você gerou no disco ou baixou de uma URL enviada pelo usuário. Você também pode querer anexar um arquivo de fixação em um model de test. Para fazer isso, forneça uma Hash contendo pelo menos um objeto open IO e um filename:
@message.images.attach(io: File.open('/path/to/file'), filename: 'file.pdf')
Quando possível, forneça um tipo de conteúdo também. O Active Storage tenta determinar o tipo de conteúdo de um arquivo a partir de seus dados. Depende do tipo de conteúdo que você fornece se não for possível.
@message.images.attach(io: File.open('/path/to/file'), filename: 'file.pdf', content_type: 'application/pdf')
Você pode ignorar a inferência do tipo de conteúdo a partir dos dados passando
identify: false
junto com o content_type
.
@message.images.attach(
io: File.open('/path/to/file'),
filename: 'file.pdf',
content_type: 'application/pdf',
identify: false
)
Se você não fornecer um tipo de conteúdo e o Active Storage não puder determinar o tipo de conteúdo do arquivo automaticamente, o padrão é application/octet-stream.
4 Removendo arquivos
Para remover um arquivo anexado de um model, use o método purge
no anexo.
Se a aplicação está configurada para usar Active Job, a remoção pode ser feita de maneira assíncrona
chamando purge_later
. Purge
remove o blob (O arquivo em sua versão binaria salvo no banco de
dados) e o arquivo do seu serviço de armazenamento.
# Maneira usada para remover um avatar e seus arquivos de maneira síncrona.
user.avatar.purge
# Maneira usada para remover um avatar e seus arquivos de maneira assíncrona.
user.avatar.purge_later
5 Serving Files
Active Storage supports two ways to serve files: redirecting and proxying.
All Active Storage controllers are publicly accessible by default. The generated URLs are hard to guess, but permanent by design. If your files require a higher level of protection consider implementing Authenticated Controllers.
5.1 Redirect Mode
To generate a permanent URL for a blob, you can pass the blob to the
url_for
view helper. This generates a
URL with the blob's signed_id
that is routed to the blob's RedirectController
url_for(user.avatar)
# => /rails/active_storage/blobs/:signed_id/my-avatar.png
The RedirectController
redirects to the actual service endpoint. This
indirection decouples the service URL from the actual one, and allows, for
example, mirroring attachments in different services for high-availability. The
redirection has an HTTP expiration of 5 minutes.
To create a download link, use the rails_blob_{path|url}
helper. Using this
helper allows you to set the disposition.
rails_blob_path(user.avatar, disposition: "attachment")
To prevent XSS attacks, Active Storage forces the Content-Disposition header to "attachment" for some kind of files. To change this behaviour see the available configuration options in Configuring Rails Applications.
If you need to create a link from outside of controller/view context (Background
jobs, Cronjobs, etc.), you can access the rails_blob_path
like this:
Rails.application.routes.url_helpers.rails_blob_path(user.avatar, only_path: true)
5.2 Proxy Mode
Optionally, files can be proxied instead. This means that your application servers will download file data from the storage service in response to requests. This can be useful for serving files from a CDN.
You can configure Active Storage to use proxying by default:
# config/initializers/active_storage.rb
Rails.application.config.active_storage.resolve_model_to_route = :rails_storage_proxy
Or if you want to explicitly proxy specific attachments there are URL helpers you can use in the form of rails_storage_proxy_path
and rails_storage_proxy_url
.
<%= image_tag rails_storage_proxy_path(@user.avatar) %>
5.2.1 Putting a CDN in Front of Active Storage
Additionally, in order to use a CDN for Active Storage attachments, you will need to generate URLs with proxy mode so that they are served by your app and the CDN will cache the attachment without any extra configuration. This works out of the box because the default Active Storage proxy controller sets an HTTP header indicating to the CDN to cache the response.
You should also make sure that the generated URLs use the CDN host instead of your app host. There are multiple ways to achieve this, but in general it involves tweaking your config/routes.rb
file so that you can generate the proper URLs for the attachments and their variations. As an example, you could add this:
# config/routes.rb
direct :cdn_image do |model, options|
if model.respond_to?(:signed_id)
route_for(
:rails_service_blob_proxy,
model.signed_id,
model.filename,
options.merge(host: ENV['CDN_HOST'])
)
else
signed_blob_id = model.blob.signed_id
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
and then generate routes like this:
<%= cdn_image_url(user.avatar.variant(resize_to_limit: [128, 128])) %>
5.3 Authenticated Controllers
All Active Storage controllers are publicly accessible by default. The generated
URLs use a plain signed_id
, making them hard to
guess but permanent. Anyone that knows the blob URL will be able to access it,
even if a before_action
in your ApplicationController
would otherwise
require a login. If your files require a higher level of protection, you can
implement your own authenticated controllers, based on the
ActiveStorage::Blobs::RedirectController
,
ActiveStorage::Blobs::ProxyController
,
ActiveStorage::Representations::RedirectController
and
ActiveStorage::Representations::ProxyController
To only allow an account to access their own logo you could do the following:
# config/routes.rb
resource :account do
resource :logo
end
# app/controllers/logos_controller.rb
class LogosController < ApplicationController
# Through ApplicationController:
# include Authenticate, SetCurrentAccount
def show
redirect_to Current.account.logo.url
end
end
<%= image_tag account_logo_path %>
And then you might want to disable the Active Storage default routes with:
config.active_storage.draw_routes = false
to prevent files being accessed with the publicly accessible URLs.
6 Baixando arquivos
Algumas vezes você vai precisar processar um blob depois dele ter sido
uploaded (Transferir um arquivo da maquina do cliente para o servidor da sua
aplicação) para, por exemplo, converte-lo para um formato diferente. Use o
download
para ler o conteúdo binário do blob na
memória:
binary = user.avatar.download
Caso deseje baixar o blob para um arquivo no disco para um programa externo
(Um antivírus, por exemplo) possa operar nele. Use o método open
para baixar o blob para um arquivo temporário no disco:
message.video.open do |file|
system '/caminho/para/o/antivirus', file.path
# ...
end
É importante saber que o arquivo ainda não está disponível no callback after_create
, mas apenas no after_create_commit
.
7 Analyzing Files
Active Storage analyzes files once they've been uploaded by queuing a job in Active Job. Analyzed files will store additional information in the metadata hash, including analyzed: true
. You can check whether a blob has been analyzed by calling analyzed?
on it.
Image analysis provides width
and height
attributes. Video analysis provides these, as well as duration
, angle
, display_aspect_ratio
, and video
and audio
booleans to indicate the presence of those channels. Audio analysis provides duration
and bit_rate
attributes.
8 Displaying Images, Videos, and PDFs
Active Storage supports representing a variety of files. You can call
representation
on an attachment to display an image variant, or a
preview of a video or PDF. Before calling representation
, check if the
attachment can be represented by calling representable?
. Some file formats
can't be previewed by Active Storage out of the box (e.g. Word documents); if
representable?
returns false you may want to link to
the file instead.
<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: "Download file" %>
<% end %>
<% end %>
</li>
<% end %>
</ul>
Internally, representation
calls variant
for images, and preview
for
previewable files. You can also call these methods directly.
8.1 Lazy vs Immediate Loading
By default, Active Storage will process representations lazily. This code:
image_tag file.representation(resize_to_limit: [100, 100])
Will generate an <img>
tag with the src
pointing to the
ActiveStorage::Representations::RedirectController
. The browser will
make a request to that controller, which will return a 302
redirect to the
file on the remote service (or in proxy mode, return the file
contents). Loading the file lazily allows features like
single use URLs to work without slowing down your initial page loads.
This works fine for most cases.
If you want to generate URLs for images immediately, you can call .processed.url
:
image_tag file.representation(resize_to_limit: [100, 100]).processed.url
The Active Storage variant tracker improves performance of this, by storing a
record in the database if the requested representation has been processed before.
Thus, the above code will only make an API call to the remote service (e.g. S3)
once, and once a variant is stored, will use that. The variant tracker runs
automatically, but can be disabled through config.active_storage.track_variants
.
If you're rendering lots of images on a page, the above example could result
in N+1 queries loading all the variant records. To avoid these N+1 queries,
use the named scopes on 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 Transforming Images
Transforming images allows you to display the image at your choice of dimensions.
To create a variation of an image, call variant
on the attachment. You
can pass any transformation supported by the variant processor to the method.
When the browser hits the variant URL, Active Storage will lazily transform
the original blob into the specified format and redirect to its new service
location.
<%= image_tag user.avatar.variant(resize_to_limit: [100, 100]) %>
If a variant is requested, Active Storage will automatically apply transformations depending on the image's format:
Content types that are variable (as dictated by
config.active_storage.variable_content_types
) and not considered web images (as dictated by [config.active_storage.web_image_content_types
][]), will be converted to PNG.If
quality
is not specified, the variant processor's default quality for the format will be used.
Active Storage can use either Vips or MiniMagick as the variant processor.
The default depends on your config.load_defaults
target version, and the
processor can be changed by setting [config.active_storage.variant_processor
][].
The two processors are not fully compatible, so when migrating an existing application between MiniMagick and Vips, some changes have to be made if using options that are format specific:
<!-- 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 }) %>
8.3 Pré-visualização de arquivos
Alguns arquivos que não são imagens podem ser pré-visualizados: isto é, eles podem
ser apresentados como imagens. Por exemplo, um arquivo de vídeo pode ser pré-visualizado
através da extração de seu primeiro frame. O Active Storage por padrão já oferece
suporte para a pré-visualização de vídeos e documentos PDF. Para criar um link e
gerar um preview use o método preview
:
<%= image_tag message.video.preview(resize_to_limit: [100, 100]) %>
Para adicionar suporte para outros formatos, adicione seu próprio visualizador. Veja a documentação de
ActiveStorage::Preview
para mais informações.
9 Uploads Diretos
O Active Storage com a sua biblioteca JavaScript incluída suporta uploads direto do cliente (front-end) para a nuvem.
9.1 Uso
-
Inclua o
activestorage.js
na sua aplicação.Usando o asset pipeline:
//= require activestorage
Usando o pacote npm:
import * as ActiveStorage from "@rails/activestorage" ActiveStorage.start()
-
Adicione
direct_upload: true
no seufile_field
.<%= form.file_field :attachments, multiple: true, direct_upload: true %>
Se você não está usando um FormBuilder adicione o
direct_upload: true
diretamente:<input type="file" data-direct-upload-url="<%= rails_direct_uploads_url %>" />
Configure o serviço de armazenamento de terceiros do CORS para permitir requisições de upload direto.
E é isso! Os uploads começam após o envio do formulário.
9.2 Configuração do Cross-Origin Resource Sharing (CORS)
Para que o upload direto a partir de terceiros funcione você vai precisar configurar o seu serviço de nuvem para aceitar requisições de múltiplas origens. Consulte a documentação sobre CORS do seu serviço:
Tome cuidado em permitir:
- Todas as origens pela qual a sua aplicação é acessada.
- O método de requisição
PUT
- Os seguintes cabeçalhos de requisição:
Origin
Content-Type
Content-MD5
-
Content-Disposition
(exceto para o Azure Storage) -
x-ms-blob-content-disposition
(somente para o Azure Storage) -
x-ms-blob-type
(somente para o Azure Storage) -
Cache-Control
(para GCS, somente secache_control
estiver definido)
Se você for utilizar seu disco como armazenamento e ele compartilhar a mesma origem da sua aplicação não é necessário configurar o CORS.
9.2.1 Exemplo: Configuração do CORS para o S3
[
{
"AllowedHeaders": [
"*"
],
"AllowedMethods": [
"PUT"
],
"AllowedOrigins": [
"https://www.example.com"
],
"ExposeHeaders": [
"Origin",
"Content-Type",
"Content-MD5",
"Content-Disposition"
],
"MaxAgeSeconds": 3600
}
]
9.2.2 Exemplo: Configuração do CORS para o Google Cloud Storage
[
{
"origin": ["https://www.example.com"],
"method": ["PUT"],
"responseHeader": ["Origin", "Content-Type", "Content-MD5", "Content-Disposition"],
"maxAgeSeconds": 3600
}
]
9.2.3 Exemplo: Configuração do CORS para o 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 Upload Direto do JavaScript
Nome do evento | Alvo do evento | Dados do evento (event.detail ) |
Descrição |
---|---|---|---|
direct-uploads:start |
<form> |
Nenhum | Um formulário contendo campos para upload direto foi submetido. |
direct-upload:initialize |
<input> |
{id, file} |
Disparado para cada arquivo após a submissão do formulário. |
direct-upload:start |
<input> |
{id, file} |
O upload direto está iniciando. |
direct-upload:before-blob-request |
<input> |
{id, file, xhr} |
Antes de fazer uma requisição de upload direto de metadados para a sua aplicação. |
direct-upload:before-storage-request |
<input> |
{id, file, xhr} |
Antes de fazer uma requisição para armazenar um arquivo. |
direct-upload:progress |
<input> |
{id, file, progress} |
O progresso da requisição para armazenar um arquivo. |
direct-upload:error |
<input> |
{id, file, error} |
Um erro ocorreu. Um alert deve ser exibido, a menos que esse evento seja cancelado. |
direct-upload:end |
<input> |
{id, file} |
Um upload direto foi finalizado. |
direct-uploads:end |
<form> |
Nenhum | Todos os uploads diretos foram finalizados. |
9.4 Exemplo
Você pode usar esses eventos para exibir o progresso de um upload.
Para mostrar os arquivos enviados em um formulário:
// 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")
})
Adicionar 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 Integrando com Bibliotecas ou Frameworks
Se você quer utilizar a funcionalidade de upload direto a partir de um framework JavaScript, ou se você quiser integrar uma funcionalidade de drag and drop (arrastar e soltar), você poderá utilizar a classe DirectUpload
para fazer a integração. Ao receber um arquivo da sua biblioteca de escolha, instancie um DirectUpload
e chame o seu método de criação. O método de criação recebe uma callback para ser executada quando o upload é concluído.
import { DirectUpload } from "@rails/activestorage"
const input = document.querySelector('input[type=file]')
// Vincular ao arquivo solto - use o onDrop em um elemento pai ou use uma
// biblioteca com o Dropzone
const onDrop = (event) => {
event.preventDefault()
const files = event.dataTransfer.files;
Array.from(files).forEach(file => uploadFile(file))
}
// Vincular à seleção de arquivo normal
input.addEventListener('change', (event) => {
Array.from(input.files).forEach(file => uploadFile(file))
// você pode limpar os arquivos selecionados da entrada
input.value = null
})
const uploadFile = (file) => {
// seu formulário precisa do file_field direct_upload: true, que
// fornece o data-direct-upload-url
const url = input.dataset.directUploadUrl
const upload = new DirectUpload(file, url)
upload.create((error, blob) => {
if (error) {
// Trata o erro
} else {
// Adiciona uma entrada oculta apropriadamente nomeada ao formulário com o
// valor blob.signed_id, assim os blob ids podem ser
// transmitidos no fluxo normal de upload
const hiddenField = document.createElement('input')
hiddenField.setAttribute("type", "hidden");
hiddenField.setAttribute("value", blob.signed_id);
hiddenField.name = input.name
document.querySelector('form').appendChild(hiddenField)
}
})
}
Se você precisa acompanhar o progresso de upload do arquivo, você pode passar um terceiro
parâmetro para o construtor do DirectUpload
. Durante o upload, o DirectUpload
irá chamar o método directUploadWillStoreFileWithXHR
do objeto. Você poderá então
vincular o seu manipulador de progresso no XHR.
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) {
// Trata o erro
} else {
// Adiciona uma entrada oculta apropriadamente nomeada no formulário
// com o valor blob.signed_id
}
})
}
directUploadWillStoreFileWithXHR(request) {
request.upload.addEventListener("progress",
event => this.directUploadDidProgress(event))
}
directUploadDidProgress(event) {
// Usa o event.loaded e o event.total para atualizar a barra de progresso
}
}
NOTA: O uso de Uploads Diretos às vezes pode resultar em um arquivo que é carregado, mas nunca anexado a um registro. Considere limpar uploads não anexados.
10 Testando
Use fixture_file_upload
para testar o upload de um arquivo em um teste de integração ou controller.
O Rails lida com arquivos como qualquer outro parâmetro.
class SignupController < ActionDispatch::IntegrationTest
test "can sign up" 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 Descartando Arquivos Criados Durante os Testes
10.1.1 System Tests
Os testes de sistema limpam os dados de testes revertendo uma transação. Como o
destroy
nunca é chamado em um objeto, os arquivos anexados nunca são limpos. Se
quiser limpar os arquivos, podemos usar um callback after_teardown
.
Fazendo isso garantimos que todas as conexões criadas durante o teste sejam
concluídas sem que recebamos um erro do Active Storage informando que não
foi possível encontrar um arquivo.
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
# ...
def after_teardown
super
FileUtils.rm_rf(ActiveStorage::Blob.service.root)
end
# ...
end
Se você estiver usando [testes paralelos][] e o DiskService
, você deve configurar cada processo para usar sua própria
pasta para o Active Storage. Dessa forma, o retorno de chamada teardown
só excluirá arquivos do processo do teste relevante.
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
# ...
parallelize_setup do |i|
ActiveStorage::Blob.service.root = "#{ActiveStorage::Blob.service.root}-#{i}"
end
# ...
end
Se os testes de sistema verificarem a exclusão de um model com anexos e estivermos usando o Active Job, configure seu ambiente de testes para usar o adaptador de fila para que o trabalho de limpeza seja executado imediatamente, em vez de em um momento desconhecido no futuro.
# Usa o processamento de trabalho em linha para fazer as coisas
# acontecerem imediatamente
config.active_job.queue_adapter = :inline
10.1.2 Testes de Integração
Similar aos testes de sistema, arquivos enviados durante testes de integração
não serão automaticamente descartados. Se você deseja limpar esses arquivos, você
pode fazer isso usando o callback teardown
.
class ActionDispatch::IntegrationTest
def after_teardown
super
FileUtils.rm_rf(ActiveStorage::Blob.service.root)
end
end
Se você estiver usando [testes paralelos][] e o serviço em disco, deverá configurar cada processo para usar sua própria
pasta para o Active Storage. Dessa forma, o retorno de chamada teardown
só excluirá arquivos do processo do teste relevante.
class ActionDispatch::IntegrationTest
parallelize_setup do |i|
ActiveStorage::Blob.service.root = "#{ActiveStorage::Blob.service.root}-#{i}"
end
end
10.2 Adicionando Arquivos em Fixtures
Você pode adicionar anexos às suas fixtures. Primeiro, você desejará criar um serviço de armazenamento separado:
# config/storage.yml
test_fixtures:
service: Disk
root: <%= Rails.root.join("tmp/storage_fixtures") %>
Isso informa ao Active Storage para onde "carregar" os arquivos de fixture, então deve ser um diretório temporário. Ao fazê-lo
um diretório diferente do seu serviço test
regular, você pode separar os arquivos de fixtures dos arquivos carregados durante um teste.
Em seguida, crie arquivos de fixture para as classes 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" %>
Em seguida, coloque um arquivo em seu diretório de fixtures (o caminho padrão é test/fixtures/files
) com o nome de arquivo correspondente.
Veja a documentação ActiveStorage::FixtureSet
para mais informações.
Depois que tudo estiver configurado, você poderá acessar os anexos em seus testes:
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 Limpando as Fixtures
Enquanto os arquivos enviados nos testes são limpos no final de cada teste, você só precisa limpar os arquivos de fixtures uma vez: quando todos os seus testes forem concluídos.
Se você estiver usando testes paralelos, chame parallelize_teardown
:
class ActiveSupport::TestCase
# ...
parallelize_teardown do |i|
FileUtils.rm_rf(ActiveStorage::Blob.services.fetch(:test_fixtures).root)
end
# ...
end
Se você não estiver executando testes paralelos, use Minitest.after_run
ou equivalente para seu teste
framework (por exemplo, after(:suite)
para RSpec):
# test_helper.rb
Minitest.after_run do
FileUtils.rm_rf(ActiveStorage::Blob.services.fetch(:test_fixtures).root)
end
11 Implementando Suporte a Outros Serviços Cloud
Se for necessário dar suporte a algum outro serviço cloud além desses, você precisa implementá-lo. Cada serviço estende ActiveStorage::Service
implementando os métodos necessários para fazer o upload e download de arquivos para a nuvem.
12 Purging Unattached Uploads
There are cases where a file is uploaded but never attached to a record. This can happen when using Direct Uploads. You can query for unattached records using the unattached scope. Below is an example using a custom rake task.
namespace :active_storage do
desc "Purges unattached Active Storage blobs. Run regularly."
task purge_unattached: :environment do
ActiveStorage::Blob.unattached.where(created_at: ..2.days.ago).find_each(&:purge_later)
end
end
The query generated by ActiveStorage::Blob.unattached
can be slow and potentially disruptive on applications with larger databases.
Feedback
Você é incentivado a ajudar a melhorar a qualidade deste guia.
Por favor, contribua caso veja quaisquer erros, inclusive erros de digitação. Para começar, você pode ler nossa sessão de contribuindo com a documentação.
Você também pode encontrar conteúdo incompleto ou coisas que não estão atualizadas. Por favor, adicione qualquer documentação em falta na main do Rails. Certifique-se de checar o Edge Guides (en-US) primeiro para verificar se o problema já foi resolvido ou não no branch main. Verifique as Diretrizes do Guia Ruby on Rails para estilo e convenções.
Se, por qualquer motivo, você encontrar algo para consertar, mas não conseguir consertá-lo, por favor abra uma issue no nosso Guia.
E por último, mas não menos importante, qualquer tipo de discussão sobre a documentação do Ruby on Rails é muito bem vinda na forum oficial do Ruby on Rails e nas issues do Guia em português.