1. Code
  2. Ruby
  3. Ruby on Rails

Subiendo Con Rails y Carrierwave

Este es otro artìuclo en la serie "Subiendo con Rails. Hoy vamos a conocer Carrierwave---una de las soluciones de subida màs populares para Rails. Me gusta Carrierwave porque es fácil comenzar, tiene muchas caracterìsticas fuera de la caja, y proporciona docenas de artículos "cómo hacer" escritos por los miembros de la comunidad, así que no te perderás.
Scroll to top

Spanish (Español) translation by Rafael Chavarría (you can also view the original English article)

Este es otro artìuclo en la serie "Subiendo con Rails. Hoy vamos a conocer Carrierwave---una de las soluciones de subida màs populares para Rails. Me gusta Carrierwave porque es fácil comenzar, tiene muchas caracterìsticas fuera de la caja, y proporciona docenas de artículos "cómo hacer" escritos por los miembros de la comunidad, así que no te perderás.

En este artìculo, aprenderás cómo:

  • Integrar Carrierwave a tu aplicaciòn Rails
  • Agregar validaciones
  • Persistir archivos a lo largo de peticiones
  • Remover archivos
  • Generar viñetas
  • Subir archivos desde ubicaciones remotas
  • Introducir múltiples subidas de archivos
  • Agregar soporte para almacenamiento en la nube

El código fuente para este artículo está disponible en GitHub. ¡Disfruta leyendo!

Sentando las Bases

Como siempre, comienza creando una nueva aplicación Rails:

1
rails new UploadingWithCarrierwave -T

Para este demo estaré usando Rails 5.0.2. Por favor nota que Carrierwave 1 soporta solo Rails 4+ y Ruby 2. Si aún estás usando Rails 3, entonces usa Carrierwave versión 0.11.

Para ver a Carrierwave en acción, vamos a crear una aplicación muy simple de blogging con un solo modelo Post. Tendrá los siguientes atributos principales:

  • title (string)
  • body (text)
  • image (string)---este campo va a contener una imagen (un nombre de archivo, para ser preciso) adjunto a la publicación-

Genera y aplica una nueva migración:

1
rails g model Post title:string body:text image:string
2
rails db:migrate

Establece algunas reglas:

config/routes.rb

1
resources :posts
2
root to: 'posts#index'

También, crea un controlador muy básico:

posts_controller.rb

1
class PostsController < ApplicationController
2
  before_action :set_post, only: [:show, :edit, :update]
3
4
  def index
5
    @posts = Post.order('created_at DESC')
6
  end
7
8
  def show
9
  end
10
11
  def new
12
    @post = Post.new
13
  end
14
15
  def create
16
    @post = Post.new(post_params)
17
    if @post.save
18
      redirect_to posts_path
19
    else
20
      render :new
21
    end
22
  end
23
24
  def edit
25
  end
26
27
  def update
28
    if @post.update_attributes(post_params)
29
      redirect_to post_path(@post)
30
    else
31
      render :edit
32
    end
33
  end
34
35
  private
36
37
  def post_params
38
    params.require(:post).permit(:title, :body, :image)
39
  end
40
41
  def set_post
42
    @post = Post.find(params[:id])
43
  end
44
end

Ahora construyamos la vista index:

views/posts/index.html.erb

1
<h1>Posts</h1>
2
3
<%= link_to 'Add post', new_post_path %>
4
5
<%= render @posts %>

Y el parcial correspondiente:

views/posts/_post.html.erb

1
<h2><%= link_to post.title, post_path(post) %></h2>
2
3
<p><%= truncate(post.body, length: 150) %></p>
4
5
<p><%= link_to 'Edit', edit_post_path(post) %></p>
6
<hr>

Aquí estoy usando el método de Rails truncate para mostrar solo los primeros 150 símbolos de la publicación. Antes de crear otras vistas y formar un parcial, primero integremos Carrierwave a la aplicación.

Integrando Carrierwave

Pon una nueva gem en el Gemfile:

Gemfile

1
gem 'carrierwave', '~> 1.0'

Ejecuta:

1
bundle install

Carrierwave almacena su configuración dentro de uploaders que están incluidos en tus modelos. Para generar un uploader, usa el siguiente comando:

1
rails generate uploader Image

Ahora, dentro de app/uploaders, encontrarás un nuevo archivo llamado image_uploader.rb. Nota que tiene algunos comentarios útiles y ejemplos, así que podrías usarlos para comenzar. En este demo vamos a usar ActiveRecord, pero Carrierwave también tiene soporte para Mongoid, Sequel y DataMapper.

Después, necesitamos incluir o montar este uploader al modelo:

models/post.rb

1
mount_uploader :image, ImageUploader

El uploader ya tiene ajustes por defecto, pero al menos necesitamos elegir en dónde serán almacenados los archivos subidos. Por ahora, empleemos almacenamiento de archivos:

uploaders/image_uploader.rb

1
storage :file

Por defecto, los archivos serán colocados dentro del directorio public/uploads, así que es mejor exluirlo del sistema de control de versión:

.gitignore

1
public/uploads

Podrás también modificar el método store_dir dentro de tu uploader para elegir alguna otra ubicación.

En este punto, vamos a crear una nueva vista y una forma parcial para comenzar a subir archivos:

views/posts/new.html.erb

1
<h1>Add post</h1>
2
3
<%= render 'form', post: @post %>

views/posts/_form.html.erb

1
<%= form_for post do |f| %>
2
  <div>
3
    <%= f.label :title %>
4
    <%= f.text_field :title %>
5
  </div>
6
7
  <div>
8
    <%= f.label :body %>
9
    <%= f.text_area :body %>
10
  </div>
11
12
  <div>
13
    <%= f.label :image %>
14
    <%= f.file_field :image %>
15
  </div>
16
17
  <%= f.submit %>
18
<% end %>

Nota que el PostController no necesita ser modificado ya que ya permitimos el atributo image.

Por último, crea la vista editar:

views/posts/edit.html.erb

1
<h1>Edit post</h1>
2
3
<%= render 'form', post: @post %>

¡Eso es todo! Debes iniciar el servidor e intentar crear una publicación con una imagen. El problema es que esta imagen no es visible en ningún lugar, así que procedamos a la siguiente sección y agreguemos una página para mostrar! 

Mostrando Imágenes

Así qué, la única vista que no hemos creado aún es show. Agrégala ahora:

views/posts/show.html.erb

1
<%= link_to 'All posts', posts_path %>
2
<h1><%= @post.title %></h1>
3
4
<%= image_tag(@post.image.url, alt: 'Image') if @post.image? %>
5
6
<p><%= @post.body %></p>
7
8
<p><%= link_to 'Edit', edit_post_path(@post) %></p>

Como puedes  ver, mostrar un adjunto es realmente sencillo, todo lo que necesitas hacer es decir @post.image.url para tomar la URL de una imagen. Para obtener una ruta al archivo, usa el método current_path. Nota que Carrierwave también proporciona un método image? para que revisemos si un adjunto está presente en absoluto (el método image por si mismo nunca regresará nil, incluso si el archivo no está presente).

Ahora, después de navegar a una publicación, deberías ver una imagen, pero podría aparecer muy grande: después de todo, no estamos restringiendo dimensiones en ningún lugar. Por supuesto, podríamos haber escalado la imagen con algunas reglas CSS, pero es mucho mejor generar una viñeta después de que el archivo ha sido cargado. Esto, sin embargo, requiere algunos pasos adicionales.

Generando Viñetas

Para poder cortar y escalar imágenes, necesitamos una herramienta por separado.  Fuera de la caja Carrierwave tiene soporte para gems RMagick y MiniMagick que, en su momento, son usadas para manipular imágenes con la ayuda de ImageMagick. ImageMagick es una solución de código abierto que te permite editar imágenes existentes y generar nuevas, así que antes de proceder, necesitas descargarlo e instalalrlo. Después, eres libre de elegir cualquiera de las dos gems. Me quedaré con MiniMagick, porque es mucho más sencillo de instalar y tiene mejor soporte:

Gemfile

1
gem 'mini_magick'

Ejecuta:

1
bundle install

Después incluye MiniMagick en tu uploader:

uploaders/image_uploader.rb

1
include CarrierWave::MiniMagick

Ahora simplemente necesitamos introducir una nueva versión a nuestro uploader. El concepto de versiones (o estilos) es usado en muchas librerías de carga de archivos; simplemente significa que archivos adicionales basados en el adjunto original serán creados con, por ejemplo, diferentes dimensiones o formatos. Introduce una nueva versión llamada thumb:

uploaders/image_uploader.rb

1
version :thumb do
2
    process resize_to_fill: [350, 350]
3
end

Podrías tener tantas versiones como quieras y, lo que es más, las versiones pueden incluso ser construidas encima de otras:

uploaders/image_uploader.rb

1
version :small_thumb, from_version: :thumb do
2
    process resize_to_fill: [20, 20]
3
end

Si ya has subido algunas imágenes, no tendrán miniaturas disponibles. Esto no es un problema, sin embargo, ya que puedes re-crearlas desde la consola Rails:

1
rails c
2
Post.find_each {|post| post.image.recreate_versions!(:thumb) if post.image?}

Por último, muestra tu miniatura con un enlace a la imagen original:

views/posts/show.html.erb

1
<%= link_to(image_tag(@post.image.thumb.url, alt: 'Image'), @post.image.url, target: '_blank') if @post.image? %>

¡Inicia el servidor y observa el resultado!

Agregando Validaciones

Actualmente nuestra subida funciona, pero no hay entrada de usuario validada en absoluto, lo que es, por supuesto, malo. Mientras queremos trabajar solo con imágenes, aceptemos extensiones .png, .jpg, .gif.

uploaders/image_uploader.rb

1
def extension_whitelist
2
    %w(jpg jpeg gif png)
3
end

También podrías agregar revisiones de tipo de contenido definiendo un método content_type_whitelist:

uploaders/image_uploader.rb

1
def content_type_whitelist
2
    /image\//
3
end

De manera alternativa, es posible bloquear algunos tipos de archivo, por ejemplo, ejecutables, definiendo el método content_type_blacklist:

Aparte de revisar el tipo y extensión de un archivo, forcémoslo a ser menor a 1 megabyte. Para hacerlo, requeriremos gem de soporte de validación de archivo para ActiveModel:

Gemfile

1
gem 'file_validators'

Instálalo:

1
bundle install

Ahora introducimos las validaciones deseadas (nota que también estoy agregando revisiones para los atributos title y body):

models/post.rb

1
validates :title, presence: true, length: {minimum: 2}
2
validates :body, presence: true
3
validates :image, file_size: { less_than: 1.megabytes }

La siguiente cosa para hacer es agregar traducciones l18n para mensajes de error de Carrierwave:

config/locales/en.yml

1
en:
2
  errors:
3
    messages:
4
      carrierwave_processing_error: "Cannot resize image."
5
      carrierwave_integrity_error: "Not an image."
6
      carrierwave_download_error: "Couldn't download image."
7
      extension_whitelist_error: "You are not allowed to upload %{extension} files, allowed types: %{allowed_types}"
8
      extension_blacklist_error: "You are not allowed to upload %{extension} files, prohibited types: %{prohibited_types}"

Actualmente, no mostramos errores de validación en ningún lado, así que creemos un parcial compartido:

views/shared/_errors.html.erb

1
<% if object.errors.any? %>
2
  <h3>Some errors were found:</h3>
3
  <ul>
4
    <% object.errors.full_messages.each do |message| %>
5
      <li><%= message %></li>
6
    <% end %>
7
  </ul>
8
<% end %>

Emplea este parcial dentro del formulario:

views/posts/_form.html.erb

1
<%= render 'shared/errors', object: post %>

Ahora intenta cargar algunos archivos válidos y observa el resultado. Debería funcionar, pero si eliges un archivo válido y no llenas el title o body, entonces las revisiones aún fallarán y un error se mostrará. Sin embargo, el campo de archivo será limpiado y el usuario necesitará elegir la imagen de nuevo, lo que no es muy conveniente. Para corregirlo, necesitamos agregar otro campo al formulario.

Archivos Persistentes A lo Largo de Peticiones

Archivos persistentes a lo largo de redisplays de formulario son realmente fáciles. Lo único que necesitas hacer es agregar un nuevo campo oculto y permitirlo dentro del controlador:

views/shared/_form.html.erb

1
<%= f.label :image %>
2
<%= f.file_field :image %><br>
3
<%= f.hidden_field :image_cache %>

posts_controller.rb

1
params.require(:post).permit(:title, :body, :image, :image_cache)

Ahora el image_cache será poblado automáticamente y la imagen no se perderá. Podría ser de utilidad para mostrar una miniatura para que el usuario entienda que la imagen fue procesada exitósamente:

views/shared/_form.html.erb

1
<% if post.image? %>
2
    <%= image_tag post.image.thumb.url %>
3
<% end %>

Quitando Imágenes

Otra característica muy común es la habilidad de remover archivos adjuntos cuando se edita un registro. Con Carrierwave, implementar esta característica no es un problema. Agrega una nueva casilla al formulario:

views/shared/_form.html.erb

1
<% if post.image? %>
2
    <%= image_tag post.image.thumb.url %>
3
    <div>
4
      <%= label_tag :remove_image do %>
5
        Remove image
6
        <%= f.check_box :remove_image %>
7
      <% end %>
8
    </div>
9
<% end %>

Y permite el atributo remove_image:

posts_controller.rb

1
params.require(:post).permit(:title, :body, :image, :remove_image, :image_cache)

¡Eso es! Para remover una imagen manualmente, usa el método remove_image!:

1
@post.remove_image!

Subiendo Desde una Ubicación Remota

Carrierwave también proporciona una característica muy interesante fuera de la caja: la habilidad de subir archivos desde ubicaciones remotas por su URL. Introduzcamos esta habilidad ahora para agregar un nuevo campo y permitiendo el atributo correspondiente:

views/shared/_form.html.erb

1
<%= f.text_field :remote_image_url %>
2
<small>Enter URL to an image</small>

posts_controller.rb

1
params.require(:post).permit(:title, :body, :image, :remove_image, :image_cache, :remote_image_url)

¿Qué tan genial es eso? No necesitas hacer ningún cambio en absoluto, ¡y puedes probar esta característica inmediatamente!

Trabajando Con Múltiples Subidas

Supón que queremos que nuestra publicación tenga varios adjuntos disponibles. Con la configuración actual no es posible, pero por suerte, Carrierwave soporta dicho escenario también. Para implementar esta característica, necesitas agregar ya sea un campo serializado (para SQLite) o un campo JSON (para Postgres o MySQL). Prefiero la última opción, así que cambiemos a un nuevo adaptador de base de datos ahora. Remueve el gem sqlite3 del Gemfile y agrega pg en su lugar:

Gemfile

1
gem 'pg'

Instálalo:

1
bundle install

Modifica la configuración de base de datos como esto:

config/database.yml

1
default: &default
2
  adapter: postgresql
3
  pool: 5
4
  timeout: 5000
5
6
development:
7
  <<: *default
8
  database: upload_carrier_dev
9
  username: 'YOUR_USER'
10
  password: 'YOUR_PASSWORD'
11
  host: localhost

Crea la base de datos Postgres correspondiente, y después genera y aplica la migración:

1
rails g migration add_attachments_to_posts attachments:json
2
rails db:migrate

Si prefieres apegarte con SQLite, sigue las instrucciones listadas en la documentación de Carrierwave.

Ahora monta los cargadores (¡nota la forma en plural!)

model/post.rb

1
mount_uploaders :attachments, ImageUploader

Estoy usando el mismo cargador para adjuntos, pero por supuesto puedes generar un nuevo con configuración diferente.

Agrega el campo de archivos múltiples a tu formulario:

views/shared/_form.html.erb

1
<div>
2
    <%= f.label :attachments %>
3
    <%= f.file_field :attachments, multiple: true %>
4
</div>

Mientras que el campo attachments va a contener una arreglo, debería ser permitido de la siguiente manera:

posts_controller.rb

1
params.require(:post).permit(:title, :body, :image, :remove_image, :image_cache, :remote_image_url, attachments: [])

Por último, debes iterar sobre los adjuntos de la publicación y mostrarlos de manera usual:

views/shared/show.html.erb

1
<% if @post.attachments? %>
2
  <ul>
3
    <% @post.attachments.each do |attachment| %>
4
      <li><%= link_to(image_tag(attachment.thumb.url, alt: 'Image'), attachment.url, target: '_blank') %></li>
5
    <% end %>
6
  </ul>
7
<% end %>

Nota que cada adjunto va a tener una miniatura como se configuró en nuestro ImageUploader. ¡Bien!

Usando Almacenamiento en la Nube

Apegarse al almacenamiento de archivos no siempre es conveniente y/o posible ya que, por ejemplo, en Heroku no es posible almacenar archivos personalizados. Así pues podrías preguntarte ¿cómo casar Carrierwave con el almacenamiento en la nube de Amazon S3? Bueno, esa es una tarea bastante fácil también. Carrierwave depende de la gem fog-aws para implementar esta característica:

Gemfile

1
gem "fog-aws"

Instálalo:

1
bundle install

Creemos un inicializador para Carrierwave y configuremos el almacenamiento en la nube de manera global:

config/initializers/carrierwave.rb

1
CarrierWave.configure do |config|
2
  config.fog_provider = 'fog/aws'
3
  config.fog_credentials = {
4
      provider:              'AWS',
5
      aws_access_key_id:     ENV['S3_KEY'],
6
      aws_secret_access_key: ENV['S3_SECRET'],
7
      region:                ENV['S3_REGION'],
8
  }
9
  config.fog_directory  = ENV['S3_BUCKET']
10
end

Hay algunas otras opciones disponibles, las cuáles pueden ser encontradas en la documentación.

Estoy usando el gem dotenv-rails para establecer las variables de sesión de manera segura, pero podrías querer elegir cualquier otra opción. Sin embargo, asegúrate de que tu par de llave S3 no esté disponible de manera pública, ¡porque de otro modo cualquier puede subir cualquier cosa a tu cubeta!

Después, reemplaza la línea storage :file con:

uploaders/image_uploader.rb

1
storage :fog

Además de S3, Carrierwave soporta subidas para Google Storage y Rackspace. Estos servicios son fáciles de configurar también.

Conclusión

¡Esto es todo por hoy! Hemos cubierto todas las características mayores de Carrierwave, y ahora puedes comenzar a usarlo en tus proyectos. Tiene algunas opciones adicionales disponibles, así que navega la documentación.

Si te atoras, no dudes en publicar tus preguntas. También, podría ser útil echar un vistazo al wiki de Carrierwave, que hospeda artículos "como hacer" útiles contestando muchas preguntas canónicas.

Así que gracias por quedarte conmigo, ¡y feliz codificación!