owned this note
owned this note
Published
Linked with GitHub
# Recebendo imagens com ActiveStorage
###### tags: `tutorial` `rails` `docker` `s3` `amazon`
---
[ToC]
## Introdução
Neste tutorial iremos criar um novo fluxo para fazer upload de imagens. Para isso iremos passar pelos tópicos:
- Configurar **`ActiveStorage`**
- Opções de serviços para envio de imagens
- Criar modelo para salvar informações de imagem
- Criar classes de suporte para fazer upload das imagens
## Configurando o ActiveStorage
O ActiveStorage é um utilitário que serve para armazenar arquivos. Ele se integra muito facilmente com o Rails e vamos utilizá-lo para que o usuário possa enviar imagens. O primeiro passo é determinar onde que iremos armazenas os documentos. Temos a opção de usar serviços, como por exemplo:
- [Google Cloud Storage](https://cloud.google.com/free?authuser=1)
- [Amazon S3](https://aws.amazon.com/free/)
- [Azure Storage Container](https://azure.microsoft.com/en-us/free)
Ou podemos usar coisas mais simples para apenas aprender como funciona, que é criar arquivos locais ou salvar numa tabela do PG com a funcção de *Larges Objects*.
:::info
:memo: **Desafio:** Sugiro tentar criar uma conta grátis em algum dos serviços listados para experimentá-los. [Nesse link](https://edgeguides.rubyonrails.org/active_storage_overview.html#s3-service-amazon-s3-and-s3-compatible-apis) existe um tutorial de como configurar esses serviços.
:::
:::danger
:exclamation: **Nota:** Usar o Postgre como armazenamento de arquivos não é indicado para um ambiente de produção, pois há uma limitação na quantidade de arquivos que podem ser armazenados, mas para pequenos apps ou testes é uma boa opção por ser barata e não precisar de muita configuração
:::
A fim de aprendizado, iremos utilizar dois métodos: via PG e via arquivos locais como backup. Para usar o PG, precisamos instalar uma gema, então no `Gemfile`, adicione essa gema:
```ruby=28
gem 'active_storage-postgresql'
```
E então instalamos ela com `bundle install`. Quando tudo for atualizado, precisamos rodar dois utilitários no terminal:
```shell
rails active_storage:install
rails active_storage:postgresql:install
```
O primeiro cria as tabelas do ActiveStorage e o segundo cria as tabelas para usar o PG como armazenamento de arquivos. Após gerar as migrações, iremos configurar nossos *storages* no arquivo `config/storage.yml`. Trocamos o código dele por:
```yaml=
local:
pg:
mirror:
test:
```
No `local` iremos adicionar as configurações para salvar local, então adicionamos o seguinte:
```yaml=2
service: Disk
root: <%= Rails.root.join('storage') %>
```
Em `pg` iremos adicionar a configuração para salvar no PG, com:
```yaml=6
service: PostgreSQL
```
No `mirror` iremos juntar os dois, escolhendo um (`local` nesse exemplo) para ser o principal e o PG como espelho:
```yaml=9
service: Mirror
primary: local
mirrors: [ pg ]
```
Por fim, no `test` iremos configurar como será o ambiente de testes, que será similar ao local, porém salva na pasta `tmp/storage`, assim:
```yaml=14
service: Disk
root: <%= Rails.root.join('tmp/storage') %>
```
No final, nosso arquivo deve estar assim:
```yaml=1
local:
service: Disk
root: <%= Rails.root.join('storage') %>
pg:
service: PostgreSQL
mirror:
service: Mirror
primary: local
mirrors: [ pg ]
test:
service: Disk
root: <%= Rails.root.join('tmp/storage') %>
```
Agora precisamos usar essas configurações que criamos, e fazemos isso nos arquivos de ambiente, em `config/environments/`. Primeiro vamos cuidar do ambiente de desenvolvimento, então no arquivo `config/environments/development.rb` vamos trocar
```ruby=32
config.active_storage.service = :local
```
por
```ruby=32
config.active_storage.service = :mirror
```
para usar a configuração `mirror`.
Podemos fazer o mesmo para o arquivo `config/environments/production.rb` ou deixar `:local`, é opcional.
## Criando o modelo
Agora que temos tudo configurado, vamos precisar de um modelo para armazenar as nossas imagens e seus metadados, tipo título, dimensões, etc. O Rails tem um utilitário para isso `rails g model NOME atributo:tipo`, mas queremos mais do que apenas um modelo, queremos o fluxo inteiro (controlador, view e modelo). Então podemos usar o `scaffold`, assim:
```shell=
rails g scaffold image title:string user:references
```
Mas ainda temos que unir o modelo com o arquivo, então no `app/models/image.rb` vamos adicionar a seguinte linha dentro da classe:
```ruby=2
has_one_attached :file
```
:::warning
:exclamation: **has_one_attached** é o que faz o Rails entender que aquele modelo (`Image`) possui um `file` do `ActiveStorage`. Também é possível utilizar o **`has_many_attached`**, para quando se deseja diversos arquivos.
:::
E agora temos que dizer que cada usuário possui diversas imagens, colocando isso no arquivo `app/models/user.rb` dentro da classe do usuário:
```ruby=7
has_many :images
```
Com tudo no banco de dados criado, podemos rodar nossa migração, com `docker-compose exec web rails db:migrate` (a aplicação deve estar rodando).
## Enviando imagems
Antes de tudo, vamos limpar um pouco o código deletando os arquivos `app/assets/stylesheets/scaffolds.scss` e `app/assets/stylesheets/images.scss`, eles não são necessários já que estamos usando o Bootstrap. Também podemos deletar todos os arquivos em `app/views/images` que possuam a extensão `.json.jbuilder`, já que são para uso em APIs, e nosso App não vai possuir (por enquanto).
Na nossa *home page*, vamos colocar um link para a lista de imagens que temos. Atualmente já temos o link, mas ele não faz nada e não é tão bonito. Então no `app/views/home/index.html.erb` vamos mudar
```htmlembedded=14
<p class="card-text">Por favor, faça login ou se cadastre:</p>
<div class="btn-group d-flex" role="group">
<%= link_to 'Upload de Imagens', '', :class => 'navbar-link' %>
</div>
```
para
```htmlembedded=14
<p class="card-text">Escolha uma das ações a serem feitas:</p>
<div class="btn-group d-flex" role="group">
<%= link_to 'Upload de Imagens', images_path, :class => 'btn btn-primary' %>
</div>
```
E se acessarmos o link, nos deparamos com essa tela:
![](https://i.imgur.com/C6q06fX.png)
Falta algums coisas. Se apertarmos no botão de nova imagem, temos apenas a opção de inserir um título, mas não temos a opção de enviar arquivos. Vamos começar a mudança disso no `app/views/images/new.html.erb`. Aqui é simples a alteração necessária, apenas vamos adicionar uma `div` ao redor do arquivo, mudar o título para H1 e opcional, podemos colocar uma linha separando o link de voltar. O meu ficou assim:
```htmlembedded=
<div>
<h1 class="text-center">Nova Imagem</h1>
<%= render 'form', image: @image %>
<hr>
<%= link_to 'Voltar', images_path, class: 'btn btn-secondary' %>
</div>
```
Podemos fazer isso com o `app/views/images/edit.html.erb` também! O meu ficou assim:
```htmlembedded=
<div>
<h1 class="text-center">Editar imagem</h1>
<%= render 'form', image: @image %>
<hr>
<div class="btn-group">
<%= link_to 'Detalhes', @image, class: 'btn btn-primary' %>
<%= link_to 'Voltar', images_path, class: 'btn btn-secondary' %>
</div>
</div>
```
A mudança maior acontece no `app/views/images/_form.html.erb`, onde podemos substituir tudo por esse código:
```htmlembedded=
<%= form_with(model: image, local: true) do |form| %>
<% if image.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(image.errors.count, "error") %> prohibited this image from being saved:</h2>
<ul>
<% image.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<%= form.hidden_field(:user_id, value: current_user.id) %>
<div class="form-group">
<%= form.label :title, 'Título', class: 'form-label' %>
<%= form.text_field :title, required: true, class: 'form-control' %>
</div>
<div class="form-group">
<div class="custom-file">
<%= form.file_field :file, required: true, class: 'custom-file-input' %>
<%= form.label :file, 'Escolha o arquivo...', class: 'custom-file-label' %>
<div class="invalid-feedback">Example invalid custom file feedback</div>
</div>
</div>
<div class="btn-group d-flex">
<%= form.submit 'Enviar', class: 'btn btn-success' %>
</div>
<% end %>
```
Aqui as mudanças são as seguintes: primeiro adicionamos classes do bootstrap e adicionamos mais dois componentes, que são:
```htmlembedded=14
<%= form.hidden_field(:user_id, value: current_user.id) %>
```
que é um campo invisível na tela carregando o valor `user_id`. Se entrar na nossa migração (`db/migrate/*_create_images.rb`) vamos ver que tem uma linha que faz referencia ao `user` e ela faz o Rails entender que esse modelo vai ter um `user_id` para **relacionar** os objetos. Essa linha é importante pois com ela que dizemos que a imagem que estamos enviando pertence ao usuário X.
E o outro componente é
```htmlembedded=
<div class="form-group">
<div class="custom-file">
<%= form.file_field :file, required: true, class: 'custom-file-input' %>
<%= form.label :file, 'Escolha o arquivo...', class: 'custom-file-label' %>
<div class="invalid-feedback">Example invalid custom file feedback</div>
</div>
</div>
```
que é o nosso input de arquivos. Perceba que usamos uma classe chamada `custom-file-input`.
Se tentarmos enviar uma imagem agora, perceba que quando selecionamos o arquivo, o texto continua dizendo "Escolha o arquivo...", e seria legal ter um pouco mais de informação. Então vamos mudar isso.
No arquivo `app/javascript/packs/application.js`, vamos adicionar um **EventListener**, que adiciona um evento toda vez que algum componente de classe `custom-file-input` sofre modificação (podemos colocar esse código no final do arquivo):
```javascript=13
document.addEventListener('turbolinks:load', () => {
$('.custom-file-input').on('change',function(){
const fileName = $(this).val().split('\\');
$(this).next('.custom-file-label').html(fileName[fileName.length - 1]);
})
})
```
:::info
:bulb: **Nota:** Um EventListener é um artifício para "escutar eventos", ou seja, sempre que algo específico acontece ele é chamado. Neste caso, o evento se chama **`turbolinks:load`**, que é sempre que uma página do site é carregada.
:::
E agora se recarregarmos a página e selecionarmos um arquivo, vai aparecer o nome dele:
![](https://i.imgur.com/UBhX05O.png)
Agora o que falta é a gente permitir que o nosso controlador receba essa imagem.
Por causa do TOC, vamos primeiramente refatorar o controlador para ficar mais apresentável e tirar o código que não precisamos. Todos os método sem conteúdo (tais como o `def show`) podem virar *single line*, e podemos tirar todas as referências de `format.json`, já que não vamos tratar de API por enquanto.
A minha refatoração ficou assim:
```ruby=
class ImagesController < ApplicationController
before_action :set_image, only: %i[show edit update destroy]
def index
@images = Image.all
end
def show; end
def new
@image = Image.new
end
def edit; end
def create
@image = Image.new(image_params)
if @image.save
redirect_to @image, notice: 'Imagem criada!'
else
render :new, status: :unprocessable_entity
end
end
def update
if @image.update(image_params)
redirect_to @image, notice: 'Imagem atualizada!'
else
render :edit, status: :unprocessable_entity
end
end
def destroy
@image.destroy
redirect_to images_url, notice: 'Imagem deletada!'
end
private
def set_image
@image = Image.find(params[:id])
end
def image_params
params.require(:image).permit(:title)
end
end
```
:::info
:bulb: **Refatoração:** é um tratamento que damos ao código para melhora a leitura, visibilidade, aplicar padrões de engenharia e ao geral deixar um código mais agradável de ler e editar.
:::
Essa refatoração não mudou em nada o comportamento do controlador, ele continua não aceitando envio de arquivos. O que ele aceita está descrito no método `#image_params`, e podemos observar que ele permite apenas o `:title` porém precisamos do titulo, do arquivo e do id do usuário. Podemos facilmenter adicionr esses campos modificando a linha
```ruby=47
params.require(:image).permit(:title)
```
para
```ruby=47
params.require(:image).permit(:title, :file, :user_id)
```
:::info
:bulb: **Nota:** Em documentações, existem alguns padrões para se referir a métodos de classe ou métodos de instância. Métodos de classe são aqueles que a gente chama direto pela classe, como por exemplo **`Image.find`**, e nas documentaçãos usamos o **`.`** como sufixo, assim: **`.find`**. Métodos de instância são aqueles que chamamos através de instâncias, tipo **`@image.update`** e nas documentações usamos **`#`** como sufixo, assim: **`#update`**.
:::
E assim podemos enviar nossas imagens!
Mas ainda falta coisa, já que não tem nenhum jeito de vermos as imagens que temos.
Primeiro vamos alterar o `app/views/images/show.html.erb`, já que é o lugar que acessamos após enviar um arquivo. Podemos mudar o código para esse:
```htmlembedded=
<div class="card mx-auto" style="width: 50%; min-width: 200px; margin-top: 1em">
<%= image_tag @image.file, class: 'card-img-top' %>
<div class="card-body">
<h5 class="card-title"><%= @image.title %>!</h5>
<div class="btn-group d-flex">
<%= link_to 'Editar', edit_image_path(@image), class: 'btn btn-warning' %>
<%= link_to 'Voltar', images_path, class: 'btn btn-secondary' %>
</div>
</div>
</div>
```
que a principal mudança é o `<%= image_tag @image.file, class: 'card-img-top' %>`, que é o jeito de mostrar a imagem na tela. O resto das alterações é usar os [cards](https://getbootstrap.com/docs/4.0/components/card/) do Bootstrap.
Agora o que nos falta é a a view que mostra todas as imagens. Podemos usar as tabelas do Bootstrap para mostrar uma imagem por vez. No arquivo `app/views/images/index.html.erb` vamos trocar o código por:
```htmlembedded=
<div>
<h1 class="text-center">Images</h1>
<table class="table table-striped">
<thead class="thead-dark">
<tr>
<th scope="col">Título</th>
<th scope="col">Imagem</th>
<th scope="col">Ações</th>
</tr>
</thead>
<tbody>
<% @images.each do |image| %>
<tr>
<th scope="row"><%= image.title %></th>
<td><%= image_tag url_for image.file.representation(resize_to_limit: [52, 52]) %></td>
<td>
<div class="btn-group">
<%= link_to 'Detalhes', image, class: 'btn btn-primary' %>
<%= link_to 'Editar', edit_image_path(image), class: 'btn btn-warning' %>
<%= link_to 'Deletar', image, method: :delete, data: { confirm: 'Certeza?' }, class: 'btn btn-danger' %>
</div>
</td>
</tr>
<% end %>
</tbody>
</table>
<hr>
<%= link_to 'Nova imagem', new_image_path, class: 'btn btn-primary float-right' %>
</div>
```
A única novidade é o uso do `image.file.representation(resize_to_limit: [52, 52])`, que é um código que permite a gente buscar a imagem, redimensionar ela e apresentar ao usuário, assim ele não precisa trafegar tantos dado. Também usamos as classes de [tabela](https://getbootstrap.com/docs/4.0/content/tables/) do Bootstrap.
Porém ao acessar a página de imagens, nos deparamos com esse probleminha:
![](https://i.imgur.com/OEOl1Zx.png)
E se olharmos o nosso console do servidor, veremos essa mensagem de erro:
```shell=
web_1 | DEPRECATION WARNING: Generating image variants will require the image_processing gem in Rails 6.1. Please add `gem 'image_processing', '~> 1.2'` to your Gemfile. (called from transformer at /usr/local/bundle/gems/activestorage-6.0.3.5/app/models/active_storage/variation.rb:64)
web_1 | LoadError (cannot load such file -- mini_magick):
```
Isso acontece porque estamos com duas dependências faltando, a gema `image_processing` e o ImageMagick (ou a versão enxuta chamada MiniMagick). Para adicionar a gema é facil, basta tirar o comentario dessa linha no `Gemfile`:
```ruby=29
gem 'image_processing', '~> 1.2'
```
Porém para o ImageMagick, por ser uma biblioteca de sistema, temos que instalar no nosso `Dockerfile`. Então vamos adicionar
```dockerfile=5
RUN echo 'http://dl-cdn.alpinelinux.org/alpine/v3.5/main' >> /etc/apk/repositories
RUN apk add --no-cache imagemagick=6.9.6.8-r1 imagemagick-dev=6.9.6.8-r1
```
depois da linha `RUN apk add --update --no-cache shared-mime-info tzdata yarn nodejs build-base postgresql-dev`
COmo mexemos no `Dockerfile`, devemos matar nosso app, digitar `docker-compose build` e então levantar ele de novo.
E assim temos nossa imagem aparecendo:
![](https://i.imgur.com/tXOMTSm.png)
## Limitando o acesso
Se outro usuário criar uma imagem, todo mundo vai ver as imagens de todo mundo. Isso pode ser o comportamento esperado, mas neste caso não quero isso. Quero que cada usuário esteja limitado às suas próprias imagens. Para isso, basta uma simples alteração no `app/controllers/images_controller.rb`, no método `index`, onde alteramos
```ruby=5
@images = Image.all
```
para
```ruby=5
@images = current_user.images
```
Isso vai limitar a busca de imagems para apenas as do usuário atual.
:::warning
:rocket: **Desafio:** Usando o ActiveStorage, adicione um avatar pro usuário, para que ele possa enviar um quando cria a conta e apareca na Navbar ou na tela de editar perfil.
:::
:::success
:rocket: **Próximo passo:** Siga o link ➜ [Processamento em plano de fundo com Sidekiq](https://hackmd.io/uOs-ccO8QgypgwaP4LbKKA)
:::