
Você já precisou fazer Upload de arquivos no Rails de forma assíncrona (sem precisar dar reload na página)?
Nesse Tutorial que o Daniel Moreto (aluno do Bootcamp Super Full Stack \o/) fez para o OneBitCode, nós vamos aprender o passo a passo para implementar essa feature no seu software sem precisar usar plugins Jquery externos de maneira limpa e rápida.
*Obrigado Daniel pelo conteúdo incrível 🙂
Objetivo
Fazer Upload assíncrono de imagens no Rails
Ingredientes
- Ruby
- Rails On Rails
Introdução:
E aí galeera!
Sou o Daniel Moreto e hoje eu vou mostrar pra vocês como fazer um upload de arquivo via Ajax no Ruby On Rails.
Lembrando, esta é apenas uma forma de fazer isso. Bem rápida e sem plugins jquery adicionais.
Bem, partiu código então!
Gerando nosso Projeto
1 – Primeiro, rode no console para gerar o projeto de exemplo:
1 |
rails new async_upload |
2 – Se estiverem utilizando Rails 5, precisaremos colocar no Gemfile a gem jquery-rails
1 |
gem 'jquery-rails' |
3 – Agora para instalar a nova gem rode no console:
1 |
bundle install |
4 – Depois disso, remover o rails_ujs do application.js e colocar o jquery e o jquery_ujs. O jquery_ujs vamos precisar pra nos dar um help quando formos fazer uma chamada Ajax e não termos problema com CSRF Token Authenticity
1 2 |
//= require jquery //= require jquery_ujs |
Criando nosso Controller
Bem, agora vamos aos códigos que possibilitam este nosso upload. Vou começar explicando a estrutura dos arquivos backend pra vocês.
1 – Rode no console para criar um novo controller
1 |
rails g controller async_uploads |
2 – Coloque no controller gerado:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
class AsyncUploadsController < ApplicationController def index @files = Dir[Rails.root.join("public/sample_files/*")].map do |file| next if File.directory?(file) file_name = file.match(/\/([\w+\-?]*\.\w+)$/)[1] { name: file_name, link: "/sample_files/#{file_name}" } end.compact end def new end def create require "file_builder" params["files"].each do |key, file_to_upload| file_path = "public/sample_files" file_builder = FileBuilder.new(file_path,file_to_upload['fileName'], file_to_upload['fileContent']) file_builder.from_base_64 end redirect_to async_uploads_path end end |
Explicação:
Os meus arquivos serão salvos na pasta “public/sample_files/”, então na action “index” do meu controller eu estou simplesmente listando todos os arquivos com os respectivos nome (:name) e link e renderizo o meu template :index.
Na action #new eu apenas renderizo o template :new (Onde o usuário vai poder subir o arquivo).
Na action #create eu precisei criar um recurso chamado FileBuilder que vou explicar em seguida. Nesta action #create eu recebo como parâmetro uma chave “files” e nela vem as listas de arquivos que enviei a partir do front-end com o nome e conteúdo do arquivo. Então eu percorro a lista e pego cada arquivo para ser reconstruído usando o FileBuilder a partir do conteúdo. Como o conteúdo chegou aqui eu vou explicar daqui a pouco, antes eu preciso mostrar o FileBuilder, que está nos ajudando a construir nosso arquivo.
3 – Atualize o seu config/routes.rb colocando:
1 2 3 4 5 |
Rails.application.routes.draw do get '/async_uploads', to: 'async_uploads#index' get '/async_uploads/new', to: 'async_uploads#new' post '/async_uploads', to: 'async_uploads#create' end |
Criando nossa Lib
1 – Em lib/ crie um arquivo chamado file_builder.rb e coloque nele:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
class FileBuilder attr_reader :full_path def initialize(file_path, file_name, file_content) require "fileutils" FileUtils.mkdir_p file_path @full_path = "#{file_path}#{file_path.last != '/' ? '/' : ''}#{file_name}" @file_content = file_content end def from_base_64 content = Base64.decode64(@file_content.match(/,(.*)/)[1]) generate_file content @full_path end private def generate_file(decoded_content) File.open(@full_path, "w:binary") do |f| f.write decoded_content end end end |
Explicação:
O FileBuilder é uma lib (diretório lib/ do Rails) que eu criei que vai receber o path do arquivo que eu quero construir, o nome e conteúdo. Se você reparar, na linha 20 do meu AsyncUploadsController eu instancio o FileBuilder e na linha 21 eu chamo um método file_builder.from_base_64 para criar o arquivo. Mas, como eu faço isso?
Quando eu vou enviar alguma coisa via Ajax, não é possível passar nada além de texto, então para enviar um arquivo, seja ela uma imagem, .doc, .xlsx ou qualquer outra coisa, eu preciso pegar o conteúdo do meu arquivo, que é binário, e transformar numa cadeia de caracteres (texto). Pra fazer isso, nós recorremos à codificação Base64, que é feita no JS.
Apenas para esclarecer o Base64 é um método de codificação de dados (binário) em texto para transferência pela Internet.
Neste método from_base_64 eu faço justamente a decodificação desta sequência Base64 (o oposto do que o JS faz), que volta a ser binário e passo como parâmetro do método generate_file que visa criar o arquivo com o conteúdo em binário.
Mas e como eu faço essa sopa de letrinha chegar até o servidor? Bem, vamos aos arquivos front-end agora.
Criando nossas Views
1 – Para vermos a lista de arquivos, coloque na view “app/views/async_uploads/index”
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<h3>Files</h3> <hr> <ul> <% @files.each do |file| %> <li>name: <%= file[:name] %> | link: <a href="<%= file[:link] %>" target="_blank"><%= file[:link] %></a></li> <% end %> </ul> <hr> <a href="/async_uploads/new">Upload New File</a> |
2 – Para poder fazer o upload do arquivo no front end coloque na view “app/views/async_uploads/new”:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<h2>Upload a File</h2> <%= form_tag “/async_uploads”, method: :post do %> <div class="form-group"> <%= label_tag "async_upload_file", "File to Upload", class: "control-label" %> <%= file_field_tag "async_upload_file", id: "file_to_upload" %><br /> </div> <%= button_tag "Send", class: "btn btn-success", type: :button, id: "send_file" %> <% end %> |
Criando nosso JS
Nós precisamos de um arquivo JS para capturar o evento do click no botão send e realizar o upload via ajax do nosso arquivo.
1 – Crie um arquivo chamado (“app/assets/javascripts/async_upload.js”) e coloque nele:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
$(document).on("turbolinks:load", function(){ let files_to_upload = []; $("#file_to_upload").change(function(){ files_to_upload = [] let upload_files = $(this)[0].files; for(let count = 0; count < upload_files.length; count++) { readfile(upload_files[count], function(filecontent){ files_to_upload.push({ fileName: upload_files[count].name, fileContent: filecontent }); }); } }); $("button#send_file").click(function(){ $.post("/async_uploads", { files: files_to_upload }, function(){ alert("yahoo!"); }).fail(function(){ alert("ouch!") }); }); }); function readfile(file, callback_exec) { let reader = new FileReader(); reader.onload = function(e) { callback_exec(e.target.result) } reader.readAsDataURL(file); } |
Explicação:
Repare o seguinte: no final no arquivo há uma função chamada readfile que aceita um parâmetro file e um outro callback_exec. Bem, é nesta função que está o segredo da nossa codificação Base64.
Eu utilizo um recurso nativo do JS chamado FileReader. Primeiramente, na linha 31, eu instancio esta classe. Depois eu pego o objeto e atribuo ao onload dele uma função que eu criei. Esta função vai chamar o meu callback_exec internamente passado como parâmetro e vai enviar como argumento para ela o e.target.result que já é o conteúdo do arquivo que eu enviei como argumento file com a codificação Base64.
Mas, onde eu chamo este onload? Ele é um callback (outro) da função que eu chamo logo abaixo, a reader.readAsDataURL e este callback onload recebe como argumento o mesmo file que envio para a readAsDataURL.
Mas de onde vem este callback_exec? Por que ele é chamado? Já chegaremos lá!
Vamos ao topo do arquivo. Primeiro eu crio uma lista files_to_upload vazia. Depois eu atribuo ao meu campo input file um event change. Ou seja, toda vez que alguma coisa for anexada lá, esse evento vai disparar.
Meu próximo passo é pegar os arquivos que estão anexados neste campo, que faço na linha 6. Depois em cada arquivo que foi anexado nesta lista, eu chamo a minha função readfile que está lá embaixo e passo como um dos argumentos uma função que vai acrescentar na minha lista files_to_upload um objeto contendo o nome e o conteúdo do arquivo.
Mas por que criar um callback dentro do change pra isso? Justamente porque o FileReader funciona por meio de chamada callback. Apenas quando o conteúdo do arquivo é carregado para o objeto que a função onload do FileReader será chamada. A saída para isso é colocar o onload do FileReader para executar uma função minha, preenchendo a minha lista =).
Nas linhas seguintes eu apenas atribuo um evento click
ao meu botão de enviar e chamo a função jquery $.post para fazer um post assíncrono enviando a minha variável files_to_upload, que chamará a minha action :create do meu AsyncUploadsController e o resto vocês já sabem. =D
Uma observação que eu acho importante é a seguinte: quando a gente utiliza este métodos, o nosso log de aplicação vai ficar bem poluído, pois como tudo é texto, o Rails vai colocar todo o conteúdo Base64 do arquivo no log. Para deixar ele mais limpo, podemos adicionar um filtro no parâmetro no config/application.rb.
1 |
config.filter_parameters += [:files] |
Dessa forma, nossos logs virão com um [FILTERED] on lugar de todo o conteúdo de caracteres Base64.
Upload de multiplos Files
Outra coisa legal, é que o campo de arquivos (async_upload_file) que criamos aceita apenas um arquivo por vez, mas nossa estrutura já vem preparada para receber vários arquivos de uma vez, se for necessário. Então, para poder enviar mais de um arquivo de uma vez, basta colocar nosso campo como multiple:
1 |
<%= file_field_tag "async_upload_file", id: "file_to_upload", multiple: true % |
Assim podemos selecionar vários arquivos e enviar =)

Não perca nenhum conteúdo
Receba nosso resumo semanal com os novos posts, cursos, talks e vagas o/
Conclusão
Bem, galera, é isso.
Se tiverem alguma dúvida, eu tenho um repositório com o código de exemplo no link: https://github.com/dfmoreto/async_upload_ajax_rails
Sintam-se à vontade para enviar perguntas nos comentários abaixo ou até mesmo me contactar por redes sociais.
Github: https://github.com/dfmoreto
Linkedin: https://www.linkedin.com/in/dfmoreto/
Facebook: https://www.facebook.com/daniel.moreto.3
Até Breve =)
Daniel Moreto
Você é novo por aqui?
Primeira vez no OneBitCode? Curtiu esse conteúdo?
O OneBitCode tem muito mais para você!
O OneBitCode trás conteúdos de qualidade e em português sobre programação com foco em Ruby on Rails e outras tecnologias como Angular, Ionic, React, desenvolvimento de Chatbots e etc.
Se você deseja aprender mais, de uma forma natural e dentro de uma comunidade ativa, visite nosso Facebook e nosso Twitter, veja os screencasts e talks no Youtube, alguns acontecimentos no gay dating kansas city, ouça os top gay men e faça parte de nossa Newsletter.
Além disso, também estamos com alguns e-Books muito interessantes para quem deseja se aprimorar como programador e também como freelancer (os e-Books são gratuitos!):
- ryan single dating site graphic designer brooklyn new york
- Desenvolvendo seus projetos como um profissional
- https://onebitcode.com/reddit-top-free-dating-apps/
- PDF com links fundamentais para quem quer ser um freelancer de sucesso
- europe dating site
- Baixe gratuitamente seu e-Book com 60 Gems separadas por categorias
Espero que curta nossos conteúdos e sempre que precisar de ajuda com os tutoriais, fala com a gente!
Seja por Facebook ou e-mail, estamos aqui para você 🙂
Bem-vindo à família OneBitCode \o/
Extremamente interessante e funcional. Mas como conseguiria aplicar este caso em um cadastro de clientes por exemplo? Ou seja, criar a pasta no public específica por id de cliente?
Ei, Francisco! Entao, para este caso eu recomendo você criar uma estrutura de diretórios proprio para o upload e fora da pasta public.
Nesta estrutura você pode separar por id do cliente sem problema.
E um ultimo ponto é você armazenar o nome original do arquivo numa tabela e na pasta vc salvar com um aleatório (uma sequencia de caracteres, por exemplo) para evitar que ocorra uma sobrescita de arquivo caso alguém suba dois arquivos diferentes com o mesmo nome.
Parabéns pelo post Daniel, muito bem explicado e útil!
Rodou legal aqui na minha máquina quando enviei uma imagem .jpg, mas quando testei uma outra extensão da erro. (undefined method `[]’ for nil:NilClass).
E o engraçado é que mesmo assim a imagem é enviada, mas depois disso ele fica em um looping do erro, mesmo parando o servidor.
A forma que resolveu, não a ideal, foi remover a imagem do diretório.
Onde é possível fazer a definição de extensão aceitável pra upload das imagens?
Você obteve esse erro também?
Abraços