Comment utiliser ActiveStorage dans votre application Rails 5.2+

Younes Serraj
Younes Serraj22 mars 2019

Comment utiliser ActiveStorage

Comment utiliser ActiveStorage

Ruby on Rails, notre framework bien-aimé, propose un nouveau standard pour le téléchargement de fichiers. Bienvenue à ActiveStorage !

Cet article est un guide très rapide et direct pour démarrer avec ActiveStorage.

J'ai hébergé une application d'exemple fonctionnelle sur Github pour que vous puissiez l'essayer telle quelle. Elle illustre la plupart des éléments présentés dans cet article. Le lien se trouve à la fin de l'article.


Table des matières :

1. Comment ajouter ActiveStorage à votre projet Ruby on Rails 5.2+ ?
2. Comment choisir où stocker les documents téléchargés (sur le disque local, sur Amazon S3, etc.)
3. Comment faire pour qu'un modèle ait une seule pièce jointe (has_one_attached)
4. Comment faire pour qu'un modèle ait plusieurs pièces jointes (has_many_attached)
5. Comment vérifier la présence d'une pièce jointe, créer un lien vers celle-ci ou en lire le contenu ?
6. Comment détruire les pièces jointes
7. Comment faire des manipulations de base sur les fichiers téléchargés (créer des variantes, des aperçus, lire les métadonnées, ...)
8. Comment attacher un fichier local (utile dans les tests et semences)
9. Comment ajouter des validations sur les fichiers téléchargés
10. Comment trouver des enregistrements avec des pièces jointes


1. Comment ajouter ActiveStorage à votre projet Ruby on Rails 5.2+ ?

Il n'y a pas de gemme à ajouter à votre Gemfile car Rails 5.2 est livré avec ActiveStorage intégré. Il suffit d'exécuter rails active_storage:install qui générera un fichier de migration, puis d'exécuter rake db:migrate.

Si vous lisez cette migration (soyez toujours curieux !), vous voyez qu'elle ajoute deux tables à votre base de données :

  • active_storage_blobs : cette table enregistre les blobs qui sont des informations relatives aux fichiers (nom de fichier, métadonnées, taille, etc.).

  • active_storage_attachments : il s'agit d'une table de jonction entre les modèles et les blobs de votre application.


Jusqu'à présent, vous avez probablement été habitué à :

  • ajouter un attribut à votre modèle/tableau pour lui permettre d'avoir une seule pièce jointe

  • créer une table associée lorsque vous voulez que votre modèle ait plusieurs pièces jointes.

ActiveStorage supprime ces deux étapes. Vous n'avez plus besoin de générer une migration pour que vos modèles aient une ou plusieurs pièces jointes.

With ActiveStorage, all attachments of all models will be recorded in active_storage_blobs et active_storage_attachments (une association polymorphe) sera le lien entre vos modèles et vos blobs. Si tout cela est encore confus pour vous, ne vous inquiétez pas, nous reviendrons sur ce point sous peu, c'est en fait assez facile à comprendre.

Pour l'instant, concentrons-nous sur la configuration. Nous avons généré une migration et migré la base de données, nous devons maintenant indiquer à ActiveStorage où stocker les fichiers téléchargés.

2. Comment choisir où stocker les documents téléchargés (sur le disque local, sur Amazon S3, etc.) ?

Lisez d'abord config/storage.yml. Ce fichier vous permet de définir plusieurs stratégies de stockage. Chaque environnement se verra attribuer une stratégie de stockage.

Voici le fichier config/storage.yml: généré par défaut:

test:
  service: Disk
  root: <%= Rails.root.join("tmp/storage") %>
local:
  service: Disk
  root: <%= Rails.root.join("storage") %>
# Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
# amazon:
#   service: S3
#   access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
#   secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
#   region: us-east-1
#   bucket: your_own_bucket
# Remember not to checkin your GCS keyfile to a repository
# google:
#   service: GCS
#   project: your_project
#   credentials: <%= Rails.root.join("path/to/gcs.keyfile") %>
#   bucket: your_own_bucket
# Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key)
# microsoft:
#   service: AzureStorage
#   storage_account_name: your_account_name
#   storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %>
#   container: your_container_name
# mirror:
#   service: Mirror
#   primary: local
#   mirrors: [ amazon, google, microsoft ]

Chaque stratégie de stockage indique essentiellement deux choses à ActiveStorage :

Quel service utiliser (choisir entre Disk,S3,GCS,AzureStorage et Mirror)

  • Comment configurer le service choisi (quel chemin, quelles informations d'identification sont requises, ...).

La liste des services est assez simple à comprendre :

  • Disk : Stocker les fichiers sur votre disque local

  • S3 : Utiliser Amazon S3 (exigence: ajouter gem 'aws-sdk-s3' à votre Gemfile)

  • GCS : Utiliser Google Cloud Storage (exigence: add gem 'google-cloud-storage', '~> 1.11' à votre Gemfile)

  • AzureStorage : Utiliser Microsoft Azure Storage (exigence: add gem 'azure-storage' à votre Gemfile)

Ensuite, il y a MirrorMirror qui indique à ActiveStorage d'utiliser à la fois une stratégie de stockage primaire et une collection d'autres stratégies pour faire des copies de vos documents téléchargés. Vous vouliez un moyen facile de créer des sauvegardes pour les documents téléchargés ? Mirror est une bonne solution.

Une dernière chose à propos du service miroir : bien que des copies soient faites, toutes vos requêtes et téléchargements seront effectués sur/à partir de la stratégie primaire. Il s'agit d'un mécanisme de sauvegarde, pas d'un mécanisme d'équilibrage de charge.

Revenons donc à config/storage.yml et à votre liste de stratégies de stockage.

Comme dans l'exemple ci-dessus, vous pourriez choisir d'avoir :

  • un test pour le moment où vous exécutez votre rspec/minitest/whatever. Dans cette stratégie, vous voudrez probablement stocker les fichiers téléchargés dans Rails.root.join("tmp/storage")afin de pouvoir les nettoyer en exécutant rake tmp:clean.

  • un local pour l'environnement de développement. Cela permettrait de stocker les fichiers téléchargés dans un espace de stockage non volatile, disons dans Rails.root.join("storage") par exemple.

  • un amazon pour un environnement de production. Cela permettrait de stocker les fichiers téléchargés dans un seau Amazon S3.

Je ne vais pas expliquer les spécificités de la configuration de chaque service car elles sont assez explicites. Lisez simplement les exemples ci-dessus et vous aurez pratiquement terminé. Oh et, évidemment, n'oubliez pas de configurer vos services externes sur leurs plateformes respectives au préalable (ex : pour S3, créez un bucket et définissez les bonnes permissions).

Une fois que vous avez rédigé vos stratégies de stockage (vous pouvez conserver les stratégies par défaut pour l'instant), vous devez attribuer une stratégie à chaque environnement que vous exécutez.

Concrètement: dans chaque config/environments/*.rb , définir l'attribut config.active_storage.service à la stratégie que vous souhaitez.

Par exemple, j'ai généralement dans config/environments/development.rb la ligne suivante: config.active_storage.service = :local.


3. Comment faire pour qu'un modèle n'ait qu'une seule pièce jointe (has_one_attached)


Côté modèle :

  • Etape 1: choisissez un nom pour votre pièce jointe. Disons que vous voulez ajouter une image avatar à un Profile de profil.

  • Etape 2: Ajoutez à votre modèle les éléments suivants : has_one_attached :avatar

Rappel : vous n'avez pas besoin d'ajouter une nouvelle colonne à votre table de base de données !

Maintenant vous pouvez utiliser some_profile.avatar.attached? pour vérifier si un fichier est présent ou non.


Côté contrôleur :

Pour permettre le téléchargement d'un avatar, ajoutez :avatar à votre autorisation

params.require(:profile).permit(:some_attribute, :some_other_attribute, :avatar)



Côté vue :

<%= form.file_field :avatar %>


That’s it!


4. Comment faire en sorte qu'un modèle ait de nombreuses pièces jointes (has_many_attached)


Côté modèle :

  • Etape 1: choisissez un nom pour votre pièce jointe. Disons que vous voulez ajouter des contracts pdf de contrats à un modèle de Customer.

  • Etape 2: Ajoutez à votre modèle les éléments suivants : has_many_attached:contracts


Côté contrôleur :

Pour permettre le téléchargement de nouveaux contrats, ajoutez contracts: [] à vos paramètres autorisés : 

params.require(:customer).permit(:some_attribute, :yet_another_attribute, contracts: [])

Vous pouvez maintenant utiliser l'option some_customer.contracts.attached? pour vérifier si au moins un fichier est présent ou non.



Côté vue:

<%= form.file_field :contracts, multiple: true %>


5. Comment vérifier la présence, le lien ou la lecture du contenu d'une pièce jointe ?


Vérifiez la présence de

some_profile.avatar.attached?


Lien vers

Comme l'emplacement du fichier dépend de la stratégie de stockage, ActiveStorage fournit une aide qui crée un lien de redirection temporaire vers le fichier.


Créez un lien de redirection qui durera 5 minutes :

url_for(some_profile.avatar)

Créez un lien de téléchargement en utilisant rails_blob_url or rails_blob_path:

rails_blob_path(some_profile.avatar, disposition: 'attachment')


Lire le contenu du fichier

binary_data = some_profile.avatar.download

Faites attention lorsque vous le faites sur des fichiers volumineux stockés sur le cloud !


6. Comment détruire les pièces jointes

Vous pouvez également détruire une pièce jointe :

  • de manière synchrone : some_profile.avatar.purge

  • de manière asynchrone : some_profile.avatar.purge_later. Cela va programmer un ActiveJob pour s'en occuper.

Vous pouvez également souhaiter autoriser un utilisateur à supprimer les pièces jointes. Je peux vous proposer deux solutions :

  1. La première consiste à écrire vos propres contrôleurs/actions/routes. L'avantage est que vous pouvez facilement ajouter des politiques et autoriser/refuser la destruction d'un document en fonction de vos propres contraintes.

  2. L'autre solution consiste à ajouter accept_nested_attributes_for. Laissez-moi vous expliquer celle-ci.

Je suppose que vous avez l'habitude d'utiliser accept_nested_attributes_for.

Lorsque vous ajoutez has_many_attached :contracts à un modèle de Customer, ActiveStorage injecte également has_many_attached :contracts dans votre modèle.

Lisez ceci pour savoir ce qui se passe précisément derrière la scène: https://github.com/rails/rails/blob/0f57f75008242d1739326fec38791c01852c9aa7/activestorage/lib/active_storage/attached/model.rb

Voici comment vous autoriserez la destruction des pièces jointes des contrats :

# Model
class Profile < ApplicationRecord
  has_one_attached :avatar
  accepts_nested_attributes_for :avatar_attachment, allow_destroy: true
end

# Controller
class ProfilesController < ApplicationController
  before_action :set_profile, only: [:show, :edit, :update, :destroy]

  # [...]

  # PATCH/PUT /profiles/1
  # PATCH/PUT /profiles/1.json
  def update
    respond_to do |format|
      if @profile.update(profile_params)
        format.html { redirect_to @profile, notice: 'Profile was successfully updated.' }
        format.json { render :show, status: :ok, location: @profile }
      else
        format.html { render :edit }
        format.json { render json: @profile.errors, status: :unprocessable_entity }
      end
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_profile
      @profile = Profile.find(params[:id])
    end

    # Never trust parameters from the scary internet, only allow the white list through.
    def profile_params
      params.require(:profile).permit(
        :last_name, :first_name,
        :avatar,
        avatar_attachment_attributes: [:id, :_destroy]
      )
    end
end

# View
<p id="notice"><%= notice %></p>
<p>
  <strong>Avatar:</strong><br />
  <% if @profile.avatar.attached? %>
    <%= form_for @profile do |f| %>
      <%= f.fields_for :avatar_attachment_attributes do |avatar_form| %>
	<%= avatar_form.hidden_field :id, value: @profile.avatar_attachment.id %>
      	<%= avatar_form.hidden_field :_destroy, value: true %>
      <% end %>
      <%= f.submit "Delete avatar" %>
    <% end %>
  <% end %>
</p>


7. Comment faire des manipulations de base sur les fichiers téléchargés (créer des variantes, des aperçus, lire les métadonnées, ...)

ActiveStorage est livré avec quelques assistants intégrés pour vous aider à effectuer des opérations de base telles que l'extraction de métadonnées ou la génération d'aperçus et de variantes sur certains formats de fichiers. Ces aides délèguent le vrai travail à des gemmes spécialisées et/ou à des binaires système. Par conséquent, si vous voulez les utiliser, vous devez d'abord satisfaire aux exigences.

La documentation indique :

L'extraction des aperçus nécessite des applications tierces, FFmpeg pour les vidéos et muPDF pour les PDF, et sur macOS également XQuartz et Poppler. Ces bibliothèques ne sont pas fournies par Rails. Vous devez les installer vous-même pour utiliser les prévisualisateurs intégrés. Avant d'installer et d'utiliser un logiciel tiers, assurez-vous de comprendre les implications de cette opération en termes de licence.

  • Pour générer des variantes à partir d'images, installer MiniMagick (http://www.imagemagick.org) sur votre système, puis ajoutez gem 'image_processing', '~> 1.2' à votre Gemfile.


Générer une variante d'une image

Voici un exemple de base de la façon de générer une variante :

<%= image_tag profile.avatar.variant(resize_to_limit: [75, 75]) %>

Lire https://github.com/janko/image_processing pour une liste complète des transformations possibles.

De même, si vous souhaitez qu'une variante ne soit générée qu'une seule fois puis stockée et réutilisée (pour des raisons de performances), utilisez la méthode
 #processed :

<%= image_tag profile.avatar.variant(resize_to_limit: [75, 75]) %>

Ce qu'il fait : il vérifie d'abord l'existence de la variante demandée. Si elle est trouvée, elle est utilisée, sinon elle est générée, stockée puis utilisée.



Conseils: utiliser #variable? d'abord pour s'assurer que vous pouvez créer une variante : some_profile.avatar.variable?. Appeler #variant si MiniMagick n'est pas installé ou si le format du fichier ne le permet pas, une erreur sera levée.


Générer un aperçu

Lorsque vous travaillez avec le PDF d'une vidéo, vous pouvez générer un aperçu :

<%= image_tag(customer.contract.preview(resize: '200x200') %>

Conseil: utiliser #previewable? d'abord pour vous assurer que vous pouvez créer un aperçu : some_profile.avatar.previewable?


Laisser ActiveStorage décider si c'est #variant ou #preview qui doit être appelé

Une belle enveloppe s'en charge : #representation:

<%= image_tag(record.attachment.representation(resize: '500x500') %>

Conseil: utiliser #representable? assurez-vous d'abord que vous pouvez créer une variante ou un aperçu: some_profile.avatar.representable?


Lisez les métadonnées, la taille du fichier, etc.

Vous souvenez-vous que nous avons commencé par créer deux tables de base de données ? Il s'agissait de blobs (informations sur le fichier joint) et de pièces jointes (une table de jonction entre vos modèles et les blobs). Si vous avez besoin de récupérer des informations sur un fichier téléchargé, vous savez où chercher.

Quelques exemples :

  • Pour une largeur de pièce jointe image, vous devez lire some_record.image.metadata[:width].

  • Pour un type de contenu de pièce jointe à un document, vous devez lire some_record.document.content_type

  • Pour la taille d'un fichier video joint, vous devez lire some_record.video.byte_size

Je vous conseille de lire la documentation officielle ou - encore mieux - le code source d'ActiveStorage pour trouver une liste exhaustive.

Notez que selon la façon dont vous avez joint un fichier (via le téléchargement ou en utilisant #attach), le fichier peut ou non avoir été analysé. L'analyse du fichier extrait des informations comme les dimensions de l'image et les enregistre dans les métadonnées du blob.

8. Comment joindre un fichier local (utile dans les tests)

Revenons à l'exemple de :avatar ci-dessus. Voici comment joindre un fichier local à votre enregistrement : 

some_profile.avatar.attach(io: File.open('/path/to/file'), filename: 'avatar.png')

Ce faisant, nous devons également demander explicitement à ActiveStorage d'analyser le fichier et de remplir l'attribut de métadonnées du blob lorsque cela est nécessaire :

some_profile.avatar.attach(io: File.open('/path/to/file'), filename: 'avatar.png')


9. Comment ajouter des validations sur les fichiers téléchargés

ActiveStorage ne fournit pas de mécanisme de validation pour l'instant. Pour ajouter vos validations, vous devrez simplement écrire des validations personnalisées dans votre modèle. Cela ressemblerait à ceci :

class MyModel < ActiveRecord::Base
  has_one_attached :image
  validate :image_size

  private

  def image_size
    errors.add :image, 'too big' if image.blob.byte_size > 4096
  end
end


10. Comment trouver des enregistrements avec des pièces jointes

Terminons cet article sur ce dernier sujet.

ActiveStorage ajoute une portée with_attached_<attribute> qui empêche les requêtes N+1 lors du chargement des enregistrements et de l'accès à leurs pièces jointes. Dans l'exemple d'un modèle de Profile avec has_one_attached :avatar, la portée serait with_attached_avatar.

Vous l'utiliseriez de cette façon : Profile.with_attached_avatar.find_each { ... }


Ce champ d'application est excellent, mais nous sommes souvent confrontés à la situation où nous voulons lister les enregistrements qui ont effectivement des pièces jointes. George Claghorn a répondu à cette même question de la manière la plus simple et la plus claire qui soit : https://github.com/rails/rails/issues/32295#issuecomment-374304126


Voici son extrait de code :

# Assuming a model defined like so:
class Post < ApplicationRecord
  has_one_attached :image
end

# ...you can join against :image_attachment to select posts having attached images:
Post.joins(:image_attachment).where('published_at >= ?', Time.now)

C'est tout pour cet article ! Comme toujours, je vous conseille de lire le ActiveStorage source code pour mieux comprendre son fonctionnement, l'étendre et, pourquoi pas, y contribuer.

Github-hosted example application: https://github.com/yoones/demo_activestorage

Merci d'avoir lu!

Partager
Younes Serraj
Younes Serraj22 mars 2019

Blog de Capsens

Capsens est une agence spécialisée dans le développement de solutions fintech. Nous aimons les startups, la méthodologie scrum, le Ruby et le React.

Ruby Biscuit

La newsletter française des développeurs Ruby on Rails.
Retrouve du contenu similaire gratuitement tous les mois dans ta boîte mail !
S'inscrire