Shrine

Shrine es una gema para manejar de manera simple la tarea de subir y adjuntar archivos.

¿Por qué la usamos?

Anteriormente usábamos Paperclip, hasta que fue deprecada en favor de la solución incluida en Rails, ActiveStorage. Dado esto, nos cambiamos a ActiveStorage (AS) para no quedarnos con una herramienta sin soporte.

Sin embargo, hay algunos detalles que se manejan mejor en Shrine, por ejemplo:

  • Shrine tiene un diseño modular, utilizando plugins para las distintas funcionalidades que ofrece. Esto permite cargar solo las que en verdad se usen

  • Shrine promueve separación de responsabilidades, introduciendo el concepto de Uploader. Estos se encargan de la lógica de subida de un tipo de archivo en particular

  • AS no tiene validaciones, por ejemplo de tamaño o tipo de archivo. En Shrine se pueden agregar fácilmente usando el plugin validation_helpers

  • Recién en Rails 6.1 se está dando soporte a acceso público de archivos, con Shrine se puede definir a nivel de configuración o por Uploader

  • Con Shrine se pueden setear default_url por Uploader, para cuando el archivo es nil

¿Cómo la usamos?

Instalación

La gema viene instalada si el proyecto se generó usando Potassium con la opción Shrine elegida como storage. También se incluyen un par de uploaders que sirven como base. Si el proyecto se generó sin una opción de storage, se puede agregar corriendo potassium install file_storage y seleccionando Shrine.

Ejemplo básico

Para estos ejemplos se utilizó la versión 3.2.1 de Shrine

Supongamos que tenemos un Uploader de imágenes genérico, que podría verse así:

class ImageUploader < Shrine
plugin :validation_helpers
Attacher.validate do
validate_mime_type %w[image/jpeg image/jpg image/png image/svg+xml image/gif]
end
end

Aquí tenemos el plugin validation_helpers, que nos permite usar validate_mime_type para verificar que el tipo del archivo corresponda efectivamente a una imagen.

Con esto ya podemos empezar a incluir imágenes en los modelos. Si queremos un attachment llamado photo habría que agregar la columna photo_data a la tabla (tipo text o jsonb) y agregar lo siguiente en el modelo:

include ImageUploader::Attachment(:photo)

El nombre de la columna siempre debe incluir el sufijo _data

Ejemplo: heredando de un uploader

Ahora, imaginemos que se quiere agregar una imagen de perfil para los usuarios. Podríamos usar el ImageUploader definido antes, pero se quieren un par de cosas extra para esta profile_picture:

  • Límite de peso, 5 MB. Pueden haber muchas imágenes de perfil distintas en una misma vista y no queremos que quede muy pesada

  • El usuario puede elegir no tener una foto de perfil

  • Comúnmente se usará la imagen en dos tamaños

Como esta sigue siendo una imagen y queremos mantener la validación definida en ImageUploader, vamos a definir un nuevo uploader que herede de este:

class ProfilePictureUploader < ImageUploader
end

Límite de peso

Para esto recurrimos nuevamente al validations_helper, esta vez usando validate_max_size. Para mantener las validaciones de la clase padre, se debe llamar a super(), quedando así:

Attacher.validate do
super()
validate_max_size 5*1024*1024
end

Sin foto de perfil

En estos casos queremos poner una imagen por defecto, que indique claramente la ausencia de la foto de perfil. Digamos que tenemos una imagen para este propósito en /app/assets/images/no-profile-picture.png. Podemos usar el plugin default_url para usarla siempre que no haya una imagen de perfil:

plugin :default_url
Attacher.default_url do |**options|
ActionController::Base.helpers.image_url('no-profile-picture.png')
end

ActionController::Base.helpers.image_url nos ayuda a obtener la url final de uno de los assets del proyecto.

Con esto, todo usuario cuya profile_picture sea nil retornará la url del asset al hacer user.profile_picture_url.

Distintos tamaños

Vamos a procesar la imagen para tener dos tamaños aparte del original: small y medium. Shrine tiene dos opciones para realizar el procesamiento: dinámicamente cuando se pide la transformación (on-the-fly) o con anterioridad, guardando el resultado. Para este ejemplo usaremos la segunda opción.

Como prerequisito necesitamos agregar la gema image_processing. Luego podemos definir las derivatives para los tamaños small y medium:

require "image_processing/mini_magick"
...
plugin :derivatives
Attacher.derivatives do |original|
magick = ImageProcessing::MiniMagick.source(original)
{
small: magick.resize_to_limit!(300, 300),
medium: magick.resize_to_limit!(500, 500),
}
end

Después en el controlador se debe gatillar la creación de estas derivadas:

user = User.new(..., profile_picture: file)
if user.valid?
user.profile_picture_derivatives! if user.profile_picture_changed?
user.save
end

Y para acceder a la url de una de las derivadas:

User.last.profile_picture_url(:small)

Resultado

Con todo esto, nuestro Uploader que hereda de ImageUploader quedaría así:

require "image_processing/mini_magick"
class ProfilePictureUploader < ImageUploader
plugin :default_url
plugin :derivatives
Attacher.validate do
super()
validate_max_size 5*1024*1024
end
Attacher.default_url do |**options|
ActionController::Base.helpers.image_url('no-profile-picture.png')
end
Attacher.derivatives do |original|
magick = ImageProcessing::MiniMagick.source(original)
{
small: magick.resize_to_limit!(300, 300),
medium: magick.resize_to_limit!(500, 500)
}
end
end

Y para agregar el attachment al modelo de usuario:

include ProfilePictureUploader::Attachment(:profile_picture)

Recursos útiles

  • Documentación oficial: muy buena, con guías y secciones explicando los distintos plugins

  • Direct S3 Upload / Direct App Upload: ambos sirven para subir un archivo antes de que se le haga submit a un form. La diferencia radica en que el de S3 lo sube directamente a AWS, mientras que el otro lo sube a un upload_endpoint de la aplicación. Para ambientes que no tienen un bucket S3 (como development, en que se guardan los archivos en el filesystem) habría que usar Direct App Upload

  • Demo Direct Upload + Vue: presentación al equipo de Platanus para introducir Shrine. Se arma un componente Vue para manejar el Direct Upload que puede ser usado dentro de un form de Rails

  • Testing: en la sección de Test data dan un ejemplo de un helper que puede ser usado en las factories. En la sección de Acceptance tests dan un ejemplo de como agregar un archivo como parámetro en tests de controladores usando Rack::Test::UploadedFile

Recursos útiles para plataneros