edge
Plus sur rubyonrails.org: Plus de Ruby on Rails

Aperçu d'Active Storage

Ce guide explique comment attacher des fichiers à vos modèles Active Record.

Après avoir lu ce guide, vous saurez :

1 Qu'est-ce qu'Active Storage ?

Active Storage facilite le téléchargement de fichiers vers un service de stockage en nuage tel que Amazon S3, Google Cloud Storage ou Microsoft Azure Storage, et l'attachement de ces fichiers à des objets Active Record. Il est livré avec un service basé sur un disque local pour le développement et les tests, et prend en charge la duplication des fichiers vers des services subordonnés pour les sauvegardes et les migrations.

En utilisant Active Storage, une application peut transformer les téléchargements d'images ou générer des représentations d'images à partir de téléchargements non-images tels que des PDF et des vidéos, et extraire des métadonnées à partir de fichiers arbitraires.

1.1 Exigences

Diverses fonctionnalités d'Active Storage dépendent de logiciels tiers que Rails n'installera pas et qui doivent être installés séparément :

  • libvips v8.6+ ou ImageMagick pour l'analyse et les transformations d'images
  • ffmpeg v3.4+ pour les aperçus vidéo et ffprobe pour l'analyse vidéo/audio
  • poppler ou muPDF pour les aperçus PDF

L'analyse et les transformations d'images nécessitent également le gem image_processing. Décommentez-le dans votre Gemfile, ou ajoutez-le si nécessaire :

gem "image_processing", ">= 1.2"

CONSEIL : Comparé à libvips, ImageMagick est plus connu et plus largement disponible. Cependant, libvips peut être jusqu'à 10 fois plus rapide et consommer 1/10 de la mémoire. Pour les fichiers JPEG, cela peut être encore amélioré en remplaçant libjpeg-dev par libjpeg-turbo-dev, qui est 2 à 7 fois plus rapide.

AVERTISSEMENT : Avant d'installer et d'utiliser des logiciels tiers, assurez-vous de comprendre les implications de licence qui en découlent. MuPDF, en particulier, est sous licence AGPL et nécessite une licence commerciale pour certaines utilisations.

2 Configuration

$ bin/rails active_storage:install
$ bin/rails db:migrate

Cela configure l'application et crée les trois tables utilisées par Active Storage : active_storage_blobs, active_storage_attachments et active_storage_variant_records.

Table Objectif
active_storage_blobs Stocke les données sur les fichiers téléchargés, telles que le nom de fichier et le type de contenu.
active_storage_attachments Une table de jointure polymorphique qui lie vos modèles aux blobs. Si le nom de classe de votre modèle change, vous devrez exécuter une migration sur cette table pour mettre à jour le record_type sous-jacent avec le nouveau nom de classe de votre modèle.
active_storage_variant_records Si le suivi des variantes est activé, stocke les enregistrements pour chaque variante générée.

AVERTISSEMENT : Si vous utilisez des UUID à la place des entiers comme clé primaire sur vos modèles, vous devez définir Rails.application.config.generators { |g| g.orm :active_record, primary_key_type: :uuid } dans un fichier de configuration.

Déclarez les services Active Storage dans config/storage.yml. Pour chaque service utilisé par votre application, fournissez un nom et la configuration requise. L'exemple ci-dessous déclare trois services nommés local, test et amazon :

local:
  service: Disk
  root: <%= Rails.root.join("storage") %>

test:
  service: Disk
  root: <%= Rails.root.join("tmp/storage") %>

amazon:
  service: S3
  access_key_id: ""
  secret_access_key: ""
  bucket: ""
  region: "" # par exemple 'us-east-1'

Indiquez à Active Storage quel service utiliser en définissant Rails.application.config.active_storage.service. Étant donné que chaque environnement utilisera probablement un service différent, il est recommandé de le faire pour chaque environnement. Pour utiliser le service de disque à partir de l'exemple précédent dans l'environnement de développement, vous ajouteriez ce qui suit à config/environments/development.rb :

# Stocker les fichiers localement.
config.active_storage.service = :local

Pour utiliser le service S3 en production, vous ajoutez ce qui suit à config/environments/production.rb :

# Stocker les fichiers sur Amazon S3.
config.active_storage.service = :amazon

Pour utiliser le service de test lors des tests, vous ajoutez ce qui suit à config/environments/test.rb :

# Stocker les fichiers téléchargés sur le système de fichiers local dans un répertoire temporaire.
config.active_storage.service = :test

REMARQUE : Les fichiers de configuration spécifiques à un environnement auront la priorité : en production, par exemple, le fichier config/storage/production.yml (s'il existe) aura la priorité sur le fichier config/storage.yml.

Il est recommandé d'utiliser Rails.env dans les noms de bucket pour réduire davantage le risque de destruction accidentelle de données de production.

amazon:
  service: S3
  # ...
  bucket: your_own_bucket-<%= Rails.env %>

google:
  service: GCS
  # ...
  bucket: your_own_bucket-<%= Rails.env %>

azure:
  service: AzureStorage
  # ...
  container: your_container_name-<%= Rails.env %>

Continuez à lire pour plus d'informations sur les adaptateurs de service intégrés (par exemple, Disk et S3) et la configuration dont ils ont besoin.

2.1 Service Disk

Déclarez un service Disk dans config/storage.yml :

local:
  service: Disk
  root: <%= Rails.root.join("storage") %>

2.2 Service S3 (Amazon S3 et API compatibles S3)

Pour se connecter à Amazon S3, déclarez un service S3 dans config/storage.yml :

amazon:
  service: S3
  access_key_id: ""
  secret_access_key: ""
  region: ""
  bucket: ""

Fournissez éventuellement des options client et d'upload :

amazon:
  service: S3
  access_key_id: ""
  secret_access_key: ""
  region: ""
  bucket: ""
  http_open_timeout: 0
  http_read_timeout: 0
  retry_limit: 0
  upload:
    server_side_encryption: "" # 'aws:kms' or 'AES256'
    cache_control: "private, max-age=<%= 1.day.to_i %>"

CONSEIL : Définissez des délais d'attente HTTP et des limites de réessai raisonnables pour votre application. Dans certains scénarios d'échec, la configuration client AWS par défaut peut entraîner des connexions maintenues pendant plusieurs minutes et entraîner une mise en file d'attente des demandes.

Ajoutez la gem aws-sdk-s3 à votre Gemfile :

gem "aws-sdk-s3", require: false

REMARQUE : Les fonctionnalités principales de Active Storage nécessitent les autorisations suivantes : s3:ListBucket, s3:PutObject, s3:GetObject et s3:DeleteObject. L'accès public nécessite en plus s3:PutObjectAcl. Si vous avez d'autres options d'upload configurées, telles que la définition des ACL, des autorisations supplémentaires peuvent être nécessaires.

REMARQUE : Si vous souhaitez utiliser des variables d'environnement, des fichiers de configuration standard du SDK, des profils, des profils d'instance IAM ou de tâche, vous pouvez omettre les clés access_key_id, secret_access_key et region dans l'exemple ci-dessus. Le service S3 prend en charge toutes les options d'authentification décrites dans la documentation du SDK AWS.

Pour vous connecter à une API de stockage d'objets compatible S3 telle que DigitalOcean Spaces, fournissez l'endpoint :

digitalocean:
  service: S3
  endpoint: https://nyc3.digitaloceanspaces.com
  access_key_id: ...
  secret_access_key: ...
  # ...et d'autres options

Il existe de nombreuses autres options disponibles. Vous pouvez les vérifier dans la documentation de AWS S3 Client.

2.3 Service de stockage Microsoft Azure

Déclarez un service de stockage Azure dans config/storage.yml :

azure:
  service: AzureStorage
  storage_account_name: ""
  storage_access_key: ""
  container: ""

Ajoutez la gem azure-storage-blob à votre Gemfile :

gem "azure-storage-blob", "~> 2.0", require: false

2.4 Service de stockage Google Cloud

Déclarez un service de stockage Google Cloud dans config/storage.yml :

google:
  service: GCS
  credentials: <%= Rails.root.join("path/to/keyfile.json") %>
  project: ""
  bucket: ""

Fournissez éventuellement un Hash de credentials au lieu d'un chemin de fichier de clé :

google:
  service: GCS
  credentials:
    type: "service_account"
    project_id: ""
    private_key_id: <%= Rails.application.credentials.dig(:gcs, :private_key_id) %>
    private_key: <%= Rails.application.credentials.dig(:gcs, :private_key).dump %>
    client_email: ""
    client_id: ""
    auth_uri: "https://accounts.google.com/o/oauth2/auth"
    token_uri: "https://accounts.google.com/o/oauth2/token"
    auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs"
    client_x509_cert_url: ""
  project: ""
  bucket: ""

Fournissez éventuellement un Cache-Control metadata à définir sur les assets uploadés :

google:
  service: GCS
  ...
  cache_control: "public, max-age=3600"

Utilisez éventuellement IAM au lieu des credentials lors de la signature des URLs. Cela est utile si vous authentifiez vos applications GKE avec Workload Identity, consultez ce billet de blog Google Cloud pour plus d'informations.

google:
  service: GCS
  ...
  iam: true

Utilisez éventuellement un GSA spécifique lors de la signature des URLs. Lors de l'utilisation d'IAM, le serveur de métadonnées sera contacté pour obtenir l'e-mail du GSA, mais ce serveur de métadonnées n'est pas toujours présent (par exemple, les tests locaux) et vous pouvez souhaiter utiliser un GSA non par défaut.

google:
  service: GCS
  ...
  iam: true
  gsa_email: "[email protected]"

Ajoutez la gem google-cloud-storage à votre Gemfile :

gem "google-cloud-storage", "~> 1.11", require: false

2.5 Service de miroir

Vous pouvez maintenir plusieurs services synchronisés en définissant un service de miroir. Un service de miroir réplique les uploads et les suppressions sur deux services subordonnés ou plus.

Un service de miroir est destiné à être utilisé temporairement lors d'une migration entre services en production. Vous pouvez commencer à faire des miroirs vers un nouveau service, copier les fichiers préexistants de l'ancien service vers le nouveau, puis passer complètement au nouveau service.

REMARQUE : La mise en miroir n'est pas atomique. Il est possible qu'un upload réussisse sur le service principal et échoue sur l'un des services subordonnés. Avant de passer complètement à un nouveau service, vérifiez que tous les fichiers ont été copiés.

Définissez chacun des services que vous souhaitez mettre en miroir comme décrit ci-dessus. Référencez-les par leur nom lors de la définition d'un service de miroir :

s3_west_coast:
  service: S3
  access_key_id: ""
  secret_access_key: ""
  region: ""
  bucket: ""

s3_east_coast:
  service: S3
  access_key_id: ""
  secret_access_key: ""
  region: ""
  bucket: ""

production:
  service: Mirror
  primary: s3_east_coast
  mirrors:
    - s3_west_coast

Bien que tous les services secondaires reçoivent les uploads, les téléchargements sont toujours gérés par le service principal.

Les services de miroir sont compatibles avec les uploads directs. Les nouveaux fichiers sont directement téléchargés vers le service principal. Lorsqu'un fichier téléchargé directement est attaché à un enregistrement, un travail en arrière-plan est mis en file d'attente pour le copier vers les services secondaires.

2.6 Accès public

Par défaut, Active Storage suppose un accès privé aux services. Cela signifie la génération d'URL signées et à usage unique pour les blobs. Si vous préférez rendre les blobs accessibles publiquement, spécifiez public: true dans le fichier config/storage.yml de votre application :

gcs: &gcs
  service: GCS
  project: ""

private_gcs:
  <<: *gcs
  credentials: <%= Rails.root.join("path/to/private_key.json") %>
  bucket: ""

public_gcs:
  <<: *gcs
  credentials: <%= Rails.root.join("path/to/public_key.json") %>
  bucket: ""
  public: true

Assurez-vous que vos compartiments sont correctement configurés pour un accès public. Consultez la documentation sur la façon d'activer les autorisations de lecture publique pour les services de stockage Amazon S3, Google Cloud Storage et Microsoft Azure. Amazon S3 exige également que vous ayez l'autorisation s3:PutObjectAcl.

Lors de la conversion d'une application existante pour utiliser public: true, assurez-vous de mettre à jour chaque fichier individuel dans le compartiment pour qu'il soit accessible en lecture publique avant de passer à cette option.

3 Attacher des fichiers aux enregistrements

3.1 has_one_attached

La macro has_one_attached établit une correspondance un à un entre les enregistrements et les fichiers. Chaque enregistrement peut avoir un fichier attaché.

Par exemple, supposons que votre application ait un modèle User. Si vous voulez que chaque utilisateur ait un avatar, définissez le modèle User comme suit :

class User < ApplicationRecord
  has_one_attached :avatar
end

ou si vous utilisez Rails 6.0+, vous pouvez exécuter une commande de génération de modèle comme ceci :

bin/rails generate model User avatar:attachment

Vous pouvez créer un utilisateur avec un avatar :

<%= form.file_field :avatar %>
class SignupController < ApplicationController
  def create
    user = User.create!(user_params)
    session[:user_id] = user.id
    redirect_to root_path
  end

  private
    def user_params
      params.require(:user).permit(:email_address, :password, :avatar)
    end
end

Appelez avatar.attach pour attacher un avatar à un utilisateur existant :

user.avatar.attach(params[:avatar])

Appelez avatar.attached? pour déterminer si un utilisateur particulier a un avatar :

user.avatar.attached?

Dans certains cas, vous voudrez peut-être remplacer un service par défaut pour une pièce jointe spécifique. Vous pouvez configurer des services spécifiques par pièce jointe en utilisant l'option service :

class User < ApplicationRecord
  has_one_attached :avatar, service: :s3
end

Vous pouvez configurer des variantes spécifiques par pièce jointe en appelant la méthode variant sur l'objet attachable renvoyé :

class User < ApplicationRecord
  has_one_attached :avatar do |attachable|
    attachable.variant :thumb, resize_to_limit: [100, 100]
  end
end

Appelez avatar.variant(:thumb) pour obtenir une variante de pouce d'un avatar :

<%= image_tag user.avatar.variant(:thumb) %>

Vous pouvez également utiliser des variantes spécifiques pour les aperçus :

class User < ApplicationRecord
  has_one_attached :video do |attachable|
    attachable.variant :thumb, resize_to_limit: [100, 100]
  end
end
<%= image_tag user.video.preview(:thumb) %>

3.2 has_many_attached

La macro has_many_attached établit une relation un à plusieurs entre les enregistrements et les fichiers. Chaque enregistrement peut avoir plusieurs fichiers attachés.

Par exemple, supposons que votre application ait un modèle Message. Si vous voulez que chaque message ait plusieurs images, définissez le modèle Message comme suit :

class Message < ApplicationRecord
  has_many_attached :images
end

ou si vous utilisez Rails 6.0+, vous pouvez exécuter une commande de génération de modèle comme ceci :

bin/rails generate model Message images:attachments

Vous pouvez créer un message avec des images :

class MessagesController < ApplicationController
  def create
    message = Message.create!(message_params)
    redirect_to message
  end

  private
    def message_params
      params.require(:message).permit(:title, :content, images: [])
    end
end

Appelez images.attach pour ajouter de nouvelles images à un message existant :

@message.images.attach(params[:images])

Appelez images.attached? pour déterminer si un message particulier a des images :

@message.images.attached?

La substitution du service par défaut se fait de la même manière que has_one_attached, en utilisant l'option service :

class Message < ApplicationRecord
  has_many_attached :images, service: :s3
end

La configuration de variantes spécifiques se fait de la même manière que has_one_attached, en appelant la méthode variant sur l'objet attachable renvoyé :

class Message < ApplicationRecord
  has_many_attached :images do |attachable|
    attachable.variant :thumb, resize_to_limit: [100, 100]
  end
end

3.3 Attachement d'objets fichier/IO

Parfois, vous devez attacher un fichier qui n'arrive pas via une requête HTTP. Par exemple, vous voudrez peut-être attacher un fichier que vous avez généré sur le disque ou téléchargé à partir d'une URL soumise par l'utilisateur. Vous voudrez peut-être également attacher un fichier de fixture dans un test de modèle. Pour cela, fournissez un Hash contenant au moins un objet IO ouvert et un nom de fichier :

@message.images.attach(io: File.open('/path/to/file'), filename: 'file.pdf')

Dans la mesure du possible, fournissez également un type de contenu. Active Storage tente de déterminer le type de contenu d'un fichier à partir de ses données. Il utilise le type de contenu que vous fournissez s'il ne peut pas le déterminer. ruby @message.images.attach(io: File.open('/path/to/file'), filename: 'file.pdf', content_type: 'application/pdf')

Vous pouvez contourner l'inférence du type de contenu à partir des données en passant identify: false avec le content_type.

@message.images.attach(
  io: File.open('/path/to/file'),
  filename: 'file.pdf',
  content_type: 'application/pdf',
  identify: false
)

Si vous ne fournissez pas de type de contenu et que Active Storage ne peut pas déterminer automatiquement le type de contenu du fichier, il se définit par défaut sur application/octet-stream.

4 Suppression de fichiers

Pour supprimer une pièce jointe d'un modèle, appelez purge sur la pièce jointe. Si votre application est configurée pour utiliser Active Job, la suppression peut être effectuée en arrière-plan en appelant purge_later. La suppression supprime le blob et le fichier du service de stockage.

# Détruit synchroniquement l'avatar et les fichiers de ressources réels.
user.avatar.purge

# Détruit de manière asynchrone les modèles associés et les fichiers de ressources réels, via Active Job.
user.avatar.purge_later

5 Fourniture de fichiers

Active Storage prend en charge deux façons de fournir des fichiers : la redirection et la mise en proxy.

AVERTISSEMENT : Tous les contrôleurs Active Storage sont accessibles publiquement par défaut. Les URL générées sont difficiles à deviner, mais permanentes par conception. Si vos fichiers nécessitent un niveau de protection plus élevé, envisagez de mettre en œuvre des contrôleurs authentifiés.

5.1 Mode de redirection

Pour générer une URL permanente pour un blob, vous pouvez passer le blob à l'aide de l'helper de vue url_for. Cela génère une URL avec l'signed_id du blob qui est routé vers le RedirectController du blob.

url_for(user.avatar)
# => /rails/active_storage/blobs/:signed_id/my-avatar.png

Le RedirectController redirige vers le point de terminaison réel du service. Cette indirection dissocie l'URL du service de l'URL réelle et permet, par exemple, de dupliquer les pièces jointes dans différents services pour une haute disponibilité. La redirection a une expiration HTTP de 5 minutes.

Pour créer un lien de téléchargement, utilisez l'helper rails_blob_{path|url}. Cela vous permet de définir la disposition.

rails_blob_path(user.avatar, disposition: "attachment")

AVERTISSEMENT : Pour prévenir les attaques XSS, Active Storage force l'en-tête Content-Disposition à "attachment" pour certains types de fichiers. Pour modifier ce comportement, consultez les options de configuration disponibles dans Configuration des applications Rails.

Si vous avez besoin de créer un lien en dehors du contexte du contrôleur/vue (tâches en arrière-plan, Cronjobs, etc.), vous pouvez accéder à rails_blob_path de cette manière :

Rails.application.routes.url_helpers.rails_blob_path(user.avatar, only_path: true)

5.2 Mode de proxy

En option, les fichiers peuvent également être mis en proxy. Cela signifie que vos serveurs d'application téléchargeront les données de fichier à partir du service de stockage en réponse aux demandes. Cela peut être utile pour servir des fichiers à partir d'un CDN.

Vous pouvez configurer Active Storage pour utiliser le mode de proxy par défaut :

# config/initializers/active_storage.rb
Rails.application.config.active_storage.resolve_model_to_route = :rails_storage_proxy

Ou si vous souhaitez mettre explicitement en proxy des pièces jointes spécifiques, il existe des helpers d'URL que vous pouvez utiliser sous la forme de rails_storage_proxy_path et rails_storage_proxy_url.

<%= image_tag rails_storage_proxy_path(@user.avatar) %>

5.2.1 Mettre un CDN devant Active Storage

De plus, pour utiliser un CDN pour les pièces jointes Active Storage, vous devrez générer des URLs avec le mode de proxy afin qu'elles soient servies par votre application et que le CDN mette en cache la pièce jointe sans aucune configuration supplémentaire. Cela fonctionne directement car le contrôleur de proxy Active Storage par défaut définit un en-tête HTTP indiquant au CDN de mettre en cache la réponse.

Vous devez également vous assurer que les URLs générées utilisent l'hôte du CDN au lieu de l'hôte de votre application. Il existe plusieurs façons d'y parvenir, mais en général, cela implique de modifier votre fichier config/routes.rb afin de pouvoir générer les URLs appropriées pour les pièces jointes et leurs variations. Par exemple, vous pouvez ajouter ceci :

# config/routes.rb
direct :cdn_image do |model, options|
  expires_in = options.delete(:expires_in) { ActiveStorage.urls_expire_in }

  if model.respond_to?(:signed_id)
    route_for(
      :rails_service_blob_proxy,
      model.signed_id(expires_in: expires_in),
      model.filename,
      options.merge(host: ENV['CDN_HOST'])
    )
  else
    signed_blob_id = model.blob.signed_id(expires_in: expires_in)
    variation_key  = model.variation.key
    filename       = model.blob.filename

    route_for(
      :rails_blob_representation_proxy,
      signed_blob_id,
      variation_key,
      filename,
      options.merge(host: ENV['CDN_HOST'])
    )
  end
end

et ensuite générer des routes comme ceci :

<%= cdn_image_url(user.avatar.variant(resize_to_limit: [128, 128])) %>

5.3 Contrôleurs authentifiés

Tous les contrôleurs Active Storage sont accessibles publiquement par défaut. Les URL générées utilisent un signed_id simple, ce qui les rend difficiles à deviner mais permanentes. Toute personne connaissant l'URL du blob pourra y accéder, même si un before_action dans votre ApplicationController nécessiterait normalement une connexion. Si vos fichiers nécessitent un niveau de protection plus élevé, vous pouvez mettre en œuvre vos propres contrôleurs authentifiés, basés sur ActiveStorage::Blobs::RedirectController, ActiveStorage::Blobs::ProxyController, ActiveStorage::Representations::RedirectController et ActiveStorage::Representations::ProxyController

Pour permettre uniquement à un compte d'accéder à son propre logo, vous pouvez faire ce qui suit : ```ruby

config/routes.rb

resource :account do resource :logo end ```

# app/controllers/logos_controller.rb
class LogosController < ApplicationController
  # Via ApplicationController:
  # include Authenticate, SetCurrentAccount

  def show
    redirect_to Current.account.logo.url
  end
end
<%= image_tag account_logo_path %>

Et ensuite, vous devriez désactiver les routes par défaut d'Active Storage avec :

config.active_storage.draw_routes = false

pour empêcher l'accès aux fichiers via des URL accessibles publiquement.

6 Téléchargement de fichiers

Parfois, vous devez traiter un blob après son téléchargement, par exemple pour le convertir dans un format différent. Utilisez la méthode download de la pièce jointe pour lire les données binaires d'un blob en mémoire :

binary = user.avatar.download

Vous voudrez peut-être télécharger un blob vers un fichier sur le disque afin qu'un programme externe (par exemple, un scanner de virus ou un transcodeur multimédia) puisse y travailler. Utilisez la méthode open de la pièce jointe pour télécharger un blob vers un fichier temporaire sur le disque :

message.video.open do |file|
  system '/path/to/virus/scanner', file.path
  # ...
end

Il est important de savoir que le fichier n'est pas encore disponible dans le rappel after_create mais seulement dans le after_create_commit.

7 Analyse des fichiers

Active Storage analyse les fichiers une fois qu'ils ont été téléchargés en mettant en file d'attente un travail dans Active Job. Les fichiers analysés stockent des informations supplémentaires dans le hachage de métadonnées, y compris analyzed: true. Vous pouvez vérifier si un blob a été analysé en appelant analyzed? sur celui-ci.

L'analyse d'image fournit les attributs width et height. L'analyse vidéo fournit ceux-ci, ainsi que duration, angle, display_aspect_ratio, et les booléens video et audio pour indiquer la présence de ces canaux. L'analyse audio fournit les attributs duration et bit_rate.

8 Affichage d'images, de vidéos et de PDF

Active Storage prend en charge la représentation de différents types de fichiers. Vous pouvez appeler representation sur une pièce jointe pour afficher une variante d'image, ou un aperçu d'une vidéo ou d'un PDF. Avant d'appeler representation, vérifiez si la pièce jointe peut être représentée en appelant representable?. Certains formats de fichier ne peuvent pas être prévisualisés par Active Storage par défaut (par exemple, les documents Word) ; si representable? renvoie false, vous voudrez peut-être lier le fichier à la place.

<ul>
  <% @message.files.each do |file| %>
    <li>
      <% if file.representable? %>
        <%= image_tag file.representation(resize_to_limit: [100, 100]) %>
      <% else %>
        <%= link_to rails_blob_path(file, disposition: "attachment") do %>
          <%= image_tag "placeholder.png", alt: "Télécharger le fichier" %>
        <% end %>
      <% end %>
    </li>
  <% end %>
</ul>

En interne, representation appelle variant pour les images et preview pour les fichiers pouvant être prévisualisés. Vous pouvez également appeler ces méthodes directement.

8.1 Chargement différé vs immédiat

Par défaut, Active Storage traitera les représentations de manière différée. Ce code :

image_tag file.representation(resize_to_limit: [100, 100])

Générera une balise <img> avec le src pointant vers le ActiveStorage::Representations::RedirectController. Le navigateur effectuera une demande à ce contrôleur, qui effectuera les opérations suivantes :

  1. Traiter le fichier et télécharger le fichier traité si nécessaire.
  2. Renvoyer une redirection 302 vers le fichier, soit vers
    • le service distant (par exemple, S3).
    • ou ActiveStorage::Blobs::ProxyController qui renverra le contenu du fichier si le mode proxy est activé.

Le chargement différé du fichier permet aux fonctionnalités telles que les URL à usage unique de fonctionner sans ralentir le chargement initial de la page.

Cela fonctionne bien pour la plupart des cas.

Si vous souhaitez générer immédiatement des URL pour les images, vous pouvez appeler .processed.url :

image_tag file.representation(resize_to_limit: [100, 100]).processed.url

Le suivi des variantes d'Active Storage améliore les performances de ceci, en stockant un enregistrement dans la base de données si la représentation demandée a déjà été traitée. Ainsi, le code ci-dessus ne fera qu'un appel à l'API du service distant (par exemple, S3) une seule fois, et une fois qu'une variante est stockée, elle sera utilisée. Le suivi des variantes s'exécute automatiquement, mais peut être désactivé via config.active_storage.track_variants.

Si vous affichez de nombreuses images sur une page, l'exemple ci-dessus pourrait entraîner des requêtes N+1 pour charger tous les enregistrements de variantes. Pour éviter ces requêtes N+1, utilisez les portées nommées sur ActiveStorage::Attachment.

message.images.with_all_variant_records.each do |file|
  image_tag file.representation(resize_to_limit: [100, 100]).processed.url
end

8.2 Transformation des images

La transformation des images vous permet d'afficher l'image aux dimensions de votre choix. Pour créer une variation d'une image, appelez variant sur la pièce jointe. Vous pouvez passer toute transformation prise en charge par le processeur de variantes à la méthode. Lorsque le navigateur accède à l'URL de la variante, Active Storage transforme de manière différée le blob d'origine dans le format spécifié et redirige vers son nouvel emplacement de service.

<%= image_tag user.avatar.variant(resize_to_limit: [100, 100]) %>

Si une variante est demandée, Active Storage appliquera automatiquement des transformations en fonction du format de l'image :

  1. Les types de contenu variables (tels que dictés par config.active_storage.variable_content_types) et qui ne sont pas considérés comme des images web (tels que dictés par config.active_storage.web_image_content_types), seront convertis en PNG.

  2. Si quality n'est pas spécifié, la qualité par défaut du processeur de variantes pour le format sera utilisée.

Active Storage peut utiliser soit Vips soit MiniMagick comme processeur de variantes. Le choix par défaut dépend de la version cible de votre config.load_defaults, et le processeur peut être modifié en définissant config.active_storage.variant_processor.

Les deux processeurs ne sont pas entièrement compatibles, donc lors de la migration d'une application existante entre MiniMagick et Vips, certains changements doivent être effectués si des options spécifiques au format sont utilisées :

<!-- MiniMagick -->
<%= image_tag user.avatar.variant(resize_to_limit: [100, 100], format: :jpeg, sampling_factor: "4:2:0", strip: true, interlace: "JPEG", colorspace: "sRGB", quality: 80) %>

<!-- Vips -->
<%= image_tag user.avatar.variant(resize_to_limit: [100, 100], format: :jpeg, saver: { subsample_mode: "on", strip: true, interlace: true, quality: 80 }) %>

Les paramètres disponibles sont définis par la gem image_processing et dépendent du processeur de variantes que vous utilisez, mais les deux prennent en charge les paramètres suivants :

Paramètre Exemple Description
resize_to_limit resize_to_limit: [100, 100] Réduit la taille de l'image pour s'adapter aux dimensions spécifiées tout en conservant le rapport d'aspect d'origine. Redimensionne uniquement l'image si elle est plus grande que les dimensions spécifiées.
resize_to_fit resize_to_fit: [100, 100] Redimensionne l'image pour s'adapter aux dimensions spécifiées tout en conservant le rapport d'aspect d'origine. Réduit la taille de l'image si elle est plus grande que les dimensions spécifiées ou l'agrandit si elle est plus petite.
resize_to_fill resize_to_fill: [100, 100] Redimensionne l'image pour remplir les dimensions spécifiées tout en conservant le rapport d'aspect d'origine. Si nécessaire, recadre l'image dans la dimension la plus grande.
resize_and_pad resize_and_pad: [100, 100] Redimensionne l'image pour s'adapter aux dimensions spécifiées tout en conservant le rapport d'aspect d'origine. Si nécessaire, remplit la zone restante avec une couleur transparente si l'image source a un canal alpha, sinon avec du noir.
crop crop: [20, 50, 300, 300] Extrait une zone d'une image. Les deux premiers arguments sont les bords gauche et supérieur de la zone à extraire, tandis que les deux derniers arguments sont la largeur et la hauteur de la zone à extraire.
rotate rotate: 90 Fait pivoter l'image selon l'angle spécifié.

image_processing propose plus d'options disponibles (comme saver qui permet de configurer la compression de l'image) dans sa propre documentation pour les processeurs Vips et MiniMagick.

8.3 Aperçu des fichiers

Certains fichiers non image peuvent être prévisualisés : c'est-à-dire, ils peuvent être présentés comme des images. Par exemple, un fichier vidéo peut être prévisualisé en extrayant sa première image. Par défaut, Active Storage prend en charge la prévisualisation des vidéos et des documents PDF. Pour créer un lien vers une prévisualisation générée de manière paresseuse, utilisez la méthode preview de la pièce jointe :

<%= image_tag message.video.preview(resize_to_limit: [100, 100]) %>

Pour ajouter la prise en charge d'un autre format, ajoutez votre propre visualiseur. Consultez la documentation de ActiveStorage::Preview pour plus d'informations.

9 Téléchargements directs

Active Storage, avec sa bibliothèque JavaScript incluse, prend en charge le téléchargement directement depuis le client vers le cloud.

9.1 Utilisation

  1. Inclure activestorage.js dans le bundle JavaScript de votre application.

    Utilisation du pipeline d'assets :

    //= require activestorage
    

    Utilisation du package npm :

    import * as ActiveStorage from "@rails/activestorage"
    ActiveStorage.start()
    
  2. Ajouter direct_upload: true à votre champ de fichier :

    <%= form.file_field :attachments, multiple: true, direct_upload: true %>
    

    Ou, si vous n'utilisez pas un FormBuilder, ajoutez directement l'attribut de données :

    <input type="file" data-direct-upload-url="<%= rails_direct_uploads_url %>" />
    
  3. Configurer CORS sur les services de stockage tiers pour autoriser les requêtes de téléchargement direct.

  4. C'est tout ! Les téléchargements commencent dès la soumission du formulaire.

9.2 Configuration du partage de ressources entre origines différentes (CORS)

Pour que les téléchargements directs vers un service tiers fonctionnent, vous devrez configurer le service pour autoriser les requêtes entre origines différentes depuis votre application. Consultez la documentation CORS de votre service :

Veillez à autoriser :

  • Toutes les origines à partir desquelles votre application est accessible
  • La méthode de requête PUT
  • Les en-têtes suivants :
    • Origin
    • Content-Type
    • Content-MD5
    • Content-Disposition (sauf pour Azure Storage)
    • x-ms-blob-content-disposition (uniquement pour Azure Storage)
    • x-ms-blob-type (uniquement pour Azure Storage)
    • Cache-Control (pour GCS, uniquement si cache_control est défini) Aucune configuration CORS n'est requise pour le service Disk car il partage l'origine de votre application.

9.2.1 Exemple: Configuration CORS S3

[
  {
    "AllowedHeaders": [
      "*"
    ],
    "AllowedMethods": [
      "PUT"
    ],
    "AllowedOrigins": [
      "https://www.example.com"
    ],
    "ExposeHeaders": [
      "Origin",
      "Content-Type",
      "Content-MD5",
      "Content-Disposition"
    ],
    "MaxAgeSeconds": 3600
  }
]

9.2.2 Exemple: Configuration CORS Google Cloud Storage

[
  {
    "origin": ["https://www.example.com"],
    "method": ["PUT"],
    "responseHeader": ["Origin", "Content-Type", "Content-MD5", "Content-Disposition"],
    "maxAgeSeconds": 3600
  }
]

9.2.3 Exemple: Configuration CORS Azure Storage

<Cors>
  <CorsRule>
    <AllowedOrigins>https://www.example.com</AllowedOrigins>
    <AllowedMethods>PUT</AllowedMethods>
    <AllowedHeaders>Origin, Content-Type, Content-MD5, x-ms-blob-content-disposition, x-ms-blob-type</AllowedHeaders>
    <MaxAgeInSeconds>3600</MaxAgeInSeconds>
  </CorsRule>
</Cors>

9.3 Événements JavaScript de téléchargement direct

Nom de l'événement Cible de l'événement Données de l'événement (event.detail) Description
direct-uploads:start <form> Aucune Un formulaire contenant des fichiers pour les champs de téléchargement direct a été soumis.
direct-upload:initialize <input> {id, file} Déclenché pour chaque fichier après la soumission du formulaire.
direct-upload:start <input> {id, file} Un téléchargement direct est en cours de démarrage.
direct-upload:before-blob-request <input> {id, file, xhr} Avant de faire une demande à votre application pour les métadonnées de téléchargement direct.
direct-upload:before-storage-request <input> {id, file, xhr} Avant de faire une demande pour stocker un fichier.
direct-upload:progress <input> {id, file, progress} Au fur et à mesure que les demandes de stockage de fichiers progressent.
direct-upload:error <input> {id, file, error} Une erreur s'est produite. Une alerte s'affiche sauf si cet événement est annulé.
direct-upload:end <input> {id, file} Un téléchargement direct est terminé.
direct-uploads:end <form> Aucune Tous les téléchargements directs sont terminés.

9.4 Exemple

Vous pouvez utiliser ces événements pour afficher la progression d'un téléchargement.

direct-uploads

Pour afficher les fichiers téléchargés dans un formulaire:

// direct_uploads.js

addEventListener("direct-upload:initialize", event => {
  const { target, detail } = event
  const { id, file } = detail
  target.insertAdjacentHTML("beforebegin", `
    <div id="direct-upload-${id}" class="direct-upload direct-upload--pending">
      <div id="direct-upload-progress-${id}" class="direct-upload__progress" style="width: 0%"></div>
      <span class="direct-upload__filename"></span>
    </div>
  `)
  target.previousElementSibling.querySelector(`.direct-upload__filename`).textContent = file.name
})

addEventListener("direct-upload:start", event => {
  const { id } = event.detail
  const element = document.getElementById(`direct-upload-${id}`)
  element.classList.remove("direct-upload--pending")
})

addEventListener("direct-upload:progress", event => {
  const { id, progress } = event.detail
  const progressElement = document.getElementById(`direct-upload-progress-${id}`)
  progressElement.style.width = `${progress}%`
})

addEventListener("direct-upload:error", event => {
  event.preventDefault()
  const { id, error } = event.detail
  const element = document.getElementById(`direct-upload-${id}`)
  element.classList.add("direct-upload--error")
  element.setAttribute("title", error)
})

addEventListener("direct-upload:end", event => {
  const { id } = event.detail
  const element = document.getElementById(`direct-upload-${id}`)
  element.classList.add("direct-upload--complete")
})

Ajoutez des styles:

/* direct_uploads.css */

.direct-upload {
  display: inline-block;
  position: relative;
  padding: 2px 4px;
  margin: 0 3px 3px 0;
  border: 1px solid rgba(0, 0, 0, 0.3);
  border-radius: 3px;
  font-size: 11px;
  line-height: 13px;
}

.direct-upload--pending {
  opacity: 0.6;
}

.direct-upload__progress {
  position: absolute;
  top: 0;
  left: 0;
  bottom: 0;
  opacity: 0.2;
  background: #0076ff;
  transition: width 120ms ease-out, opacity 60ms 60ms ease-in;
  transform: translate3d(0, 0, 0);
}

.direct-upload--complete .direct-upload__progress {
  opacity: 0.4;
}

.direct-upload--error {
  border-color: red;
}

input[type=file][data-direct-upload-url][disabled] {
  display: none;
}

9.5 Solutions personnalisées de glisser-déposer

Vous pouvez utiliser la classe DirectUpload à cette fin. Lors de la réception d'un fichier de votre bibliothèque de choix, instanciez un DirectUpload et appelez sa méthode create. create prend un rappel à invoquer lorsque le téléchargement est terminé.

import { DirectUpload } from "@rails/activestorage"

const input = document.querySelector('input[type=file]')

// Lier à la dépose de fichiers - utiliser ondrop sur un élément parent ou utiliser une
//  bibliothèque comme Dropzone
const onDrop = (event) => {
  event.preventDefault()
  const files = event.dataTransfer.files;
  Array.from(files).forEach(file => uploadFile(file))
}

// Lier à la sélection normale de fichiers
input.addEventListener('change', (event) => {
  Array.from(input.files).forEach(file => uploadFile(file))
  // vous pouvez effacer les fichiers sélectionnés de l'entrée
  input.value = null
})

const uploadFile = (file) => {
  // votre formulaire doit avoir le champ de fichier file_field direct_upload: true, qui
  //  fournit data-direct-upload-url
  const url = input.dataset.directUploadUrl
  const upload = new DirectUpload(file, url)

  upload.create((error, blob) => {
    if (error) {
      // Gérer l'erreur
    } else {
      // Ajouter un champ caché avec un nom approprié au formulaire avec une
      //  valeur de blob.signed_id afin que les identifiants de blob soient
      //  transmis dans le flux de téléchargement normal
      const hiddenField = document.createElement('input')
      hiddenField.setAttribute("type", "hidden");
      hiddenField.setAttribute("value", blob.signed_id);
      hiddenField.name = input.name
      document.querySelector('form').appendChild(hiddenField)
    }
  })
}

9.6 Suivre la progression du téléchargement du fichier

Lors de l'utilisation du constructeur DirectUpload, il est possible d'inclure un troisième paramètre. Cela permet à l'objet DirectUpload d'appeler la méthode directUploadWillStoreFileWithXHR pendant le processus de téléchargement. Vous pouvez ensuite attacher votre propre gestionnaire de progression à la XHR selon vos besoins. ```js import { DirectUpload } from "@rails/activestorage"

class Uploader { constructor(file, url) { this.upload = new DirectUpload(this.file, this.url, this) }

upload(file) { this.upload.create((error, blob) => { if (error) { // Gérer l'erreur } else { // Ajouter un champ caché avec un nom approprié au formulaire // avec une valeur de blob.signed_id } }) }

directUploadWillStoreFileWithXHR(request) { request.upload.addEventListener("progress", event => this.directUploadDidProgress(event)) }

directUploadDidProgress(event) { // Utiliser event.loaded et event.total pour mettre à jour la barre de progression } } ```

9.7 Intégration avec des bibliothèques ou des frameworks

Une fois que vous avez reçu un fichier de la bibliothèque que vous avez sélectionnée, vous devez créer une instance de DirectUpload et utiliser sa méthode "create" pour lancer le processus de téléchargement, en ajoutant les en-têtes supplémentaires nécessaires. La méthode "create" nécessite également une fonction de rappel qui sera déclenchée une fois le téléchargement terminé.

import { DirectUpload } from "@rails/activestorage"

class Uploader {
  constructor(file, url, token) {
    const headers = { 'Authentication': `Bearer ${token}` }
    // INFO: L'envoi des en-têtes est un paramètre facultatif. Si vous choisissez de ne pas envoyer d'en-têtes,
    //       l'authentification sera effectuée à l'aide de cookies ou de données de session.
    this.upload = new DirectUpload(this.file, this.url, this, headers)
  }

  upload(file) {
    this.upload.create((error, blob) => {
      if (error) {
        // Gérer l'erreur
      } else {
        // Utiliser blob.signed_id comme référence de fichier dans la prochaine requête
      }
    })
  }

  directUploadWillStoreFileWithXHR(request) {
    request.upload.addEventListener("progress",
      event => this.directUploadDidProgress(event))
  }

  directUploadDidProgress(event) {
    // Utiliser event.loaded et event.total pour mettre à jour la barre de progression
  }
}

Pour implémenter une authentification personnalisée, un nouveau contrôleur doit être créé sur l'application Rails, similaire à celui-ci :

class DirectUploadsController < ActiveStorage::DirectUploadsController
  skip_forgery_protection
  before_action :authenticate!

  def authenticate!
    @token = request.headers['Authorization']&.split&.last

    return head :unauthorized unless valid_token?(@token)
  end
end

NOTE : L'utilisation des Direct Uploads peut parfois entraîner le téléchargement d'un fichier qui n'est jamais attaché à un enregistrement. Considérez la possibilité de purger les téléchargements non attachés.

10 Test

Utilisez fixture_file_upload pour tester le téléchargement d'un fichier dans un test d'intégration ou de contrôleur. Rails traite les fichiers comme n'importe quel autre paramètre.

class SignupController < ActionDispatch::IntegrationTest
  test "can sign up" do
    post signup_path, params: {
      name: "David",
      avatar: fixture_file_upload("david.png", "image/png")
    }

    user = User.order(:created_at).last
    assert user.avatar.attached?
  end
end

10.1 Suppression des fichiers créés lors des tests

10.1.1 Tests système

Les tests système nettoient les données de test en annulant une transaction. Comme destroy n'est jamais appelé sur un objet, les fichiers attachés ne sont jamais nettoyés. Si vous voulez supprimer les fichiers, vous pouvez le faire dans un rappel after_teardown. Le faire ici garantit que toutes les connexions créées pendant le test sont terminées et vous ne recevrez pas d'erreur d'Active Storage indiquant qu'il ne peut pas trouver un fichier.

class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
  # ...
  def after_teardown
    super
    FileUtils.rm_rf(ActiveStorage::Blob.service.root)
  end
  # ...
end

Si vous utilisez des tests parallèles et le service DiskService, vous devez configurer chaque processus pour utiliser son propre dossier pour Active Storage. De cette façon, le rappel teardown ne supprimera que les fichiers des tests du processus concerné.

class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
  # ...
  parallelize_setup do |i|
    ActiveStorage::Blob.service.root = "#{ActiveStorage::Blob.service.root}-#{i}"
  end
  # ...
end

Si vos tests système vérifient la suppression d'un modèle avec des pièces jointes et que vous utilisez Active Job, configurez votre environnement de test pour utiliser l'adaptateur de file d'attente en ligne afin que le travail de purge soit exécuté immédiatement plutôt qu'à un moment inconnu dans le futur.

# Utilisez le traitement des tâches en ligne pour que les choses se produisent immédiatement
config.active_job.queue_adapter = :inline

10.1.2 Tests d'intégration

De la même manière que les tests système, les fichiers téléchargés lors des tests d'intégration ne seront pas automatiquement nettoyés. Si vous voulez supprimer les fichiers, vous pouvez le faire dans un rappel teardown.

class ActionDispatch::IntegrationTest
  def after_teardown
    super
    FileUtils.rm_rf(ActiveStorage::Blob.service.root)
  end
end

Si vous utilisez des tests parallèles et le service Disk, vous devez configurer chaque processus pour utiliser son propre dossier pour Active Storage. De cette façon, le rappel teardown ne supprimera que les fichiers des tests du processus concerné.

class ActionDispatch::IntegrationTest
  parallelize_setup do |i|
    ActiveStorage::Blob.service.root = "#{ActiveStorage::Blob.service.root}-#{i}"
  end
end

10.2 Ajout de pièces jointes aux fixtures

Vous pouvez ajouter des pièces jointes à vos fixtures existantes. Tout d'abord, vous devez créer un service de stockage séparé :

# config/storage.yml

test_fixtures:
  service: Disk
  root: <%= Rails.root.join("tmp/storage_fixtures") %>

Cela indique à Active Storage où "uploader" les fichiers de la fixture, il doit donc s'agir d'un répertoire temporaire. En le rendant différent du service test habituel, vous pouvez séparer les fichiers de la fixture des fichiers téléchargés lors d'un test. Ensuite, créez des fichiers de fixture pour les classes Active Storage :

# active_storage/attachments.yml
david_avatar:
  name: avatar
  record: david (User)
  blob: david_avatar_blob
# active_storage/blobs.yml
david_avatar_blob: <%= ActiveStorage::FixtureSet.blob filename: "david.png", service_name: "test_fixtures" %>

Ensuite, placez un fichier dans votre répertoire de fixtures (le chemin par défaut est test/fixtures/files) avec le nom de fichier correspondant. Consultez la documentation de ActiveStorage::FixtureSet pour plus d'informations.

Une fois que tout est configuré, vous pourrez accéder aux pièces jointes dans vos tests :

class UserTest < ActiveSupport::TestCase
  def test_avatar
    avatar = users(:david).avatar

    assert avatar.attached?
    assert_not_nil avatar.download
    assert_equal 1000, avatar.byte_size
  end
end

10.2.1 Nettoyage des fixtures

Alors que les fichiers téléchargés lors des tests sont nettoyés à la fin de chaque test, vous n'avez besoin de nettoyer les fichiers de fixture qu'une seule fois : lorsque tous vos tests sont terminés.

Si vous utilisez des tests parallèles, appelez parallelize_teardown :

class ActiveSupport::TestCase
  # ...
  parallelize_teardown do |i|
    FileUtils.rm_rf(ActiveStorage::Blob.services.fetch(:test_fixtures).root)
  end
  # ...
end

Si vous n'exécutez pas de tests parallèles, utilisez Minitest.after_run ou l'équivalent pour votre framework de test (par exemple, after(:suite) pour RSpec) :

# test_helper.rb

Minitest.after_run do
  FileUtils.rm_rf(ActiveStorage::Blob.services.fetch(:test_fixtures).root)
end

10.3 Configuration des services

Vous pouvez ajouter config/storage/test.yml pour configurer les services à utiliser dans l'environnement de test. Cela est utile lorsque l'option service est utilisée.

class User < ApplicationRecord
  has_one_attached :avatar, service: :s3
end

Sans config/storage/test.yml, le service s3 configuré dans config/storage.yml est utilisé - même lors de l'exécution des tests.

La configuration par défaut serait utilisée et les fichiers seraient téléchargés vers le fournisseur de services configuré dans config/storage.yml.

Dans ce cas, vous pouvez ajouter config/storage/test.yml et utiliser le service Disk pour le service s3 pour éviter d'envoyer des requêtes.

test:
  service: Disk
  root: <%= Rails.root.join("tmp/storage") %>

s3:
  service: Disk
  root: <%= Rails.root.join("tmp/storage") %>

11 Mise en œuvre de la prise en charge d'autres services cloud

Si vous avez besoin de prendre en charge un service cloud autre que ceux-ci, vous devrez implémenter le service. Chaque service étend ActiveStorage::Service en implémentant les méthodes nécessaires pour télécharger et télécharger des fichiers vers le cloud.

12 Purge des téléchargements non attachés

Il arrive que des fichiers soient téléchargés mais jamais attachés à un enregistrement. Cela peut se produire lors de l'utilisation de Direct Uploads. Vous pouvez interroger les enregistrements non attachés en utilisant la portée non attachée. Voici un exemple utilisant une tâche rake personnalisée.

namespace :active_storage do
  desc "Purges unattached Active Storage blobs. Run regularly."
  task purge_unattached: :environment do
    ActiveStorage::Blob.unattached.where(created_at: ..2.days.ago).find_each(&:purge_later)
  end
end

AVERTISSEMENT : La requête générée par ActiveStorage::Blob.unattached peut être lente et potentiellement perturbatrice sur les applications avec de plus grandes bases de données.

Retour d'information

Vous êtes encouragé à contribuer à l'amélioration de la qualité de ce guide.

Veuillez contribuer si vous trouvez des fautes de frappe ou des erreurs factuelles. Pour commencer, vous pouvez lire notre contribution à la documentation section.

Vous pouvez également trouver du contenu incomplet ou des informations qui ne sont pas à jour. Veuillez ajouter toute documentation manquante pour la version principale. Assurez-vous de vérifier Edge Guides d'abord pour vérifier si les problèmes ont déjà été résolus ou non sur la branche principale. Consultez les Directives des guides Ruby on Rails pour le style et les conventions.

Si pour une raison quelconque vous repérez quelque chose à corriger mais ne pouvez pas le faire vous-même, veuillez ouvrir un problème.

Et enfin, toute discussion concernant la documentation de Ruby on Rails est la bienvenue sur le Forum officiel de Ruby on Rails.