Uma introdução de Active Storage em Rails 7
Tags: rails, activestorage
Published: 2022-12-14 21:04:46

Se você quer que sua aplicação Rails seja capaz de lidar arquivos, como imagens, videos, pdfs, etc. Você está no lugar certo.

Hoje, nós vamos aprender sobre Active Storage.

## O que é Active Storage

Active Storage é uma ferramenta que facilita o upload de arquivos para serviço de armazenamento em nuvem como Amazon S3, Google Cloud Storage ou Microsoft Azure e anexa esses arquivos a objetos Active Record. Ele também vem com um serviço baseado em disco local para desenvolvimento e teste e suporta espelhamento de arquivos para serviços subordinados para backups e migrações.

Observação: Eu não vou mostrar como que integrar o Active Storage com o serviço de armazenamento em nuvem. Eu só vou focar em ambiente de desenvolvimento.

Mas, antes de instalar Active Storage na sua aplicação, é necessário alguns software de terceiro para ter suporte em analise e processamento de arquivos.

Eu vou deixar uma lista de software.

- [libvips] (https://github.com/libvips/libvips) v8.6+ ou [ImageMagick] (https://imagemagick.org/index.php) para analise e transformação de imagem.
- [ffmpeg] (https://ffmpeg.org/) v3.4+ para pré-visualizações de vídeo e ffprobe para analise de video/audio
- [poppler] (https://poppler.freedesktop.org/) ou [muPDF] (https://mupdf.com/) para pré-visualizações de PDF

E também é necessário colocar uma gem `image_processing`, se sua aplicação vai usar arquivo do tipo imagem.

> 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.

Eu vou ensinar a instalar o Active Storage criando uma aplicação, porque eu acho muito mais melhor você ver a construção da aplicativo.

Então vamos criar aplicação

## Criando uma aplicação

A aplicação que eu vou criar vai ser um simples armazenamento de arquivos, em que vai ter dois models e controllers, `Usuario` e `Arquivo`.

```bash
rails new armazenamento
cd armazenamento
```

Vamos colocar uma gem no `Gemfile` para que possa ter suporte de imagem.

```bash
echo "gem 'image_processing'" >> Gemfile
bundle install
```

Com gem instalado, vamos instalar o Active Storage.

```bash
rails active_storage:install
```

Esse comando cria uma migração, em que tem três tabelas `active_storage_blobs`, `active_storage_attachments` e `active_storage_variant_records`.

Eu não vou explicar cada um dele, mas saiba que são essas tabelas que vai armazenar os arquivos.

Como ele gerou uma migração, vamos migrar ele.

```bash
rails db:migrate
```

Agora, vamos criar os models e os controllers.

Antes de criar um model, tenho que falar que existe duas maneiras de anexar o arquivo no model, uma é usando o generator e outro é colocando o código na mão, o que não é complicado de fazer.

Primeiro, vamos usar o generator no model `Usuario`.

```bash
rails g model Usuario email senha avatar:attachment
```

Como você pode ver o tipo `attachment`, ele indica que esse model vai ter um anexo, ou seja, vai ter somente um arquivo, se você quer que seja vários arquivos, basta usar o tipo `attachments`.

Beleza, vamos ver o que ele gerou para nós, vá para o arquivo `app/models/usuario.rb`.

```ruby
# app/models/usuario.rb
class Usuario < ApplicationRecord
  has_one_attached :avatar
end
```

O `has_one_attached` é uma macro que configura uma relação um para um entre o registro e o arquivo, ou seja, cada registro só pode ter um arquivo.

Isso é perfeito para gente, pois nós queremos que seja uma imagem do usuário.

E o que generator gerou foi isso, ou seja, se eu não quiser usar o generator, basta colocar esse código no model e pronto.

Então, vamos criar um model sem usar o tipo `attachment` ou `attachments` e colocar o código na mão.

```bash
rails g model Arquivo 
```

Agora abra no arquivo `app/models/arquivo.rb` para colocar o código.

```ruby
# app/models/arquivo.rb
class Arquivo < ApplicationRecord
  has_many_attached :arquivos
end
```

Você pode ter percebido que eu escrevi diferente do código de cima.

O `has_many_attached` é uma macro que configura uma relação de um para muitos entre o registro e os arquivos, ou seja, cada registro pode ter mais de um arquivo.

Viu, é simples implementar sem o generator.

Agora que criamos os models, vamos migrar eles.

```bash
rails db:migrate
```

Com models criado e migrado, vamos criar os controllers.

```bash
rails g controller Usuarios index show new edit
rails g controller Arquivos index show new edit
```

Agora vamos editar esses controllers. vamos primeiro para `app/cotrollers/usuarios_controller.rb`.

```ruby
# app/cotrollers/usuarios_controller.rb
class UsuariosController < ApplicationController
  before_action :define_usuario, except: [:index, :new, :create]

  def index
    @usuarios = Usuario.all
  end

  def show
  end

  def new
    @usuario = Usuario.new
  end

  def create
    @usuario = Usuario.new(usuario_params)

    if @usuario.save
      redirect_to usuario_path(@usuario)
    else
      render :new
    end
  end

  def edit
  end

  def update
    if @usuario.update(arquivo_params)
      redirect_to usuario_path(@usuario)
    else
      render :edit
    end
  end

  def destroy
    if @usuario.destroy
      redirect_to usuarios_path
    end
  end

  private 

  def usuario_params
    params.require(:usuario).permit(:email, :senha, :avatar)
  end

  def define_usuario
    begin
      @usuario = Usuario.find(params[:id])
    rescue ActiveRecord::RecordNotFound
      redirect_to usuarios_path, status: :not_found
    end
  end
end
```

Como você viu o código que eu escrevi, não tem nada demais, ou seja, a imagem vai ser salva, se o usuário colocar uma imagem no form.

Só que a história muda um pouquinho, quando nós estamos lidando em que aceita vários arquivos.

Então abra o arquivo `app/controllers/arquivos_controller.rb` e coloque o código abaixo.

```ruby
# app/controllers/arquivos_controller.rb
class ArquivosController < ApplicationController
  before_action :define_arquivo, except: [:index, :new, :create]

  def index
    @arquivos = Arquivo.all
  end

  def show
  end

  def new
    @arquivo = Arquivo.new
  end

  def create
    @arquivo = Arquivo.new(arquivo_params)

    if @arquivo.save
      redirect_to arquivo_path(@arquivo)
    else
      render :new
    end
  end

  def edit
  end

  def update
    if @arquivo.update(arquivo_params)
      redirect_to arquivo_path(@arquivo)
    else
      render :edit
    end
  end

  def destroy
    if @arquivo.destroy
      redirect_to arquivos_path
    end
  end

  private 

  def arquivo_params
    params.require(:arquivo).permit(arquivos: [])
  end

  def define_arquivo
    @arquivo = Arquivo.where(id: params[:id]).includes(arquivos_attachments: :blob)
    @arquivo = @arquivo[0]
    redirect_to arquivos_path, status: :not_found if @arquivo.nil?
  end
end
```

Se você observar o código, talvez você vai ver algo que nunca tenha visto antes, percebeu?

Se não percebeu, observe no método `arquivo_params` e me diz o que tem diferente do controller `Usuario` e `Arquivo`?

No controller usuario, o avatar está igual aos outros atributos, enquanto no controller arquivo, ele está com um array.

Então, lembre-se de que se você estiver lidando com muitos arquivos, coloque o array, se não, use um `symbol`.

E também outra diferença, no método `define_arquivo`, eu utilizei o `where` e o `includes`. O motivo de fazer isso foi para evitar o problema de n+1, se não sabe sobre esse problema, eu recomendo que pesquise sobre isso.

Agora, com controllers criado, vamos editar as rotas, abra o arquivo `config/routes.rb`.

```ruby
# config/routes.rb
Rails.application.routes.draw do
  resources :arquivos
  resources :usuarios
end
```

Com models, controllers e rotas editado, agora faltam os views.

Primeiro, vamos editar views de usuarios.

```erb
<!-- app/views/usuarios/index.html.erb -->
<% @usuarios.each do |usuario| %>
  <%= link_to usuario.email, usuarios_path(usuario)%>
<% end %>

<%= link_to "new", new_usuario_path %>
```

```erb
<!-- app/views/usuarios/new.html.erb -->
<%= form_with model: @usuario, url: usuarios_path do |form| %>
  <%= form.label :email %>
  <%= form.text_field :email %>

  <%= form.label :senha %>
  <%= form.password_field :senha %>

  <%= form.label :avatar %>
  <%= form.file_field :avatar %>

  <%= form.submit %>
<% end %>
```

Para que possa enviar um arquivo a partir de um `form`, você pode usar o `file_field`.

```erb
<!-- app/views/usuarios/show.html.erb -->
avatar: <%= image_tag(@usuario.avatar) %> <br>
email: <%= @usuario.email %>
```

Para poder exibir uma imagem, você pode usar o `image_tag`.

Agora rode o servidor e vai para url `localhost:3000/usuarios` e crie um usuario com imagem e veja a coisa funciona.

Agora, vamos editar os views de arquivos.

```erb
<!-- app/views/arquivos/index.html.erb -->
<% @arquivos.each do |arquivo| %>
  <%= link_to arquivo.id, arquivo_path(arquivo)%>
<% end %>

<%= link_to "new", new_arquivo_path %>
```

```erb
<!-- app/views/arquivos/new.html.erb -->
<%= form_with model: @arquivo, url: arquivos_path do |form| %>
  <%= form.label :arquivos %>
  <%= form.file_field :arquivos, multiple: true %>

  <%= form.submit %>
<% end %>
```

Pode perceber que `file_field` é diferente do usuario, o `multiple: true` indica que esse campo aceita vários arquivos.

```erb
<!-- app/views/arquivos/show.html.erb -->
<% @arquivo.arquivos.each do |arquivo| %>
  <% if arquivo.content_type.start_with? "image" %>
  	<%= image_tag arquivo %>
  <% elsif arquivo.content_type.start_with? "video" %>
  	<%= video_tag( url_for(arquivo), controls: true ) %>
        <!-- o video_tag recebe o primeiro argumento uma url do arquivo e esse controls é para ativar o controle do video -->
  <% else %>
    <!-- Se não for uma imagem e nem video, baixe ele -->
    <%= link_to(arquivo.filename, rails_blob_path(arquivo, disposition: "attachment")) %>
    <!-- o arquivo.filename mostra o nome do arquivo -->
    <!-- o rails_blob_path cria um link de download -->
  <% end %>
  <br>
<% end %>
```

Agora vá para url `localhost:3000/arquivos` e adicione vários arquivos de tipo diferente.

Você pode notar, se você colocou um arquivo que não seja imagem e video, ao clicar no arquivo, ele vai fazer download.

E nós acabamos por aqui.

Então, espero que tenha ajudado e tchau!