En utilisant Dragonfly nous nous sommes rendus compte qu’au moment de servir des vidéos pour Safari et Safari mobile depuis notre application Rails, cela ne fonctionnait pas.
En creusant un peu il s’avère que Safari est assez exigeant puisqu’il impose un support du byte-range de la part du serveur qui sert la vidéo.
C’est à dire que le player veut pouvoir demander une tranche de la vidéo uniquement, de sorte à gérer sa mise en cache et ne pas nécessairement tout télécharger d’un coup. Il demande donc un contenu partiel (code HTTP 206).
Pour cela il envoie un entête byte-range
dans la requête et attend en retour un entête Accept-Ranges
si le serveur le supporte.
Exemple :
curl --verbose --range 0-99 https://example.com/video.mp4 -o /dev/null
Comme notre serveur le supporte nous recevons ce type d’entête :
Accept-Ranges: bytes
Content-Range: bytes 0-99/55766047
Content-Type: video/mp4
Content-Length: 100
La première idée serait de faire en sorte que notre application Rails puisse produire des réponses partielles.
En fouillant un peu il existe un ticket ouvert sur le projet Dragonfly accompagné d’un gist qui vise à introduire un nouveau middleware en charge de produire un morceau de réponse depuis le fichier final.
Je pense que c’est une mauvaise idée de s’appuyer sur ce code pour plusieurs raisons :
Une autre solution serait d’utiliser Rack::File
qui gère les entêtes de type range mais autant utiliser notre reverse proxy pour faire ce travail plus efficacement.
La première chose à faire est de demander à Nginx de prendre la main lorsque nous renvoyons des fichiers (send_file
) dans Rails de sorte à ce que le flux de requêtes soit géré ainsi :
Pour cela nous activons l’entête qui va bien :
config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect'
Ce paramètre peut être configuré soit par environnement dans le fichier correspondant, soit dans le fichier config/application.rb
pour l’ensemble des environnements. Attention toutefois à la précédence, car si vous mettez la configuration dans les deux fichiers c’est celle du fichier d’environnement qui prendra le pas.
Une fois fait, lors des demandes de fichier, Rack enverra un entête à Nginx en lui demandant de charger le fichier et en joignant une réponse (body) vide.
Côté Rails, le travail est fini. Il a passé la main à Nginx et peut passer à la requête suivante.
C’est bien gentil mais Nginx a besoin de savoir où trouver le fichier en question. Dans le cas d’une requête type https://example.com/files/foo.zip
le mapping est simple, la configuration de Nginx utilise le répertoire public
de Rails comme root et il va donc le trouver sans configuration supplémentaire.
Dans le cas de Dragonfly, on a des URL un peu plus complexes type /media/W1siZiIsIjIwMTcvMDMvMDEvMTI/video.mp4?sha=ueiu
Dragonfly utilise Rack::Cache
pour mettre en cache les réponses et les servir plus rapidement. Il faut donc que nous expliquions à Nginx comment servir ces requêtes.
En premier lieu nous allons raffiner un peu la configuration de Rack::Cache pour définir où nous voulons stocker notre cache.
Dans le fichier application.rb
, pour le généraliser à tous les environnements, ou dans production.rb
pour la production uniquement, nous remplaçons config.action_dispatch.rack_cache = true
par :
config.action_dispatch.rack_cache = {
verbose: false,
metastore: URI.encode("file:#{File.expand_path(Rails.root.join('../../shared/cache/dragonfly/meta'))}"),
entitystore: URI.encode("file:#{File.expand_path(Rails.root.join('../../shared/cache/dragonfly/body'))}")
}
Ici nous sommes dans le cadre d’un déploiement Capistrano, on veut donc stocker le cache dans le dossier shared pour qu’il persiste les différents déploiements (attention à purger le cache de temps à autre).
Maintenant il faut que Nginx prenne connaissance de ces URL afin de pouvoir les servir. Dans sa configuration nous allons définir ces chemins comme internes :
location /chemin/complet/vers/dragonfly/body {
internal;
alias /chemin/complet/vers/dragonfly/body;
}
Ici location
et alias
sont identiques mais, sans même parler de Rails, on pourrait faire en sorte que Nginx serve les fichiers depuis /var/uploads
lorsqu’il reçoit une requête sur /files
:
location /files {
internal;
alias /var/uploads;
}
Dans ce cas, si on demande /files/foo.zip
il servira /var/uploads/foo.zip
.
Dernière chose à faire côté Nginx, renvoyer un entête de mapping des fichiers.
location {
proxy_set_header X-Accel-Mapping /chemin/complet/vers/dragonfly/body=/chemin/complet/vers/dragonfly/body;
}
Lorsque Rack va recevoir cet entête de mapping, il va pouvoir en retour indiquer où se trouve le fichier à aller chercher (car l’URL d’entrée est toujours /media/…
) et demander à Nginx de le servir directement via l’entête ‘X-Accel-Redirect’.
En clair, il va lui dire /media/zzz
se trouve dans /chemin/complet/vers/dragonfly/body/A1
et Nginx pourra le servir lui même.
J’espère que cet article vous aidera à servir des contenus partiels dans le cas d’une stack Rails + Nginx ou tout du moins à délester votre serveur applicatif en confiant à votre reverse proxy le soin de servir vos fichiers.
L’équipe Synbioz.
Libres d’être ensemble.
Nos conseils et ressources pour vos développements produit.