Advertisement
  1. Code
  2. Ruby on Rails

Subiendo Archivos Con Rails y Dragonfly

Scroll to top
Read Time: 15 min

() translation by (you can also view the original English article)

Hace algún tiempo escribí un artículo Subiendo Archivos con Rails y Shrine que explicó cómo introducir una característica de carga de archivos en tu aplicación Rails con la ayuda de la gem Shrine. Hay, sin embargo, muchas soluciones similares disponibles, y una de mis favoritas es Dragonfly---una solución de carga fácil de usar para Rails y Rack creada por Mark Evans.

Cubrimos esta librería a principios del año pasado pero, como con la mayoría del software, ayuda echar un vistazo a las librerías de vez en cuando para ver qué ha cambiado y cómo podemos emplearlas en nuestra aplicación.

En este artículo te guiaré a través de la configuración de Dragonfly y explicaré cómo utilizar sus características principales. Aprenderás como:

  • Integrar Dragonfly en tu aplicación
  • Configurar modelos para trabajar con Dragonfly
  • Introducir un mecanismo de subida básico
  • Introduce validaciones
  • Genera miniaturas de imagen
  • Realiza procesamiento de archivo
  • Almacena metadatos para archivos subidos
  • Prepara una aplicación para despliegue

Para hacer las cosas más interesantes, vamos a crear una pequeña aplicación musical. Esta presentará álbumes y canciones asociadas que pueden ser administradas y reproducidas en el sitio web.

El código fuente para este artículo está disponible en GitHub. También puedes revisar el demo funcionando de la aplicación.

Escuchando y Administrando Álbumes

Para comenzar, crea una nueva aplicación Rails sin la suite de prueba por defecto:

1
rails new UploadingWithDragonfly -T

Para este artículo estaré usando Rails 5, pero la mayoría de los conceptos descritos aplican a versiones antiguas también.

Creando el Modelo, Controlador y Rutas

Nuestro sitio musical va a contener dos modelos, Album y Song. Por ahora, creemos el primero con los siguientes campos:

  • title (string)---contiene el título del álbum.
  • singer (string)---intérprete del álbum
  • image_uid (string)---un campo especial para almacenar la imagen previa del álbum. Este campo puede ser nombrado como cualquier cosa que quieras, pero debe contener el sufijo _uid como se instruye en la documentación Dragonfly.

Crea y aplica la migración correspondiente:

1
rails g model Album title:string singer:string image_uid:string
2
rails db:migrate

Ahora creemos un controlador muy genérico para administrar álbumes con todas las acciones por defecto:

albums_controller.rb

1
class AlbumsController < ApplicationController
2
  def index
3
    @albums = Album.all
4
  end
5
6
  def show
7
    @album = Album.find(params[:id])
8
  end
9
10
  def new
11
    @album = Album.new
12
  end
13
14
  def create
15
    @album = Album.new(album_params)
16
    if @album.save
17
      flash[:success] = 'Album added!'
18
      redirect_to albums_path
19
    else
20
      render :new
21
    end
22
  end
23
24
  def edit
25
    @album = Album.find(params[:id])
26
  end
27
28
  def update
29
    @album = Album.find(params[:id])
30
    if @album.update_attributes(album_params)
31
      flash[:success] = 'Album updated!'
32
      redirect_to albums_path
33
    else
34
      render :edit
35
    end
36
  end
37
38
  def destroy
39
    @album = Album.find(params[:id])
40
    @album.destroy
41
    flash[:success] = 'Album removed!'
42
    redirect_to albums_path
43
  end
44
45
  private
46
47
  def album_params
48
    params.require(:album).permit(:title, :singer)
49
  end
50
end

Por último, agrega las rutas:

config/routes.rb

1
resources :albums

Integrando Dragonfly

Es tiempo para que Dragonfly haga su aparición. Primero, agrega la gem al Gemfile:

Gemfile

1
gem 'dragonfly'

Ejecuta:

1
bundle install

2
rails generate dragonfly

El último comando creará un inicializador llamado dragonfly.rb con la configuración por defecto. Lo dejaremos de lado por ahora, pero podrías querer sobre las varias opciones en el sitio web oficial de Dragonfly.

La siguiente cosa importante es equipar nuestro modelo con los métodos de Dragonfly. Esto es hecho usando el dragonfly_accessor:

models/album.rb

1
dragonfly_accessor :image

Nota que aquí estoy diciendo :image---se relaciona directamente a la columna image_uid que creamos en la sección anterior. Si tu, por ejemplo, nombraste a tu columna photo_uid, entonces el método dragonfly_accessor necesitaría recibir :photo como un argumento.

Si estás usando Rails 4 o 5, otro paso importante es marcar el campo :image (¡no :image_uid!) como permitido en el controlador:

albums_controller.rb

1
params.require(:album).permit(:title, :singer, :image)

Esto es prácticamente todo---¡estamos listos para crear vistas y comenzar a subir archivos!

Creando Vistas

Comienza con la vista index:

views/albums/index.html.erb

1
<h1>Albums</h1>
2
3
<%= link_to 'Add', new_album_path %>
4
5
<ul>
6
  <%= render @albums %>
7
</ul>

Ahora el parcial:

views/albums/_album.html.erb

1
<li>
2
  <%= image_tag(album.image.url, alt: album.title) if album.image_stored? %>
3
  <%= link_to album.title, album_path(album) %> by
4
  <%= album.singer %>
5
  | <%= link_to 'Edit', edit_album_path(album) %>
6
  | <%= link_to 'Remove', album_path(album), method: :delete, data: {confirm: 'Are you sure?'} %>
7
</li>

Hay dos métodos Dragonfly para notar aquí:

  • album.image.url devuelve la ruta a la imagen.
  • album.image_stored? dice si el registro tiene un archivo subido en lugar.

Ahora agrega las páginas new y edit:

views/albums/new.html.erb

1
<h1>Add album</h1>
2
3
<%= render 'form' %>

views/albums/edit.html.erb

1
<h1>Edit <%= @album.title %></h1>
2
3
<%= render 'form' %>

views/albums/_form.html.erb

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

El formulario no es nada elegante, pero de nuevo nota que estamos diciendo :image, no :image_uid, cuando generamos la entrada de archivo.

¡Ahora puedes reiniciar el servidor y probar la característica de subida!

Quitando Imágenes

Así que los usuarios pueden crear y editar álbumes, pero hay un problema: ellos no tienen manera de quitar una imagen, solo reemplazarla con otra. Afortunadamente, esto es muy sencillo de arreglar introduciendo una casilla "remover imagen":

views/albums/_form.html.erb

1
<% if @album.image_thumb_stored? %>
2
    <%= image_tag(@album.image.url, alt: @album.title)  %>
3
    <%= f.label :remove_image %>
4
    <%= f.check_box :remove_image %>
5
<% end %>

Si el álbum tiene una imagen asociada, la mostramos y generamos una casilla. Si la casilla está marcada, la imagen será removida. Nota que si tu campo es nombrado photo_uid, entonces el método correspondiente para remover adjunto será remove_photo. Simple, ¿no es así?

La única otra cosa por hacer es permitir el atributo remove_image en tu controlador.

albums_controller.rb

1
params.require(:album).permit(:title, :singer, :image, :remove_image)

Agregando Validaciones

En esta etapa, todo está funcionando bien, pero no estamos revisando la entrada del usuario en absoluto, lo que no es particularmente grandioso. Así pues, agreguemos validaciones para el modelo Álbum:

models/album.rb

1
validates :title, presence: true
2
validates :singer, presence: true
3
validates :image, presence: true
4
validates_property :width, of: :image, in: (0..900)

validates_property es el método Dragonfly que revisaría varios aspectos de tu ajunto: podrías validar una extensión de archivo, tipo de MIME, tamaño, etc.

Ahora creemos un parcial genérico para mostrar los errores que fueron encontrados:

views/shared/_errors.html.erb

1
<% if object.errors.any? %>
2
  <div>
3
    <h4>The following errors were found:</h4>
4
5
    <ul>
6
      <% object.errors.full_messages.each do |msg| %>
7
        <li><%= msg %></li>
8
      <% end %>
9
    </ul>
10
  </div>
11
<% end %>

Emplea este parcial dentro del formulario:

views/albums/_form.html.erb

1
<%= form_for @album do |f| %>
2
    <%= render 'shared/errors', object: @album %>
3
    <%# ... %>
4
<% end %>

Estiliza los campos con errores un poco para representarlos visualmente:

stylesheets/application.scss

1
.field_with_errors {
2
  display: inline;
3
  label {
4
    color: red;
5
  }
6
  input {
7
    background-color: lightpink;
8
  }
9
}

Reteniendo una Imagen Entre Peticiones

Habiendo introducido validaciones, nos encontramos con otro problema (todo un escenario típico, ¿no?): si el usuario ha cometido errores mientras completa el formulario, el o ella necesitará elegir el archivo de nuevo después de dar clic al botón Enviar.

Dragonfly puede ayudarte a resolver este problema así como usar un campo oculto retained_*:

views/albums/_form.html.erb

1
<%= f.hidden_field :retained_image %>

No olvides permitir este campo también:

albums_controller.rb

1
params.require(:album).permit(:title, :singer, :image, :remove_image, :retained_image)

¡Ahora la imagen será persistente entre peticiones! El único problema pequeño, sin embargo, es que la entrada de carga de archivo aún mostrará el mensaje "elige un archivo", pero esto puede ser arreglado con algún estilo y un poco de JavaScript.

Procesando Imágenes

Generando Miniaturas

Las imágenes subidas por nuestros usuarios pueden tener dimensiones muy diferentes, lo que puede (y probablemente será) causar un impacto negativo en el diseño del sitio web. Probablemente te gustaría escalar las imágenes a alguna dimensión fija, y por supuesto esto es posible utilizando los estilos width y height. Esto es, sin embargo, una aproximación no óptima: el navegador aún necesitará descargar imágenes de tamaño completo y encogerlas.

Otra opción  (que usualmente es mucho mejor) es generar miniaturas de imagen con algunas dimensiones predefinidas en el servidor. Esto es realmente simple de lograr con Dragonfly:

views/albums/_album.html.erb

1
<li>
2
  <%= image_tag(album.image.thumb('250x250#').url, alt: album.title) if album.image_stored? %>
3
  <%# ... %>
4
</li>

250x250 son, por supuesto, las dimensiones, en donde # es la geometría que significa "redimensiona y recorta si es necesario para mantener la relación de aspecto con gravedad central". Podrías encontrar información sobre otras geometrías en el sitio web de Dragonfly.

El método thumb es soportado por ImageMagick----una gran solución para crear y manipular imágenes. De ahí, para poder ver el demo funcionando de manera local, necesitarás instalar ImageMagick (todas las plataformas principales están soportadas).

El soporte para ImageMagick está habilitado por defecto dentro del inicializador de Dragonfly:

config/initializers/dragonfly.rb

1
plugin :imagemagick

Ahora las miniaturas están siendo generadas, pero no se almacenan en ningún lugar. Esto significa que cada vez que un usuario visite la página de álbumes, las miniaturas serán generadas. Hay dos maneras de superar este problema: generándolas después de que el registro es guardado o realizando la generación al vuelo.

La primera opción involucra introducir una nueva columna para almacenar la miniatura y modificar el método dragonfly_accessor. Crea y aplica una nueva migración:

1
rails g migration add_image_thumb_uid_to_albums image_thumb_uid:string
2
rails db:migrate

Ahora modifica el modelo:

models/album.rb

1
dragonfly_accessor :image do
2
    copy_to(:image_thumb){|a| a.thumb('250x250#') }
3
end
4
5
dragonfly_accessor :image_thumb

Nota que ahora la primera llamada a dragonfly_accessor envía un bloque que realmente genera la miniatura por nosotros y la copia en la image_thumb. Ahora solo usa el método image_thumb en tus vistas:

views/albums/_album.html.erb

1
<%= image_tag(album.image_thumb.url, alt: album.title) if album.image_thumb_stored? %>

Esta solución es la más simple, pero no es recomendada por la documentación oficial y, lo que es peor, al momento de escribir no funciona con los campos retained_*.

Así pues, déjame mostrarte otra opción: generar miniaturas al vuelo. Esto involucra crear un nuevo modelo y modificar el archivo de configuración de Dragonfly. Primero, el modelo:

1
rails g model Thumb uid:string job:string
2
rake db:migrate

La tabla thumbs hospedará tus miniaturas, pero serán generadas bajo demanda. Para hacer que esto suceda, necesitamos redefinir el método url dentro del inicializador Dragonfly:

config/initializers/dragonfly.rb

1
Dragonfly.app.configure do
2
    define_url do |app, job, opts|
3
        thumb = Thumb.find_by_job(job.signature)
4
        if thumb
5
          app.datastore.url_for(thumb.uid, :scheme => 'https')
6
        else
7
          app.server.url_for(job)
8
        end
9
    end
10
    
11
    before_serve do |job, env|
12
        uid = job.store
13
        
14
        Thumb.create!(
15
            :uid => uid,
16
            :job => job.signature
17
        )
18
    end
19
    # ...
20
end

Ahora agrega un nuevo álbum y visita la página raíz. La primera vez que lo haces, la siguiente salida será impresa en los registros:

1
DRAGONFLY: shell command: "convert" "some_path/public/system/dragonfly/development/2017/02/08/3z5p5nvbmx_Folder.jpg" "-resize" "250x250^^" "-gravity" "Center" "-crop" "250x250+0+0" "+repage" "some_path/20170208-1692-1xrqzc9.jpg"

Esto significa efectivamente que la miniatura está siendo generada por nosotros por ImageMagick. Si recargas la página, sin embargo, esta línea no aparecerá más, ¡significando que la miniatura fue cacheada! Podrías leer un poco más sobre esta característica en el sitio web de Dragonfly.

Más Procesamiento

Podrías realizar virtualmente cualquier manipulación a tus imágenes después de que fueron cargadas. Esto puede hacerse dentro del callback after_assign. Convirtamos, por ejemplo, todas nuestras imágenes a formato JPEG con 90% de calidad:

1
dragonfly_accessor :image do
2
    after_assign {|a| a.encode!('jpg', '-quality 90') }
3
end

Hay más acciones que puedes realizar: rotar y cortar las imágenes, codificar con un formato diferente, escribir texto en ellas, mezclar con otras imágenes (por ejemplo, colocar una marca de agua), etc. Para ver algunos otros ejemplos, refiérete a la sección ImageMagick en el sitio web de Dragonfly.

Subiendo y Administrando Canciones

Por supuesto, la parte principal de nuestro sitio musical son canciones, así que vamos a agregarlas ahora. Cada canción tiene un título y un archivo musical, y pertenece a un álbum:

1
rails g model Song album:belongs_to title:string track_uid:string
2
rails db:migrate

Engancha los métodos Dragonfly, como hicimos con el modelo Album:

models/song.rb

1
dragonfly_accessor :track

No olvides establecer una relación has_many:

models/album.rb

1
has_many :songs, dependent: :destroy

Agrega nuevas rutas. Una canción siempre existe en el alcance de un álbum, así que haré estas rutas anidadas:

config/routes.rb

1
resources :albums do
2
    resources :songs, only: [:new, :create]
3
end

Crea un controlador muy simple (una vez más, no olvides permitir el campo track):

songs_controller.rb

1
class SongsController < ApplicationController
2
  def new
3
    @album = Album.find(params[:album_id])
4
    @song = @album.songs.build
5
  end
6
7
  def create
8
    @album = Album.find(params[:album_id])
9
    @song = @album.songs.build(song_params)
10
    if @song.save
11
      flash[:success] = "Song added!"
12
      redirect_to album_path(@album)
13
    else
14
      render :new
15
    end
16
  end
17
18
  private
19
20
  def song_params
21
    params.require(:song).permit(:title, :track)
22
  end
23
end

Muestra las canciones y un enlace para agregar una nueva:

views/albums/show.html.erb

1
<h1><%= @album.title %></h1>
2
<h2>by <%= @album.singer %></h2>
3
4
<%= link_to 'Add song', new_album_song_path(@album) %>
5
6
<ol>
7
  <%= render @album.songs %>
8
</ol>

Codifica el formulario:

views/songs/new.html.erb

1
<h1>Add song to <%= @album.title %></h1>
2
3
<%= form_for [@album, @song] do |f| %>
4
  <div>
5
    <%= f.label :title %>
6
    <%= f.text_field :title %>
7
  </div>
8
9
  <div>
10
    <%= f.label :track %>
11
    <%= f.file_field :track %>
12
  </div>
13
14
  <%= f.submit %>
15
<% end %>

Por último, agrega el parcial _song:

views/songs/_song.html.erb

1
<li>
2
  <%= audio_tag song.track.url, controls: true %>
3
  <%= song.title %>
4
</li>

Aquí estoy usando la etiqueta HTML5 audio, que no funcionará para navegadores más antiguos. Así que, si estás apuntando a soportar dichos navegadores, usa un polyfill.

Como ves, todo el proceso es muy sencillo. A Dragonfly realmente no le importa qué tipo de archivo quieras subir; todo lo que necesitas hacer es proporcionar un método dragonfly_accessor, agregar un campo apropiado, permitirlo, y generar una etiqueta de archivo de entrada.

Almacenando Metadatos

Cuando abro una lista de reproducción, espero ver alguna información adicional sobre cada canción, somo su duración o bitrate. Por supuesto, por defecto esta información no es almacenada en ningún lugar, pero podemos arreglar eso fácilmente. Dragonfly nos permite proporcionar información adicional sobre cada archivo subido y recuperarla después usando el método meta.

Las cosas, sin embargo, son un poco más complejas cuando estamos trabajando con audio o video, porque para recuperar sus metadatos, se necesita un gem streamio-ffmpeg especial. Esta gem, en su momento, depende de FFmpeg, así que para poder proceder necesitarás instalarlo en tu PC.

Agrega streamio-ffmpeg al Gemfile:

Gemfile

1
gem 'streamio-ffmpeg'

Instálalo:

1
bundle install

Ahora podemos emplear el mismo callback after_assign ya visto en las secciones anteriores:

models/song.rb

1
dragonfly_accessor :track do
2
    after_assign do |a|
3
      song = FFMPEG::Movie.new(a.path)
4
      mm, ss = song.duration.divmod(60).map {|n| n.to_i.to_s.rjust(2, '0')}
5
      a.meta['duration'] = "#{mm}:#{ss}"
6
      a.meta['bitrate'] = song.bitrate ? song.bitrate / 1000 : 0
7
    end
8
end

Nota que aquí estoy usando un método path, no url, porque en este punto vamos a trabajar con un archivo temporal. Después solo extraemos la duración de la canción (convirtiéndola a minutos y segundos con ceros a la izquierda) y su bitrate (convirtiéndolo a kilobytes por segundo).

Por último, muestra metadatos en la vista:

views/songs/_song.html.erb

1
<li>
2
  <%= audio_tag song.track.url, controls: true %>
3
  <%= song.title %> (<%= song.track.meta['duration'] %>, <%= song.track.meta['bitrate'] %>Kb/s)
4
</li>

Si revisas los contenidos de la carpeta public/system/dragonfly (la ubicación por defecto para hospedar las subidas), notarás algunos archivos .yml---están almacenando toda la información meta en formato YAML.

Desplegando a Heroku

El último tema que cubriremos hoy es cómo preparar tu aplicación antes de desplegarla a la plataforma en la nube de Heroku. El problema principal es que Heroku no te permite almacenar campos personalizados (como subidas), así que debemos depender de un servicio de almacenamiento en la nube como Amazon S3. Afortunadamente, Dragonfly puede ser integrado con este fácilmente.

Todo lo que necesitas hacer es registrar una nueva cuenta en AWS (si no la tienes ya), crea un usuario con permiso para acceder a cubetas S3, y escribir el par de llaves del usuario en una ubicación segura. Podrías usar un par de llaves raíz, pero esto realmente no es recomendado. Por último, crea una cubeta S3.

Regresando a nuestra aplicación Rails, agrega una nueva gem:

Gemfile

1
group :production do
2
  gem 'dragonfly-s3_data_store'
3
end

Instálalo:

1
bundle install

Después modifica la configuración de Dragonfly para usar S3 en un entorno de producción:

config/initializers/dragonfly.rb

1
if Rails.env.production?
2
    datastore :s3,
3
              bucket_name: ENV['S3_BUCKET'],
4
              access_key_id: ENV['S3_KEY'],
5
              secret_access_key: ENV['S3_SECRET'],
6
              region: ENV['S3_REGION'],
7
              url_scheme: 'https'
8
else
9
    datastore :file,
10
        root_path: Rails.root.join('public/system/dragonfly', Rails.env),
11
        server_root: Rails.root.join('public')
12
end

Para proporcionar variables ENV en Heroku, usa este comando:

1
heroku config:add SOME_KEY=SOME_VALUE

Si quisieras probar la integración con S3 de manera local, podrías usar una gem como dotenv-rails para administrar variables de entorno. Recuerda, sin embargo, ¡que tu par de llaves AWS no debe ser expuesta de manera pública!

Otro pequeño asunto con el que me he encontrado mientras despliego a Heroku fue la ausencia de FFmpeg. La cosa es que cuando una nueva aplicación Heroku está siendo creada, tiene un conjunto de servicios que están siendo usado comúnmente (por ejemplo, ImageMagick está disponible por defecto). Otros servicios pueden ser instalado como complementos Heroku o en la forma de buildpacks. Para agregar un buildpack FFmpeg, ejecuta el siguiente comando:

1
heroku buildpacks:add https://github.com/HYPERHYPER/heroku-buildpack-ffmpeg.git

Ahora todo está listo, ¡y puedes compartir tu aplicación musical con el mundo!

Conclusión

¿Esto fue un largo viaje, no es así? Hoy hemos discutido Dragonfly---una solución para subir archivos en Rails. Hemos visto su configuración básica, algunas opciones de configuración, generación de miniaturas, procesamiento y almacenamiento de metadatos. También, hemos integrado Dragonfly con el servicio Amazon S3 y preparado nuestra aplicación para despliegue en producción.

Por supuesto, no hemos discutido todos los aspectos de Dragonfly en este artículo, así que asegúrate de navegar a su sitio web oficial para encontrar documentación extensiva y ejemplos útiles. Si tienes alguna pregunta o te atoras con algún ejemplo de código, no dudes en contactarme.

Gracias por permanecer conmigo, ¡y nos vemos pronto!

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.