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 localS3
: Utiliser Amazon S3 (exigence: ajoutergem 'aws-sdk-s3'
à votre Gemfile)
GCS
: Utiliser Google Cloud Storage (exigence: addgem 'google-cloud-storage', '~> 1.11'
à votre Gemfile)
AzureStorage
: Utiliser Microsoft Azure Storage (exigence: addgem 'azure-storage'
à votre Gemfile)
Ensuite, il y a Mirror
. Mirror
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 dansRails.root.join("tmp/storage")
afin de pouvoir les nettoyer en exécutantrake 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 dansRails.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
à unProfile
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 deCustomer
.
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 :
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.
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.
Pour générer des aperçus de pdfs, installez soit mupdf (https://mupdf.com/) soit Poppler (https://poppler.freedesktop.org/) sur votre système.
Pour générer des aperçus de vidéos, installer FFmpeg (https://www.ffmpeg.org/) sur votre système.
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 liresome_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 liresome_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!