Renvoyer une image par défaut avec ActiveStorage stocké sur disque
Pour une application utilisant le stockage DiskService
d’ActiveStorage
, j’ai eu besoin de copier des données de la base de production dans ma base de développement.
Je ne voulais pas récupérer les fichiers stockés, j’ai donc fait en sorte d’afficher une image ActiveStorage
par défaut au lieu d’une erreur 404.
Nous devons modifier le code de la gemme ActiveStorage
à deux endroits :
ActiveStorage::DiskController
ActiveStorage::Service::DiskService
Il est préférable d’éviter les monkeypatches en général, et quand c’est inévitable de limiter au maximum le volume de code modifié. Malheureusement, le code original ne permet pas d’intercepter les erreurs sans recopier une méthode entière.
DiskController
L’action DiskController#show
intercepte l’exception Errno::ENOENT
renvoyée par Ruby quand le chemin ne correspond à rien de connu sur le disque. Nous devons modifier cette ligne pour qu’elle renvoie l’image par défaut au lieu de renvoyer l’en-tête HTTP not_found
.
Grâce au patch suivant, si une image n’est pas trouvée sur le disque, c’est l’image par défaut qui sera affichée.
ActiveStorage::Service::DiskService
Si les images sont utilisées hors d’un contexte web, il faut également surcharger la méthode DiskService#download
.
Dans mon cas les images sont insérées dans des PDF, j’avais donc besoin de patcher cette partie de la gemme également.
Cette fois, au lieu de lever l’exception ActiveStorage::FileNotFoundError
, nous allons renvoyer le contenu du fichier image_unavailable.jpg
.
Note : je n’ai été jusqu’à patcher le streaming vu que ça ne me concernait pas, mais si c’est votre cas il faudra adapter le code.
Comme pour la méthode du contrôleur, nous sommes obligés de surcharger la méthode entière pour pouvoir remplacer une seule ligne, ce qui est dommage mais inévitable.
La petite feinte c’est qu’en mode développement les classes sont chargées à la volée, il faut en tenir compte lorsqu’on applique le patch. En effet, la classe ActiveStorage::Service::DiskService
n’est pas chargée au moment où le patch est appliqué, il faut forcer Rails à lire l’original en premier.
# lib/extras/active_storage_fallback.rb
return unless Rails.env.development?
module ActiveStorageControllerFallback
def show
if key = decode_verified_key
serve_file named_disk_service(key[:service_name]).path_for(key[:key]), content_type: key[:content_type], disposition: key[:disposition]
else
head :not_found
end
rescue Errno::ENOENT
serve_fallback
end
def serve_fallback
default_image = Rails.root.join("app", "assets", "images", "image_unavailable.jpg")
serve_file default_image, content_type: "image/jpg", disposition: :inline
end
end
ActiveStorage::DiskController.prepend ActiveStorageControllerFallback
module ActiveStorageServiceFallback
def download(key, &block)
if block_given?
instrument :streaming_download, key: key do
stream key, &block
end
else
instrument :download, key: key do
File.binread path_for(key)
rescue Errno::ENOENT
File.binread Rails.root.join("app", "assets", "images", "image_unavailable.jpg")
end
end
end
end
# ActiveStorage does not autoload services, so we need to force reopening the class first
module ActiveStorage; class Service::DiskService < Service; end; end
ActiveStorage::Service::DiskService.prepend ActiveStorageServiceFallback
Je suis la convention qui recommande de placer les patches dans le répertoire lib/extras
, qui n’est pas chargé automatiquement. Pour que les patches soient chargés, il faut indiquer à Rails de charger le contenu des fichiers placés à cet endroit dans un bloc config.to_prepare
.
# app/config/application.rb
module YourAppName
class Application < Rails::Application
# Execute all monkey patches
config.to_prepare do
Rails.root.join("lib", "extras").children.each do |file|
require file
end
end
end
end
Il est peut-être possible d’améliorer ce code mais il fait ce dont j’ai besoin pour me faciliter le développement, je préfère donc me consacrer à des tâches plus utiles. J’espère que cela vous aidera si vous avez la même problématique !