
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 :
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é à :
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.
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)
La liste des services est assez simple à comprendre :
Disk : Stocker les fichiers sur votre disque localS3 : 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 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 :
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.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.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.
has_one_attached)Côté modèle :
avatar à un Profile de profil. has_one_attached :avatarRappel : 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!
has_many_attached)Côté modèle :
contracts pdf de contrats à un modèle de Customer. has_many_attached:contractsCô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 %>some_profile.avatar.attached?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 utilisantrails_blob_urlorrails_blob_path:
rails_blob_path(some_profile.avatar, disposition: 'attachment')binary_data = some_profile.avatar.downloadFaites attention lorsque vous le faites sur des fichiers volumineux stockés sur le cloud !
Vous pouvez également détruire une pièce jointe :
some_profile.avatar.purgesome_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 :
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>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.
gem 'image_processing', '~> 1.2' à votre Gemfile.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.
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?
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?
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 :
image, vous devez lire some_record.image.metadata[:width].some_record.document.content_typevideo joint, vous devez lire some_record.video.byte_sizeJe 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.
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')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
endTerminons 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!